Введение в GraphQL с помощью Node.js и TypeScript

Привет, ботаники, давно не виделись!

В этом посте я помогу вам получить твердое понимание работы с GraphQL в Node.js и TypeScript с помощью библиотеки под названием TypeGraphQL. TypeGraphQL — это потрясающий способ создания ваших резольверов GraphQL, и он обладает возможностями бесшовной интеграции с такими ORM, как TypeORM (мы будем использовать его в этом посте!) и mikro-orm. Он использует классы и декораторы, чтобы красиво генерировать наши схемы, используя очень мало кода.

Также задержитесь до конца, чтобы найти несколько задач для закрепления ваших навыков!

Что мы будем делать

  • Сначала мы создадим базовый TypeScript-проект.
  • Затем мы настроим TypeORM для взаимодействия с нашей базой данных.
    • Мы создадим объект базы данных Task и подключим его к TypeORM.
  • После этого мы настроим базовый веб-сервер Apollo/Express.
  • И, наконец, мы создадим наш собственный резольвер GraphQL с помощью TypeGraphQL с функцией CRUD (создание, чтение, обновление, удаление).

Итак, давайте начнем!

Настройка проекта TypeScript

Сначала создадим пустой каталог graphql-crud.

$ mkdir graphql-crud
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Вы можете открыть этот каталог с помощью выбранного вами редактора (я буду использовать Visual Studio Code).

Теперь давайте инициализируем его как проект NPM с помощью команды

npm init -y
Войти в полноэкранный режим Выйти из полноэкранного режима

Это создаст базовый package.json.

{
  "name": "graphql-crud",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Отлично!

Теперь, когда у нас установлен проект NPM, мы можем установить TypeScript и определения типов для Node:

yarn add typescript
Войти в полноэкранный режим Выйти из полноэкранного режима

и

yarn add -D @types/node
Войти в полноэкранный режим Выход из полноэкранного режима

Примечание: В этом посте я буду использовать Yarn, не стесняйтесь использовать NPM.

Также нам нужно сделать файл tsconfig.json для настройки компилятора TypeScript, поэтому для этого мы будем использовать библиотеку tsconfig.json.

$ npx tsconfig.json
Вход в полноэкранный режим Выйти из полноэкранного режима

Выберите node из опций

Теперь будет создан TSConfig в вашем корневом каталоге.

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "lib": ["dom", "es6", "es2017", "esnext.asynciterable"],
    "skipLibCheck": true,
    "sourceMap": true,
    "outDir": "./dist",
    "moduleResolution": "node",
    "removeComments": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "resolveJsonModule": true,
    "baseUrl": "."
  },
  "exclude": ["node_modules"],
  "include": ["./src/**/*.ts"]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь давайте создадим простой файл TypeScript.

src/index.ts

console.log("hellooooo");
Вход в полноэкранный режим Выход из полноэкранного режима

Мы не можем запустить этот файл напрямую с помощью Node, поэтому нам нужно скомпилировать его в JavaScript. Для этого создадим сценарий watch в нашем package.json, который будет следить за изменениями наших TypeScript файлов и компилировать их в JavaScript в каталоге dist/.

{
  "name": "graphql-crud",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "watch": "tsc -w"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, если мы запустим npm watch в нашем терминале, он создаст папку dist с нашим скомпилированным JavaScript-кодом. Мы создадим команду dev для запуска этого скомпилированного кода с помощью следующего сценария:

"scripts": {
    "watch": "tsc -w",
    "dev": "nodemon dist/index.js"
},
Войти в полноэкранный режим Выйти из полноэкранного режима

Кстати, убедитесь, что вы установили nodemon либо глобально, либо в этом проекте, чтобы эта команда работала.

Теперь, чтобы запустить этот код, вам нужно запустить yarn watch и yarn dev вместе, чтобы скомпилировать наш TypeScript и запустить скомпилированный код автоматически.

Отлично, теперь наш TypeScript-проект готов к работе! 🔥🔥

Настройка TypeORM

TypeORM — это замечательный ORM, который мы можем использовать для взаимодействия с различными базами данных. Он также имеет очень хорошую поддержку TypeScript, и то, как мы определяем сущности базы данных в TypeORM, будет очень полезно при настройке TypeGraphQL далее в этом посте.

В этом руководстве я буду использовать PostgreSQL в качестве базы данных, но на самом деле вы можете использовать любую реляционную базу данных, которая у вас установлена.

Давайте установим TypeORM и собственный драйвер Postgres для Node:

yarn add typeorm pg
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем заменить код в src/index.ts на следующий:

import { Connection, createConnection } from "typeorm";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "username", // replace with your database user's username
    password: "pass", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [], // we'll add our database entities here later.
  });
};

