GraphQL + Typescript = любовь. TypeGraphQL v1.0

oq_itrrbev4abzbws_dkqyyyd_e.png


19 августа вышел в релиз фреймворк TypeGraphQL, упрощающий работу с GraphQL на Typescript. За два с половиной года проект обзавёлся солидным комьюнити и поддержкой нескольких компаний и уверено набирает популярность. Спустя более 650 коммитов у него более 5000 звёзд и 400 форков на гитхабе, плод упорной работы польского разработчика Михала Литека. В версии 1.0 значительно улучшилась производительность, схемы получили изоляцию и избавились от прежней избыточности, появились две крупные фичи — директивы и расширения, фреймворк был приведён к полной совместимости с GraphQL.

Для чего этот фреймворк?


Михал, ссылаясь на свой опыт работы с голым GraphQL, называет процесс разработки «болезненным» из-за избыточности и сложности изменения существующего кода:

  1. Сначала нужно создать все необходимые типы GraphQL в формате SDL (Schema Definition Language);
  2. Затем мы создаём модели данных в ORM (Object-Relational Mapping), чтобы описать объекты в базе данных;
  3. После этого надо написать преобразователи для всех запросов, мутаций и полей, что вынуждает нас…
  4. Создавать тайпскриптовые интерфейсы для всех аргументов, инпутов и даже типов объектов.
  5. Только после этого преобразователями можно пользоваться, не забывая при этом вручную проверять рутинные вещи вроде валидации, авторизации и загрузки зависимостей.

Звучит не очень практично, и при таком подходе главная проблема — в избыточности кода, которая затрудняет синхронизацию всех параметров при его написании и добавляет рисков при изменениях. Чтобы добавить новое поле к нашей сущности, мы должны перебрать все файлы: изменить класс сущности, затем изменить часть схемы и интерфейс. То же самое и с входными данными или аргументами, легко забыть обновить один элемент или ошибиться в одном типе.

Для борьбы с избыточностью и автоматизации всего этого ручного труда и был создан 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. Теперь, с нарастающей популярностью и финансовой поддержкой, он может стать настоящим стандартом в своей области.

Сайт
Гитхаб
Доки
Твиттер Михала

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru