Seznam GraphQL – Jak používat pole ve schématu GraphQL (GraphQL Modifiers)

Úvod

V REST API je často běžnou praxí vracet odpověď JSON s polem objektů. V GraphQL bychom chtěli tento vzor také následovat. V tomto článku si projdeme modifikátory, speciální skupinu typů, která nám umožňuje upravit výchozí chování jiných typů. V GraphQL se zabýváme různými skupinami typů. Tyto skupiny jsou následující:

  • Skaláry a vlastní skaláry
  • Výčty
  • Objekty a typy vstupních objektů
  • Abstraktní typy – Rozhraní a typy sjednocení
  • Modifikátory

Může být užitečné nejprve projít výše uvedené články. Poté, co získáte základní znalosti o dalších typech, jako jsou skaláry a typy objektů, můžete přejít k modifikátorům. Dále můžeme začít pracovat na nastavení projektu, abychom mohli otestovat naše dotazy. Předpokládáme, že verze npm, git a Node.js vyšší než 8 jsou již na vašem počítači nainstalovány. Nyní můžete tento příkaz provést ve vašem shellu

git clone [email protected]:atherosai/graphql-gateway-apollo-express.git

nainstalovat závislosti s

npm i

a spusťte vývoj serveru

npm run dev

Poté se můžete přesunout na GraphQL Playground a spustit dotazy dostupné v tomto článku. V modelovém projektu používáme in-memory databáze s falešnými daty pro provádění našich dotazů.

Schéma modelu

Podívejme se nejprve na toto modelové schéma, které bylo vytištěno pomocí funkce printSchema z utilit graphql-js. Modelové schéma v úložišti je vytvořeno s přístupem založeným na třídách pomocí knihovny graphql-js. Často je mnohem přehlednější zobrazit celé schéma napsané v jazyce definice schématu (SDL). Již nějakou dobu je SDL součástí specifikace a často se používá k sestavení samotného schématu pomocí nástroje sestavení schématu nebo knihovny nazvané graphql-tools.

"""Input payload for creating user"""
input CreateUserInput {
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
  role: UserRoleEnum = ACCOUNTANT
}

"""User type definition"""
type CreateUserPayload {
  user: User!
}

"""User type definition"""
type CreateUsersPayload {
  users: [User]
}

"""An ISO-8601 encoded UTC date string."""
scalar DateTime

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload
  createUsers(input: [CreateUserInput!]!): CreateUsersPayload
}

type Query {
  users(role: UserRoleEnum): [User!]!
}

"""User type definition"""
type User {
  id: ID!
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
  role: UserRoleEnum!
  createdAt: DateTime!
  updatedAt: DateTime
}

enum UserRoleEnum {
  ADMIN
  ACCOUNTANT
}

Vidíme, že jsme definovali jeden výstupní typ objektu s názvem Uživatel s následujícími poli:id , uživatelské jméno , e-mail , telefon , křestní jméno , příjmení , vytvořenoA , aktualizováno v . Pole id se zadává jako skalární ID a ostatní pole se zadávají jako řetězce. Také jsme definovali dotazy uživatel a uživatelé . Uživatelský dotaz vrátí objekt User na základě předaného id. Uživatelský dotaz pak vrátí seznam uživatelů. Definovali jsme také nepovinnou roli typu enum, která se používá v uživatelském dotazu jako argument pro filtrování výsledku. V tomto jednoduchém schématu jsme použili modifikátory poměrně hodně. Ve zbytku článku si tyto případy použití projdeme.

Modifikátory

Nejprve formálně definujme modifikátor. Jak jsme již zmínili, modifikátor je speciální skupina typů v GraphQL . Tyto typy lze definovat následovně:

Z této definice je zřejmé, že vždy musíme definovat typ, na který modifikátor aplikujeme. V aktuálním GraphQL specifikace, máme tyto dva typy modifikátorů. Každý z modifikátorů je klasifikován jako samostatný typ:

  • Seznam
  • Není nulová

V tomto článku se zaměříme především na modifikátor seznamu. Umožní nám definovat, zda chceme vrátit posloupnost typů. Modifikátor Non-Null nám umožňuje definovat, zda je typ/pole vyžadován. To může být null (výchozí chování v GraphQL) nebo je vyžadováno a server GraphQL vyvolá chybu. V tomto článku se zaměříme především na Seznam modifikátory a zanechat podrobnější diskusi o Non-Null modifikátory pro jiný článek.