main().catch((err) => console.error(err));
Вход в полноэкранный режим Выйти из полноэкранного режима

Это, по сути, просто определяет все опции для подключения к базе данных. Мы используем функцию main, потому что ожидание на верхнем уровне — это не вещь, если только вы не используете ES7 или что-то подобное.

Создание нашего Entity.

Прежде всего, я думаю, что некоторые люди не совсем понимают, что такое сущность, поэтому я сейчас немного объясню эту часть.

Как вы уже должны знать, базы данных SQL (такие как Postgres, MySQL и т.д.) состоят из таблиц и столбцов. Как электронная таблица Excel. Каждая таблица содержит поля, связанные с ней. Например:

  • Таблица автомобилей может содержать такие столбцы, как производитель, тип двигателя, цвет и т.д.

Entity в основном определяет структуру таблицы базы данных и соответствующие ей столбцы. В этом посте мы будем выполнять наши CRUD-операции с задачами или Todos. Итак, давайте создадим сущность для задачи.

Во-первых, создайте новый файл в каталоге src/entities.

Чтобы не усложнять, у нас будет 2 колонки для таблицы Task:

  • Название задачи
  • Описание задачи

У нас также будет колонка id, created, и updated.

На самом деле мы не будем использовать созданную и обновленную колонки, но это своего рода лучшая практика 😉.

src/entities/Task.ts

import {
  BaseEntity,
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from "typeorm";

@Entity()
export class Task extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: number;

  @CreateDateColumn()
  created: Date;

  @UpdateDateColumn()
  updated: Date;

  @Column()
  title: string;

  @Column()
  description: string;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Воу, воу, что это?!

Это, мой друг, ✨ магия декораторов ✨.

Этот код чрезвычайно чист и самодокументирован:

  • Мы создаем класс Task с декоратором Entity, указывающим, что этот класс является Entity.
    • Мы расширяем этот класс от BaseEntity, так что некоторые полезные методы, такие как create, delete и т.д. будут открыты для нас с помощью этого класса. Позже вы увидите, что я имею в виду.
  • Затем мы создаем первичный столбец для нашего ID. Это поле ID является целым числом, и оно автоматически генерируется TypeORM!
  • Далее идет столбец «Создано и обновлено», который также автоматически генерируется TypeORM.
  • title и description — это обычная колонка, содержащая заголовок и описание нашей задачи.

И не забудьте добавить сущность Task в массив entities в конфигурации TypeORM:

src/index.ts

import { Connection, createConnection } from "typeorm";
import { Task } from "./entities/Task";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "postgres", // replace with your database user's username
    password: "postgres", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [Task], // we'll add our database entities here later.
  });
};

main().catch((err) => console.error(err));
Вход в полноэкранный режим Выход из полноэкранного режима

Фух! Наконец, давайте приступим к части GraphQL!

Настройка Express с Apollo Server

Мы будем использовать Express в качестве сервера, и мы скажем Express использовать Apollo Server в качестве промежуточного ПО.

Но что такое Apollo Server?

Чтобы понять, что делает Apollo Server, вам нужно знать, как работает GraphQL в своей основе. По сути, в API будет конечная точка REST для GraphQL (вроде как иронично, но да), откуда вы можете запускать запросы и мутации от ваших резольверов. Что делает Apollo Server, так это создает конечную точку для вашего GraphQL, которая будет обслуживаться с некоторыми дополнительными инструментами разработки, такими как GraphQL Playground, который помогает вам тестировать ваши GraphQL-запросы в крутой среде.

