conf-talks

GraphQL Schema

GraphQL Schema – это описание ваших типов данных на сервере, связей между ними и логики получения этих самых данных.

Еще раз по пунктам:

Так как же данные? В каком формате и какой базе данных они будут храниться? В любой! Это неважно, когда у вас есть вами написанные resolve-методы, в которых вы напишете куда и как сходить за данными и обработать перед тем как отдать клиенту.

GraphQL задает канву, формат того как вы описываете доступ к своим данным. Это дело описывается через GraphQL-спецификацию. Которую ребята из Фейсбука очень кропотливо прорабатывали и в 2017 опубликовали под OWFa 1.0 соглашением (грубо говоря MIT-лицензией).

Так, хорошо. А какой язык программирования мне нужно использовать? Любой! Спецификацию уже реализовали на большинстве языков программирования.

Работает с любыми базами данных, на любом языке программирования - звучит хорошо!

Описание схемы на сервере (build phase)

Чтобы запустить свой GraphQL-сервер, первым делом вам необходимо объявить схему GraphQLSchema. Схема содержит в себе описания всех типов, полей и методов получения данных. Все типы в рамках GraphQL-схемы должны иметь уникальные имена. Не должно быть двух разных типов с одним именем.

GraphQL-схема это точка входа, это корень всего вашего API. Правда у этого корня, три “головы”:

В GraphQLSchema обязательным параметром является только query, без него схема просто не запустится. Инициализация схемы выглядит следующим образом:

import { GraphQLSchema, GraphQLObjectType, graphql } from 'graphql';

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({ name: 'Query', fields: { getUserById, findManyUsers } }),
  mutation: new GraphQLObjectType({ name: 'Mutation', fields: { createUser, removeLastUser } }),
  subscriptions: new GraphQLObjectType({ name: 'Subscription', fields: ... }),
  // ... и ряд других настроек
});

Особо хочется остановиться на состоянии операций, в GraphQL их два:

Также важно рассмотреть различия в выполнении операции для query и mutation. Если все операции (поля) в query вызываются параллельно, то в mutation они вызываются последовательно. Например:

query {
  getUserById { ... }
  findManyUsers { ... }
  # `getUserById` и `findManyUsers` будут запрошены параллельно
}
mutation {
  # сперва выполнится операция создания пользователя
  createUser { ... }
  # а после того как пользователь создан, выполнится операция на удаление
  removeLastUser { ... }
}

Выполнение GraphQL-запросов (runtime phase)

Предположим у нас объявлена следующая схема:

import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';

const schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve: () => 'world',
      }
    }
  })
});

export default schema;

После инициализации GraphQL-схемы вы можете выполнять GraphQL-запросы. Делается это достаточно просто:

import { graphql } from 'graphql';
import { schema } from './your-schema';

const query = '{ hello }';
const result = await graphql(schema, query); // returns: { data: { hello: "world" } }

Для выполнения запроса, необходимо вызвать метод graphql() из пакета graphql, который решает следующие задачи:

GraphQL по натуре строго типизированный, поэтому во всех запросах проверяются входящие переменные и аргументы, формат возвращаемого ответа на соответствие типов, которые объявлены в GraphQL-схеме. Если что-то некорректно запросили или вернули, то будет возвращена ошибка.

Пакет graphql ничего не знает о сети, правах доступа, не слушает никакой порт. Всё это дело реализуется на другом уровне абстракции - GraphQL сервере. В разделе GraphQL-сервер вы найдете больше подробностей о том, что должен делать сервер, и какие популярные пакеты сейчас доступны в npm. Важно знать, что все сервера используют под капотом метод graphql() из пакета graphql.

SDL (Schema Definition Language)

В спецификации GraphQL для описания типов используется Schema Definition Language (SDL). Это простой, выразительный и интуитивно понятный формат описания типов, который не зависит ни от какого языка программирования. Не путать с Query Language — т.к. это другой формат для написания GraphQL-запросов, а не схемы.

Output-тип

Простое описание output-типа с помощью SDL выглядит следующим образом:

type Post {
  id: Int!
  title: String!
  publishedAt: DateTime!
  comments(limit: Int = 10): [Comment]
}

Тип имеет имя Post и состоит из четырёх полей:

Input-тип

input Credentials {
  login: String!
  password: String!
}

Enum-тип

enum Direction {
  NORTH
  EAST
  SOUTH
  WEST
}

Interface

interface NamedEntity {
  name: String
}

interface ValuedEntity {
  value: Int
}

type Business implements NamedEntity & ValuedEntity {
  name: String
  value: Int
  employeeCount: Int
}

Unions

union SearchResult = Photo | Person

type Person {
  name: String
  age: Int
}

type Photo {
  height: Int
  width: Int
}

type SearchQuery {
  firstSearchResult: SearchResult
}

Custom scalars

scalar Time
scalar Url

Модификаторы типов

SDL Значение
[Int!] null или массив чисел
[Int]! массив чисел или null, пустой массив
[Int!]! массив чисел или пустой массив
[[Int]] целочисленный массив массивов

Директивы

directive @example on FIELD_DEFINITION | ARGUMENT_DEFINITION

type SomeType {
  field(arg: Int @example): String @example
}

В SDL у пакета graphql есть встроенная директива @deprecated для пометки полей как устаревшее и не рекомендуемое к использованию:

directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

Пример использования:

type ExampleType {
  newField: String
  oldField: String @deprecated(reason: "Use `newField`.")
}

Описание схемы для клиента (интроспекция)

Когда у вас есть на сервере GraphQL-схема, то вы можете предоставить клиенту описание типов, без resolve-методов. Т.е. клиент будет знать что может вернуть сервер, но внутренняя реализация того, как это происходит будет от него скрыта. Описание типов GraphQL-схемы называется интроспекцией и выглядит следующим образом в формате SDL:

# The main root type of your Schema
type Query {
  book(id: Int): Book
  author(name: String): Author
}

# Author model
type Author {
  id: Int!
  name: String!
}

# Description for Book model
type Book {
  id: Int!
  name: String!
  authors: [Author]
}

Пример интроспекции побольше в формате SDL на 130 типов и она же в формате JSON. А можно посмотреть интроспекцию всех сервисов AWS Cloud размером 1.8Mb, содержащию описание для более чем 10000 типов.

Зачем нужна интроспекция клиенту?

Самому клиенту интроспекция может и не нужна, но вот для инструментария будет очень полезна:

Как сгенерировать интроспекцию?

Самый простой способ получить интроспекцию в формате JSON, это запросить ее у уже запущенного GraphQL-сервиса (если на сервере по причине безопасности её не запретили). Например GraphiQL и GraphQL Playground запрашивают её по http отправляя следующий GraphQL-запрос.

В JS сгенерировать файл с интроспекцией схемы можно с помощью следующих скриптов:

Генерация интроспекции в формате SDL

import fs from 'fs';
import { printSchema } from 'graphql';
import schema from './your-schema';

fs.writeFileSync('./schema.graphql', printSchema(schema));

Генерация интроспекции в формате JSON

import fs from 'fs';
import { getIntrospectionQuery } from 'graphql';
import schema from './your-schema';

async function prepareJsonFile() {
  const result = await graphql(schema, getIntrospectionQuery());
  fs.writeFileSync('./schema.json', JSON.stringify(result, null, 2));
}

prepareJsonFile();

Автоматизируй генерацию схем