使用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时一定要尝试使用。