Как построить безопасный для типов API GraphQL с помощью Pothos и Kysely


Введение

Существует два популярных способа создания GraphQL api: схема-первый и код-первый. Если вы предпочитаете использовать подход schema-first в проекте на TypeScript, мы всегда можем использовать codegen из схемы api. Однако существует тысяча и один способ создать api, используя подход code-first.

И чтобы сделать выбор еще более сложным, в сообществе JavaScript у нас очень много библиотек по сравнению с другими сообществами. Но в последнее время в сообществе TypeScript я заметил одну закономерность — популяризацию автоматического вывода типов данных, потому что это улучшает опыт разработки.

Что мы будем создавать сегодня?

В сегодняшней статье мы создадим GraphQL api, используя фреймворк Koa вместе с библиотекой GraphQL Yoga и Pothos. Кроме того, мы будем использовать Kysely, который представляет собой конструктор запросов, полностью написанный на TypeScript.

Начало работы

В качестве первого шага создайте каталог проекта и перейдите в него:

mkdir gql-ts-api
cd gql-ts-api
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем инициализируйте проект TypeScript:

npm init -y
npm install typescript @types/node --save-dev
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее создайте файл tsconfig.json и добавьте в него следующую конфигурацию:

{
  "compilerOptions": {
    "target": "ESNext",
    "lib": ["ESNext"],
    "module": "ESNext",
    "rootDir": "src",
    "moduleResolution": "node",
    "baseUrl": ".", 
    "types": ["node"],
    "resolveJsonModule": true,
    "allowJs": true,
    "outDir": "dist",
    "removeComments": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы можем установить необходимые зависимости:

npm install koa graphql @graphql-yoga/node @pothos/core @pothos/plugin-simple-objects kysely better-sqlite3 --save
npm install @types/koa @types/better-sqlite3 ts-standard --save-dev
Войти в полноэкранный режим Выйти из полноэкранного режима

Далее, в package.json добавьте следующие свойства:

{
  "ts-standard": {
    "noDefaultIgnore": false,
    "ignore": [
      "dist"
    ],
    "project": "./tsconfig.json",
    "report": "stylish"
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Всякий раз, когда вы захотите отмыть проект, просто выполните следующую команду:

npx ts-standard --fix
Войти в полноэкранный режим Выйти из полноэкранного режима

Создание схемы базы данных

Когда наш проект настроен, мы можем начать с определения схемы нашей базы пальцев, и для этого мы создадим таблицу dog, которая будет содержать колонки name и breed. Этот способ:

// @/src/db/index.ts
import {
  Kysely,
  SqliteDialect,
  Generated
} from 'kysely'
import SQLite from 'better-sqlite3'

interface DogTable {
  id: Generated<number>
  name: string
  breed: string
}

interface Database {
  dog: DogTable
}

export const db = new Kysely<Database>({
  dialect: new SqliteDialect({
    database: new SQLite('dev.db')
  })
})
Войти в полноэкранный режим Выйти из полноэкранного режима

В сегодняшней статье я не собираюсь выполнять миграции с помощью Kysely, поэтому рекомендую вам создать таблицу под названием dog вместе со свойствами, которые были определены ранее.

Создание конструктора схем

Класс SchemaBuilder используется для создания типов, которые будут вшиты в схему GraphQL. Типы, определенные в нем, инферируются в резолверах, таких как контекст, кроме того, он также позволяет нам регистрировать необходимые плагины.

// @/src/builder.ts
import SchemaBuilder from '@pothos/core'
import SimpleObjectsPlugin from '@pothos/plugin-simple-objects'

import { db } from './db'

interface Root<T> {
  Context: T
}

export interface Context {
  db: typeof db
}

const builder = new SchemaBuilder<Root<Context>>({
  plugins: [SimpleObjectsPlugin]
})

builder.queryType({})
builder.mutationType({})

export { builder }
Вход в полноэкранный режим Выход из полноэкранного режима

Определение некоторых типов

Теперь мы можем определить некоторые типы в схеме нашего GraphQL api, сначала мы создадим объект, который будет представлять некоторую информацию о данных, которые будут возвращены (которые в случае этой статьи разделяются между несколькими резольверами):

// @/src/schema/typeDefs.ts
import { builder } from '../builder'

export const DogObjectType = builder.simpleObject('CreateDogResponse', {
  fields: (t) => ({
    id: t.id(),
    name: t.string(),
    breed: t.string()
  })
})

export const DogObjectInput = builder.inputType('DogObjectInput', {
  fields: (t) => ({
    name: t.string({ required: true }),
    breed: t.string({ required: true }),
    id: t.int()
  })
})
Войти в полноэкранный режим Выход из полноэкранного режима

В приведенном выше коде мы определили объекты без определения типов данных, но у нас все еще есть безопасность типов.

Определение некоторых полей

Следующим шагом будет добавление некоторых полей в нашу схему, таких как запросы и мутации. В каждом из резольверов мы получим объект из базы данных через контекст и выполним необходимые операции.

Мы также будем использовать созданные типы для определения возвратов каждого из резольверов, а также аргументов.

// @/src/schema/resolvers.ts
import { builder } from '../builder'

import { DogObjectType, DogObjectInput } from './typeDefs'

builder.queryField('getDogs', (t) =>
  t.field({
    type: [DogObjectType],
    resolve: async (root, args, ctx) => {
      return await ctx.db.selectFrom('dog').selectAll().execute()
    }
  })
)

builder.queryField('getDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      id: t.arg.int({ required: true })
    },
    resolve: async (root, args, ctx) => {
      return await ctx.db.selectFrom('dog').selectAll().where('id', '=', args.id).executeTakeFirstOrThrow()
    }
  })
)

builder.mutationField('createDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      input: t.arg({
        type: DogObjectInput,
        required: true
      })
    },
    resolve: async (root, args, ctx) => {
      return await ctx.db.insertInto('dog').values({
        name: args.input.name,
        breed: args.input.breed
      }).returningAll().executeTakeFirstOrThrow()
    }
  })
)

builder.mutationField('updateDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      input: t.arg({
        type: DogObjectInput,
        required: true
      })
    },
    resolve: async (root, args, ctx) => {
      const data = {
        id: args.input.id as number,
        name: args.input.name,
        breed: args.input.breed
      }
      return await ctx.db.insertInto('dog').values(data)
        .onConflict((oc) => oc.column('id').doUpdateSet(data))
        .returningAll().executeTakeFirstOrThrow()
    }
  })
)

builder.mutationField('removeDog', (t) =>
  t.field({
    type: DogObjectType,
    args: {
      id: t.arg.int({ required: true })
    },
    resolve: async (root, args, ctx) => {
      return await ctx.db.deleteFrom('dog').where('id', '=', args.id).returningAll().executeTakeFirstOrThrow()
    }
  })
)
Вход в полноэкранный режим Выход из полноэкранного режима

У нас уже определено много вещей, связанных со схемой GraphQL, однако нам все еще нужно скомпилировать нашу схему с кодом в нечто, что сможет интерпретировать наш сервер GraphQL.

// @/src/schema/index.ts
import path from 'path'
import fs from 'fs'

import { printSchema, lexicographicSortSchema } from 'graphql'

import { builder } from '../builder'

import './resolvers'

export const schema = builder.toSchema({})

const schemaAsString = printSchema(lexicographicSortSchema(schema))
fs.writeFileSync(path.join(process.cwd(), './src/schema/schema.gql'), schemaAsString)
Вход в полноэкранный режим Выход из полноэкранного режима

Создание сервера GraphQL

И последнее, но не менее важное: нам просто нужно создать файл входа api, который будет содержать конфигурацию сервера GraphQL, в который мы добавим схему, созданную нами, и добавим экземпляр Kysely в контекст нашего api.

// @/src/main.ts
import Koa from 'koa'
import { createServer } from '@graphql-yoga/node'

import { schema } from './schema'
import { Context } from './builder'
import { db } from './db'

const app = new Koa()

const graphQLServer = createServer<Koa.ParameterizedContext>({
  schema,
  context: (): Context => ({ db })
})

app.use(async (ctx) => {
  const response = await graphQLServer.handleIncomingMessage(ctx.req, ctx)
  ctx.status = response.status
  response.headers.forEach((value, key) => {
    ctx.append(key, value)
  })
  ctx.body = response.body
})

app.listen(4000, () => {
  console.log('Running a GraphQL API server at http://localhost:4000/graphql')
})
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

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

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

Оцените статью
devanswers.ru
Добавить комментарий