vai al contenuto principale

Formulare chiamate GraphQL

Vediamo come autenticarsi alla API e come formulare ed eseguire query e mutazioni.

Nella pagina Lo schema GraphQL abbiamo dato le definizioni di query e mutation relativamente all’SDL. Concentriamoci ora invece sul linguaggio di query e vediamo nella pratica come formulare ed eseguire una chiamata GraphQL.

Endpoint generati #

Per ogni vista applicativa (Application Schema) definita nel modello, viene generato un endpoint GraphQL disponibile all’indirizzo:

https://<CloudletURL>/auth/api/graphql/<ApplicationName>

Ciascun endpoint fa riferimento a una API GraphQL distinta. Questi endpoint rimangono costanti durante il ciclo di vita dell’applicazione generata, cambiando eventualmente se si rigenera l’applicazione dopo aver sostituito o modificato il modello originale.

<CloudletURL> dipende dal dominio su cui la Cloudlet è dispiegata, dallo username dell’account proprietario e dal nome della Cloudlet. Ad esempio, data la vista Administration sulla Cloudlet Workforce dell’account JohnDoe, l’endpoint GraphQL corrispondente è:

https://hs4.fhoster.com/JohnDoe/Workforce/auth/api/graphql/Administration

Una lista di tutti gli endpoint generati per la Cloudlet è disponibile nel pannello API & Public URLs, raggiungibile dalla Dashboard cliccando sull’icona .

Comunicare con GraphQL #

In REST, il tipo di richiesta HTTP determina il tipo di operazione da eseguire. Le chiamate GraphQL sono invece sempre richieste di tipo POST a prescindere dall’operazione, in quanto contengono sempre un body codificato in JSON.

Il modo più semplice per interrogare la API è mediante GraphiQL. È anche possibile usare cURL o qualsiasi altra libreria HTTP. Con cURL, ad esempio, il payload JSON deve contenere una chiave "query", avente come valore la query scritta nel linguaggio GraphQL:

curl -i -X POST $MY_ENDPOINT \
 -H "Content-type: application/json" \
 -H "Authorization: Basic $MY_TOKEN" \
 -d '{"query": "'"$MY_QUERY"'"}'

Autenticazione #

Possono comunicare con GraphQL tutti gli utenti registrati della Cloudlet. Come per tutte le risorse della Cloudlet sotto /auth, l’autenticazione in GraphQL è di tipo Basic e richiede di fornire username e password. Nel caso dell’utente-amministratore della Cloudlet, queste credenziali coincidono con quelle dell’account Livebase proprietario della stessa.

A basso livello, ogni richiesta HTTP alla API deve includere l’header Authorization: Basic <MyToken>; il token è risultato dell’encoding Base64 della stringa <Username>:<Password>; se si utilizza cURL è necessario quindi specificare sempre l’header come mostrato sopra. Librerie a più alto livello, come Apollo, vanno configurate opportunamente fornendo il token di autenticazione.

Dettaglio: generare il token

Graphql Login Prompt

Se si utilizza il client GraphiQL, è sufficiente digitare le credenziali nel dialog che compare in popup al primo accesso; il client riutilizzerà queste credenziali per comunicare col server.

Formulare query e mutation #

I due tipi di operazione consentiti sono query e mutation (mutazioni). Rispetto a REST, una query è equivalente a una GET, mentre le mutazioni sono analoghe a POST, PATCH o DELETE a seconda dell’implementazione. Sia query che mutazioni condividono un formato simile, con alcune importanti differenze.

Questi sono i passi necessari per formulare una chiamata GraphQL:

  1. si specifica l’operation type, usando la keyword query o mutation a seconda dell’operazione da compiere. Se si vuole effettuare una query e non c’è ambiguità nel codice, è possibile omettere del tutto la keyword;

  2. si richiede uno speciale oggetto root (radice), ovvero il punto di ingresso nello schema contenente tutti i servizi disponibili di tipo query (o mutation);

  3. su di esso si specifica il nome del servizio da invocare (un servizio è un campo dell’oggetto root, che restituisce un oggetto contenente il risultato dell’invocazione del servizio);

  4. se il servizio li richiede, si specificano eventuali argomenti tra parentesi tonde di fianco al nome del servizio;

  5. si specificano, sull’oggetto di output, i campi che si vogliono ottenere dal server. Se si vuole ottenere un campo che è a sua volta un oggetto (con sotto-campi) è necessario espandere ciascun nodo di questo tipo fino a raggiungere una foglia, ovvero un campo di tipo scalar. Questo garantisce che la risposta abbia sempre una forma inequivocabile. L’ordine dei campi non è rilevante.

