vai al contenuto principale

Lo schema GraphQL

GraphQL rappresenta un cambiamento architettonico e concettuale significativo rispetto alle API REST, e porta con sé alcuni termini non familiari.

In questa pagina riportiamo un estratto della documentazione ufficiale GraphQL (🇺🇸 Schemas and Types), arricchito di dettagli relativi alla nostra implementazione e al mapping delle classi Livebase in GraphQL. Nel paragrafo Glossario, in fondo, abbiamo raccolto un cheat sheet con i vocaboli GraphQL che useremo in questa documentazione.

Struttura dello schema #

Lo schema è al centro dell’implementazione del server GraphQL e descrive le funzionalità disponibili per i client che si connettono alla API. Quando il server riceve una richiesta, questa viene convalidata ed eseguita in base allo schema. Lo schema definisce il sistema dei tipi (type system), ovvero l’insieme dei dati che possono essere convalidati, interrogati ed eseguiti sulla API.

Fondamentalmente, possiamo suddividere il sistema dei tipi in due categorie:

  • tipi che descrivono il modello dati trattato dalla API (oggetti, scalar ed enum);
  • tipi che individuano le operazioni consentite sul modello dati (servizi e oggetti di input).

Come affermato nell’introduzione, la sintassi scelta per scrivere uno schema prende il nome di Schema Definition Language (SDL), è simile al linguaggio di query e, come quest’ultimo, è indipendente dall’implementazione del server.

Oggetti e campi #

Alla base del modello dati troviamo gli oggetti (GraphQL Object Type, o semplicemente object type); nell’SDL, un oggetto è individuato dalla keyword type e rappresenta semplicemente un insieme di campi (field) che è possibile recuperare dal server in una struttura aggregata.

type Employee {
  _id: ID
  full_name: String
  age: Int
  team: Team
}

type Team {
  _id: ID
  name: String
}

Ciascun campo di un oggetto è caratterizzato da un nome e un tipo; un campo può contenere a sua volta un oggetto (ad esempio, il campo team di Employee è di tipo Team), oppure risolvere in un’informazione semplice (ad esempio full_name e age di Employee). Parleremo più avanti dei tipi di dato primitivi.

Sul tipo del campo è possibile applicare i seguenti modificatori:

  • !: il campo è non nullo (non-nullable). Il server promette di ritornare sempre un valore quando viene richiesto;
  • [<TypeName>]: il campo ritorna una lista di valori di quel tipo.

Infine, un campo può essere caratterizzato da zero o più argomenti (arguments), raccolti tra parentesi tonde come nell’esempio:

type Foo {
  campoSemplice: String
  campoNonNull: String!
  campoConArgomenti(arg1: String!, arg2: Int = 42): String
}

Anche un argomento può essere marcato con ! per renderlo obbligatorio; in tal caso, la chiamata non è valida se si richiede il campo senza specificare un valore per quell’argomento. Al contrario, se l’argomento non è obbligatorio, esso assumerà il valore di default, come definito nello schema (ad esempio 42 per arg2).

Servizi e oggetti di input #

Le operazioni consentite sono raccolte in due tipi speciali: Query e Mutation. Entrambi definiscono il punto di ingresso di ogni chiamata GraphQL. Richiedendo uno o più campi di Query, il client può navigare il grafo di oggetti per leggere le informazioni presenti; analogamente, i campi di Mutation consentono di inviare dati al server, modificare il grafo e leggere il risultato della scrittura.

schema {
  query: Query
  mutation: Mutation
}

type Query {
  # una query
  Employee___get(_id: ID!): Employee
}

type Mutation {
  # una mutation
  Employee___create(data: EmployeeCreate!): Employee
}

Come suggerisce l’esempio, nella nostra implementazione tutti i servizi sono parametrici e hanno almeno un argomento obbligatorio. Quello che non abbiamo detto, introducendo gli argomenti nel paragrafo precedente, è che è possibile passare oggetti come argomenti (ad esempio, il servizio Employee___create richiede l’argomento data di tipo EmployeeCreate). Un oggetto di input di questo tipo è detto input type, ed è individuato nello schema dalla keyword input:

input EmployeeCreate {
  full_name: String
  age: Int
  team: Team
}

Convenzioni sulla nomenclatura dei servizi #

Nella pratica, Query e Mutation sono normali object type, e i loro campi hanno come tipo la struttura dati ritornata dal server in seguito all’esecuzione dell’operazione. Nell’esempio di sopra, sia Employee___get che Employee___create restituiscono un oggetto Employee, ma il primo esiste già sul server e viene richiesto in lettura, mentre il secondo viene letto contestualmente alla sua creazione.

Ma cosa sono allora Employee___get e Employee___create? Per convenzione, in questa documentazione usiamo il termine servizio per riferirci ai campi di Query e Mutation:

  • con servizio di lettura o servizio di tipo query ci riferiamo al nome di un campo di Query;
  • con servizio di scrittura o servizio di tipo mutation ci riferiamo al nome di un campo di Mutation;

Tipi di dato primitivi #

I tipi di dato primitivi prendono il nome di scalar; essi rappresentano le “foglie” di una query, si risolvono sempre in dati concreti (come stringhe o numeri) e non hanno sotto-campi.

Scalar di sistema #

GraphQL gestisce i seguenti tipi di scalar:

  • String: sequenze alfanumeriche di caratteri espresse con codifica UTF-8;
  • Int: interi a 32 bit con segno;
  • Float: numeri in virgola mobile a doppia precisione con segno;
  • Boolean: valori logici true e false;
  • ID: identificatore utilizzato per individuare in modo univoco risorse e oggetti. È pensato per non essere human-readable.

Tutti i tipi di dato omonimi gestiti da Livebase sono mappati in GraphQL sul corrispettivo tipo scalar (ad esempio, un attributo di tipo string nel modello è una String in GraphQL). Il platform attribute __id è mappato sul tipo ID, mentre il tipo Float non è utilizzato.

Scalar Livebase #

La nostra implementazione aggiunge inoltre i seguenti scalar per mappare i rimanenti tipi di dato:

  • Real: analogo del tipo real;
  • Date: analogo del tipo date;
  • Time: analogo del tipo time;
  • Datetime: analogo del tipo datetime;
  • Text: analogo di text;
  • Serial: analogo di serial;
  • Year: analogo di year;

Tutti i tipi elencati sono serializzati come stringhe, a eccezione di Year che è serializzato come numero intero (come Int).

Formato di date e numeri #

Il formato di rappresentazione di campi di tipo Date, Time, Datetime o Real dipende dalle impostazioni di visualizzazione custom dell’utente della Cloudlet e dalle impostazioni di default della Cloudlet stessa.

Nei servizi di lettura che ritornano Date, Time o Datetime, su campi di questo tipo è disponibile l’argomento opzionale format, di tipo String, che consente di specificare un formato (tra quelli supportati dalla Cloudlet) diverso da quello impostato per l’utente.

type Employee {
  _id: ID
  date_of_birth(format: String = "default"): Date
  # altri campi
}
type Query {
  Employee___get(_id: ID!): Employee
}
query {
Employee___get(id: "10101") {
date_of_birth(format: "MMM-d-yyyy")
}
}

Enumerati #

Un altro tipo di oggetto che troveremo nello schema generato sono gli enumerati (enum). Un enum è un tipo speciale di scalar, limitato a un dato insieme di valori consentiti.

Mappatura modello - schema #

Per ogni vista applicativa (Application Schema) definita nel modello, viene generata una API GraphQL distinta. Lo schema di ciascuna API rispetta i vincoli, la manageability, i filtri e permessi definiti nel modello per quella specifica vista applicativa.

