如何在Prisma Schema中实现@createdBy与@updatedBy审计注解?
在NestJS + Prisma中实现CreatedBy/UpdatedBy自动审计
一、先在Prisma Schema中定义审计字段
首先要给需要审计的模型添加createdBy和updatedBy字段,建议用片段复用避免重复代码:
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) name String // 关联审计字段的反向关系,注意区分关系名 createdPosts Post[] @relation("CreatedBy") updatedPosts Post[] @relation("UpdatedBy") } // 审计字段复用片段 fragment AuditFields on Model { createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @updatedAt @db.Timestamp(6) createdBy Int updatedBy Int creator User @relation("CreatedBy", fields: [createdBy], references: [id]) updater User @relation("UpdatedBy", fields: [updatedBy], references: [id]) } // 示例模型:引用审计片段 model Post { id Int @id @default(autoincrement()) title String content String? ...AuditFields // 注入审计字段 }
执行npx prisma migrate dev同步数据库结构。
二、用AsyncLocalStorage存储请求上下文
Prisma中间件无法直接获取请求中的用户信息,需要用Node.js的AsyncLocalStorage在请求生命周期内保存当前用户ID:
// src/common/context/context.service.ts import { Injectable, Scope } from '@nestjs/common'; import { AsyncLocalStorage } from 'async_hooks'; @Injectable({ scope: Scope.REQUEST }) export class ContextService { private readonly als = new AsyncLocalStorage<{ userId: number }>(); runWithUserId(userId: number, callback: () => void) { this.als.run({ userId }, callback); } getUserId(): number | undefined { return this.als.getStore()?.userId; } }
然后创建拦截器,从请求中提取用户ID并注入上下文:
// src/common/interceptors/context.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { ContextService } from '../context/context.service'; @Injectable() export class ContextInterceptor implements NestInterceptor { constructor(private readonly contextService: ContextService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); // 这里假设用户信息已通过认证守卫存入request.user,根据你的认证逻辑调整 const userId = request.user?.id; if (userId) { return new Observable(observer => { this.contextService.runWithUserId(userId, () => { next.handle().subscribe({ next: val => observer.next(val), error: err => observer.error(err), complete: () => observer.complete(), }); }); }); } return next.handle(); } }
在AppModule中全局注册拦截器:
// src/app.module.ts import { Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { ContextInterceptor } from './common/interceptors/context.interceptor'; import { ContextService } from './common/context/context.service'; @Module({ providers: [ ContextService, { provide: APP_INTERCEPTOR, useClass: ContextInterceptor, }, ], }) export class AppModule {}
三、编写Prisma中间件自动填充审计字段
通过Prisma中间件拦截CRUD操作,自动注入createdBy和updatedBy:
// src/prisma/middlewares/audit.middleware.ts import { Prisma } from '@prisma/client'; import { ContextService } from '../../common/context/context.service'; export function auditMiddleware(contextService: ContextService): Prisma.Middleware { return async (params, next) => { const userId = contextService.getUserId(); // 无用户ID时(如后台任务)直接执行原操作 if (!userId) return next(params); const { model, action } = params; // 可自定义需要审计的模型白名单 const auditableModels = ['Post', 'Comment', 'User']; if (!auditableModels.includes(model)) return next(params); // 创建操作:同时填充createdBy和updatedBy if (action === 'create') { params.args.data = { ...params.args.data, createdBy: userId, updatedBy: userId }; } // 更新/批量更新:填充updatedBy if (action === 'update' || action === 'updateMany') { params.args.data = { ...params.args.data, updatedBy: userId }; } // Upsert操作:创建时填充createdBy,更新时填充updatedBy if (action === 'upsert') { params.args.create = { ...params.args.create, createdBy: userId, updatedBy: userId }; params.args.update = { ...params.args.update, updatedBy: userId }; } return next(params); }; }
在Prisma服务中注册中间件:
// src/prisma/prisma.service.ts import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; import { auditMiddleware } from './middlewares/audit.middleware'; import { ContextService } from '../common/context/context.service'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { constructor(private readonly contextService: ContextService) { super(); } async onModuleInit() { this.$use(auditMiddleware(this.contextService)); await this.$connect(); } }
四、验证使用
现在在业务服务中调用Prisma的CRUD方法时,无需手动传递createdBy/updatedBy,中间件会自动填充:
// src/posts/posts.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreatePostDto } from './dto/create-post.dto'; @Injectable() export class PostsService { constructor(private readonly prisma: PrismaService) {} async create(createPostDto: CreatePostDto) { // 无需手动添加createdBy/updatedBy return this.prisma.post.create({ data: { title: createPostDto.title, content: createPostDto.content, }, }); } async update(id: number, updatePostDto: UpdatePostDto) { return this.prisma.post.update({ where: { id }, data: { title: updatePostDto.title, content: updatePostDto.content, }, }); } }
内容的提问来源于stack exchange,提问作者Dimitri Kopriwa
相关产品推荐
相关产品推荐