Nella sua forma più semplice, una query è strutturata in questo modo (le righe prefissate con # non vengono interpretate):

query {
 ServiceName {
  # insieme di campi da selezionare
 }
}

Tutti i nodi intermedi vanno espansi fino a richiede solo scalar (foglie):

query {
QueryServiceName {
scalarField1
scalarField2
nestedField {
scalarField3
}
}
}

Una mutation richiede sempre almeno un argomento che specifica quali dati si vogliono inviare al server:

mutation {
 MutationServiceName(mutationArgument: "Hello world!") {
  # insieme di campi da selezionare
 }
}

Anche i servizi di tipo mutation restituiscono oggetti in output, come le query, su cui vanno selezionati uno o più scalar:

mutation {
MutationServiceName(mutationArgument: "Hello world!") {
scalarField1
scalarField2
nestedField {
scalarField3
}
}
}

L’input può essere anche un oggetto complesso:

mutation {
MutationServiceName(data: {first_name: "John", last_name: "Doe", age: 42}) {
scalarField1
scalarField2
nestedField {
scalarField3
}
}
}

Lavorare con variabili #

Le variabili possono rendere le chiamate GraphQL più dinamiche e potenti, e aiutano a ridurre la complessità delle mutation quando si passano oggetti di input complessi.

Ecco una query d’esempio con una singola variabile:

query($number_of_employees: Int!) {
  Employee___getPage(
    options: {next: $number_of_employees}
  ) {
    items {
      full_name
    }
  }
}
{
  "number_of_employees": 30
}

Le variabili vanno definite fuori dalla query in un oggetto JSON valido, che può includere un numero arbitrario di variabili da usare nella chiamata. Per usare le variabili così dichiarate nell’operazione GraphQL è necessario:

  1. Passare le variabili come argomenti all’operazione:

    query($number_of_employees: Int!) {
    

    L’argomento è una coppia chiave-valore, dove la chiave è il nome prefissato da $, e il valore è il tipo della variabile (ad esempio Int). È possibile aggiungere un ! per marcare l’argomento come obbligatorio. Il tipo va sempre incluso per rispettare la tipizzazione statica di GraphQL, così che il server possa validare le variabili passate rispetto allo schema.

  2. Utilizzare le variabili nell’operazione:

    Employee___getPage(options: {next: $number_of_employees}) {
    

    In questo esempio, sostituiamo la variabile con il numero di impiegati da recuperare.

Quando si invia una chiamata con variabili, è compito del client includere la struttura dati che le contiene nella POST HTTP, insieme alla query GraphQL. Con cURL, ad esempio, le variabili vanno incluse nel payload JSON alla chiave "variables":

curl -i -X POST $MY_ENDPOINT \
 -H "Content-type: application/json" \
 -H "Authorization: Basic $MY_TOKEN" \
 -d '{"query": "query($number_of_employees: ID!) { Employee___get(_id:$number_of_employees) { full_name } }", "variables": {"id": "10101"}}'

Questo processo rende dinamico l’argomento della query. Possiamo semplicemente modificare il valore nell’oggetto variables e mantenere inalterato il resto della query, favorendone il riuso.

Maggiori informazioni sulle variabili sono disponibili qui: 🇺🇸 Variables.

Esempio di query #

Vediamo una query tipica, in cui recuperiamo informazioni da un oggetto Employee usando il servizio di lettura singola get.

{
  Employee___get(_id: "12345") {
    full_name
    age
    team {
      name
    }
    assignments {
      totalCount
      items {
        start_date
        end_date
        project_ {
          director
        }
      }
    }
  }
}

Esaminando la query riga per riga:

  1. utilizziamo il servizio Query.Employee___get, che consente di ottenere un oggetto Employee. Questo servizio richiede l’argomento _id, uno scalar di tipo ID;
  2. dell’impiegato ritornato, chiediamo il suo nome completo (full_name) e la sua età (age), due scalar rispettivamente di tipo string e number;
  3. chiediamo inoltre informazioni su due classi satellite: i campi team e assignments sono rispettivamente di tipo Team e Project_assignment, pertanto espandiamo questi oggetti e selezioniamo degli scalar;
  4. per team (ruolo a uno) chiediamo il nome del team di cui fa parte l’impiegato (name);
  5. per assignments (ruolo a molti) chiediamo il numero di incarichi dell’impiegato (totalCount) e usiamo il campo items dell’oggetto Project_assignmentPage per selezionare quali campi mostrare per ciascun incarico;
  6. scegliamo gli scalar start_date ed end_date, di tipo date, ovvero data di inizio e fine dell’incarico, e mostriamo il direttore del progetto associato all’incarico;
  7. infine, mostriamo il nome del direttore del progetto associato all’incarico operando una selezione sull’oggetto di tipo Project associato all’incarico sul campo project_.

Esempio di mutation #

Le mutation necessitano spesso di informazioni che possono essere recuperate solo eseguendo prima una query. In questo esempio usiamo una query per recuperare l’ID di un oggetto Employee esistente e una mutation per aggiornare i suoi dati, usando i servizi di lettura paginata (getPage) e di update.

query FindEmployeeID {
  Employee___getPage(options: {filter: {team_name___eq: "Cool Coders"}}) {
    items {
      _id
      full_name
      address {
        _id
        city
      }
    }
  }
}

mutation ChangeEmployeeCity {
  Employee___update(
    data: {
      _id: "10101"
      supervisor: {
        set: "10102"
      }
      address: {
        update: {
          _id: "222000"
          city: "Milan"
        }
      }
    }
  ) {
    _id
    full_name
    address {
      city
    }
    supervisor {
      _id
      full_name
    }
  }
}

Esaminiamo l’esempio: il compito è di aggiornare la città di residenza di un impiegato esistente appena trasferitosi, e di assegnargli un supervisore facente parte del suo stesso team. Di entrambi gli impiegati, naturalmente, conosciamo nome e cognome.

  1. consultando la documentazione generata per il nostro schema, troviamo il servizio Mutation.Employee___update, che necessita dell’input EmployeeUpdate, un tipo che ha sua volta ha il campo _id obbligatorio (marcato con il simbolo !);
  2. dobbiamo quindi recuperare l’ID dell’impiegato, ma abbiamo bisogno anche dell’ID del suo supervisore, perché notiamo che il campo supervisor di EmployeeUpdate è un input di tipo EmployeeRoleRef (struttura di update per ruolo a 1 verso Team sulla classe Employee), con a sua volta il campo set, di tipo ID, per indicare l’impiegato da associare;
  3. usiamo il servizio di lettura paginata Query.Employee___getPage per recuperare i due impiegati, entrambi oggetti della classe Employee;
  4. siccome sappiamo che entrambi gli impiegati fanno parte dello stesso team, possiamo restringere i risultati ritornati dalla get paginata. Includiamo quindi l’argomento options valorizzandolo con l’input EmployeePageOptions; qui specifichiamo il campo filter, scegliendo come valore team_name___eq e scrivendo il nome del team, che sappiamo essere "Cool Coders";
  5. dall’oggetto di tipo EmployeePage ritornato, usiamo il campo items per selezionare quali campi mostrare per ciascun impiegato. Scegliamo di mostrare full_name, e oltre a _id; abbiamo anche bisogno dell’ID dell’oggetto part Address.

Eseguendo la query otteniamo la seguente risposta:

{
  "data": {
    "Employee___getPage": {
      "items": [
        {
          "_id": "10101",
          "full_name": "Frank Jaeger",
          "address": {
            "_id": "222000",
            "city": "Rome"
          }
        },
        {
          "_id": "10101",
          "full_name": "Levi Smith",
          "address": {
            "_id": "222754",
            "city": "Milan"
          }
        }
        // ... altri impiegati dello stesso team
      ]
    }
  }
}

Noti gli ID, passiamo ora alla mutation:

  1. come accennato prima, vogliamo usare il servizio Mutation.Employee___update: popoliamo l’argomento data con un oggetto di tipo EmployeeUpdate, inserendo sul campo obbligatorio _id l’ID del nostro impiegato, ovvero "Frank Jaeger";
  2. aggiungiamo il campo supervisor come detto sopra, specificando il campo set a cui facciamo corrispondere l’ID del supervisore, ovvero "Levi Smith";
  3. per cambiare la città dobbiamo modificare l’oggetto Address (part a 1), che corrisponde in EmployeeUpdate al campo address di tipo AddressRoleObject (struttura di update per part a 1 verso Address sulla classe Employee), con a sua volta il campo update, di tipo AddressUpdate;
  4. anche AddressUpdate richiede l’ID dell’oggetto; lo forniamo insieme al campo city così da aggiornare la città da "Rome" a "Milan";
  5. infine, specifichiamo quali campi vogliamo ottenere dal server in output al termine dell’operazione di update. Il tipo ritornato da Employee___update è ancora Employee e chiediamo di mostrare la nuova città e il nome del supervisore, che ci aspettiamo essere rispettivamente "Milan" e "Levi Smith".

Eseguendo la mutation otteniamo questa risposta:

{
  "data": {
    "Employee___update": {
      "_id": "10101",
      "full_name": "Frank Jaeger",
      "address": {
        "city": "Milan"
      },
      "supervisor": {
        "_id": "10102",
        "full_name": "Levi Smith"
      }
    }
  }
}

In questo esempio abbiamo lavorato con una selezione di campi dello schema: avremmo potuto chiedere molte altre informazioni tra quelle disponibili per i tipi Employee e Address, ma grazie a GraphQL ci siamo potuti concentrare solo su quelle di nostro interesse in questo contesto. Abbiamo inoltre eseguito due sole chiamate al server, risparmiando tempo e banda.

Un’ultima nota: quando si passano più campi in un oggetto di input, la sintassi può diventare ingombrante. In questi casi, può essere utile spostare i campi in una variabile. Ecco come avremmo potuto riscrivere la mutazione originale usando una variabile:

mutation ChangeEmployeeCity($myVar: EmployeeUpdate!) {
  Employee___update(data: $myVar) {
    _id
    full_name
    address {
      city
    }
    supervisor {
      _id
      full_name
    }
  }
}
{
  "myVar": {
    "_id": "10101",
    "supervisor": {
      "set": "10102"
    },
    "address": {
      "update": {
        "_id": "222000",
        "city": "Milan"
      }
    }
  }
}

Ulteriori letture #

Oltre alle variabili, GraphQL offre molti altri costrutti utili per formulare chiamate che sono supportate dalla nostra implementazione, ma che vanno oltre lo scopo di questa guida. Per eventuali approfondimenti, puoi trovare maggiori informazioni sulla documentazione ufficiale (graphql.org):