Jak napsat a uspořádat schéma GraphQL v JavaScriptu

Jak napsat schéma GraphQL pomocí struktury složek a souborů, která usnadňuje pochopení a údržbu.

V aplikaci, která pro svou datovou vrstvu používá GraphQL – tedy věc, kterou vaše aplikace používá k načítání a manipulaci s daty – je schéma základním pilířem mezi klientem a serverem.

Zatímco schémata v GraphQL mají pravidla pro to, jak je píšete, neexistují žádná pravidla pro to, jak je uspořádat jim. Ve velkých projektech je organizace klíčem k udržení hladkého chodu.

Začínáme

Pro tento tutoriál použijeme jako výchozí bod CheatCode Node.js Boilerplate. To nám umožní přístup k fungujícímu serveru GraphQL s již připojeným schématem. Toto schéma upravíme a prodiskutujeme jeho organizaci, abychom vám pomohli informovat organizaci o vašem vlastním schématu GraphQL.

Nejprve naklonujme kopii základního kódu z Github:

Terminál

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

Dále cd do kotle a nainstalujte jeho závislosti:

Terminál

cd nodejs-server-boilerplate && npm install

S nainstalovanými závislostmi nyní můžeme spustit vývojový server:

Terminál

npm run dev

Díky tomu jsme připraveni začít.

Nastavení základní struktury složek

V aplikaci používající GraphQL existují dvě základní části:vaše schéma GraphQL a váš server GraphQL (nezávislý na vašem HTTP serveru). Schéma je přiloženo na server, takže když přijde požadavek, server rozumí tomu, jak jej zpracovat.

Protože tyto dva kusy fungují v tandemu, je nejlepší je uložit vedle sebe. V ukázkovém projektu, který jsme právě naklonovali, jsou tyto umístěny v /api/graphql adresář. Zde je /api adresář obsahuje složky, které popisují různé typy dat v naší aplikaci. Když se spojí, naše schéma a server představují GraphQL API pro naši aplikaci (odtud umístění).

Uvnitř této složky — /api/graphql —oddělíme naše schéma a deklarace serveru do dvou souborů:/api/graphql/schema.js a /api/graphql/server.js . Vpřed se zaměříme na schéma součástí této rovnice, ale pokud byste se chtěli dozvědět více o nastavení serveru GraphQL, doporučujeme přečíst si tento další tutoriál CheatCode o nastavení serveru GraphQL. Než skončíme, probereme, jak funguje připojení schématu, které zapisujeme na server GraphQL.

Uspořádání typů, překladačů dotazů a překladačů mutací

Dále bude hlavní částí našeho organizačního vzoru to, jak oddělíme různé typy, překladače dotazů a překladače mutací v našem GraphQL API. V našem vzorovém projektu je navržená struktura držet vše organizované pod /api adresář, o kterém jsme se dozvěděli dříve. V této složce by každé datové „téma“ mělo dostat svou vlastní složku. „Téma“ popisuje kolekci nebo tabulku ve vaší databázi, rozhraní API třetí strany (např. /api/google ), nebo jakýkoli jiný odlišný typ dat ve vaší aplikaci.

├── /api
│   ├── /documents
│   │   ├── /graphql
│   │   │   ├── mutations.js
│   │   │   ├── queries.js
│   │   │   └── types.js

Pokud jde o GraphQL, do složky tématu přidáme graphql složku pro uspořádání všech našich souborů souvisejících s GraphQL pro dané téma. Ve výše uvedené vzorové struktuře je naše téma documents . Pro toto téma máme v kontextu GraphQL několik vlastních typů (types.js ), překladače dotazů (queries.js ) a překladače mutací (mutations.js ).

/api/documents/graphql/types.js

const DocumentFields = `
  title: String
  status: DocumentStatus
  createdAt: String
  updatedAt: String
  content: String
`;

export default `
  type Document {
    _id: ID
    userId: ID
    ${DocumentFields}
  }

  enum DocumentStatus {
    draft
    published
  }

  input DocumentInput {
    ${DocumentFields}
  }
`;

V našem types.js exportujeme řetězec definovaný pomocí backtics `` abychom mohli využít interpolaci řetězců JavaScriptu (od verze standardu ES6) (což nám umožňuje zahrnout a interpretovat výrazy JavaScriptu v řetězci). Zde, jako organizační technika, když máme sadu vlastností, které se používají ve více typech, extrahujeme tato pole do řetězce (definovaného pomocí zpětného zaškrtnutí pro případ, že potřebujeme provést nějakou interpolaci) a uložíme je do proměnné nahoře. našeho souboru (zde DocumentFields ).