Ogni classe <ClassName> del modello (tra quelle abilitate per quella vista applicativa e raggiungibili), viene mappata sullo schema nella struttura type <ClassName>, per accedere alla quale vengono generati diversi servizi, il cui nome segue il formato <ClassName>___<ServiceType>. Come vale per tutte le interfacce dell’applicazione generata, anche in GraphQL ciascun servizio lavora sul grafo di oggetti raggiungibile a partire da quella classe; per questo motivo, i servizi vengono generati solo per classi main e non per le classi part, in quanto la gestione di questi oggetti è subordinata alle classi whole.

Oltre <ClassName>, vengono generati ulteriori object type e input type di supporto ai servizi su quella classe; i nomi di queste strutture seguono il formato type <ClassName><TypeName> o input <ClassName><InputName> (ad esempio, type EmployeePage o input EmployeeCreate).

Gli attributi su <ClassName> vengono mappati come segue:

  • il nome dell’attributo coincide col nome definito sul modello, mentre il tipo segue il mapping mostrato in Tipi di dato primitivi;
  • di default, il platform attribute __id viene reso visibile per tutte le classi, con nome _id di tipo ID (in GraphQL, il doppio underscore __ è un prefisso riservato a metadati);
  • nelle strutture di input, attributi required nel modello sono marcati con !;

Sia su ClassName che sulle sue strutture di supporto viene inoltre abilitato un campo opzionale esclusivo dello schema GraphQL: _clientId: ID. Nei servizi di scrittura, i client possono compilare questo campo con valori arbitrari (ad esempio hash o timestamp).

I ruoli uscenti su <ClassName> vengono mappati in modo differente in base al tipo di servizio; per maggiori informazioni rimandiamo a Servizi di lettura e Servizi di scrittura.

Struttura della risposta #

GraphQL pone l’accento sulla consistenza e prevedibilità dei risultati, pertanto le risposte del server hanno sempre una struttura prevedibile. Vediamo ora il formato comune di tutte le risposte GraphQL (il formato della richiesta è descritto qui: Formulare query e mutation).

L’oggetto JSON restituito dal server può assumere due forme:

  • Se la richiesta è stata eseguita con successo, sarà presente un campo data, contenente, per ciascun servizio invocato dalla query, una chiave con il nome del servizio e come valore l’oggetto restituito dal servizio.

    {
      "data": {
        "ClassName___ServiceType": { ... }
      }
    }
    
  • Se il server ha riscontrato errori nel risolvere la richiesta, sarà presente un campo errors, contenente una lista degli errori riscontrati.

    {
      "errors": [ ... ],
      "data": {
        "ClassName___ServiceType": null
      }
    }
    

In accordo con la specifica, se non sono stati restituiti errori, il campo errors è assente, mentre se non vengono restituiti dati per un servizio, il campo data conterrà comunque il nome del servizio, con valore null.

Gli elementi del campo errors hanno, a loro volta, una struttura definita. Ciascun errore contiene i seguenti campi:

  • message: una stringa con la descrizione dell’errore. È l’unico campo sempre presente;
  • locations: array di posizioni, ciascuna caratterizzata dai campi line e column, in cui si è verificato un errore;
  • path: array di path, rappresentati da stringhe (in caso di campi) ed interi (in caso di indici);
  • extensions: informazioni aggiuntive fornite dal server (vedi Issue).

Le tipologie di errore ricadono in due categorie: errori nella richiesta ed errori che si verificano sul server. Nel primo caso, il client ha inviato una richiesta sintatticamente non valida, oppure che viola il type checking, ovvero la validazione dello schema GraphQL. Nel secondo caso, il client ha inviato una richiesta corretta, ma si sono verificati errori sul server durante la sua risoluzione.

Dettaglio: una richiesta che viola il type checking

Errori lato server (Issue) #

Quando riceve una chiamata valida, il server controlla che siano rispettati tutti i vincoli definiti sul modello per gli elementi coinvolti (nel contesto della vista applicativa su cui è definito lo schema). Ad esempio, in una scrittura, il server controlla se sono rispettati i vincoli sugli attributi e se l’utente ha un profilo che gli permette di scrivere su quell’attributo.

