今天要來介紹 prisma 的 extension ,前幾天我們透過 Prisma.validator 讓我們的 prisma 的 queries 有 type safe 的功能,但其實還有別種 customer 的方式,今天我們將介紹如何使用 zod 去幫我們確保 prisma 的 query input ,那我們廢話不多說走起~
以下是今天的 model
user 可以有多個 post
email model 去紀錄有使用過的 email 有哪些model User {
id Int @id @default(autoincrement())
email String
firstName String
lastName String
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(true)
content String?
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model Email {
id Int @id @default(autoincrement())
value String @unique
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}
之後 migrate 好 DB 後我們先到 prisma studio 塞一些 email data ,這邊得 email list 的用處會是等下用來驗證 user create 的 email

接著我們寫一個 getEmails function 去拿到所有合法的 email list
const getEmails = async () => {
const emails = await prismaClient.email.findMany({})
return emails
}
結果如下,可以看到這邊清楚記錄合法 email 存在的時間以及修改的時間
[
{
id: 1,
value: 'hiunji64@gmail.com',
created_at: 2024-10-02T14:02:21.987Z,
updated_at: 2024-10-02T14:02:13.491Z
}
]
之後我們 install zod
>npm i zod
接著我們定好 user create 的 zod schema:
firstName 為必填lastName 為必填email 必須是 email 的格式,同時 value 只能用 getEmails return 的 email list
const userCreateInputSchema = z.object({
firstName: z.string(),
lastName: z.string(),
email: z
.string()
.email('this is not validate emails')
.refine(async (email) => {
const emails = await getEmails()
return emails.findIndex(({ value }) => value === email) !== -1
}, 'This email is not in our database')
})
現在我們做好了一個合法的 email list ,也定好了 input schema ,之後我們需要跟 prisma client 溝通,以確保我們的 query 可以透過我們定好的 schema 去驗證,在 prisma client 有一個 Extension 可以做到,至於怎麼做到的我們接著看以下的 Demo~
在 $extends 中有一個 query fields 裡面有各種我們的 model key ,這邊因為我們要驗證 user create ,所以我們就驗證 user 底下的 methods
const prismaClient = new PrismaClient().$extends({
query: {
user: {
create: async ({ args, query }) => {
args.data = await userCreateInputSchema.parseAsync(args.data)
return query(args)
},
update: async ({ args, query }) => {
args.data = await userCreateInputSchema.parseAsync(args.data)
return query(args)
},
updateMany: async ({ args, query }) => {
args.data = await userCreateInputSchema.partial().parseAsync(args.data)
return query(args)
},
upsert: async ({ args, query }) => {
args.create = await userCreateInputSchema.parseAsync(args.create)
args.update = await userCreateInputSchema.partial().parseAsync(args.update)
return query(args)
},
}
}
})
每個 methods 都有兩個屬性,args 跟 query , args 會是在使用 prisma client 時你 input 的 value
例如我們在 create User 時候我們會用 create api 在 data 輸入的 object 像是 firstName 等等他等同於 args.data ,所以我們才會用userCreateInputSchema.parseAsync 去 parse args.data
const user = await prismaClient.user.create({
data: {
firstName: 'Danny',
lastName: 'Wu',
email: "hiunji64@gmail.com"
}
})
那 query(args) 就是讓 prisma 去執行我們輸入的 input
create: async ({ args, query }) => {
args.data = await userCreateInputSchema.parseAsync(args.data)
return query(args)
}
然後我們寫一個 main function 去測試我們 prisma 的 $extends 結果
const main = async () => {
try {
const user = await prismaClient.user.create({
data: {
firstName: 'Danny',
lastName: 'Wu',
email: "sdfsdf@gmail.com"
}
})
} catch (error) {
if (error instanceof ZodError) {
console.log(error.formErrors)
}
}
}
你會看到當我輸入一個不存在 DB 的 email 時候,會自動跳出 This email is not in our database 的 message 去提醒你的 input 是無效的
{
formErrors: [],
fieldErrors: { email: [ 'This email is not in our database' ] }
}
甚至如果 email 格式不對,也會跳 zod error 提醒你格式要符合 email
const user = await prismaClient.user.create({
data: {
firstName: 'Danny',
lastName: 'Wu',
email: "sdfsdf"
}
})
{
formErrors: [],
fieldErrors: {
email: [
'this is not validate emails',
'This email is not in our database'
]
}
}
所以如果輸入合法的 email 的話就可以成功 create user
const user = await prismaClient.user.create({
data: {
firstName: 'Danny',
lastName: 'Wu',
email: "hiunji64@gmail.com"
}
})
看到以下的 result 就代表你成功了~
{
id: 4,
email: 'hiunji64@gmail.com',
firstName: 'Danny',
lastName: 'Wu',
fullName: 'Danny Wu'
}
最後加碼一個內容就是,有的時候我們可能需要根據特定的欄位去結合我們要的資料,但又不想動到 DB 的欄位的時候,我們可以使用 Computed 的功能,舉個例子來說,以我們的 User Model 為例子,我們有 firstName 跟 lastName ,我們很常會需要一個 fullName 的資料呈現,這時用 Computed 就很適合,使用的方式很簡單我們一樣要在 $extends 加上一個 result 欄位:
const prismaClient = new PrismaClient().$extends({
query: {
user: {
create: async ({ args, query }) => {
args.data = await userCreateInputSchema.parseAsync(args.data)
return query(args)
},
update: async ({ args, query }) => {
args.data = await userCreateInputSchema.parseAsync(args.data)
return query(args)
},
updateMany: async ({ args, query }) => {
args.data = await userCreateInputSchema.partial().parseAsync(args.data)
return query(args)
},
upsert: async ({ args, query }) => {
args.create = await userCreateInputSchema.parseAsync(args.create)
args.update = await userCreateInputSchema.partial().parseAsync(args.update)
return query(args)
},
}
},
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute: (user) => {
return `${user.firstName} ${user.lastName}`
}
}
}
}
})
result : 用來管理 model return 的內容needs : 表示哪些欄位必須要有 value 你才能夠 compute
compute : compute return 的結果之後我們 query 一下 data
const user = await prismaClient.user.findFirst({
where: {
email: 'hiunji64@gmail.com'
}
})
你會看到我們成功多一個 fullName 欄位了,是不是很開心又激動呢~
{
id: 1,
email: 'hiunji64@gmail.com',
firstName: 'Danny',
lastName: 'Wu',
fullName: 'Danny Wu'
}
✅ 前端社群 :
https://lihi3.cc/kBe0Y