Seznam

Obecně platí, že seznam GraphQL představuje sekvenci hodnot . Tyto hodnoty je možné zobrazit jako pole (např. v Javascriptu), i když analogie není zcela přesná. Jak jsme zmínili, seznam udržuje položky v pořadí. V SDL je modifikátor seznamu zapsán jako hranaté závorky se zabalenou instancí typu v závorce. V našem schématu jsme pomocí modifikátoru seznamu definovali, že pokud dotaz zavoláme uživatelé, vrátí posloupnost typů Uživatel z databáze. Toho je dosaženo definováním schématu takto:

type Query {
  user(id: ID!): User
  users(role: RoleEnum): [User!]!
}

Zavoláním dotazovaných uživatelů očekáváme, že vrátíme seznam uživatelů. Podívejme se, jak to vypadá, když použijeme knihovnu graphql-js. Dotazy v našem úložišti jsou definovány takto:

import {
  GraphQLList,
  GraphQLNonNull,
} from 'graphql';
import { getUsers } from '../../operations/users-operations';
import User from './UserType';
import UserRoleEnum from './UserRoleEnumType';

const UserQueries = {
  users: {
    type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))),
    args: {
      role: {
        type: UserRoleEnum,
      },
    },
    resolve: (_source, { role }) => {
      const result = getUsers();
      if (role != null) {
        return result.filter((user) => user.role === role);
      }
      return result;
    },
  },
};

export default UserQueries;

Vidíme, že dosahujeme stejné funkčnosti jako u SDL. GraphQLList třída představuje Seznam. Instanci této třídy jsme použili na instanci User . Nyní jsme schopni načíst data provedením uživatelského dotazu v GraphQL Playground pomocí Play tlačítko.

Měli bychom získat tato data a získat uživatele jako seznam.

{
  "data": {
    "users": [
      {
        "id": "7b838108-3720-4c50-9de3-a7cc04af24f5",
        "firstName": "Berniece",
        "lastName": "Kris",
        "username": "Ana_Quigley"
      },
      {
        "id": "66c9b0fd-7df6-4e2a-80c2-0e4f8cdd89b1",
        "firstName": "Bradly",
        "lastName": "Lind",
        "username": "Winona_Kulas12"
      },
      {
        "id": "718590a1-33ac-4e61-9fef-b06916acd76b",
        "firstName": "Leila",
        "lastName": "Schowalter",
        "username": "Isabell.Kautzer"
      },
      {
        "id": "411df0f3-bb2c-4f5f-870f-3db9c30d754f",
        "firstName": "Laila",
        "lastName": "Breitenberg",
        "username": "Christophe.Oberbrunner"
      },
      {
        "id": "e1254480-d205-4be8-abfa-67ad7dcd03fb",
        "firstName": "Joe",
        "lastName": "Crist",
        "username": "Dahlia.Gerhold56"
      },
      {
        "id": "d0087200-9621-4787-a3db-cebbede163e6",
        "firstName": "Bettye",
        "lastName": "Bartoletti",
        "username": "Thad_Mayert"
      }
    ]
  }
}

Dalším případem použití pro modifikátory seznamu je návrh createUsers mutace, kam můžeme přidávat uživatele v dávce. Existuje několik důvodů, proč navrhnout mutace tímto způsobem. Možná budeme muset přidat uživatele v transakci, proto nemůžeme mít jiný kontext resolveru nebo jen chceme zjednodušit API nebo zlepšit výkon a rychleji provést mutaci pro více uživatelů. Toto je skvělý případ použití pro použití modifikátoru List na naši vstupní datovou část. Typ vstupního objektu můžeme definovat jen jednou takto:

import {
  GraphQLString,
  GraphQLInputObjectType,
  GraphQLNonNull,
} from 'graphql';
import UserRole from './UserRoleEnumType';