Nel dettaglio, il server analizza la chiamata in tre fasi, verificando per ciascun passaggio una data categoria di vincoli:

  1. Grant: vincoli relativi ai diritti del profilo dell’utente sugli elementi del modello disponibili nello schema, definiti a livello di Profile Schema e Permission Schema. I grant vengono controllati sia per letture che per scritture, in quanto un elemento del modello può essere acceso in un’applicazione ma spento per un profilo che vi accede.
  2. Veto Action: vincoli relativi ai Class Warning (definiti a livello di Application Schema). GraphQL “simula” una action quando si invoca un servizio di scrittura.
  3. Data Validation: vincoli relativi alla validazione degli input, controllati solo nelle scritture. Comprendono i vincoli relativi ai Class Warning valutati in seguito al persist sul database, e gli stessi vincoli a livello di Database Schema su domini degli attributi, cardinalità dei ruoli e vincoli di unicità.

Se si verifica un errore in una qualunque tra le fasi elencate, il server interrompe il recupero dei dati e restituisce un errore. La struttura per rappresentare l’errore è l’oggetto Issue, inserito nel campo extensions:

type Issue {
  userMessage: String
  issueLevel: IssueLevel
  issueReferenceType: IssueReferenceType
  issueType: IssueType
  entityName: String
  entityID: ID
  attributeNames: [String!]
  roleNames: [String!]
  applicationName: String!
  profileName: String!
  traceId: String
}

Esaminiamola campo per campo:

  • userMessage è un messaggio pensato per la visualizzazione nel client per notificare il problema all’utente;
  • issueLevel, issueReferenceType e issueType descrivono gerarchicamente la tipologia di problema che si è verificato. Le relative strutture sono di tipo enum. Maggiori informazioni nel dettaglio;
  • entityName ed entityID si riferiscono alla Entity (intesa come classe del modello), o alla particolare istanza su cui è stato riscontrato il problema;
  • attributeNames e roleNames arricchiscono il contesto, indicando quali attributi/ruoli della entity sono coinvolti. Sono valorizzati solo se issueReferenceType è ENTITY_ATTRIBUTE o ENTITY_ROLE;
  • applicationName e profileName indicano rispettivamente su quale vista applicativa e con quale profilo è stato riscontrato il problema;
  • traceId è un identificatore interno di supporto, utile per risalire alla richiesta effettuata per la segnalazione di bug.
Dettaglio: IssueLevel, IssueReferenceType e IssueType

Effettuare il debug di una richiesta #

Tutti i servizi generati hanno un argomento opzionale insight. Questo, se incluso, consente di chiedere al server informazioni relative al processo di recupero dei dati (ad esempio, il tempo di esecuzione); ciò è utile per identificare possibili colli di bottiglia o eventuali bug nel server GraphQL.

query {
  # valori consentiti: FULL, LIGHT
  Employee___get(id: "10101", insight: FULL) {
    full_name
  }
}

Nella risposta, le informazioni di insight richieste si trovano in un campo aggiuntivo extensions, allo stesso livello di data ed errors.

{
  "data": { "Employee___get": { ... }},
  "extensions": { ... }
}

Introspezione #

Negli scenari in cui non è possibile accedere a GraphiQL, la API generata supporta comunque l’introspezione come strumento alternativo per scoprire informazioni sullo schema. Ciò significa che sono disponibili i seguenti servizi standard di lettura:

  • __schema: recupera metadati su tutti i tipi definiti sullo schema e sullo schema stesso;
  • __type(name: String!): recupera metadati su un tipo dello schema, dato il suo nome.

Per maggiori informazioni sui campi dei servizi di introspezione rimandiamo alla documentazione ufficiale: 🇺🇸 Introspection.

Glossario #

Di seguito abbiamo raccolto, in ordine alfabetico, i termini GraphQL usati in questa guida.

