GraphQL + Typescript = любовь. TypeGraphQL v1.0
19 августа вышел в релиз фреймворк TypeGraphQL, упрощающий работу с GraphQL на Typescript. За два с половиной года проект обзавёлся солидным комьюнити и поддержкой нескольких компаний и уверено набирает популярность. Спустя более 650 коммитов у него более 5000 звёзд и 400 форков на гитхабе, плод упорной работы польского разработчика Михала Литека. В версии 1.0 значительно улучшилась производительность, схемы получили изоляцию и избавились от прежней избыточности, появились две крупные фичи — директивы и расширения, фреймворк был приведён к полной совместимости с GraphQL.
Для чего этот фреймворк?
Михал, ссылаясь на свой опыт работы с голым GraphQL, называет процесс разработки «болезненным» из-за избыточности и сложности изменения существующего кода:
- Сначала нужно создать все необходимые типы GraphQL в формате SDL (Schema Definition Language);
- Затем мы создаём модели данных в ORM (Object-Relational Mapping), чтобы описать объекты в базе данных;
- После этого надо написать преобразователи для всех запросов, мутаций и полей, что вынуждает нас…
- Создавать тайпскриптовые интерфейсы для всех аргументов, инпутов и даже типов объектов.
- Только после этого преобразователями можно пользоваться, не забывая при этом вручную проверять рутинные вещи вроде валидации, авторизации и загрузки зависимостей.
Звучит не очень практично, и при таком подходе главная проблема — в избыточности кода, которая затрудняет синхронизацию всех параметров при его написании и добавляет рисков при изменениях. Чтобы добавить новое поле к нашей сущности, мы должны перебрать все файлы: изменить класс сущности, затем изменить часть схемы и интерфейс. То же самое и с входными данными или аргументами, легко забыть обновить один элемент или ошибиться в одном типе.
Для борьбы с избыточностью и автоматизации всего этого ручного труда и был создан TypeGraphQL. В его основе лежит идея хранить всю информацию в одном месте, описывая схему данных через классы и декораторы. Ручной труд по внедрению зависимостей, валидации данных и авторизации фреймворк также берёт на себя, разгружая разработчика.
Принцип работы
Разберём работу TypeGraphQL на примере GraphQL API для базы рецептов.
Так будет выглядеть схема в SDL:
type Recipe {
id: ID!
title: String!
description: String
creationDate: Date!
ingredients: [String!]!
}
Перепишем её в виде класса Recipe:
class Recipe {
id: string;
title: string;
description?: string;
creationDate: Date;
ingredients: string[];
}
Снабдим класс и свойства декораторами:
@ObjectType()
class Recipe {
@Field(type => ID)
id: string;
@Field()
title: string;
@Field({ nullable: true })
description?: string;
@Field()
creationDate: Date;
@Field(type => [String])
ingredients: string[];
}
Подробные правила описания полей и типов в соответствующем разделе документации
Затем мы опишем обычные CRUD запросы и мутации. Для этого создадим контроллер RecipeResolver c RecipeService, переданным в конструктор:
@Resolver(Recipe)
class RecipeResolver {
constructor(private recipeService: RecipeService) {}
@Query(returns => Recipe)
async recipe(@Arg("id") id: string) {
const recipe = await this.recipeService.findById(id);
if (recipe === undefined) {
throw new RecipeNotFoundError(id);
}
return recipe;
}
@Query(returns => [Recipe])
recipes(@Args() { skip, take }: RecipesArgs) {
return this.recipeService.findAll({ skip, take });
}
@Mutation(returns => Recipe)
@Authorized()
addRecipe(
@Arg("newRecipeData") newRecipeData: NewRecipeInput,
@Ctx("user") user: User,
): Promise {
return this.recipeService.addNew({ data: newRecipeData, user });
}
@Mutation(returns => Boolean)
@Authorized(Roles.Admin)
async removeRecipe(@Arg("id") id: string) {
try {
await this.recipeService.removeById(id);
return true;
} catch {
return false;
}
}
}
Здесь декоратор @Authorized () применяется для ограничения доступа для неавторизованных (или обладающих недостаточными правами) пользователей. Подробнее про авторизацию можно почитать в документации.
Пора добавить NewRecipeInput и RecipesArgs:
@InputType()
class NewRecipeInput {
@Field()
@MaxLength(30)
title: string;
@Field({ nullable: true })
@Length(30, 255)
description?: string;
@Field(type => [String])
@ArrayMaxSize(30)
ingredients: string[];
}
@ArgsType()
class RecipesArgs {
@Field(type => Int, { nullable: true })
@Min(0)
skip: number = 0;
@Field(type => Int, { nullable: true })
@Min(1) @Max(50)
take: number = 25;
}
Length, Min и @ArrayMaxSize — это декораторы из класса-валидатора, которые автоматически выполняют проверку полей.
Последний шаг — собственно сборка схемы. Этим занимается функция buildSchema:
const schema = await buildSchema({
resolvers: [RecipeResolver]
});
И всё! Теперь у нас есть полностью рабочая схема GraphQL. В скомпилированном виде она выглядит так:
type Recipe {
id: ID!
title: String!
description: String
creationDate: Date!
ingredients: [String!]!
}
input NewRecipeInput {
title: String!
description: String
ingredients: [String!]!
}
type Query {
recipe(id: ID!): Recipe
recipes(skip: Int, take: Int): [Recipe!]!
}
type Mutation {
addRecipe(newRecipeData: NewRecipeInput!): Recipe!
removeRecipe(id: ID!): Boolean!
}
Это пример базовой функциональности, на самом деле TypeGraphQL умеет использовать ещё кучу инструментов из арсенала TS. Ссылки на документацию вы уже видели :)
Что нового в 1.0
Вкратце пройдёмся по основным изменениям релиза:
Производительность
TypeGraphQL — это по сути дополнительный слой абстракции над библиотекой graphql-js, и он всегда будет работать медленнее её. Но теперь, по сравнению с версией 0.17, на выборке из 25000 вложенных объектов фреймфорк добавляет в 30 раз меньше оверхеда — с 500% до 17% с возможностью ускорения до 13%. Отдельные нетривиальные способы оптимизации описаны в документации.
Изоляция схем
В старых версиях схема строилась из всех метаданных, получаемых из декораторов. Каждый последующий вызов buildSchema возвращал одну и ту же схему, построенную из всех доступных в хранилище метаданных. Теперь же схемы изолированы и buildSchema выдаёт только те запросы, которые напрямую связаны с заданными параметрами. То есть изменяя лишь параметр resolvers мы поулчаем разные операции над схемами GraphQL.
Директивы и расширения
Это два способа добавить метаданные в элементы схемы: директивы GraphQL являются часть SDL и могут быть объявлены прямо в схеме. Также они могут изменять её и выполнять специфические операции, например, сгенерировать тип соединения для пагинации. Применяются они с помощью декораторов @Directive и @Extensions и различаются подходом к построению схемы. Документация Directives, Документация Extensions.
Преобразователи и аргументы для полей интерфейсов
Последний рубеж полной совместимости с GraphQL лежал здесь. Теперь можно определять преобразователи для полей интерфейса, используя синтаксис @ObjectType:
@InterfaceType()
abstract class IPerson {
@Field()
avatar(@Arg("size") size: number): string {
return `http://i.pravatar.cc/${size}`;
}
}
Немногочисленные исключения описаны здесь.
Преобразование вложенных инпутов и массивов
В предыдущих версиях экземпляр класса инпута создавался только на первом уровне вложенности. Это создавало проблемы и баги с их валидацией. Fixed.
Заключение
В течение всего времени разработки, проект оставался открытым к идеям и критике, опенсорсным и зажигательным. 99% кода написал сам Михал Литек, но и сообщество внесло огромный вклад в развитие TypeGraphQL. Теперь, с нарастающей популярностью и финансовой поддержкой, он может стать настоящим стандартом в своей области.
Сайт
Гитхаб
Доки
Твиттер Михала