当项目越来越大 GraphQL API 变得复杂时,手动创建
schema和Resolves可能会降低开发效率,二者必须具有相同的结构。否则,可能会导致错误和不可预测的行为。当schema和Resolves发生变化时,这两个组件可能会意外不同步。GraphQL 模式是以字符串形式定义的,因此对于 SDL (Schema Definition Language). 代码没有自动补全和构建时错误检查。
1. GraphQL Pothos介绍
推荐使用 Pothos 来构建 GraphQL Schema,可以使用编程语言来构建 API,这有多重好处:
- 与 TypeScript 完美集成,可以在开发时获得完整的类型提示
- 支持模块化的 Schema 定义,可以更好地管理大型 GraphQL API
- 通过 @pothos/plugin-prisma 插件,可以直接从 Prisma 模型生成 GraphQL 类型
2. Pothos 实践应用
-
安装依赖
npm install @pothos/plugin-prisma @pothos/core -
更新 prisma schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
generator pothos {
provider = "prisma-pothos-types"
}
- 创建 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,
},
})
- 重新定义Graphql Schema
每个模型可以用独立的文件来区分,定义各自的schema
// graphql/schema.ts
import { builder } from './builder'
import './types/User'
import './types/Post'
export const schema = builder.toSchema()
- 重新调整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)
}
- 重新定义查询和更新接口
借助 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)