使用GraphQL和Prisma:使用『PlanetGraphQL』快速创建GraphQL API试试看

首先

本次将介绍在应用开发中使用GraphQL API实现所使用的库。
所介绍的库是『PlanetGraphQL』。

PlanetGraphQL的特点

    • Node.js用ORMのPrismaと親和性が高く、DBのモデル定義からCode Firstで簡単にGraphQLスキーマの生成・カスタマイズが可能

 

    バリデーションや権限、ページネーションなど様々な機能に対応

让我们实现一个简单的GraphQL API。

让我们来看一下如何通过使用库来实现GraphQL API的流程。
值得注意的是,本次我们将根据PlanetGraphQL的示例来大致了解概述。

安装

npm install @planet-graphql/core

GraphQL模式的定义

我們將初始化 Prisma,並對於模式檔案進行寫入 DB 模型的定義。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// PlanetGraphQLのジェネレータを設定
generator pg {
  provider = "planet-graphql"
}

// Userモデルを定義
model User {
  id        Int      @id @default(autoincrement())
  email     String
  firstName String
  lastName  String
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

// Postモデルを定義
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  isPublic  Boolean
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  tags      String[]
}

我們將從這裡開始以程式碼的方式定義GraphQL架構。

首先,我们需要自定义并定义User模型。

export const user = pgpc.redefine({
  name: 'User',
  fields: (f, b) => ({
    ...omit(f, 'email'),
    posts: f.posts.prismaArgs(() => args.findManyPost.build()),
    fullName: b.string(),
    latestPost: b.object(() => postWithoutRelation).nullable(),
  }),
  relations: () => getRelations('User'),
})

此外,通过重新定义方法来定义自定义模型,但是每个属性表示以下内容。

name: モデル名

fields: Userモデルのfield

relations: リレーションの型を正しく推論するために必要なもの(今回の場合は、postsフィールドの型を後述する再定義したPostの型として推論される)

在GraphQL中,可以在fields中定义模型的字段。特别是可以添加、修改和删除字段,可以满足许多不同的要求,如在数据库中不需要但在GraphQL模式中希望有的情况,反之亦然。

在上述的User模型中,执行了以下4个操作。

    • 元々のemailというフィールドを削除

 

    • 元々のpostsというフィールドを変更

fullNameというstring型のフィールドを新たに追加

latestPostというオブジェクト型のフィールドを新たに追加

让我们逐个详细看一下。

...omit(f, 'email'),

在GraphQL模式中,已经删除了原先在DB模型中存在的名为”email”的字段。

posts: f.posts.prismaArgs(() => args.findManyPost.build()),

为了字段”posts”,在原始的”posts”字段上使用prismaArgs()添加参数args。

fullName: b.string(),

我正在添加一个名为fullName的新字段。

latestPost: b.object(() => postWithoutRelation).nullable(),

我添加了一个名为latestPost的新字段,并将其与接下来提到的postWithoutRelation的另一个模型相关联。
此外,由于使用了nullable(),该字段可以接受null值。

接下来,我们将定义一个名为Post的模型。

export const post = pgpc.redefine({
  name: 'Post',
  fields: (f) => ({
    ...f,
    attachments: f.attachments.relay(),
  }),
  relations: () => getRelations('Post'),
})

export const postWithoutRelation = post.copy({
  name: 'PostWithoutRelation',
  fields: (f) => omit(f, 'author', 'attachments'),
})

与User模型类似,我们在字段(fields)中定义GraphQL中的Post模型的字段。

attachments: f.attachments.relay(),

使用relay()可以重新定义与后续的attachments模型相关联的字段,以支持分页。

此外,您可以通过以下方式来定义新模型。

export const postWithoutRelation = post.copy({
  name: 'PostWithoutRelation',
  fields: (f) => omit(f, 'author', 'attachments'),
})

这次我们使用copy()函数,在利用已经定义的post的基础上,进行字段的定制化定义。

接下来,我们将同样定义attachments模型。

export const attachmentWithoutRelation = () =>
  objects.Attachment.copy({
    name: 'AttachmentWithoutRelation',
    fields: (f) => omit(f, 'post'),
  })

解析器的定义

首先,我们将在解析器中定义如何操作之前在User模型中添加的fullName和latestPost字段的数据。

user.implement((f) => ({
  fullName: f.fullName.resolve(({ source }) => source.firstName + source.lastName),
  latestPost: f.latestPost.resolve((params) => {
    return pg.dataloader(params, async (userList) => {
      const posts: any[] = await prisma.$queryRaw`
        SELECT *
        FROM "Post"
        WHERE
          "authorId" IN (${Prisma.join(userList.map((x) => x.id))}) AND
          NOT EXISTS (
            SELECT *
            FROM "Post" AS InnerPost
            WHERE InnerPost."authorId" = "Post"."authorId" AND InnerPost."createdAt" > "Post"."createdAt"
          )
      `
      return userList.map((user) => posts.find((x) => x.authorId === user.id) ?? null)
    })
  }),
}))

