a

GraphQL-palvelin

Tälläkin kurssilla moneen kertaan käytetty REST on ollut pitkään vallitseva tapa toteuttaa palvelimen selaimelle tarjoama rajapinta ja yleensäkin verkossa toimivien sovellusten välinen integraatio.

RESTin rinnalle selaimessa ja mobiililaitteessa toimivan logiikan ja palvelimien väliseen kommunikointiin on viime vuosina noussut alunperin Facebookin kehittämä GraphQL.

GraphQL on filosofialtaan todella erilainen RESTiin verrattuna. REST on resurssipohjainen. Jokaisella resurssilla, esim. käyttäjällä on oma sen identifioiva osoite, esim. /users/10 ja kaikki resursseille tehtävät operaatiot toteutetaan tekemällä URL:ille kohdistuvia pyyntöjä, joiden toiminta määrittyy käytetyn HTTP-metodin avulla.

RESTin resurssiperustaisuus toimii hyvin useissa tapauksissa, joissain tapauksissa se voi kuitenkin olla hieman kankea.

Oletetaan että blogilistasovelluksemme sisältäisi somemaista toiminnallisuutta ja haluaisimme esim. näyttää sovelluksessa listan, joka sisältää kaikkien seuraamiemme (follow) käyttäjien blogeja kommentoineiden käyttäjien lisäämien blogien nimet.

Jos palvelin toteuttaisi REST API:n, joutuisimme todennäköisesti tekemään monia HTTP-pyyntöjä selaimen koodista, ennen kuin saisimme muodostettua halutun datan. Pyyntöjen vastauksena tulisi myös paljon ylimääräistä dataa ja halutun datan keräävä selaimen koodi olisi todennäköisesti kohtuullisen monimutkainen.

Jos kyseessä olisi usein käytetty toiminnallisuus, voitaisiin sitä varten toteuttaa oma REST-endpoint. Jos vastaavia skenaarioita olisi paljon, esim. kymmeniä, tulisi erittäin työlääksi toteuttaa kaikille toiminnallisuuksille oma REST-endpoint.

GraphQL:n avulla toteutettu rajapinta sopii tämänkaltaisiin tilanteisiin hyvin.

GraphQL:ssä periaatteena on, että selaimen koodi muodostaa kyselyn, joka kuvailee halutun datan ja lähettää sen API:lle HTTP POST -pyynnöllä. Toisin kuin REST:issä, GraphQL:ssä kaikki kyselyt kohdistetaan samaan osoitteeseen ja ovat POST-tyyppisiä.

Edellä kuvatun skenaarion data saataisiin haettua (suurinpiirtein) seuraavan kaltaisella kyselyllä:

query FetchBlogsQuery {
  user(username: "mluukkai") {
    followedUsers {
      blogs {
        comments {
          user {
            blogs {
              title
            }
          }
        }
      }
    }
  }
}

Palvelimen vastaus pyyntöön olisi suunnilleen seuraavanlainen JSON-olio:

{
  "data": {
    "followedUsers": [
      {
        "blogs": [
          {
            "comments": [
              {
                "user": {
                  "blogs": [
                    {
                      "title": "Goto considered harmful"
                    },
                    {
                      "title": "End to End Testing with Cypress is most enjoyable"
                    },
                    {
                      "title": "Navigating your transition to GraphQL"
                    },
                    {
                      "title": "From REST to GraphQL"
                    }
                  ]
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

Sovelluslogiikka säilyy yksinkertaisena ja selaimen koodi saa täsmälleen haluamansa datan yksittäisellä kyselyllä.

Skeema ja kyselyt

Tutustutaan GraphQL:n peruskäsitteistöön toteuttamalla GraphQL-versio osien 2 ja 3 puhelinluettelosovelluksesta.

Jokaisen GraphQL-sovelluksen ytimessä on skeema, joka määrittelee minkä muotoista dataa sovelluksessa vaihdetaan clientin ja palvelimen välillä. Puhelinluettelon alustava skeema on seuraavassa:

type Person {
  name: String!
  phone: String
  street: String!
  city: String!
  id: ID! 
}

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

Skeema määrittelee kaksi tyyppiä. Tyypeistä ensimmäinen Person määrittelee, että henkilöillä on viisi kenttää. Kentistä neljä on tyyppiä String, joka on yksi GraphQL:n määrittelemistä valmiista tyypeistä. String-arvoisista kentistä muilla paitsi puhelinnumerolla (phone) on oltava arvo, tämä on merkitty skeemaan huutomerkillä. Kentän id tyyppi on ID. Arvoltaan ID-tyyppiset kentät ovat merkkijonoja, mutta GraphQL takaa, että ne ovat uniikkeja.

Toinen skeeman määrittelemistä tyypeistä on Query. Käytännössä jokaisessa GraphQL-skeemassa määritellään tyyppi Query, joka kertoo mitä kyselyjä API:iin voidaan tehdä.

Puhelinluettelo määrittelee kolme erilaista kyselyä ja personCount palauttaa kokonaisluvun. allPersons palauttaa listan Person-tyyppisiä olioita. findPerson saa merkkijonomuotoisen parametrin ja palauttaa Person-olion.

Queryjen paluuarvon ja parametrin määrittelyssä on jälleen käytetty välillä huutomerkkiä merkkaamaan pakollisuutta, eli personCount palauttaa varmasti kokonaisluvun. Kyselylle findPerson on pakko antaa parametriksi merkkijono. Kysely palauttaa Person-olion tai arvon null. allPersons palauttaa listan Person-olioita, listalla ei ole null-arvoja.

Skeema siis määrittelee mitä kyselyjä client pystyy palvelimelta tekemään, minkälaisia parametreja kyselyillä voi olla sekä sen, minkä muotoista kyselyjen palauttama data on.

Kyselyistä yksinkertaisin personCount näyttää seuraavalta

query {
  personCount
}

Olettaen että sovellukseen olisi talletettu kolmen henkilön tiedot, vastaus kyselyyn näyttäisi seuraavalta:

{
  "data": {
    "personCount": 3
  }
}

Kaikkien henkilöiden tiedot hakeva allPersons on hieman monimutkaisempi. Koska kysely palauttaa listan Person-olioita, on kyselyn yhteydessä määriteltävä mitkä kentät kyselyn halutaan palauttavan:

query {
  allPersons {
    name
    phone
  }
}

Vastaus voisi näyttää seuraavalta:

{
  "data": {
    "allPersons": [
      {
        "name": "Arto Hellas",
        "phone": "040-123543"
      },
      {
        "name": "Matti Luukkainen",
        "phone": "040-432342"
      },
      {
        "name": "Venla Ruuska",
        "phone": null
      }
    ]
  }
}

Kysely voi määritellä palautettavaksi mitkä tahansa skeemassa mainitut kentät, esim. seuraava olisi myös mahdollista:

query {
  allPersons{
    name
    city
    street
  }
}

Vielä esimerkki parametria edellyttävästä kyselystä, joka hakee yksittäisen henkilön tiedot palauttavasta kyselystä.

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
    id
  }
}

Kyselyn parametri siis annetaan suluissa, ja sen jälkeen määritellään aaltosuluissa paluuarvona tulevan olion halutut kentät.

Vastaus on muotoa:

{
  "data": {
    "findPerson": {
      "phone": "040-123543",
      "city": "Espoo",
      "street": "Tapiolankatu 5 A"
      "id": "3d594650-3436-11e9-bc57-8b80ba54c431"
    }
  }
}

Kyselyn paluuarvoa ei oltu merkitty pakolliseksi, eli jos etsitään tuntematonta henkilöä

query {
  findPerson(name: "Donald Trump") {
    phone 
  }
}

vastaus on null

{
  "data": {
    "findPerson": null
  }
}

Kuten huomaamme, GraphQL-kyselyn ja siihen vastauksena tulevan JSON:in muodoilla on vahva yhteys, voidaan ajatella että kysely kuvailee sen minkälaista dataa vastauksena halutaan. Ero REST:issä tehtäviin pyyntöihin on suuri, REST:iä käytettäessä pyynnon url ja sen tyyppi (GET, POST, PUT, DELETE) ei kerro mitään palautettavan datan muodosta.

GraphQL:n skeema kuvaa ainoastaan palvelimen ja sitä käyttävien clientien välillä liikkuvan tiedon muodon. Tieto voi olla organisoituna ja talletettuna palvelimeen ihan missä muodossa tahansa.

Nimestään huolimatta GraphQL:llä ei itseasiassa ole mitään tekemistä tietokantojen kanssa, se ei ota mitään kantaa siihen miten data on tallennettu. GraphQL-periaattella toimivan API:n käyttämä data voi siis olla talletettu relaatiotietokantaan, dokumenttitietokantaan tai muille palvelimille, joita GraphQL-palvelin käyttää vaikkapa REST:in välityksellä.

Apollo server

Toteutetaan nyt GraphQL-palvelin tämän hetken johtavaa kirjastoa Apollo-serveriä käyttäen.

Luodaan uusi npm-projekti komennolla npm init ja asennetaan tarvittavat riippuvuudet

npm install --save apollo-server graphql

Alustava toteutus on seuraavassa

const { ApolloServer, gql } = require('apollo-server')

let persons = [
  {
    name: "Arto Hellas",
    phone: "040-123543",
    street: "Tapiolankatu 5 A",
    city: "Espoo",
    id: "3d594650-3436-11e9-bc57-8b80ba54c431"
  },
  {
    name: "Matti Luukkainen",
    phone: "040-432342",
    street: "Malminkaari 10 A",
    city: "Helsinki",
    id: '3d599470-3436-11e9-bc57-8b80ba54c431'
  },
  {
    name: "Venla Ruuska",
    street: "Nallemäentie 22 C",
    city: "Helsinki",
    id: '3d599471-3436-11e9-bc57-8b80ba54c431'
  },
]

const typeDefs = gql`
  type Person {
    name: String!
    phone: String
    street: String!
    city: String! 
    id: ID!
  }

  type Query {
    personCount: Int!
    allPersons: [Person!]!
    findPerson(name: String!): Person
  }
`

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) =>
      persons.find(p => p.name === args.name)
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`)
})

Toteutuksen ytimessä on ApolloServer, joka saa kaksi parametria

const server = new ApolloServer({
  typeDefs,
  resolvers,
})

parametreista ensimmäinen typeDefs sisältää sovelluksen käyttämän GraphQL-skeeman.

Toinen parametri on olio, joka sisältää palvelimen resolverit, eli käytännössä koodin, joka määrittelee miten GraphQL-kyselyihin vastataan.

Resolverien koodi on seuraavassa:

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) =>
      persons.find(p => p.name === args.name)
  }
}

kuten huomataan, vastaavat resolverit rakenteeltaan skeemassa määriteltyjä kyselyitä:

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

eli jokaista skeemassa määriteltyä kyselyä kohti on määritelty oma kentän Query alle tuleva kenttänsä.

Kyselyn

query {
  personCount
}

resolveri on funktio

() => persons.length

eli kyselyyn palautetaan vastauksena henkilöt tallentavan taulukon persons pituus.

Kaikki luettelossa olevat henkilöt hakevan kyselyn

query {
  allPersons {
    name
  }
}

resolveri on funktio, joka palauttaa kaikki taulukon persons oliot

() => persons

GraphQL-playground

Kun Apollo-serveriä suoritetaan sovelluskehitysmoodissa, käynnistää se osoitteeseen http://localhost:4000/graphql sovelluskehittäjälle erittäin hyödyllisen GraphQL-playground-näkymän, joka avulla on mahdollista tehdä kyselyjä palvelimelle.

Kokeillaan

1

Playgroundin kanssa pitää olla välillä tarkkana. Jos kysely on syntaktisesti virheellinen, on virheilmoitus aika huomaamaton ja kyselyn suoritusnappia painamalla ei tapahdu mitään:

2

Edellisen kyselyn tulos näkyy edelleen playgroundin oikeassa osassa kyselyn virheellisyydestä huolimatta.

Osoittamalla oikeaa kohtaa virheelliseltä riviltä saa virheilmoituksen näkyville

3

Jos Playground vaikuttaa olevan jumissa, niin sivun päivittäminen yleensä auttaa.

Klikkaamalla oikean reunan tekstiä DOCS näyttää Playground palvelimen GraphQL-skeeman.

4e

Resolverin parametrit

Yksittäisen henkilön hakevan kyselyn

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
  }
}

resolveri on funktio, joka poikkeaa kahdesta aiemmasta resolverista siinä että se saa kaksi parametria:

(root, args) => persons.find(p => p.name === args.name)

Parametreista toinen args sisältää kyselyn parametrit. Resolveri siis palauttaa taulukosta persons henkilön, jonka nimi on sama kuin args.name arvo. Ensimmäisenä olevaa parametria root resolveri ei tarvitse.

Itse asiassa kaikki resolverifunktiot saavat neljä parametria. Javascriptissa parametrit voidaan kuitenkin jättää määrittelemättä, jos niitä ei tarvita. Tulemme käyttämään resolverien ensimmäistä ja kolmatta parametria vielä myöhemmin tässä osassa.

Oletusarvoinen resolveri

Kun teemme kyselyn, esim

query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
  }
}

osaa palvelin liittää vastaukseen täsmälleen ne kentät, joita kysely pyytää. Miten tämä tapahtuu?

GraphQL-palvelimen tulee määritellä resolverit jokaiselle skeemassa määritellyn tyypin kentälle. Olemme nyt määritelleet resolverit ainoastaan tyypin Query kentille, eli kaikille sovelluksen tarjoamille kyselyille.

Koska skeemassa olevan tyypin Person kentille ei ole määritelty resolvereita, Apollo on määritellyt niille oletusarvoisen resolverin, joka toimii samaan tapaan kuin seuraavassa itse määritelty resolveri:

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) => persons.find(p => p.name === args.name)
  },
  Person: {    name: (root) => root.name,    phone: (root) => root.phone,    street: (root) => root.street,    city: (root) => root.city,    id: (root) => root.id  }}

Oletusarvoinen resolveri siis palauttaa olion vastaavan kentän arvon. Itse olioon se pääsee käsiksi resolverin ensimmäisen parametrin root kautta.

Jos oletusarvoisen resolverin toiminnallisuus riittää, ei omaa resolveria tarvitse määritellä. On myös mahdollista määritellä ainoastaan joillekin tyypin yksittäiselle kentille oma resolverinsa ja antaa oletusarvoisen resolverin hoitaa muut kentät.

Voisimme esimerkiksi määritellä, että kaikkien henkilöiden osoitteeksi tulisi Manhattan New York kovakoodaamalla seuraavat tyypin Person kenttien street ja city resolvereiksi:

Person: {
  street: (root) => "Manhattan",
  city: (root) => "New York"
}

Olion sisällä olio

Muutetaan skeemaa hiukan

type Address {  street: String!  city: String! }
type Person {
  name: String!
  phone: String
  address: Address!  id: ID!
}

type Query {
  personCount: Int!
  allPersons: [Person!]!
  findPerson(name: String!): Person
}

eli henkilöllä on nyt kenttä, jonka tyyppi on Address, joka koostuu kadusta ja kaupungista.

Osoitetta tarvitsevat kyselyt muuttuvat muotoon

query {
  findPerson(name: "Arto Hellas") {
    phone 
    address {
      city 
      street
    }
  }
}

vastauksena on henkilö-olio, joka sisältää osoite-olion:

{
  "data": {
    "findPerson": {
      "phone": "040-123543",
      "address":  {
        "city": "Espoo",
        "street": "Tapiolankatu 5 A"
      }
    }
  }
}

Talletetaan henkilöt palvelimella edelleen samassa muodossa kuin aiemmin.

let persons = [
  {
    name: "Arto Hellas",
    phone: "040-123543",
    street: "Tapiolankatu 5 A",
    city: "Espoo",
    id: "3d594650-3436-11e9-bc57-8b80ba54c431"
  },
  // ...
]

Nyt siis palvelimen tallettamat henkilö-oliot eivät ole muodoltaan täysin samanlaisia kuin GraphQL-skeeman määrittelemät tyypin Person -oliot.

Toisin kuin tyypille Person ei tyypille Address ole määritelty id-kenttää, sillä osoitteita ei ole talletettu palvelimella omaan tietorakenteeseensa.

Koska taulukkoon talletetuilla olioilla ei ole kenttää address oletusarvoinen resolveri ei enää riitä. Lisätään resolveri tyypin Person kentälle address:

const resolvers = {
  Query: {
    personCount: () => persons.length,
    allPersons: () => persons,
    findPerson: (root, args) =>
      persons.find(p => p.name === args.name)
  },
  Person: {    address: (root) => {      return {         street: root.street,        city: root.city      }    }  }}

Eli aina palautettaessa Person-oliota, palautetaan niiden kentät name, phone sekä id käyttäen oletusarvoista resolveria, kenttä address muodostetaan itse määritellyn resolverin avulla. Resolverifunktion parametrina root on käsittelyssä oleva henkilö-olio, eli osoitteen katu ja kaupunki saadaan sen kentistä.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-1.

Mutaatio

Laajennetaan sovellusta siten, että puhelinluetteloon on mahdollista lisätä uusia henkilöitä. GraphQL:ssä kaikki muutoksen aiheuttavat operaatiot tehdään mutaatioiden avulla. Mutaatiot määritellään skeemaan tyypin Mutation avaimina.

Käyttäjän lisäävä mutaation skeema näyttää seuraavalta

type Mutation {
  addPerson(
    name: String!
    phone: String
    street: String!
    city: String!
  ): Person
}

Mutaatio siis saa parametreina käyttäjän tiedot. Parametreista phone on ainoa, jolle ei ole pakko asettaa arvoa. Mutaatioilla on parametrien lisäksi paluuarvo. Paluuarvo on nyt tyyppiä Person, ideana on palauttaa operaation onnistuessa lisätyn henkilön tiedot ja muussa tapauksessa null. Mutaatiossa ei anneta parametrina kentän id arvoa, sen luominen on parempi jättää palvelimen vastuulle.

Myös mutaatioita varten on määriteltävä resolveri:

const { v1: uuid } = require('uuid')

// ...

const resolvers = {
  // ...
  Mutation: {
    addPerson: (root, args) => {
      const person = { ...args, id: uuid() }
      persons = persons.concat(person)
      return person
    }
  }
}

Mutaatio siis lisää parametreina args saamansa olion taulukkoon persons ja palauttaa lisätyn olion.

Kentälle id saadaan luotua uniikki tunniste kirjaston uuid avulla.

Uusi henkilö voidaan lisätä seuraavalla mutaatiolla

mutation {
  addPerson(
    name: "Pekka Mikkola"
    phone: "045-2374321"
    street: "Vilppulantie 25"
    city: "Helsinki"
  ) {
    name
    phone
    address{
      city
      street
    }
    id
  }
}

Kannattaa huomata, että lisättävä henkilö talletetaan taulukkoon persons muodossa

{
  name: "Pekka Mikkola",
  phone: "045-2374321",
  street: "Vilppulantie 25",
  city: "Helsinki",
  id: "2b24e0b0-343c-11e9-8c2a-cb57c2bf804f"
}

Vastaus mutaatioon on kuitenkin

{
  "data": {
    "addPerson": {
      "name": "Pekka Mikkola",
      "phone": "045-2374321",
      "address": {
        "city": "Helsinki",
        "street": "Vilppulantie 25"
      },
      "id": "2b24e0b0-343c-11e9-8c2a-cb57c2bf804f"
    }
  }
}

eli tyypin Person kentän address resolveri muotoilee vastauksena palautettavan olion oikean muotoiseksi.

Virheiden käsittely

Jos yritämme luoda uuden henkilön, mutta parametrit eivät vastaa skeemassa määriteltyä (esim. katuosoite puuttuu), antaa palvelin virheilmoituksen:

5

GraphQL:n validoinnin avulla pystytään siis jo automaattisesti hoitamaan osa virheenkäsittelyä.

Kaikkea GraphQL ei kuitenkaan pysty hoitamaan automaattisesti. Esimerkiksi tarkemmat säännöt mutaatiolla lisättävän datan kenttien muodolle on lisättävä itse. Niistä aiheutuvat virheet tulee hoitaa GraphQL:n poikkeuskäsittelymekanismilla.

Estetään saman nimen lisääminen puhelinluetteloon useampaan kertaan:

const { ApolloServer, UserInputError, gql } = require('apollo-server')
// ...

const resolvers = {
  // ..
  Mutation: {
    addPerson: (root, args) => {
      if (persons.find(p => p.name === args.name)) {        throw new UserInputError('Name must be unique', {          invalidArgs: args.name,        })      }
      const person = { ...args, id: uuid() }
      persons = persons.concat(person)
      return person
    }
  }
}

Eli jos lisättävä nimi on jo luettelossa heitetään poikkeus UserInputError.

6

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-2.

Enum

Tehdään sovellukseen vielä sellainen lisäys, että kaikki henkilöt palauttavaa kyselyä voidaan säädellä parametrilla phone siten, että kysely palauttaa vain henkilöt, joilla on puhelinnumero

query {
  allPersons(phone: YES) {
    name
    phone 
  }
}

tai henkilöt, joilla ei ole puhelinnumeroa

query {
  allPersons(phone: NO) {
    name
  }
}

Skeema laajenee seuraavasti:

enum YesNo {  YES  NO}
type Query {
  personCount: Int!
  allPersons(phone: YesNo): [Person!]!  findPerson(name: String!): Person
}

Tyyppi YesNo on GraphQL:n enum, eli lueteltu tyyppi, jolla on kaksi mahdollista arvoa, YES ja NO. Kyselyssä allPersons parametri phone on tyypiltään YesNo, mutta sen arvo ei ole pakollinen.

Resolveri muuttuu seuraavasti

Query: {
  personCount: () => persons.length,
  allPersons: (root, args) => {    if (!args.phone) {      return persons    }    const byPhone = (person) =>      args.phone === 'YES' ? person.phone : !person.phone    return persons.filter(byPhone)  },  findPerson: (root, args) =>
    persons.find(p => p.name === args.name)
},

Numeron muutos

Tehdään sovellukseen myös mutaatio, joka mahdollistaa henkilön puhelinnumeron muuttamisen. Mutaation skeema näyttää seuraavalta

type Mutation {
  addPerson(
    name: String!
    phone: String
    street: String!
    city: String!
  ): Person
  editNumber(    name: String!    phone: String!  ): Person}

ja sen toteuttaa seuraava resolveri:

Mutation: {
  // ...
  editNumber: (root, args) => {
    const person = persons.find(p => p.name === args.name)
    if (!person) {
      return null
    }

    const updatedPerson = { ...person, phone: args.phone }
    persons = persons.map(p => p.name === args.name ? updatedPerson : p)
    return updatedPerson
  }   
}

Mutaatio hakee siis hakee kentän name perusteella henkilön, jonka numero päivitetään.

Sovelluksen tämänhetkinen koodi on kokonaisuudessaan githubissa, branchissa part8-3.

Lisää kyselyistä

GraphQL:ssä on yhteen kyselyyn mahdollista yhdistää monia tyypin Query kenttiä, eli "yksittäisiä kyselyitä". Esim. seuraava kysely palauttaa puhelinluettelon henkilöiden lukumäärän sekä nimet:

query {
  personCount
  allPersons {
    name
  }
}

Vastaus näyttää seuraavalta

{
  "data": {
    "personCount": 3,
    "allPersons": [
      {
        "name": "Arto Hellas"
      },
      {
        "name": "Matti Luukkainen"
      },
      {
        "name": "Venla Ruuska"
      }
    ]
  }
}

Yhdistetty kysely voi myös viitata useampaan kertaan samaan kyselyyn. Tällöin erillisille kyselyille on kuitenkin annettava vaihtoehtoiset nimet kaksoispistesyntaksilla

query {
  havePhone: allPersons(phone: YES){
    name
  }
  phoneless: allPersons(phone: NO){
    name
  }
}

Vastaus on muotoa

{
  "data": {
    "havePhone": [
      {
        "name": "Arto Hellas"
      },
      {
        "name": "Matti Luukkainen"
      }
    ],
    "phoneless": [
      {
        "name": "Venla Ruuska"
      }
    ]
  }
}

Joissain tilanteissa voi myös olla hyötyä nimetä kyselyt. Näin on erityisesti tilanteissa, joissa kyselyillä tai mutaatiolla on parametreja. Tutustumme parametreihin pian.

Jos kyselyitä on useita, pyytää Playground valitsemaan mikä niistä suoritetaan:

7