Итак, давайте начнем!

Мы установим эти библиотеки:

  • express
$ yarn add express apollo-server-express graphql type-graphql 
Войти в полноэкранный режим Выйти из полноэкранного режима

Также установим определения типов для express:

$ yarn add -D @types/express
Войти в полноэкранный режим Выход из полноэкранного режима

Круто!

Теперь давайте создадим наше приложение Express:

src/index.ts

import { Connection, createConnection } from "typeorm";
import express, { Express } from "express";
import { Task } from "./entities/Task";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "postgres", // replace with your database user's username
    password: "postgres", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [], // we'll add our database entities here later.
  });

  const app: Express = express();

  const PORT = process.env.PORT || 8000;
  app.listen(PORT, () => console.log(`server started on port ${PORT}`));
};

main().catch((err) => console.error(err));
Войти в полноэкранный режим Выход из полноэкранного режима

Давайте также создадим тестовый маршрут, чтобы убедиться, что все работает правильно:

import { Connection, createConnection } from "typeorm";
import express, { Express } from "express";
import { Task } from "./entities/Task";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "postgres", // replace with your database user's username
    password: "postgres", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [Task], // we'll add our database entities here later.
  });

  const app: Express = express();

  app.get("/", (_req, res) => res.send("you have not screwed up!"));

  const PORT = process.env.PORT || 8000;
  app.listen(PORT, () => console.log(`server started on port ${PORT}`));
};

main().catch((err) => console.error(err));
Войти в полноэкранный режим Выход из полноэкранного режима

Примечание: Я использую _ перед req, потому что я не буду использовать эту переменную, а если вы не используете переменную, вы можете поставить перед ней знак подчеркивания.

Теперь давайте откроем наш браузер и перейдем на [localhost:8000/](http://localhost:8000/) и вы должны увидеть что-то вроде этого:

Чтобы добавить Apollo Server в качестве промежуточного ПО для Express, мы можем добавить следующий код:

import { Connection, createConnection } from "typeorm";
import express, { Express } from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "postgres", // replace with your database user's username
    password: "postgres", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [], // we'll add our database entities here later.
  });

  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [],
      validate: false,
    }),
  });

  await apolloServer.start();
  const app: Express = express();

  apolloServer.applyMiddleware({ app });

  app.get("/", (_req, res) => res.send("you have not screwed up!"));

  const PORT = process.env.PORT || 8000;
  app.listen(PORT, () => console.log(`server started on port ${PORT}`));
};

main().catch((err) => console.error(err));
Войти в полноэкранный режим Выйти из полноэкранного режима

Сейчас TypeScript накричит на вас, потому что массив resolvers пуст, но подождите немного.

По сути, мы создаем экземпляр ApolloServer и передаем нашу схему GraphQL в качестве функции buildSchema из type-graphql . Итак, TypeGraphQL преобразует наши резольверы GraphQL (классы TypeScript), которые присутствуют в массивах resolvers в SDL или GraphQL Schema Definition Language, и передает этот SDL как нашу окончательную схему GraphQL серверу Apollo Server.

Давайте также быстро создадим простой GraphQL Resolver:

Для тех из вас, кто не знает, что такое резольвер:

Резольвер — это набор функций, которые генерируют ответ на запрос GraphQL. Проще говоря, резольвер действует как обработчик запросов GraphQL.

  • tutorialspoint.com

src/resolvers/task.ts

import { Query, Resolver } from "type-graphql";

