Skip to content

Latest commit

 

History

History

schema

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

GraphQL Schema

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

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

  • у вас есть данные на сервере
  • есть методы получения этих данных (resolve-методы)
  • для этих методов вы описываете типы данных входящих и выходящих значений (описание типов)
  • берете resolve-методы и описание типов хитро перемешиваете и получаете вашу GraphQL Schema.

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

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

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

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

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

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

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

  • query - для операций получения данных
  • mutation - для операций изменения данных
  • subscription - для подписки на события

В 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 их два:

  • stateless - все операции в query и mutation должны быть без состояния, т.е. если у вас в кластере много машин обслуживающих запросы клиентов, то неважно на какой из серверов прилетел запрос. Его может выполнить любая нода.
  • statefull - должно быть у операций subscription, т.к. требуется установка постоянного подключения с клиентом, хранение данных о подписках, механизм переподключения и восстановления данных о существующих подписках. Пакет 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-запроса query
  • производит валидацию полей в запросе на соответствие GraphQL-схемы schema (если запрошены несуществующие поля, или переданы неверные аргументы, то будет возвращена ошибка)
  • выполняет запрос, пробегаясь по запрошенным Object-типам из query, вызывая их resolve-методы. Подробнее об Object-типе можно почитать в разделе о Типах.
  • валидирует возвращаемый ответ (например, если для обязательного поля вы вернули null, то будет возвращена ошибка)

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 и состоит из четырёх полей:

  • id — поле которое non-null (восклицательный знак) и имеет тип целого числа
  • title — не пустое поле с типом строки
  • publishedAt — не пустое поле с кастомным типом DateTime
  • comments — поле которое возвращает массив (квадратные скобочки) комментариев (Comment). Также этому полю можно передать аргумент с именем limit, который является числом и по умолчанию имеет значение 10.

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 типов.

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

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

  • для IDE (GraphiQL, GraphQL Playground, Altair GraphQL Client) в которой можно
    • удобно писать запросы с валидацией и автокомплитом
    • просматривать документацию
  • для линтеров, которые проверяют корректность запросов в коде. Пишете в запросе несуществующее поле или аргумент - получаете ошибку. Например для JS и eslint, есть eslint-plugin-graphql
  • для тайпчекеров (Flowtype, TypeScript). Вы пишете graphql-запрос, а вам генерируются файлы с дефинишенами для ответов и входящих аргументов. Вы импортируете сгенерированные типы, аттачите к нужным переменным и получаете офигенную статическую проверку кода между клиентом и сервером. Стоит на сервере переименовать поле или изменить у него тип, то вы получите ошибку некорректного использования в конкретных файлах вашего клиента. Например: apollo-cli или relay-compiler
  • для связывания микросервисной архитектуры. Если у вас несколько GraphQL-серверов, то их можно склеить в один большой GraphQL-сервер. Для примера, можно ознакомиться с подходом Schema Stitching от Apollo.

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

Самый простой способ получить интроспекцию в формате 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();

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

  • graphql-cli - настраиваете файл .graphqlconfig и запускаете в терминале graphql get-schema --watch
  • webpack-plugin-graphql-schema-hot - плагин для Webpack которому передается путь до файла схемы schemaPath, а он уже под капотом следит за этим файлом и всеми его зависимостями. И в случае изменений автоматически генерирует json и graphql файлы. Этот плагин полезен для изоморфных приложений, или как минимум тем, у кого серверные скрипты собираются через Webpack.
  • get-graphql-schema - запускаете в терминале get-graphql-schema ENDPOINT_URL > schema.graphql и на выходе получаете схему
  • Есть что добавить? Откройте Pull Request!