S využitím této interpolace pak zřetězíme naše DocumentFields na místě, kde jsou použity v typech vrácených v exportovaném řetězci. Díky tomu se při konečném exportu našich typů přidají „sdílená“ pole k typům, které definujeme (např. zde type Document bude mít všechny vlastnosti v DocumentFields jsou na něm definovány).

/api/documents/graphql/queries.js

import isDocumentOwner from "../../../lib/isDocumentOwner";
import Documents from "../index";

export default {
  documents: async (parent, args, context) => {
    return Documents.find({ userId: context.user._id }).toArray();
  },
  document: async (parent, args, context) => {
    await isDocumentOwner(args.documentId, context.user._id);

    return Documents.findOne({
      _id: args.documentId,
      userId: context.user._id,
    });
  },
};

Podívejte se na naše queries.js soubor dále, zde ukládáme všechny funkce resolveru pro naše dotazy související s tématem dokumentů. Abychom usnadnili organizaci, seskupujeme všechny naše funkce resolveru do jednoho objektu (v JavaScriptu je funkce definovaná na objektu známá jako metoda ) a exportujte tento nadřazený objekt ze souboru. Proč je to důležité, uvidíme později, až do schématu importujeme naše typy a resolvery.

/api/documents/graphql/mutations.js

import isDocumentOwner from "../../../lib/isDocumentOwner";
import Documents from "../index";

export default {
  documents: async (parent, args, context) => {
    return Documents.find({ userId: context.user._id }).toArray();
  },
  document: async (parent, args, context) => {
    await isDocumentOwner(args.documentId, context.user._id);

    return Documents.findOne({
      _id: args.documentId,
      userId: context.user._id,
    });
  },
};

Pokud jde o strukturu, mutations.js je identický s queries.js . Jediný rozdíl je v tom, že tyto funkce resolveru jsou zodpovědné za řešení mutací místo dotazů. Zatímco my mohli seskupte naše nástroje pro řešení dotazů a mutací do jediného resolvers.js Pokud je ponecháte oddělené, údržba je o něco snazší, protože mezi funkcemi resolveru neexistuje žádný rozdíl.

Dále, když jsou tyto soubory připraveny, abychom je mohli používat, musíme importovat a přidat jejich obsah do našeho schématu.

Import a přidání vašich typů, překladačů dotazů a překladačů mutací do schématu

Nyní, když rozumíme tomu, jak uspořádat části, které tvoří naše schéma, pojďme je dát dohromady, abychom měli funkční schéma. Podívejme se na schéma v našem vzorovém projektu a uvidíme, jak se to mapuje zpět na soubory, které jsme vytvořili výše.

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

import DocumentTypes from "../documents/graphql/types";
import DocumentQueries from "../documents/graphql/queries";
import DocumentMutations from "../documents/graphql/mutations";

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    type Query {
      document(documentId: ID!): Document
      documents: [Document]
    }

    type Mutation {
      createDocument(document: DocumentInput!): Document
      deleteDocument(documentId: ID!): Document
      updateDocument(documentId: ID!, document: DocumentInput!): Document
    }
  `,
  resolvers: {
    Query: {
      ...DocumentQueries,
    },
    Mutation: {
      ...DocumentMutations,
    },
  },
};

export default makeExecutableSchema(schema);

Snad to začíná dávat smysl. To, co vidíte výše, se mírně liší od toho, co najdete v cestě k souboru v horní části tohoto bloku kódu. Rozdíl je v tom, že zde jsme vytáhli části schématu související s uživateli, abychom zajistili, že části, které jsme dříve vytvořili, do sebe zapadají (jsou zahrnuty jako součást projektu, který jsme naklonovali z Github).

Začneme v horní části souboru, abychom vytvořili naše schéma, importujeme gql tag z graphql-tag balíček (již nainstalovaný jako součást závislostí v projektu, který jsme dříve naklonovali). gql představuje funkci, která přijímá řetězec obsahující kód napsaný v GraphQL DSL (domain specific language). Toto je speciální syntaxe, která je jedinečná pro GraphQL. Protože používáme GraphQL v JavaScriptu, potřebujeme způsob, jak interpretovat DSL v JavaScriptu.

gql funkce zde převede řetězec, který mu předáme, do AST nebo abstraktního stromu syntaxe. Toto je velký objekt JavaScriptu představující technickou mapu obsahu řetězce, který jsme předali gql . Později, když připojíme naše schéma k našemu serveru GraphQL, to implementace serveru bude předvídat a rozumět tomu, jak analyzovat tento AST.

Pokud se podíváme, kde je gql je použit v souboru výše, vidíme, že je přiřazen k typeDefs vlastnost na objektu, který jsme uložili do schema variabilní. Ve schématu typeDefs popište tvar dat, která jsou vrácena serverovými překladači dotazů a mutací, a také definujte dotazy a mutace, které lze provést.

Existují dvě varianty typů:vlastní typy, které popisují data ve vaší aplikaci, a kořenový typy. Kořenové typy jsou vestavěné typy, které si GraphQL vyhrazuje pro popis polí k dispozici pro dotazy a mutace. Konkrétněji, když se podíváme na kód výše, type Query a type Mutation bloky jsou dva ze tří dostupných typů kořenů (třetí je type Subscription který se používá pro přidávání dat v reálném čase na server GraphQL).

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

import DocumentTypes from "../documents/graphql/types";
import DocumentQueries from "../documents/graphql/queries";
import DocumentMutations from "../documents/graphql/mutations";

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    [...]
  `,
  resolvers: { [...] },
};