@Resolver()
export class TaskResolver {
  @Query()
  hello(): string {
    return "hello";
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все!

Конечно, теперь мы должны добавить этот резольвер в наш массив resolvers:

src/index.ts

import { Connection, createConnection } from "typeorm";
import express, { Express } from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { Task } from "./entities/Task";
import { TaskResolver } from "./resolvers/task";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "postgres", // replace with your database user's username
    password: "postgres", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [Task], // we'll add our database entities here later.
  });

  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [TaskResolver],
      validate: false,
    }),
  });

  await apolloServer.start();
  const app: Express = express();

  apolloServer.applyMiddleware({ app });

  app.get("/", (_req, res) => res.send("you have not screwed up!"));

  const PORT = process.env.PORT || 8000;
  app.listen(PORT, () => console.log(`server started on port ${PORT}`));
};

main().catch((err) => console.error(err));
Вход в полноэкранный режим Выход из полноэкранного режима

Круто! Теперь давайте посмотрим на наш вывод в терминале…

UnmetGraphQLPeerDependencyError: Looks like you use an incorrect version of the 'graphql' package: "16.2.0". Please ensure that you have installed a version that meets TypeGraphQL's requirement: "^15.3.0".
    at Object.ensureInstalledCorrectGraphQLPackage (/Users/dhruvasrinivas/Documents/graphql-crud/node_modules/type-graphql/dist/utils/graphql-version.js:20:15)
    at Function.checkForErrors (/Users/dhruvasrinivas/Documents/graphql-crud/node_modules/type-graphql/dist/schema/schema-generator.js:47:27)
    at Function.generateFromMetadataSync (/Users/dhruvasrinivas/Documents/graphql-crud/node_modules/type-graphql/dist/schema/schema-generator.js:26:14)
    at Function.generateFromMetadata (/Users/dhruvasrinivas/Documents/graphql-crud/node_modules/type-graphql/dist/schema/schema-generator.js:16:29)
    at buildSchema (/Users/dhruvasrinivas/Documents/graphql-crud/node_modules/type-graphql/dist/utils/buildSchema.js:10:61)
    at main (/Users/dhruvasrinivas/Documents/graphql-crud/dist/index.js:23:54)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
Войти в полноэкранный режим Выход из полноэкранного режима

УХ ОХ! У нас ошибка! Но довольно очевидно, что мы должны сделать, чтобы исправить ее. Мы просто должны использовать указанную версию пакета graphql в нашем package.json.

{
  "name": "graphql-crud",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "watch": "tsc -w",
    "dev": "nodemon dist/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/node": "^17.0.10",
    "apollo-server-express": "^3.6.2",
    "express": "^4.17.2",
    "graphql": "^15.3.0",
    "pg": "^8.7.1",
    "type-graphql": "^1.1.1",
    "typeorm": "^0.2.41",
    "typescript": "^4.5.5"
  },
  "devDependencies": {
    "@types/express": "^4.17.13"
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь давайте переустановим все наши зависимости:

$ yarn
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, если мы запустим наш код, мы не должны получить никаких ошибок!

Apollo Server обслуживает наш GraphQL в конечной точке /graphql.

Давайте откроем ее в нашем браузере.

И нас встречает страница пропаганды Apollo Server 💀.

Забавный факт: на самом деле это новое обновление Apollo Server, ранее оно напрямую открывало GraphQL Playground, интерактивную среду для тестирования наших GraphQL-запросов.

Но нет проблем, мы можем открыть GraphQL Playground с помощью этого плагина Apollo Server Plugin:

src/index.ts

import { Connection, createConnection } from "typeorm";
import express, { Express } from "express";
import { ApolloServer } from "apollo-server-express";
import { buildSchema } from "type-graphql";
import { Task } from "./entities/Task";
import { TaskResolver } from "./resolvers/task";
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core";

const main = async () => {
  const conn: Connection = await createConnection({
    type: "postgres", // replace with the DB of your choice
    database: "graphql-crud", // replace with the name of your DB
    username: "postgres", // replace with your database user's username
    password: "postgres", // replace with your database user's password
    logging: true, // this shows the SQL that's being run
    synchronize: true, // this automatically runs all the database migrations, so you don't have to :)
    entities: [Task], // we'll add our database entities here later.
  });

  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [TaskResolver],
      validate: false,
    }),
    plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
  });

  await apolloServer.start();
  const app: Express = express();

  apolloServer.applyMiddleware({ app });

  app.get("/", (_req, res) => res.send("you have not screwed up!"));

  const PORT = process.env.PORT || 8000;
  app.listen(PORT, () => console.log(`server started on port ${PORT}`));
};

