
З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<Recipe> { 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: StringcreationDate: Date!ingredients: [String!]!}input NewRecipeInput {title: String!description: Stringingredients: [String!]!}type Query {recipe(id: ID!): Reciperecipes(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. Теперь, с нарастающей популярностью и финансовой поддержкой, он может стать настоящим стандартом в своей области.
Сайт
Гитхаб
Доки
Твиттер Михала