Argument #
Coppia chiave-valore associata a un campo specifico. Un campo può avere zero o più argomenti. Tutti i servizi richiedono almeno un input type come argomento.
Enum #
Tipo speciale di scalar, limitato a un dato insieme di valori consentiti. È individuato dalla keyword enum.
Field #
Unità di dati che appartiene a un tipo nello schema. Ogni chiamata GraphQL richiede uno o più field (campi) sul root object.
Input type #
Tipo speciale di object type usato come argomento. È individuato dalla keyword input.
Mutation #
Una operazione GraphQL che crea, modifica o distrugge dati.
Object type #
Un tipo nello schema GraphQL contenente campi. È individuato dalla keyword type.
Operation #
Una singola query o mutazione che può essere interpretata dal server GraphQL.
Operation name #
Nome arbitrario assegnabile a un’operazione per distinguerla dalle altre. In GraphiQL è obbligatorio assegnare un nome a ogni operazione se sono presenti più richieste nella stessa finestra.
Operation type #
Il tipo della chiamata GraphQL. Può essere query o mutation. Se omesso, il default è query.
Type system #
Collezione di tipi che caratterizza l’insieme di dati che possono essere convalidati, interrogati ed eseguiti sulla API.
Query #
Operazione di recupero di sola lettura per richiedere dati dalla API GraphQL.
Root object #
Punto di ingresso allo schema contenente tutti i servizi disponibili del tipo scelto per l’operazione.
Scalar #
Tipo che qualifica i dati risolti da un campo GraphQL, come stringhe o numeri. Rappresenta la “foglia” di una query. Maggiori informazioni: Scalar di sistema e Scalar Livebase.
Schema #
Lo schema è al centro dell’implementazione del server GraphQL e descrive le funzionalità disponibili per i client che si connettono alla API. Definisce il type system della API GraphQL.
Lo schema risiede sul server e viene usato per validare ed eseguire le chiamate del client. Un client può chiedere informazioni riguardo lo schema mediante introspezione.
Servizio #
Un campo del tipo Query o del tipo Mutation. Nel primo caso si tratta di un servizio di lettura, nel secondo caso di un servizio di scrittura. In un’operazione è possibile accorpare l’invocazione di più servizi dello stesso tipo dell’operazione (ad esempio, una query può richiedere più servizi di lettura).
Variabile #
Un valore che può essere passato come argomento a un’operazione. Le variabili possono essere usate per sostituire argomenti o essere passate come direttive (🇺🇸 Directives).

Reference #

Tipi di Issue #

Di seguito riportiamo la lista completa degli IssueType restituiti dal server.

Categoria: errori generici #

IssueTypeDescrizione
SERVER_ERRORErrore generico del server, o errore non gestito.
MALFORMED_REQUESTLa chiamata è valida rispetto allo schema GraphQL, ma non ha superato la validazione del server.

Esempi: il campo filter_exp dell’input <ClassName>PageOptions contiene un’espressione Livebase non valida / utilizzo di _id nel <ClassName>Draft di un servizio formAction.

DATA_TYPEUna mutation contiene un campo scalar valorizzato con una stringa non valida rispetto al tipo di dato.

Esempio: stringhe invalide per campi di tipo ID, date, time, datetime o real (_id: "ciao", date: "12345"…)

ENTITY_NOT_FOUNDUna mutation menziona l’ID di un oggetto inesistente sul database, mediante il campo _id (derivata dall’eccezione EntityNotFoundException dello SPI).
ENTITY_ATTRIBUTE_NOT_FOUNDIl campo filter_exp dell’input <ClassName>PageOptions contiene un’espressione Livebase che menziona un attributo inesistente sulla classe (derivata dall’eccezione EntityAttributeNotFoundException dello SPI).
SERVICE_HANDLER_ERRORErrore del server sollevato dal codice di un Plugin, invocato tramite una mutation di tipo formAction.
ENTITY_LOCK_EDITUna mutation menziona l’ID di un oggetto di cui un altro utente della Cloudlet ha acquisito un lock temporaneo sul database.