const CreateUserInputType = new GraphQLInputObjectType({
  name: 'CreateUserInput',
  description: 'Input payload for creating user',
  fields: () => ({
    username: {
      type: new GraphQLNonNull(GraphQLString),
    },
    email: {
      type: GraphQLString,
    },
    phone: {
      type: GraphQLString,
    },
    firstName: {
      type: GraphQLString,
    },
    lastName: {
      type: GraphQLString,
    },
    role: {
      type: UserRole,
      defaultValue: UserRole.getValue('ACCOUNTANT').value,
    },
  }),
});

export default CreateUserInputType;

nebo v jazyce SDL

input CreateUserInput {
  username: String!
  email: String
  phone: String
  firstName: String
  lastName: String
}

a poté aplikujte modifikátor seznamu, abyste dosáhli možnosti předat více užitečných zatížení v jedné vstupní proměnné.


import {
  GraphQLList,
  GraphQLNonNull,
} from 'graphql';
import { isEmail } from 'validator';
import { createUser, createUsers } from '../../operations/users-operations';
import CreateUserInput from './CreateUserInputType';
import CreateUserPayload from './CreateUserPayload';
import CreateUsersPayload from './CreateUsersPayload';

const UserMutations = {
  createUser: {
    type: CreateUserPayload,
    args: {
      input: {
        type: new GraphQLNonNull(CreateUserInput),
      },
    },
    resolve: (_source, args) => {
      const { input } = args;

      if (input.email && !isEmail(input.email)) {
        throw new Error('Email is not in valid format');
      }
      return {
        user: createUser(input),
      };
    },
  },
  createUsers: {
    type: CreateUsersPayload,
    args: {
      input: {
        type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(CreateUserInput))),
      },
    },
    resolve: (_source, { input }) => {
      const createdUsers = createUsers(input);
      return {
        users: createdUsers,
      };
    },
  },
};

export default UserMutations;

Mutaci můžeme provést pomocí inline argumentů nebo chcete-li pomocí proměnných

mutation {
  createUsers(input: [{lastName: "Test", firstName: "test", username: "t1est"}, {lastName: "Test", firstName: "test", username: "te2st"}]) {
    users {
        id
        firstName
        lastName
        phone
        email
        username
    }
  }
}

Nyní si projdeme pravidla pro nátlak na výsledek a vstup. Pokud tyto pojmy neznáte, můžete se podívat na článek o skalárech, kde popisujeme vstup a výsledek nátlaku.

Nátlak na výsledek

Pro dotaz uživatelé , nátlak na výsledek je pro nás relevantní, protože bychom rádi získali pole uživatelů z provedeného dotazu. Když vynucujeme seznamy, server GraphQL musí zajistit, že vrácená data z funkce resolveru zůstanou ve stejném pořadí. Vynucování každé položky v seznamu je pak delegováno na výsledný donucení odkazovaného typu; každá položka pole musí vyhovovat uživateli zadejte nebo null hodnota. Pokud v této funkci resolveru vrátí objekt místo typu pole:

import {
  GraphQLList,
  GraphQLNonNull,
} from 'graphql';
import { getUsers } from '../../operations/users-operations';
import User from './UserType';
import UserRoleEnum from './UserRoleEnumType';

const UserQueries = {
  users: {
    type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User))),
    args: {
      role: {
        type: UserRoleEnum,
      },
    },
    resolve: (_source, { role }) => {
      const result = getUsers();
      if (role != null) {
        return result.filter((user) => user.role === role);
      }
      return result;
    },
  },
};

export default UserQueries;

server GraphQL by pak měl vyvolat tuto chybu

Expected Iterable, but did not find one for field Query.users.

To se stane, pokud vynucení modifikátoru Seznam nevyhoví. Co se ale stane, když některé položky v seznamu nebudou správně vynuceny? V takovém případě řešíme chybu podobným způsobem. Vrátíme null místo hodnoty vrácené funkcí resolveru a do odpovědi přidejte chybu.

Vstupní donucení

Při diskuzi o vynucení vstupu na Seznam modifikátory můžeme vzít v úvahu createUsers mutace a popište chování, které vyvolává chybu. Na rozdíl od vynucování výsledků, kdy některé položky z pole výsledků lze získat, i když jedna položka není správně vynucena, při vynucení vstupu nebudeme schopni provést celou mutaci, pokud nelze vynutit jeden náklad. Podívejme se na následující příklad, kde bychom chtěli předat seznam dvou datových zátěží, ale jedna zátěž nevyhovuje typu vstupu a nemá požadované uživatelské jméno pole. Po provedení této mutace se zobrazí následující chyba:

Argument "input" has invalid value [{username: "testtest", email: "[email protected]", firstName: "test", lastName: "test"}, {email: "[email protected]", firstName: "test", lastName: "test"}].
In element #1: In field "username": Expected "String!", found null.

Celá mutace selže i v případě, že nevyhovuje pouze vstupní donucení v typu vstupního objektu v jedné položce seznamu. Je však důležité zdůraznit, že pokud předáme null následovně, provede se celá mutace. To však závisí na tom, zda jsme aplikovali nějaké další modifikátory nebo ne a skládali modifikátory ve složitějším typu. Toto téma projdeme v poslední části tohoto článku o Složení modifikátoru .

Složení modifikátoru

Pokud vezmeme v úvahu definici modifikátoru výše, víme, že modifikátor v podstatě vytvoří nový typ z odkazovaného typu s další funkčností. V našem případě přidáváme chování tak, aby výsledné donucení akceptovalo seznam položek a ne pouze položku samotnou. To je také podobné funkcím vyššího řádu nebo vzoru dekorátoru a stejným způsobem můžeme v Reactu řetězit funkce vyššího řádu nebo HOC. Jsme také schopni skládat modifikátory použitím modifikátoru na typ, kde je předchozí modifikátor již aplikován. Modifikátor Non-Null můžeme zkombinovat s naším modifikátorem List následujícím způsobem. Takto v podstatě kombinujeme tři modifikátory, které jsou zřetězeny následovně

new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(User)))

Vzniká tak speciální typ. Při použití pouze modifikátoru seznamu můžeme z resolveru vrátit hodnotu null. Můžeme dokonce kombinovat položky v poli tak, aby obsahovaly hodnoty null jako v tomto poli:

mutation {
  createUsers(input: [{username: "testtest", email: "[email protected]", firstName: "test", lastName: "test"}, null]) {
    id
    username
    firstName
  }
}

Ale když použijeme složený modifikátor, jak je uvedeno výše, můžeme předat pouze pole obsahující objekty, které odpovídají typu User. Výše uvedený seznam bude proto zamítnut. Hodnota null vrácená z překladače bude také odmítnuta. Níže se můžete podívat na tabulku, která obsahuje, co který modifikátor umožní, abyste si udělali lepší představu o tom, jaké kombinace modifikátorů jsou vhodné pro různé případy použití. Jediné pravidlo v řetězení modifikátorů platí pro nenulové modifikátory. Prohlašuje, že nemůžeme zabalit jeden modifikátor Non-Null do jiného modifikátoru Non-Null.

[Uživatel] [UserObject, null] Platné
[Uživatel] null Platné
[Uživatel] [null] Platné
[Uživatel] [UserObject] Platné
[Uživatel!] [UserObject,null] Neplatné
[Uživatel!] [null] Neplatné
[Uživatel!] null Platné
[Uživatel!] [UserObject] Platné
[Uživatel!]! [UserObject, null] Neplatné
[Uživatel!]! null Neplatné
[Uživatel!]! [UserObject] Platné
Uživatel!! - Neplatné

UserObject v této tabulce se může rovnat např

{ lastName: "Test", firstName: "test", username: "t1est"}

Pro jednoduchost jsme se nezabývali rozdíly mezi vstupním a výstupním nucením pro tyto složitější typy. Chování se liší pouze tím, jak jsme diskutovali v části o vynucení výsledků a vstupu. Pokud by existoval jiný objekt UserObject, který nevyhovuje donucení typu uživatele (např. nemá vlastnost uživatelského jména), byla by zde další pravidla.

Souhrn

V tomto článku jsme pokryli jednu speciální skupinu typů v GraphQL nazvanou Modifiers . Pomocí modifikátorů můžeme do odkazovaného typu GraphQL vložit speciální chování, přidat seznam a další požadovaná pole a dokonce tyto případy použití kombinovat a vytvářet složitější typy. Modifikátory jsou skvělým nástrojem pro vytváření elegantních schémat GraphQL.