export default makeExecutableSchema(schema);

Chcete-li využít vlastní typy, které jsme napsali dříve (v /api/documents/graphql/types.js soubor), v horní části našeho schema.js zde importujeme naše typy jako DocumentTypes . Dále, uvnitř zpětných značek bezprostředně po našem volání na gql (hodnota, kterou přiřazujeme typeDefs ), používáme interpolaci řetězců JavaScript ke zřetězení našich typů do hodnoty, kterou předáváme do typeDefs . Tím se dosáhne „nahrání“ našich vlastních typů do našeho schématu GraphQL.

Dále, abychom mohli definovat, které dotazy a mutace můžeme spustit, musíme definovat pole dotazů a pole mutací uvnitř kořenového adresáře type Query a type Mutation typy. Oba jsou definovány stejným způsobem. Uvádíme název pole, u kterého očekáváme mapování na funkci resolveru v našem schématu. Volitelně také popíšeme argumenty nebo parametry, které lze do tohoto pole předat z klienta.

/api/graphql/schema.js

[...]

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    type Query {
      document(documentId: ID!): Document
      documents: [Document]
    }

    type Mutation {
      createDocument(document: DocumentInput!): Document
      deleteDocument(documentId: ID!): Document
      updateDocument(documentId: ID!, document: DocumentInput!): Document
    }
  `,
  resolvers: { [...] },
};

export default makeExecutableSchema(schema);

Zde pod type Query , document(documentId: ID!): Document říká „definujte pole, které bude vyřešeno funkcí resolveru s názvem document což vyžaduje documentId předán jako skalární typ ID a očekávat, že vrátí data ve tvaru type Document type (přidáno do našeho schématu jako součást ${DocumentTypes} řádek jsme zřetězili do našeho typeDefs přímo uvnitř volání na gql ). Toto opakujeme pro každé z polí, která chceme zpřístupnit pro dotazování pod type Query .

Opakujeme stejný vzor se stejnými pravidly pod type Mutation . Jak jsme diskutovali dříve, jediný rozdíl je v tom, že tato pole popisují mutace které můžeme spustit, ne dotazy.

Přidání překladače dotazů a mutací

Nyní, když jsme specifikovali naše vlastní typy a pole v našem kořenovém adresáři type Query a root type Mutation , dále musíme přidat funkce resolveru, které vyřeší dotazy a mutace, které jsme tam definovali. Abychom to udělali, v horní části našeho souboru importujeme náš samostatný queries.js a mutations.js soubory (pamatujte, že se jedná o export objektů JavaScriptu) jako DocumentQueries a DocumentMutations .

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

import DocumentTypes from "../documents/graphql/types";
import DocumentQueries from "../documents/graphql/queries";
import DocumentMutations from "../documents/graphql/mutations";

const schema = {
  typeDefs: gql`
    ${DocumentTypes}

    type Query {
      document(documentId: ID!): Document
      documents: [Document]
    }

    type Mutation {
      createDocument(document: DocumentInput!): Document
      deleteDocument(documentId: ID!): Document
      updateDocument(documentId: ID!, document: DocumentInput!): Document
    }
  `,
  resolvers: {
    Query: {
      ...DocumentQueries,
    },
    Mutation: {
      ...DocumentMutations,
    },
  },
};

export default makeExecutableSchema(schema);

Dále v resolvers vlastnost na objektu, který jsme přiřadili schema proměnnou, vnoříme dvě vlastnosti:Query a Mutation . Tato jména odpovídají kořenovým typům, které jsme definovali v našem typeDefs blok. Zde jsou překladače, které jsou přidruženy ke kořenovému adresáři type Query jsou nastaveny v resolvers.Query objekt a překladače, které jsou spojeny s kořenem type Mutation jsou nastaveny v resolvers.Mutation objekt. Protože jsme exportovali naše DocumentQueries a DocumentMutations jako objekty můžeme tyto objekty zde "rozbalit" pomocí ... syntaxe šíření v JavaScriptu.

Jak název napovídá, „rozprostře“ obsah těchto objektů na nadřazený objekt. Jakmile bude tento kód interpretován JavaScriptem, účinně dosáhne tohoto:

{
  typeDefs: [...],
  resolvers: {
    Query: {
      documents: async (parent, args, context) => {
        return Documents.find({ userId: context.user._id }).toArray();
      },
      document: async (parent, args, context) => {
        await isDocumentOwner(args.documentId, context.user._id);

        return Documents.findOne({
          _id: args.documentId,
          userId: context.user._id,
        });
      },
    },
    Mutation: {
      createDocument: async (parent, args, context) => {
        const _id = generateId();

        await Documents.insertOne({
          _id,
          userId: context.user._id,
          ...args.document,
          createdAt: new Date().toISOString(),
          updatedAt: new Date().toISOString(),
        });

        return {
          _id,
        };
      },
      updateDocument: async (parent, args, context) => {
        await isDocumentOwner(args.documentId, context.user._id);

        await Documents.updateOne(
          { _id: args.documentId },
          {
            $set: {
              ...args.document,
              updatedAt: new Date().toISOString(),
            },
          }
        );

        return {
          _id: args.documentId,
        };
      },
      deleteDocument: async (parent, args, context) => {
        await isDocumentOwner(args.documentId, context.user._id);

        await Documents.removeOne({ _id: args.documentId });
      },
    },
  }
}

I když to jistě dokážeme, rozdělením našich dotazů a resolverů do témat a do jejich vlastních souborů je údržba mnohem jednodušší (a méně zahlcující).

/api/graphql/schema.js

import gql from "graphql-tag";
import { makeExecutableSchema } from "@graphql-tools/schema";

[...]

const schema = {
  typeDefs: [...],
  resolvers: { [...] },
};

export default makeExecutableSchema(schema);

Nakonec v dolní části našeho souboru vyexportujeme náš schema proměnnou, ale nejprve zabalte do volání makeExecutableSchema . Podobné jako gql Když to uděláme, převede celé naše schéma na AST (abstraktní syntaktický strom), kterému rozumí servery GraphQL a další knihovny GraphQL (např. funkce middlewaru GraphQL, které pomáhají s ověřováním, omezováním rychlosti nebo zpracováním chyb ).

Technicky vzato, s tím vším máme naše schéma GraphQL! Abychom vše uzavřeli, podívejme se, jak se naše schéma načítá na server GraphQL.

Přidání schématu na server GraphQL

Naštěstí přidání schématu na server (jakmile je server definován) trvá pouze dva řádky:import schema z našeho /api/graphql/schema.js soubor a jeho přiřazení k možnostem pro náš server.

/api/graphql/server.js

import { ApolloServer } from "apollo-server-express";
import schema from "./schema";
import { isDevelopment } from "../../.app/environment";
import loginWithToken from "../users/token";
import { configuration as corsConfiguration } from "../../middleware/cors";

export default (app) => {
  const server = new ApolloServer({
    schema,
    [...]
  });

  [...]
};

A je to! Mějte na paměti, že způsob, jakým zde předáváme naše schéma, je specifický pro knihovnu serveru Apollo a ne nutně všechny Implementace serveru GraphQL (Apollo je jednou z mála serverových knihoven GraphQL).

Zabalení

V tomto tutoriálu jsme se naučili, jak uspořádat schéma GraphQL, aby byla údržba snadná. Naučili jsme se, jak analyzovat různé části našeho schématu GraphQL do jednotlivých souborů a rozdělit tyto soubory do témat přímo souvisejících s našimi daty. Také jsme se naučili, jak zkombinovat tyto samostatné soubory do schématu a poté toto schéma načíst na server GraphQL.