main().catch((err) => console.error(err));
Войти в полноэкранный режим Выйти из полноэкранного режима

Еще один забавный факт: ЭТО САМОЕ ДЛИННОЕ НАЗВАНИЕ ФУНКЦИИ, КОТОРОЕ Я КОГДА-ЛИБО ВИДЕЛ…

О боже. После того, как вы оправитесь от этого атомного удара, если вы обновите страницу, вы сможете найти что-то вроде этого:

Теперь давайте запустим наш запрос hello:

{
  hello
}
Войти в полноэкранный режим Выйти из полноэкранного режима

И вы увидите наш результат:

Потрясающе!!!

Создание CRUD функциональности

Теперь перейдем к основной части — построению нашей CRUD-функциональности. Начнем с самого простого — с получения всех задач:

НО ПОДОЖДИТЕ МИНУТКУ!
Помните ту сущность Task, которую мы создали? Лет сто назад? Да, ту самую.

Это сущность базы данных, но когда мы получаем все задачи, мы должны вернуть Task, а мы не можем вернуть сущность, потому что это глупо. Поэтому нам придется сделать так, чтобы Task стал типом GraphQL. Прежде чем вы начнете жаловаться, помните, я говорил вам, что TypeGraphQL может хорошо интегрироваться с TypeORM? Давайте посмотрим на это в действии!

src/entities/Task.ts