在这种情况下,可以使用resolve()来定义。

让我们分别看一下每个定义。

fullName: f.fullName.resolve(({ source }) => source.firstName + source.lastName),

fullName定义了在API响应时将firstName和lastName合并并发送的数据操作。

latestPost: f.latestPost.resolve((params) => {
  return pg.dataloader(params, async (userList) => {
    const posts: any[] = await prisma.$queryRaw`
      SELECT *
      FROM "Post"
      WHERE
        "authorId" IN (${Prisma.join(userList.map((x) => x.id))}) AND
        NOT EXISTS (
          SELECT *
          FROM "Post" AS InnerPost
          WHERE InnerPost."authorId" = "Post"."authorId" AND InnerPost."createdAt" > "Post"."createdAt"
        )
    `
    return userList.map((user) => posts.find((x) => x.authorId === user.id) ?? null)
  })
}),

最新的Post从数据库中获取,并定义了一个数据操作将其返回给所有的User。此外,使用dataloder()可以一次性从数据库中获取目标User的数据,在每个User的数据操作中都不会发生重复的数据库访问,从而实现了处理的效率化。

由于已经完成了与新添加字段相关的解析器定义,因此接下来将定义查询。

export const usersQuery = pg.query({
  name: 'users',
  field: (b) =>
    b
      .object(() => user)
      .relay()
      .relayCursor((node) => ({ id: node.id }))
      .relayOrderBy({ createdAt: 'desc' })
      .relayTotalCount(async () => await prisma.user.count())
      .auth(({ context }) => context.isAdmin)
      .resolve(async ({ prismaArgs }) => {
        return await prisma.user.findMany(prismaArgs)
      }),
})

使用query()函数来定义查询。

此外,在字段设置中,此次我们使用了以下的方法。

object(): query対象のオブジェクト型を設定する

relay(): 設定したオブジェクト型をページネーションに対応するrelay形式に変換する

relayCursor(): relay形式で用いるCursor句を設定する

relayOrderBy(): relay形式で用いるOrderBy句を設定する

relayTotalCount(): relay形式で用いるTotalCountフィールドを設定する

auth(): queryに対する実行権限を設定する

resolve(): queryのリゾルバを設定する

接下来,我们将为 Post 定义查询。

export const postsQuery = pg.query({
  name: 'post',
  field: (b) =>
    b
      .object(() => post)
      .prismaArgs(() =>
        args.findFirstPost
          .edit((f) => ({
            where: f.where,
          }))
          .build(),
      )
      .resolve(async ({ prismaArgs }) => {
        return await prisma.post.findFirstOrThrow(prismaArgs)
      }),
})

这里使用prismaArgs()来设置在GraphQL上调用查询时的参数。
这些参数可以作为参数传递给resolve(),并可在解析器定义中使用。

.resolve(async ({ prismaArgs }) => {
  return await prisma.post.findFirstOrThrow(prismaArgs)
}),

prismaArgs()具有以下特点:如果传递的查询包含关系字段,那么include子句会自动指定,以从数据库中获取关系数据。例如,在当前的帖子查询中指定了作者字段,那么将自动指定include子句以从数据库中获取与帖子相关的用户数据。

同时,通过自定义和指定用于传递给PrismaClient的args,可以简洁地定义解析器,将prismaArgs作为resolve()的参数传递给Prisma的各个方法。

另外,PlanetGraphQL还具有args()方法,除了prismaArgs()之外。

不管是使用哪种方法,生成的GraphQL模式都是相同的。但是,使用args()的话,由于没有像prismaArgs()中那样对Prisma进行动态处理,因此可以在resolve()内以通用的GraphQL封闭处理方式编写代码。

关于Post,让我们也来定义一下mutation。

export const createPostMutation = pg.mutation({
  name: 'createPost',
  field: (b) =>
    b
      .object(() => postWithoutRelation)
      .args(() =>
        args.createOnePost
          .edit((f) => ({
            input: f.data
              .select('PostUncheckedCreateInput')
              .edit((f) => ({
                title: f.title.validation((schema) => schema.max(20)),
                content: f.content,
                isPublic: f.isPublic.default(true),
              }))
              .validation((value) => {
                return !(value.title.length === 0 && value.isPublic)
              }),
          }))
          .build({ type: true }),
      )
      .resolve(async ({ context, args }) => {
        const created = await prisma.post.create({
          data: {
            ...args.input,
            authorId: context.userId,
          },
        })
        return created
      }),
})

在这里,使用args()来接收名为input的参数,以便创建新的post时获取所需的每个项目。

另外,在args()中使用的args.createOnePost是使用了在builder.ts中声明的方法,并且可以通过edit()进行自定义设置,例如验证和设置默认值。

export const pgpc = getPGPrismaConverter(pg, dmmf)
export const { args } = pgpc.convertBuilders()

接下来,我们也将针对附件(attachment)定义变异(mutation)的方式。