Categoria: Data validation #

IssueTypeDescrizione
ENTITY_UNIQUEUna mutation viola un vincolo di unicità sulla classe interessata dalla scrittura.
ENTITY_DOMAINUna mutation viola un vincolo definito con un Class warning, configurato per essere valutato in seguito a una persist.
ATTRIBUTE_REQUIREDUna mutation omette un campo relativo ad un attributo required.
ATTRIBUTE_RANGEUna mutation contiene un valore per un campo scalar al di fuori dell’intervallo di valori consentiti.

Esempio: valore fuori dal range per attributi di tipo integer, real, year, date, time o datetime.

ATTRIBUTE_REAL_DECIMAL_DIGITSUna mutation contiene un valore per un campo di tipo real con un numero di cifre decimali non valido.

Esempio: hourly_cost: "3,12345" (di default, le cifre decimali ammesse sono 2).

ATTRIBUTE_STRING_LENGTHUna mutation contiene un valore per un campo di tipo string che viola la lunghezza massima o minima consentita.

Esempio: phone_number: "+39 3331111222333444", con lunghezza minima 10.

ATTRIBUTE_FILE_SIZEUna mutation tenta di associare, a un campo di tipo file, un pending file che eccede la dimensione massima consentita del file per quell’attributo.
ATTRIBUTE_FILE_TYPEUna mutation tenta di associare, a un campo di tipo file, un pending file il cui tipo non è tra quelli consentiti per quell’attributo.
ROLE_CARDINALITYUna mutation supera la cardinalità minima o massima consentita per un ruolo coinvolto nella scrittura.

_Esempio: Il campo qualification_->adddell’inputEmployeeCreate contiene cinque riferimenti, ma su quel ruolo la cardinalità massima è 3._

Categoria: Veto action #

IssueTypeDescrizione
ENTITY_EDIT_VETOUna mutation di tipo create, update o save viola un vincolo definito con un Class warning, configurato per essere valutato in seguito a un evento Create o Edit.
ENTITY_DELETE_VETOUna mutation di tipo delete viola un vincolo definito con un Class warning, configurato per essere valutato in seguito a un evento Delete.

Categoria: Grant #

IssueTypeDescrizione
APPLICATION_ACCESS_FORBIDDENL’utente non dispone di un profilo abilitato per interagire con l’Application Schema / endpoint GraphQL a cui è stata rivolta la chiamata.

Esempio: Un utente con profilo “staff manager” si è rivolto all’endpoint /administration, per cui non è abilitato, invece di /staff_management.

ENTITY_GRANT_READL’utente non dispone di un profilo abilitato a leggere una classe tra quelle coinvolte nella query.
ENTITY_GRANT_CREATEL’utente non dispone di un profilo abilitato a creare oggetti di una classe in una mutation di tipo create o save.
ENTITY_GRANT_EDITL’utente non dispone di un profilo abilitato a modificare oggetti di una classe in una mutation di tipo update o save.
ENTITY_GRANT_DELETEL’utente non dispone di un profilo abilitato a eliminare oggetti di una classe in una mutation di tipo delete.
ATTRIBUTE_GRANT_READL’utente non dispone di un profilo abilitato a leggere uno o più attributi tra quelli coinvolti nella query.
ATTRIBUTE_GRANT_EDITL’utente non dispone di un profilo abilitato a scrivere uno o più attributi tra quelli coinvolti nella mutation.
ROLE_GRANT_READL’utente non dispone di un profilo abilitato a espandere uno o più ruoli tra quelli coinvolti nella query.
ROLE_GRANT_CREATEL’utente non dispone di un profilo abilitato a creare part / associare oggetti su un ruolo in una mutation di tipo create o save.
ROLE_GRANT_EDITL’utente non dispone di un profilo abilitato a modificare un ruolo in una mutation di tipo update o save.
ROLE_GRANT_DELETEL’utente non dispone di un profilo abilitato a eliminare part / rimuovere associazioni in una mutation di tipo delete.