GraphQL Schema – это описание ваших типов данных на сервере, связей между ними и логики получения этих самых данных.
Еще раз по пунктам:
resolve-методы
)описание типов
)resolve-методы
и описание типов
хитро перемешиваете и получаете вашу GraphQL Schema
.Так как же данные? В каком формате и какой базе данных они будут храниться? В любой! Это неважно, когда у вас есть вами написанные resolve-методы
, в которых вы напишете куда и как сходить за данными и обработать перед тем как отдать клиенту.
GraphQL задает канву, формат того как вы описываете доступ к своим данным. Это дело описывается через GraphQL-спецификацию. Которую ребята из Фейсбука очень кропотливо прорабатывали и в 2017 опубликовали под OWFa 1.0 соглашением (грубо говоря MIT-лицензией).
Так, хорошо. А какой язык программирования мне нужно использовать? Любой! Спецификацию уже реализовали на большинстве языков программирования.
Работает с любыми базами данных, на любом языке программирования - звучит хорошо!
Чтобы запустить свой 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 { ... }
}
Предположим у нас объявлена следующая схема:
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
, который решает следующие задачи:
query
schema
(если запрошены несуществующие поля, или переданы неверные аргументы, то будет возвращена ошибка)query
, вызывая их resolve-методы
. Подробнее об Object-типе можно почитать в разделе о Типах.null
, то будет возвращена ошибка)GraphQL по натуре строго типизированный, поэтому во всех запросах проверяются входящие переменные и аргументы, формат возвращаемого ответа на соответствие типов, которые объявлены в GraphQL-схеме
. Если что-то некорректно запросили или вернули, то будет возвращена ошибка.
Пакет graphql
ничего не знает о сети, правах доступа, не слушает никакой порт. Всё это дело реализуется на другом уровне абстракции - GraphQL сервере
. В разделе GraphQL-сервер вы найдете больше подробностей о том, что должен делать сервер, и какие популярные пакеты сейчас доступны в npm. Важно знать, что все сервера используют под капотом метод graphql()
из пакета graphql
.
В спецификации GraphQL для описания типов используется Schema Definition Language (SDL). Это простой, выразительный и интуитивно понятный формат описания типов, который не зависит ни от какого языка программирования. Не путать с Query Language — т.к. это другой формат для написания GraphQL-запросов, а не схемы.
Простое описание 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 Credentials {
login: String!
password: String!
}
enum Direction {
NORTH
EAST
SOUTH
WEST
}
interface NamedEntity {
name: String
}
interface ValuedEntity {
value: Int
}
type Business implements NamedEntity & ValuedEntity {
name: String
value: Int
employeeCount: Int
}
union SearchResult = Photo | Person
type Person {
name: String
age: Int
}
type Photo {
height: Int
width: Int
}
type SearchQuery {
firstSearchResult: SearchResult
}
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 сгенерировать файл с интроспекцией схемы можно с помощью следующих скриптов:
import fs from 'fs';
import { printSchema } from 'graphql';
import schema from './your-schema';
fs.writeFileSync('./schema.graphql', printSchema(schema));
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();
.graphqlconfig
и запускаете в терминале graphql get-schema --watch
schemaPath
, а он уже под капотом следит за этим файлом и всеми его зависимостями. И в случае изменений автоматически генерирует json
и graphql
файлы. Этот плагин полезен для изоморфных приложений, или как минимум тем, у кого серверные скрипты собираются через Webpack.get-graphql-schema ENDPOINT_URL > schema.graphql
и на выходе получаете схему