003.nestjs后台管理项目-数据库之prisma(下)

75 阅读2分钟

需求背景

给所有model添加 审计字段 ,deleted,createdAt,createdBy,updatedAt,updatedBy,deletedAt,deletedBy

衍生需求需求

  1. create的时候 自动添加createdAt,createdBy 的值
  2. update的时候 自动添加 updatedAt,updatedBy的值
  3. delete的时候 自动添加 deleted,deletedAt,deletedBy的值
  4. 查询的时候 添加deleted过滤

那么就开始吧,主要使用的prisma的$extend函数

参考链接 www.prisma.io/docs/orm/pr…

1.添加一个ExtendedClient函数

  private createExtendedClient(): PrismaClient {
    const base = new PrismaClient();

    return base.$extends({
      query: {
        $allModels: {
          
        },
      },
    }) as PrismaClient;
  }

2.判断模型是不是存在审计字段

interface AuditInfo {
  hasDeleted: boolean;
  hasCreatedAt: boolean;
  hasCreatedBy: boolean;
  hasUpdatedAt: boolean;
  hasUpdatedBy: boolean;
  hasDeletedAt: boolean;
  hasDeletedBy: boolean;
}
 private getOrBuildAuditMap(): Record<string, AuditInfo> {
    // 如果已有缓存,直接返回
    if (PrismaService.auditMapCache) {
      return PrismaService.auditMapCache;
    }

    const auditMap: Record<string, AuditInfo> = {};
    const schemaPath = path.join(process.cwd(), "prisma", "schema.prisma");
    const modelsDir = path.join(process.cwd(), "prisma", "models");

    try {
      let raw = this.collectPrismaFiles(modelsDir);

      // 若 models 目录为空,则回退到主 schema.prisma
      if (!raw.trim().length) {
        try {
          raw = fs.readFileSync(schemaPath, "utf8");
        } catch (error) {
          this.logger.warn(
            `无法读取 schema.prisma: ${error instanceof Error ? error.message : String(error)}`
          );
          raw = "";
        }
      }

      // 解析所有 model
      const modelRegex = /model\s+(\w+)\s*\{([^}]*)\}/gms;
      for (const match of raw.matchAll(modelRegex)) {
        const name = match[1];
        const body = match[2];

        const hasField = (f: string) =>
          new RegExp(`^\\s*${f}\\s+`, "m").test(body);

        auditMap[name] = {
          hasDeleted: /\bdeleted\s+Boolean\b/.test(body),
          hasCreatedAt: hasField("createdAt"),
          hasCreatedBy: hasField("createdBy"),
          hasUpdatedAt: hasField("updatedAt"),
          hasUpdatedBy: hasField("updatedBy"),
          hasDeletedAt: hasField("deletedAt"),
          hasDeletedBy: hasField("deletedBy"),
        };
      }

      // 缓存结果
      PrismaService.auditMapCache = auditMap;
      this.logger.log(
        `成功解析 ${Object.keys(auditMap).length} 个 Prisma models`
      );
    } catch (error) {
      this.logger.error(
        `解析 Prisma schema 时出错: ${error instanceof Error ? error.message : String(error)}`
      );
    }

    return auditMap;
  }

3.添加fillAuditFileds方法

function fillAuditFields(
  data: any,
  info: AuditInfo,
  type: "create" | "update" | "delete",
  actor: string
): any {
  const result = { ...data };
  const now = new Date();
  if (type === "create") {
    if (info.hasCreatedAt && result.createdAt === undefined) {
      result.createdAt = now;
    }
    if (info.hasCreatedBy && result.createdBy === undefined) {
      result.createdBy = actor;
    }
    if (info.hasDeleted && result.deleted === undefined) {
      result.deleted = false;
    }
  }

  if (type === "update" || type === "delete") {
    if (info.hasUpdatedAt && result.updatedAt === undefined) {
      result.updatedAt = now;
    }
    if (info.hasUpdatedBy && result.updatedBy === undefined) {
      result.updatedBy = actor;
    }
  }

  if (type === "delete") {
    if (info.hasDeleted) result.deleted = true;
    if (info.hasDeletedAt) result.deletedAt = now;
    if (info.hasDeletedBy) result.deletedBy = actor;
  }

  return result;
}