export const createAttachmentMutation = pg.mutation({
  name: 'createAttachment',
  field: (b) =>
    b
      .object(attachmentWithoutRelation)
      .args(() =>
        args.createOneAttachment
          .edit((f) => ({
            input: f.data.select('AttachmentUncheckedCreateInput').edit((f) => ({
              name: f.name,
              buffer: f.buffer,
              meta: f.meta,
              postId: f.postId,
            })),
          }))
          .build({ type: true }),
      )
      .resolve(async ({ context, args }) => {
        await prisma.post.findFirstOrThrow({
          where: {
            id: args.input.postId,
            authorId: context.userId,
          },
        })
        const created = await prisma.attachment.create({
          data: {
            ...args.input,
            size: args.input.buffer.byteLength,
          },
        })
        return created
      }),
})

建立模式

由于GraphQL模式和解析器的定义已经完成,我们现在可以进行构建并创建GraphQL API。

const server = createServer({
  schema: pg.build([
    usersQuery,
    postsQuery,
    createPostMutation,
    createAttachmentMutation,
  ]),
  maskedErrors: {
    formatError: (error) => {
      const e = error as GraphQLError
      e.extensions.stack = e.originalError?.stack
      return e
    },
  },
  context: {
    userId: 1,
    isAdmin: true,
  },
})

只需将定义的查询和变异作为参数传递给build()函数,即可执行构建操作。

pg.build([
  usersQuery,
  postsQuery,
  createPostMutation,
  createAttachmentMutation,
]),

尝试通过API提交查询

让我们向通过示例实现的GraphQL API发送查询。本次我们将从以下用户已经在数据库中注册的状态开始。

{
  id: 1,
  email: "xxx@example.com",
  firstName: "Taro",
  lastName: "Tanaka",
}

我们首先运行”users”,然后尝试获取全部用户。

# query
query {
  users {
    edges {
      node {
        id
        firstName
        lastName
        fullName
        posts {
          id
        }
      }
    }
  }
}

# レスポンス
{
  "data": {
    "users": {
      "totalCount": 1,
      "edges": [
        {
          "node": {
            "id": "1",
            "firstName": "Taro",
            "lastName": "Tanaka",
            "fullName": "TaroTanaka",
            "posts": []
          }
        }
      ]
    }
  }
}

由于采用了relay的格式,结果如上所示。此外,可以看到除了firstName / lastName之外,还可以获取自定义定义的fullName。

接下来,让我们尝试使用createPost来创建新的文章。

# mutation
mutation {
  createPost (input: {title: "title1", content: "content1"}) {
    id
    title
    content
    isPublic
    authorId
  }
}

# レスポンス
{
  "data": {
    "createPost": {
      "id": "1",
      "title": "title1",
      "content": "content1",
      "isPublic": true,
      "authorId": 1
    }
  }
}

可以看出Post已经被新建。
另外,isPublic设定如同args()中定义的一样,默认值为true,并且authorId被设置为执行查询的用户(在此情况下,是预先注册的用户)的ID。

接下来,让我们执行一个名为”post”的查询,尝试获取我们之前注册的帖子。

# query
query {
  post ( where: { authorId: { equals: 1 } }){
    id
    title
    content
  }
}

# レスポンス
{
  "data": {
    "post": {
      "id": "1",
      "title": "title1",
      "content": "content1",
      "author": {
        "id": "1"
      }
    }
  }
}

通过prismaFindArgs可以指定where语句,并且可以获取到作为关联字段的作者。

在这里再增加一篇帖子并提交注册后,再次执行用户命令试试看。

# query
query {
  users {
    edges {
      node {
        id
        fullName
        posts {
          id
          title
        }
        latestPost {
          id
          title
        }
      }
    }
  }
}

# レスポンス
{
  "data": {
    "users": {
      "edges": [
        {
          "node": {
            "id": "1",
            "fullName": "TaroTanaka",
            "posts": [
              {
                "id": "1",
                "title": "title1"
              },
              {
                "id": "2",
                "title": "title2"
              }
            ],
            "latestPost": {
              "id": "2",
              "title": "title2"
            }
          }
        }
      ]
    }
  }
}

我們可以看到,我們已經成功獲取到了在關聯字段中定義的“posts”和“latestPost”。
此外,根據我們在自定義的latestPost中定義的resolve()函數的內容,我們也可以確定它成功獲取了最新的帖子。

用汉语将其简洁归纳

本次介绍了一款能够轻松创建GraphQL API的库《PlanetGraphQL》,并进行了简单API的实现。
与其他类似的库相比,《PlanetGraphQL》具有以下优点:

    • Prismaとの親和性が高く、かつカスタマイズの自由度が高い

 

    • インタフェースを定義することなく、型安全に実装できる

 

    スキーマやリゾルバが比較的簡単に設定できる

请在将来实施GraphQL API时一定要尝试使用。

广告
将在 10 秒后关闭
bannerAds