全栈开发个人博客07.Pothos模块化与Prisma集成

129 阅读2分钟

当项目越来越大 GraphQL API 变得复杂时,手动创建schemaResolves可能会降低开发效率,二者必须具有相同的结构。否则,可能会导致错误和不可预测的行为。当schemaResolves发生变化时,这两个组件可能会意外不同步。GraphQL 模式是以字符串形式定义的,因此对于 SDL (Schema Definition Language). 代码没有自动补全和构建时错误检查。

1. GraphQL Pothos介绍

推荐使用 Pothos 来构建 GraphQL Schema,可以使用编程语言来构建 API,这有多重好处:

  • 与 TypeScript 完美集成,可以在开发时获得完整的类型提示
  • 支持模块化的 Schema 定义,可以更好地管理大型 GraphQL API
  • 通过 @pothos/plugin-prisma 插件,可以直接从 Prisma 模型生成 GraphQL 类型

2. Pothos 实践应用

  1. 安装依赖 npm install @pothos/plugin-prisma @pothos/core

  2. 更新 prisma schema

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
  output   = "../src/generated/prisma"
}

generator pothos {
  provider = "prisma-pothos-types"
}
  1. 创建 Pothos schema builder
// graphql/builder.ts

import SchemaBuilder from '@pothos/core'
import PrismaPlugin from '@pothos/plugin-prisma'
import prisma from '@/lib/prisma'
import { DateTimeResolver } from 'graphql-scalars'

export const builder = new SchemaBuilder<{
  PrismaTypes: any
  Context: {
    prisma: typeof prisma
  }
}>({
  plugins: [PrismaPlugin],
  prisma: {
    client: prisma,
    dmmf: (prisma as any)._dmmf,
  },
})
  1. 重新定义Graphql Schema

每个模型可以用独立的文件来区分,定义各自的schema

// graphql/schema.ts

import { builder } from './builder'

import './types/User'
import './types/Post'
export const schema = builder.toSchema()
  1. 重新调整api接口
// src/api/graphql/route.ts
import { createYoga } from 'graphql-yoga'
import { schema } from '../../../../graphql/schema'
import { NextRequest } from 'next/server'

const { handleRequest } = createYoga({
  schema,
})

export async function GET(request: NextRequest) {
  return handleRequest(request, {} as any)
}

export async function POST(request: NextRequest) {
  return handleRequest(request, {} as any)
}
  1. 重新定义查询和更新接口

借助 Pothos' Prisma plugin,可以使用 prismaObject 来直接引用 prisma中定义的字段类型来定义,编辑器也会自动提示,也可以使用 objectType 来定义新的对象类型

//types/Post.ts
import prisma from '@/lib/prisma'
import { builder } from '../builder'

builder.prismaObject('Post', {
  fields: (t: any) => ({
    id: t.exposeID('id'),
    title: t.exposeString('title'),
    summary: t.exposeString('summary'),
    content: t.exposeString('content'),
    createdAt: t.expose('createdAt', { type: 'DateTime' }),
    updatedAt: t.expose('updatedAt', { type: 'DateTime' }),
    createdById: t.exposeID('createdById'),
    createdBy: t.relation('createdBy'),
  }),
})

// 定义分页结果类型
builder.objectType('PaginatedPosts' as any, {
  fields: (t) => ({
    posts: t.field({
      type: ['Post'] as any,
      resolve: (parent) => parent.posts,
    }),
    postsCount: t.int({
      resolve: (parent) => parent.postsCount,
    }),
  }),
})

// 定义查询类型
builder.queryType({
  fields: (t) => ({
    //单个文章查询
    post: t.field({
      type: 'Post' as any,
      args: {
        id: t.arg.id(),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        const { id } = args
        const post = await ctx.prisma.post.findUnique({
          where: { id },
        })
        return post
      },
    }),
    // 分页查询
    paginatedPosts: t.field({
      type: 'PaginatedPosts' as any,
      args: {
        skip: t.arg.int({ defaultValue: 0 }),
        take: t.arg.int({ defaultValue: 10 }),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        const { skip, take } = args

        // 获取总数
        const totalCount = await ctx.prisma.post.count()

        // 获取分页数据
        const posts = await ctx.prisma.post.findMany({
          skip: skip || 0,
          take: take || 10,
          orderBy: {
            id: 'desc', // 按 ID 降序排列,最新的在前面
          },
        })

        return {
          posts,
          postsCount: totalCount,
        }
      },
    } as any),
  }),
})

// 定义添加文章的类型
builder.mutationType({
  fields: (t: any) => ({
    addPost: t.field({
      type: 'Post' as any,
      args: {
        title: t.arg.string(),
        summary: t.arg.string(),
        content: t.arg.string(),
      },
      resolve: async (_root: any, args: any, ctx: any) => {
        if (ctx.user?.role !== 'ADMIN') {
          throw new Error('you are not admin')
        }
        const { title, summary, content } = args

        const post = await ctx.prisma.post.create({
          data: { title, summary, content, createdById: ctx.user.id },
        })

        return post
      },
    }),
  }),
} as any)