4.create,createMany

 {
 		$allModels: {
				create: ({ model, args, query }: any) => {
            const info = auditMap[model];
            if (info) {
              args.data = fillAuditFields(
                args.data ?? {},
                info,
                "create",
                this.getActor()
              );
            }
            return query(args);
          },

          createMany: ({ model, args, query }: any) => {
            const info = auditMap[model];
            if (info && Array.isArray(args.data)) {
              const actor = this.getActor();
              args.data = args.data.map((d: any) =>
                fillAuditFields(d, info, "create", actor)
              );
            }
            return query(args);
          },
		}
  }

5.update,updateMany

 {
 		$allModels: {
				...,
				 update: ({ model, args, query }: any) => {
            const info = auditMap[model];
            if (info) {
              args.data = fillAuditFields(
                args.data ?? {},
                info,
                "update",
                this.getActor()
              );
            }
            return query(args);
          },

          updateMany: ({ model, args, query }: any) => {
            const info = auditMap[model];
            if (info) {
              args.data = fillAuditFields(
                args.data ?? {},
                info,
                "update",
                this.getActor()
              );
            }
            return query(args);
          },
		}
  }

6.delete,deleteMany

delete: ({ model, args, query }: any) => {
    const info = auditMap[model];
    if (!info?.hasDeleted) {
    	return query(args);
    }
    const data = fillAuditFields({}, info, "delete", this.getActor());
    return (base as any)[model].update({
    	where: args.where,
    	data,
    });
},

  deleteMany: ({ model, args, query }: any) => {
    const info = auditMap[model];
    if (!info?.hasDeleted) {
      return query(args);
    }
    const data = fillAuditFields({}, info, "delete", this.getActor());
    return (base as any)[model].updateMany({
      where: args.where,
      data,
    });
  },

7.查询相关

查询相关稍微复杂一点,2个需求

1.添加deleted查询过滤条件

2.查询的deleted,deletedAt,deletedBy三个审计字段不查询

那么就又衍生出一个方法

private applySoftDeleteFilter(
    info: AuditInfo | undefined,
    args: any,
    isUniqueQuery = false
  ): any {
    if (!info?.hasDeleted) return args;
    if (args?.includeDeleted) {
      delete args.includeDeleted;
      return args;
    }
    if (!args.where || args.where.deleted === undefined) {
      // findUnique/findUniqueOrThrow 不能使用 AND,直接在 where 上添加 deleted 字段
      if (isUniqueQuery) {
        args.where = { ...args.where, deleted: false };
      } else {
        args.where = { AND: [args.where ?? {}, { deleted: false }] };
      }
    }
    return args;
  }

方法方法如下

findMany: ({ model, args, query }: any) => {
  const info = auditMap[model];
  const shouldOmit = !args?.includeDeleted;
  const a = this.applySoftDeleteFilter(info, args);

  return query(a).then((records: any[]) => {
    if (!records || !Array.isArray(records)) return records;
    return shouldOmit && info?.hasDeleted
      ? records.map((record) => omitDeletedFields(record))
    : records;
  });
},

  findFirst: ({ model, args, query }: any) => {
    const info = auditMap[model];
    const shouldOmit = !args?.includeDeleted;
    const a = this.applySoftDeleteFilter(info, args);

    return query(a).then((record: any) =>
                         record && shouldOmit && info?.hasDeleted
                         ? omitDeletedFields(record)
                         : record
                        );
  },

    findUnique: ({ model, args, query }: any) => {
      const info = auditMap[model];
      const shouldOmit = !args?.includeDeleted;
      const a = this.applySoftDeleteFilter(info, args, true);

      return query(a).then((record: any) =>
                           record && shouldOmit && info?.hasDeleted
                           ? omitDeletedFields(record)
                           : record
                          );
    },

      findUniqueOrThrow: ({ model, args, query }: any) => {
        const info = auditMap[model];
        const shouldOmit = !args?.includeDeleted;
        const a = this.applySoftDeleteFilter(info, args, true);

        return query(a).then((record: any) =>
                             record && shouldOmit && info?.hasDeleted
                             ? omitDeletedFields(record)
                             : record
                            );
      },

          count: ({ model, args, query }: any) => {
            const info = auditMap[model];
            return query(this.applySoftDeleteFilter(info, args));
          },

          aggregate: ({ model, args, query }: any) => {
            const info = auditMap[model];
            return query(this.applySoftDeleteFilter(info, args));
          },

            groupBy: ({ model, args, query }: any) => {
              const info = auditMap[model];
              return query(this.applySoftDeleteFilter(info, args));
            },

好了 ,完结,撒花~

谢谢大家