import { Field, Int, ObjectType } from "type-graphql";
import {
  BaseEntity,
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from "typeorm";

@Entity()
@ObjectType()
export class Task extends BaseEntity {
  @PrimaryGeneratedColumn()
  @Field(() => Int)
  id!: number;

  @CreateDateColumn()
  @Field(() => String)
  created: Date;

  @UpdateDateColumn()
  @Field(() => String)
  updated: Date;

  @Column()
  @Field(() => String, { nullable: false })
  title: string;

  @Column()
  @Field(() => String, { nullable: false })
  description: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Почувствуйте магию декоратора ✨.

По сути, мы делаем следующее:

  • Указываем, что этот класс Task также является типом GraphQL!
  • Затем мы украшаем каждый столбец декоратором Field, говоря, что каждый из этих столбцов также является полем типа Task.
  • Мы также явно указываем GraphQL тип каждого Field, которые все исходят из type-graphql.
  • Мы также указываем, что поля title и description должны иметь значение и никогда не могут быть объявлены как null.

Самое замечательное в таком определении сущности и типа GraphQL то, что у вас может быть столбец в базе данных, например, пароль, который вы не хотите раскрывать в ответе, и вы можете просто не украшать его Field, чтобы сделать это!

Получение всех задач

Теперь давайте получим все наши задачи:

src/resolvers/task.ts

import { Query, Resolver } from "type-graphql";
import { Task } from "../entities/Task";

@Resolver()
export class TaskResolver {
  @Query(() => [Task])
  async tasks(): Promise<Task[]> {
    return Task.find();
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь видно, что мы указываем тип возврата GraphQL как массив Task, поскольку мы также сделали его типом GraphQL. Один недостаток, который вы можете найти в этом подходе, заключается в том, что мы определяем типы возврата дважды: один раз для типа возврата GraphQL и один раз для типа возврата функции. Но так уж мы устроены в мире TypeGraphQL 😅.

Отлично, давайте теперь запустим наш запрос:

{
  tasks {
    id
    created
    updated
    title
    description
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

И мы получим вот такой ответ:

{
  "data": {
    "tasks": []
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Массив пуст, потому что мы еще не создали ни одной задачи.

Создание задачи

Теперь я хотел бы задать вам вопрос, если мы используем Query для получения данных, сможем ли мы использовать тот же Query для изменения (создания, обновления, удаления) данных? Нет, не сможем. Для достижения нашей цели мы будем использовать нечто под названием **Mutation**.

Еще одна вещь, о которой вы, возможно, подумали: как именно мы будем принимать входные данные, ведь когда мы создаем задачу, нам нужно будет указать название и описание задачи, верно? Угадайте что, в TypeGraphQL есть декоратор для этого!

Давайте посмотрим на все это в действии. Мы определим новую функцию в нашем решателе задач.

src/resolvers/task.ts

import { Arg, Mutation, Query, Resolver } from "type-graphql";
import { Task } from "../entities/Task";

@Resolver()
export class TaskResolver {
  @Query(() => [Task])
  async tasks(): Promise<Task[]> {
    return Task.find();
  }

  @Mutation(() => Task)
  createTask(
    @Arg("title", () => String) title: string,
    @Arg("description", () => String) description: string
  ): Promise<Task> {
    return Task.create({ title, description }).save();
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Я проведу вас через эту новую функцию построчно, так как сначала она немного запутана.

  • Сначала мы объявляем эту createTask как мутацию GraphQL, которая возвращает тип GraphQL Task, который мы создали. Мы возвращаем Task, потому что после сохранения задачи в базе данных мы хотим показать, что она успешно добавлена.
  • Затем у нас есть 2 переменные, title и string, украшенные Arg. Этот Arg указывает, что эти две переменные будут передаваться в качестве аргументов при выполнении мутации (что мы сделаем через секунду). Тип GraphQL указан как String, но это необязательно, поскольку в большинстве случаев TypeGraphQL может определить тип GraphQL, посмотрев на тип переменной в TypeScript.
  • Затем мы создаем задачу с помощью Task.create и передаем ей переменные title и description, а затем вызываем .save.

Но почему мы делаем и .create, и .save?

По сути, .create делает то, что создает экземпляр класса Task!

Что-то вроде этого:

const task = new Task(....) 
Войти в полноэкранный режим Выйти из полноэкранного режима

А .save фактически сохраняет этот новый экземпляр в нашей базе данных Postgres.

Вам также может быть интересно, почему мы указываем имя переменной как в качестве аргумента для @Arg, так и для переменной TypeScript. То, что мы указываем в качестве строки, на самом деле является именем, которое мы собираемся использовать для предоставления аргумента GraphQL. Например:

@Arg("myrandomarg", () => String) arg: string
Войти в полноэкранный режим Выход из полноэкранного режима

Чтобы запустить эту мутацию, сделаем следующее:

mutation {
    myQuery(myrandomarg: "val") {
        ...
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Поскольку мы все это прояснили, давайте запустим нашу мутацию!

mutation {
  createTask(
    title: "my first post!",
    description: "this is my first post"
  ) {
    id
    created
    updated
    title
    description
  }
} 
Войти в полноэкранный режим Выйти из полноэкранного режима

И мы получаем наш ответ!

{
  "data": {
    "createTask": {
      "id": 1,
      "created": "1643090973749",
      "updated": "1643090973749",
      "title": "my first post!",
      "description": "this is my first post"
    }
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Потрясающе!

Теперь мы можем сделать еще одну вещь: поскольку мы создали задачу, мы можем попробовать получить все наши задачи снова.

{
  "data": {
    "tasks": [
      {
        "id": 1,
        "created": "1643090973749",
        "updated": "1643090973749",
        "title": "my first post!",
        "description": "this is my first post"
      }
    ]
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

И все работает на ура 🎉.

Получение отдельного поста по ID

Это должно быть довольно просто, поскольку мы уже знаем, как включать аргумент.

src/resolvers/task.ts

@Query(() => Task, { nullable: true })
async task(@Arg("id", () => Int) id: number): Promise<Task | undefined> {
  return Task.findOne({ id });
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Здесь мы говорим, что этот Query возвращает единственную Task и может вернуть null, если задача с таким ID не найдена.

Примечание: Int происходит от type-graphql.

Также тип возврата TypeScript:

Promise<Task | undefined>
Войти в полноэкранный режим Выйти из полноэкранного режима

Это в основном говорит, что эта функция может вернуть Promise of a Task, если найдена задача с таким-то и таким-то ID, но в противном случае она вернет undefined.

А мы используем Task.findOne() для получения одной задачи и предоставляем ID в качестве поискового запроса.

Итак, если мы запустим этот запрос с помощью:

{
  task (id: 1) {
    id
    title
    description
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы получим следующий ответ:

{
  "data": {
    "task": {
      "id": 1,
      "title": "my first post!",
      "description": "this is my first post"
    }
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

А если мы укажем несуществующий ID, то получим в ответ null:

{
  task (id: 1717) {
    id
    title
    description
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима
{
  "data": {
    "task": {
      "id": 1,
      "title": "my first post!",
      "description": "this is my first post"
    }
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Удаление задания

Удаление поста очень похоже на функцию, которую мы создали для получения одного поста.

src/resolvers/task.ts

@Mutation(() => Boolean)
async deleteTask(@Arg("id", () => Int) id: number): Promise<boolean> {
  if (await Task.findOne({ id })) {
    await Task.delete(id);
    return true;
  } else {
    return false;
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Здесь мы возвращаем тип Boolean GraphQL. Сначала мы проверяем, существует ли пост с указанным ID, затем удаляем его и возвращаем true, а если нет, то возвращаем false.

Давайте запустим эту мутацию:

mutation {
  deleteTask(id: 2) 
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Примечание: Сначала создайте другую задачу, а затем запустите эту мутацию.

И вы получите такой ответ!

{
  "data": {
    "deleteTask": true
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, наконец, мы создадим последнюю функцию для обновления нашей Задачи.

Обновление задачи

Чтобы обновить задачу, нам нужно получить:

  • идентификатор задачи
  • новый заголовок
  • новое описание

Затем нам нужно проверить, существует ли пост с указанным ID, если нет, то мы вернем null.

Затем мы проверим, предоставлен ли заголовок или описание, и если да, то обновим задачу с помощью Task.update.

src/resolvers/task.ts

@Mutation(() => Task, { nullable: true })
async updateTask(
  @Arg("title", () => String, { nullable: true }) title: string,
  @Arg("description", () => String, { nullable: true }) description: string,
  @Arg("id", () => Int) id: number
): Promise<Task | null> {
  const task = await Task.findOne(id);
  if (!task) {
    return null;
  }
  if (typeof title !== "undefined") {
    await Task.update({ id }, { title });
  }

  if (typeof description !== "undefined") {
    await Task.update({ id }, { description });
  }
  return task;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Все это знакомый код, просто сложность нашей операции немного выше. Давайте теперь протестируем эту мутацию:

mutation {
  updateTask(id: 1, title: "first post by me!") {
    id
    title
    description
  }
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

И мы получим наш ответ:

{
  "data": {
    "updateTask": {
      "id": 1,
      "title": "my first post!",
      "description": "this is my first post"
    }
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Если мы выполним запрос get task by ID, то увидим нашу обновленную задачу:

{
  task (id: 1) {
    id
    title
    description
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Ответ:

{
  "data": {
    "task": {
      "id": 1,
      "title": "first post by me!",
      "description": "this is my first post"
    }
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Вот и все!!! Мы закончили с нашим CRUD!!! 🚀🚀

Вызов

Как и было обещано, вы можете попробовать реализовать перечисленные ниже функции, чтобы улучшить свое понимание концепции 💪

  • Сделайте булево поле isComplete в сущности Task.
  • Сделайте мутацию markComplete для изменения значения isComplete задачи.
  • Вы также можете сделать простой запрос filter для поиска задач на основе аргумента title, заданного пользователем.

Если вам нужна помощь в реализации любого из этих запросов, оставьте комментарий, и я отвечу на ваш вопрос!

Исходный код вы можете найти ниже:

carrotfarmer / graphql-crud

Простой CRUD с использованием TypeGraphQL и TypeORM

На этом мы закончили, до встречи в следующем посте!

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *