На данный момент существует 5 способов построения GraphQL-схем в NodeJS:
makeExecutableSchema({ typeDefs, resolvers })
. (2016 Apr)На мой вкус и на текущий момент самым крутым и продвинутым является type-graphql (по состоянию на апрель 2019).
Давай построим простую GraphQL-схему на каждом из этих подходов. Представим что у нас есть два типа Author
и Article
со следующими данными
export const articles = [
{ title: 'Article 1', text: 'Text 1', authorId: 1 },
{ title: 'Article 2', text: 'Text 2', authorId: 1 },
{ title: 'Article 3', text: 'Text 3', authorId: 2 },
{ title: 'Article 4', text: 'Text 4', authorId: 3 },
{ title: 'Article 5', text: 'Text 5', authorId: 1 },
];
export const authors = [
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' },
];
Это базовая реализация спецификации GraphQL. Вы создаете свои типы, сразу указываете в них всю бизнес логику. Вы не можете редактировать и расширять типы. Пакету graphql
это и не нужно. Его задача, жестко и квадратно задать конфигурацию схемы и уже быстро выполнять на ней запросы в рантайме.
Сперва вы импортируете нужные для вашей схемы классы типов:
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
} from 'graphql';
import { authors, articles } from './data';
Далее создаете тип для Автора
:
const AuthorType = new GraphQLObjectType({
name: 'Author',
description: 'Author data',
fields: () => ({
id: { type: GraphQLInt },
name: { type: GraphQLString },
}),
});
Затем тип для Статьи
. Особое внимание обратите на поле author
, где используется созданный на предыдущем шаге тип Автор
и указывается resolve
-метод для получения данных автора согласно значению Article.authorId
. Таким образом задается связь между двумя типами. В рамках REST API это бы звалось подзапросом - для каждой записи Статьи
сделать подзапрос для получения данных Автора
:
const ArticleType = new GraphQLObjectType({
name: 'Article',
description: 'Article data with related Author data',
fields: () => ({
title: {
type: new GraphQLNonNull(GraphQLString),
},
text: {
type: GraphQLString,
},
authorId: {
type: new GraphQLNonNull(GraphQLInt),
description: 'Record id from Author table',
},
author: {
type: AuthorType,
resolve: source => {
const { authorId } = source;
return authors.find(o => o.id === authorId);
},
},
}),
});
После того как мы объявили узлы нашей схемы, нам надо описать вершину - точку входа. Корневой узел, который будет вам доступен на верхнем уровне вашей схемы для начала получения данных. Корневых узла в GraphQL-схеме три - это Query
, Mutation
и Subscriptions
.
У нас схема простая - мы только читаем данные, поэтому объявляем только Query
. Мы объявим два поля:
articles
- для получения Статей с возможностью указания лимитаauthors
- для получения полного списка Авторовconst Query = new GraphQLObjectType({
name: 'Query',
fields: {
articles: {
args: {
limit: { type: GraphQLInt, defaultValue: 3 },
},
type: new GraphQLList(ArticleType),
resolve: (_, args) => {
const { limit } = args;
return articles.slice(0, limit);
},
},
authors: {
type: new GraphQLList(AuthorType),
resolve: () => authors,
},
},
});
Когда корневой тип Query
создан, мы можем построить схему:
const schema = new GraphQLSchema({
query: Query,
});
export default schema;
Полный код построения схемы на подходе graphql
доступен в этом файле.
graphql-tools использует под капотом graphql
, только меняет принцип сборки вашей схемы. Вы отдельно объявляете все типы на SDL-языке (текстовый формат), и отдельно объявляете resolve
-методы.
Вам нужно импортировать только метод makeExecutableSchema
, который склеит ваши типы и resolve-методы:
import { makeExecutableSchema } from 'graphql-tools';
import { authors, articles } from './data';
Дальше вы объявляете типы:
const typeDefs = `
"Author data"
type Author {
id: Int
name: String
}
"Article data with related Author data"
type Article {
title: String!
text: String
"Record id from Author table"
authorId: Int!
author: Author
}
type Query {
articles(limit: Int = 10): [Article]
authors: [Author]
}
`;
Затем для ключевых типов, вы объявляете методы получения данных. Объявляем как в типе Article
надо получать автора, и как в корневом типе Query
получить список статей и авторов:
const resolvers = {
Article: {
author: source => {
const { authorId } = source;
return authors.find(o => o.id === authorId);
},
},
Query: {
articles: (_, args) => {
const { limit } = args;
return articles.slice(0, limit);
},
authors: () => authors,
},
};
Ну а теперь после того, как есть описание типов
и resolve
-методы их надо склеить вместе, чтоб получить схему. makeExecutableSchema
занимается именно этим, создавая под капотом объекты типов как бы мы это делали в самом первом подходе graphql
:
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
export default schema;
Полный код построения схемы на подходе graphql-tools
доступен в этом файле.
graphql-tools
Рекомендую использовать пакет graphql-code-generator
, который сможет вам генерировать тайпинги для резолверов исходя из SDL схемы.
Прописываете следующий конфиг codegen.yml
:
overwrite: true
schema: src/**/*.gql
documents: null
generates:
src/__generated__/graphql.ts:
plugins:
- "typescript-common"
- "typescript-server"
- "typescript-resolvers"
src/__generated__/schema.graphql.json:
plugins:
- "introspection"
src/__generated__/schema.graphql:
plugins:
- "graphql-codegen-schema-ast"
Выполняете команду:
gql-gen --config codegen.yml
А потом облагораживаете свои резолверы следующим образом:
import { IResolvers } from './__generated__/graphql';
const resolvers: IResolvers = {
Article: {
author: () => {},
},
Query: {
articles: (_, args) => {},
authors: () => authors,
},
};
И получаете отменную проверку от TypeScript’а.
graphql-compose - под капотом использует graphql
. При этом позволяет конструировать схемы несколькими способами:
graphql
с кучей синтаксического сахара при создании типов.graphql-tools
описав типы через SDL и предоставив отдельно резолверы к ним.Но самое главное graphql-compose
, позволяет модифицировать типы, перед тем как будет построена GraphQL-схема. Это открывает возможности генерировать ваши схемы, комбинировать несколько схем, либо редактировать уже существующие (например генерировать урезанную публичную схему из полной админской).
Строится GraphQL-схема следующим образом. Импортируем schemaComposer
глобальный регистр типов, которые позволяет создавать новые типы множеством удобных способов:
import { schemaComposer } from 'graphql-compose';
import { authors, articles } from './data';
Метод createObjectTC()
позволяет создать Object-тип с помощью SDL, как в graphql-tools
. Давайте объявим простой тип Author
:
const AuthorType = schemaComposer.createObjectTC(`
"Author data"
type Author {
id: Int
name: String
}
`);
Также createObjectTC()
позволяет создать Object-тип как в подходе с graphql
в формате GraphQLObjectType
. Но при этом добавляя кучу сахара:
title
- объявляем тип поля сразу через SDL String!
, под капотом заменится на { type: new GraphQLNonNull(GraphQLString) }
authorId
- т.к. надо добавить дополнительное свойство, то конфигурация поля делается через объект, где type
опять можно указать через SDL и при этом указать description
author
- тип поля можно указать через функцию. Позволяет бороться с hoisting-проблемой, когда у вас два типа импортируют друг от друга. В подходе graphql
вы можете обернуть в функцию только все поля сразу, а т.к. graphql-compose
позволяет читать и редактировать типы, то пришлось добавить эту возможность на уровень типа для каждого поля.const ArticleType = schemaComposer.createObjectTC({
name: 'Article',
description: 'Article data with related Author data',
fields: {
title: 'String!',
text: 'String',
authorId: {
type: 'Int!',
description: 'Record id from Author table',
},
author: {
type: () => AuthorType,
resolve: source => {
const { authorId } = source;
return authors.find(o => o.id === authorId);
},
},
},
});
После того, как мы создали два типа Author
и Article
нам необходимо задать поля для корневого типа Query
. Он уже сразу есть в схеме – schemaComposer.Query
. Query
это инстанс ObjectTypeComposer
который можно редактировать, добавляя или удаляя существующие поля. ObjectTypeComposer
имеет много полезных методов по чтению и редактированию конфигурации GraphQL-типа.
Чтобы добавить два новых поля articles
и authors
, мы воспользуемся методом addFields()
:
schemaComposer.Query.addFields({
authors: {
type: [AuthorType],
resolve: () => authors,
},
articles: {
args: {
limit: { type: 'Int', defaultValue: 3 },
},
type: [ArticleType], // замениться на `new GraphQLList(ArticleType)`
resolve: (_, args) => {
const { limit } = args;
return articles.slice(0, limit);
},
},
});
Ну а после того как мы добавили необходимые поля в Query
, можно сгенерировать GraphQL
-схему:
const schema = schemaComposer.buildSchema();
export default schema;
Полный код построения схемы на подходе graphql-compose
доступен в этом файле.
Мигрировать с graphql-tools
на graphql-compose
и получить все плюшки редактирования, модификации и генерации типов достаточно просто:
- import { makeExecutableSchema } from 'graphql-tools';
+ import { schemaComposer } from 'graphql-compose';
- const schema = makeExecutableSchema({
- typeDefs,
- resolvers,
- });
+ schemaComposer.addTypeDefs(typeDefs);
+ schemaComposer.addResolveMethods(resolvers);
+ const schema = schemaComposer.buildSchema();
Методы addTypeDefs
и addResolveMethods
могут вызываться много раз, позволяя собирать ваши схемы из разных модулей.
Рабочий код можно посмотреть в этом файле.
type-graphql - создает GraphQL-схему используя классы и декораторы (пока работает только c TypeScript). Из коробки предоставляются следующие виды декораторов:
@Authorized(["ADMIN", "MODERATOR"])
@MaxLength(30)
@Field({ complexity: 2 })
Для работы с пакетом type-graphql
необходимо использовать TypeScript c правильными настройками для декораторов в tsconfig.json
:
{
"compilerOptions": {
"target": "es6", // при es5 не работает
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
Также необходимо использовать полифилл reflect-metadata, который позволит использовать TypeScript дефинишены в рантайме. Т.е. при построении GraphQL-схемы значения типов будут браться из тайпскрипта. Этот полифил подключается один раз в самом начале вашего кода.
Наш пример с Authors и Articles будет выглядеть следующим образом.
Сперва импортируем необходимые методы, декораторы и типы:
import 'reflect-metadata';
import {
// methods
buildSchemaSync,
// decorators
Root,
Query,
ObjectType,
Field,
FieldResolver,
Arg,
Resolver,
// types
ID,
} from 'type-graphql';
import { authors, articles } from './data';
Строим класс Author
из которого будет сгенерирован наш GraphQL-тип благодаря декораторам @ObjectType
и @Field
. И создадим класс c резолверами AuthorResolver
, который в Query
добавит поле authors
:
@ObjectType({ description: 'Author data' })
class Author {
@Field(type => ID)
id: number;
@Field({ nullable: true })
name: string;
}
@Resolver(of => Author)
class AuthorResolver {
@Query(returns => [Author])
authors(): Array<Author> {
return authors as any;
}
}
Похожим образом добавим класс Article
и ArticleResolver
, только в резолвере помимо добавления поля в Query
через @Query
декоратор, будет еще использоваться @FieldResolver()
декоратор для описания метода получения данных автора для поля author
:
@ObjectType({ description: 'Article data with related Author data' })
class Article {
@Field()
title: string;
@Field({ nullable: true })
text: string;
@Field(type => ID)
authorId: number;
@Field({ nullable: true })
author: Author;
}
@Resolver(of => Article)
class ArticleResolver {
@Query(returns => [Article])
articles(@Arg('limit', { nullable: true }) limit: number = 3): Array<Article> {
return articles.slice(0, limit) as any;
}
@FieldResolver()
author(@Root() article: Article) {
return authors.find(o => o.id === article.authorId);
}
}
Ну и затем останется только собрать GraphQL-схему с помощью метода buildSchemaSync
:
const schema = buildSchemaSync({
resolvers: [ArticleResolver, AuthorResolver],
// Or it may be a GLOB mask:
// resolvers: [__dirname + '/**/*.ts'],
});
export default schema;
Пример рабочего кода доступен в этом файле.
nexus – декларативный конструктор схемы со встроенным генератором дефинишенов для TypeScript. Продвигается Prisma‘ой.
На данный момент чтобы заработали тайпинги, вы должны запустить сервер. Он будет генерировать вам дефинишены типов, которые будет подхватывать TypeScript. Поэтому для удобной работы в dev-режиме рекомендуется использовать nodemon или более шустрый ts-node-dev для перезапуска сервера при изменении файлов. Поменяли файл схемы, перезапустился сервер, перегенерились тайпинги, TypeScript подхватил изменения и перевалидировал код.
Для построения простой схемы Article
и Author
необходимо подключить пакет nexus
:
import { objectType, queryType, intArg, makeSchema } from 'nexus';
import { authors, articles } from './data';
Далее объявляем типы схемы очень интересным способом, используя функцию objectType
для создания типов, в котором используется метод definition(t)
для определения полей:
const Author = objectType({
name: 'Author',
definition(t) {
t.int('id', { nullable: true });
t.string('name', { nullable: true });
},
});
const Article = objectType({
name: 'Article',
definition(t) {
t.string('title');
t.string('text', { nullable: true });
t.int('authorId', { description: 'Record id from Author table' });
t.field('author', {
nullable: true,
type: 'Author',
resolve: source => {
const { authorId } = source;
return authors.find(o => o.id === authorId) as any;
},
});
},
});
А вот для конструирования корневых типов используется специальные функции, в нашем случае queryType
:
const Query = queryType({
definition(t) {
t.list.field('articles', {
nullable: true,
type: Article,
args: {
limit: intArg({ default: 3, required: true })
},
resolve: (_, args) => {
const { limit } = args;
return articles.slice(0, limit);
},
});
t.list.field('authors', {
nullable: true,
type: Author,
resolve: () => authors,
});
},
});
Ну а дальше используется метод makeSchema
в который необходимо передать все инстансы наших типов – не очень удобно если в вашей схеме сотни типов. Наверняка в будущем придумают что-нибудь получше. Также вы обязательно должны передать пути, куда будут генерироваться тайпинги при запуске сервера:
const schema = makeSchema({
types: [Query, Article, Author],
outputs: {
schema: __dirname + '/nexus-generated/schema.graphql',
typegen: __dirname + '/nexus-generated/typings.ts',
},
});
export default schema;
Рабочий код можно посмотреть в этом файле.
graphql | graphql-tools | graphql-compose | type-graphql | nexus | |
---|---|---|---|---|---|
Дата создания | 2012/2015 | 2016.04 | 2016.07 | 2018.02 | 2018.11 |
GitHub starts | |||||
NPM downloads | |||||
Язык для разработки схемы | JS, TS, Flow | JS, TS, Flow | JS, TS, Flow | TS | JS, TS |
Schema-first (SDL-first) | - | да | да | - | - |
Code-first | да | - | да | да | да |
Редактирование GraphQL-типов | - | - | да | - | - |
Статическая типизация в резолверах | 1/5 нет |
3/5 через сторонние пакеты |
2/5 кроме аргументов |
5/5 из коробки через рефлексию |
4/5 через генерацию файлов из коробки |
Простота в изучении | 3/5 | 5/5 | 2/5 | 4/5 | 4/5 |
Чистота в коде схемы | 1/5 | 5/5 | 4/5 | 4/5 | 3/5 |
Типы полей (модификатор по умолчанию) | optional | optional | optional | Required | Required |
Обратите внимание, пакет type-graphql на данный момент не имеет возможности использовать Namespaced-мутации.
Также рекомендую прочитать хорошую статью про разницу в подходах Schema-first и Code-first
Есть решения, которые позволяют вам генерировать схемы с уже имеющихся баз данных или ORM-моделей. Это совершенно отдельная каста инструментов, и чистыми инструментами по созданию схем их уже назвать нельзя. Т.к. они ограничены БД/моделью – вы не конструируете схему, она для вас генерируется.
graphql-tools
), либо пользоваться уже сгенерированным. Под капотом Scala.graphql-tools
и ститчить (склеивать) вместе несколько схем, либо использовать knex для хитрого получения данных. Под капотом Haskel.graphql-compose
собираете свою схему сразу так, как вам нужно.Обсуждение генераторов происходит в этом issue.
Есть что добавить? Откройте пожалуйста Pull Request.