跳转到内容

Prisma 模块

Prisma 数据库服务基类与模块注册,内置软删除扩展、审计字段扩展和查询工具函数。

Prisma 7+

@prisma/client 7.0 移除了 $use() 中间件 API,同时 MySQL/MariaDB 连接需要 driver adapter (如 @prisma/adapter-mariadb)。框架已内置适配,透过 $extends() 自动注册软删除和审计功能。

PrismaBaseService

由于 PrismaClientprisma generate 动态生成,框架提供泛型抽象基类,由用户继承实现:

typescript
// prisma/prisma.service.ts
import { Injectable } from '@nestjs/common'
import { PrismaClient } from '@/generated/prisma/client'  // prisma generate 生成路径
import { PrismaMariaDb } from '@prisma/adapter-mariadb'   // MySQL/MariaDB driver adapter
import { PrismaBaseService, PrismaModelProxy } from '@maxtan/nest-core'

function parseMysqlUrl(url: string) {
  const m = url.match(/^mysql:\/\/([^:]+):(.+)@([^:/?#]+)(?::(\d+))?\/([^?]+)/i)!
  return { user: m[1], password: m[2], host: m[3], port: Number(m[4]) || 3306, database: m[5] }
}

// 接口合并:让 PrismaService 拥有所有模型委托类型(user、post 等)
export interface PrismaService extends PrismaModelProxy<PrismaClient> {}

@Injectable()
export class PrismaService extends PrismaBaseService<PrismaClient> {
  constructor() {
    super()
    const { host, port, user, password, database } = parseMysqlUrl(process.env.DATABASE_URL!)
    const adapter = new PrismaMariaDb({ host, port, user, password, database })
    this.setClient(new PrismaClient({ adapter }))
  }
}

PostgreSQL / SQLite

使用 PostgreSQL 或 SQLite 时,不需要 driver adapter,直接 new PrismaClient() 即可。

PrismaBaseService<C> 提供:

属性/方法说明
client事务感知客户端:在 @PrismaTransactional 内自动返回 tx,外部返回原始 PrismaClient。this.prisma.client.xxxthis.prisma.xxx 等价
rawClient原始客户端(不受事务影响):用于连接管理、健康检查、DMMF 读取等基础设施操作
setClient(client)设置 PrismaClient 实例(子类构造函数中调用)
onModuleInit()自动连接数据库 + 注册声明式扩展
onModuleDestroy()自动断开连接
isHealthy()健康检查(SELECT 1

模型直通访问

PrismaBaseService 内置 Proxy,支持 this.prisma.user 直接访问模型委托,等价于 this.prisma.client.user。 配合 PrismaModelProxy<PrismaClient> 接口合并可获得完整类型提示。

PrismaModule

基础用法

typescript
import { PrismaModule } from '@maxtan/nest-core'
import { PrismaService } from './prisma.service'

@Module({
  imports: [
    PrismaModule.forRoot({
      service: PrismaService,
      global: true           // 默认 true
    })
  ]
})
export class AppModule {}

声明式扩展(推荐)

通过 middlewares 配置自动注册软删除和审计扩展,框架在 onModuleInit 阶段通过 $extends 自动注入:

typescript
PrismaModule.forRoot({
  service: PrismaService,
  middlewares: {
    softDelete: {
      enabled: true,
      models: 'auto'              // 自动从 DMMF 发现含 deletedAt 的模型
      // models: ['User', 'Post'] // 或手动指定
      // exclude: ['AuditLog']    // 自动发现时排除
    },
    audit: {
      enabled: true,
      getUserId: getCurrentUserId, // 参见《最佳实践》请求上下文章节
      fallbackUserId: 'system',   // 匿名/系统任务回退值
      // fields: { createdBy: 'created_by', updatedBy: 'updated_by' } // 非标字段映射
      // createManyMode: 'fast'   // 大批量写入性能模式
    }
  }
})

扩展注册顺序:softDelete → audit(框架自动管理)。

多数据源(多 Client)

每个数据源需要一个独立的 PrismaBaseService 子类,各自持有自己的 PrismaClient 实例和扩展配置,互不干扰。

1. 定义多个 PrismaService

每个数据源对应一个独立的 schema.prismaPrismaClient(由各自的 prisma generate 生成):

typescript
// prisma/primary.service.ts — 主库(业务数据)
import { Injectable } from '@nestjs/common'
import { PrismaClient } from '@/generated/primary/client'
import { PrismaMariaDb } from '@prisma/adapter-mariadb'   // MySQL/MariaDB 需要
import { PrismaBaseService, PrismaModelProxy } from '@maxtan/nest-core'

export interface PrimaryPrismaService extends PrismaModelProxy<PrismaClient> {}

@Injectable()
export class PrimaryPrismaService extends PrismaBaseService<PrismaClient> {
  constructor() {
    super()
    const adapter = new PrismaMariaDb({ host: '...', user: '...', password: '...', database: 'primary' })
    this.setClient(new PrismaClient({ adapter }))
  }
}
typescript
// prisma/secondary.service.ts — 从库(日志/分析)
import { Injectable } from '@nestjs/common'
import { PrismaClient } from '@/generated/secondary/client'
import { PrismaMariaDb } from '@prisma/adapter-mariadb'
import { PrismaBaseService, PrismaModelProxy } from '@maxtan/nest-core'

export interface SecondaryPrismaService extends PrismaModelProxy<PrismaClient> {}

@Injectable()
export class SecondaryPrismaService extends PrismaBaseService<PrismaClient> {
  constructor() {
    super()
    const adapter = new PrismaMariaDb({ host: '...', user: '...', password: '...', database: 'secondary' })
    this.setClient(new PrismaClient({ adapter }))
  }
}

Prisma 7 多 Schema 配置

Prisma 7 可通过 prisma.config.ts 中的 schema 数组或多个 generator + 不同 output 目录生成多个 Client。 每个 PrismaBaseService 子类导入各自 output 目录的 PrismaClient,互不冲突。MySQL/MariaDB 每个实例各自配置独立的 driver adapter。

2. 模块注册

typescript
@Module({
  imports: [
    // 主库:全局注册,开启软删除 + 审计
    PrismaModule.forRoot({
      service: PrimaryPrismaService,
      token: 'PRIMARY_PRISMA',
      middlewares: {
        softDelete: { enabled: true, models: 'auto' },
        audit: { enabled: true, getUserId: getCurrentUserId }
      }
    }),
    // 从库:非全局,仅在需要的模块导入
    PrismaModule.forRoot({
      service: SecondaryPrismaService,
      token: 'SECONDARY_PRISMA',
      global: false,
      middlewares: {
        softDelete: { enabled: true, models: ['LogEntry'] }
        // 从库不需要审计 → 不配 audit
      }
    })
  ]
})
export class AppModule {}
配置项主库从库说明
globaltrue(默认)false从库按需导入,避免全局污染
token'PRIMARY_PRISMA''SECONDARY_PRISMA'自定义注入令牌,区分不同数据源
softDeleteauto手动指定各自独立的扩展配置
audit启用不启用从库仅读取/写日志,无需审计

3. Service 中注入

typescript
import { Injectable, Inject } from '@nestjs/common'
import { PrimaryPrismaService } from '@/prisma/primary.service'
import { SecondaryPrismaService } from '@/prisma/secondary.service'

@Injectable()
export class AnalyticsService {
  constructor(
    // 方式一:直接按类注入(推荐,有完整类型提示)
    private readonly primaryDb: PrimaryPrismaService,

    // 方式二:按 token 注入(适用于动态切换场景)
    @Inject('SECONDARY_PRISMA')
    private readonly secondaryDb: SecondaryPrismaService
  ) {}

  /** 从主库读取用户,写入从库日志 */
  async logUserAction(userId: string, action: string) {
    // 主库读取
    const user = await this.primaryDb.user.findUnique({
      where: { id: userId }
    })

    // 从库写入日志
    await this.secondaryDb.actionLog.create({
      data: { userId, action, username: user?.username }
    })
  }
}

注入方式选择

  • 推荐按类注入private primaryDb: PrimaryPrismaService):类型安全,IDE 自动补全
  • 按 token 注入@Inject('SECONDARY_PRISMA')):适用于需要运行时动态切换数据源的场景
  • 全局注册(global: true)的数据源可在任意模块直接注入;非全局的需要在使用模块中 imports 对应的 PrismaModule 或手动导入

4. 多数据源 + 事务

@PrismaTransactional 默认从 this.prisma 获取客户端。多数据源时需通过 prismaField 指定属性名:

typescript
@Injectable()
export class OrderService {
  constructor(
    private readonly primaryDb: PrimaryPrismaService,
    private readonly secondaryDb: SecondaryPrismaService
  ) {}

  /** 在主库事务中创建订单 */
  @PrismaTransactional({ prismaField: 'primaryDb' })
  async createOrder(dto: CreateOrder) {
    // primaryDb 在事务内自动返回 tx
    const order = await this.primaryDb.order.create({ data: dto })
    await this.primaryDb.inventory.update({
      where: { id: dto.productId },
      data: { stock: { decrement: dto.quantity } }
    })
    return order
  }

  /** 在从库事务中批量写入日志 */
  @PrismaTransactional({ prismaField: 'secondaryDb' })
  async batchLog(logs: CreateLog[]) {
    for (const log of logs) {
      await this.secondaryDb.actionLog.create({ data: log })
    }
  }
}

跨库事务限制

@PrismaTransactional 不支持跨数据源的分布式事务。每个事务只能操作单个 PrismaClient 对应的数据库。 如需跨库原子性,考虑使用补偿机制(Saga 模式)或最终一致性方案。

5. 多数据源 + Repository

typescript
@Injectable()
export class UserRepository extends PrismaRepository<User> {
  constructor(private primaryDb: PrimaryPrismaService) {
    super(primaryDb, 'user')
  }
}

@Injectable()
export class LogRepository extends PrismaRepository<ActionLog> {
  constructor(
    @Inject('SECONDARY_PRISMA')
    private secondaryDb: SecondaryPrismaService
  ) {
    super(secondaryDb, 'actionLog')
  }
}

6. 多数据源最佳实践

规则说明
一个数据源一个 Service 子类各自持有独立的连接、扩展配置,生命周期隔离
主库全局注册,从库按需注册减少依赖传播,从库 global: false
事务指定 prismaField多数据源时必须明确指定属性名
不跨库事务单次事务只操作一个数据库,跨库用补偿机制
各自独立的扩展策略主库开审计,从库仅日志软删除等
健康检查覆盖所有数据源每个 Service 的 isHealthy() 独立检测

软删除扩展

自动拦截查询,为指定模型:

  • 查询时附加 deletedAt: null 条件
  • deleteupdate { deletedAt: new Date() }
  • deleteManyupdateMany { deletedAt: new Date() }

声明式注册(推荐)

通过 PrismaModule.forRootmiddlewares.softDelete 配置即可,参见上方示例。

手动注册

typescript
import { createSoftDeleteExtension } from '@maxtan/nest-core'

// 自动发现含 deletedAt 的模型
const base = new PrismaClient()
this.setClient(
  base.$extends(createSoftDeleteExtension({ models: 'auto' }, base)) as any
)

// 手动指定模型
base.$extends(createSoftDeleteExtension({ models: ['User', 'Post', 'Comment'] }))

// 自动发现 + 排除 + 自定义字段名
base.$extends(createSoftDeleteExtension({
  models: 'auto',
  exclude: ['AuditLog'],
  field: 'deletedAt'        // 默认值,可自定义
}, base))

自动发现工具

typescript
import { discoverSoftDeleteModels } from '@maxtan/nest-core'

const models = discoverSoftDeleteModels(prisma)
// => ['User', 'Post', 'Comment'] — 自动从 DMMF 扫描含 deletedAt 字段的模型

schema.prisma 中添加 deletedAt 字段:

prisma
model User {
  id        Int       @id @default(autoincrement())
  name      String
  deletedAt DateTime? @map("deleted_at")

  @@map("users")
}

审计字段扩展

自动填充 createdBy / updatedBy 字段。AuthModule 内置了 ClsModule(nestjs-cls)和 RequestContextInterceptor,自动将 request.user 写入 CLS 上下文,通过 getCurrentUserId() 即可获取当前用户 ID(参见 最佳实践):

声明式注册(推荐)

通过 PrismaModule.forRootmiddlewares.audit 配置即可,参见上方示例。

手动注册

typescript
import { createAuditExtension } from '@maxtan/nest-core'

const base = new PrismaClient()
this.setClient(
  base.$extends(createAuditExtension({
    getUserId: getCurrentUserId,
    fallbackUserId: 'system',                    // 匿名/系统任务回退
    fields: { createdBy: 'createdBy', updatedBy: 'updatedBy' }, // 字段名映射
    createManyMode: 'strict'                     // strict | fast
  }, base)) as any
)

手动注册时建议传入第二个参数 base,以便自动识别含审计字段的模型,避免误注入 createdBy/updatedBy

createManyMode 选项:

  • strict(默认):逐条注入审计字段,保证完整性
  • fast:跳过注入,提升大批量写入性能

操作规则:

操作createdByupdatedBy
create自动填充自动填充
update / updateMany不修改自动填充
upsert.create自动填充自动填充
upsert.update不修改自动填充
createMany每条自动填充每条自动填充

审计字段接口

typescript
import { PrismaAuditFields } from '@maxtan/nest-core'

interface PrismaAuditFields {
  createdAt: Date
  createdBy: string | null
  updatedAt: Date
  updatedBy: string | null
  deletedAt: Date | null
}

推荐的 schema.prisma 定义:

prisma
model User {
  id        Int       @id @default(autoincrement())
  name      String

  // 审计字段
  createdAt DateTime  @default(now()) @map("created_at")
  createdBy String?   @map("created_by")
  updatedAt DateTime  @updatedAt @map("updated_at")
  updatedBy String?   @map("updated_by")
  deletedAt DateTime? @map("deleted_at")

  @@map("users")
}

查询工具函数

prismaContains — 模糊查询

typescript
import { prismaContains } from '@maxtan/nest-core'

prismaContains({ name: 'John', status: 1 }, ['name'])
// => { name: { contains: 'John' }, status: 1 }

// 不区分大小写(需数据库支持)
prismaContains({ name: 'John' }, ['name'], 'insensitive')
// => { name: { contains: 'John', mode: 'insensitive' } }

prismaSoftDeleteWhere — 软删除条件

typescript
import { prismaSoftDeleteWhere } from '@maxtan/nest-core'

prismaSoftDeleteWhere({ status: 1 })
// => { status: 1, deletedAt: null }

// 手动指定 deletedAt 时不覆盖
prismaSoftDeleteWhere({ status: 1, deletedAt: { not: null } })
// => { status: 1, deletedAt: { not: null } }

prismaPaginate — 分页参数

typescript
import { prismaPaginate } from '@maxtan/nest-core'

prismaPaginate(2, 20)
// => { skip: 20, take: 20 }

const users = await prisma.user.findMany({
  where: { ... },
  ...prismaPaginate(query.current, query.size)
})

prismaOrderBy — 排序参数

typescript
import { prismaOrderBy } from '@maxtan/nest-core'

prismaOrderBy('createdAt', 'desc')
// => { createdAt: 'desc' }

buildPrismaQuery — 完整查询构建

整合:对象清理 + 模糊查询 + IN 查询 + 日期范围 + 软删除 + 分页 + 排序:

typescript
import { buildPrismaQuery } from '@maxtan/nest-core'

// 基础用法
const query = buildPrismaQuery({
  where: { name: 'John', email: '', status: null },
  containsFields: ['name'],
  softDelete: true,
  current: 1,
  size: 20,
  sortBy: 'createdAt',
  sortOrder: 'desc'
})
// => {
//   where: { name: { contains: 'John' }, deletedAt: null },
//   skip: 0, take: 20,
//   orderBy: { createdAt: 'desc' }
// }

增强参数:

typescript
const query = buildPrismaQuery({
  where: dto,
  containsFields: ['name'],         // 模糊查询
  equalsFields: ['code'],            // 精确匹配(不被 cleanObject 过滤)
  inFields: ['status', 'type'],      // 数组 → { in: [...] }
  dateRangeFields: [                 // 日期范围 → { gte, lte }
    { start: 'startAt', end: 'endAt', target: 'createdAt' }
  ],
  ignoreFields: ['keyword'],         // 排除字段(已在外部处理)
  defaultWhere: { orgId: currentOrgId }, // 默认条件(低优先级)
  softDelete: true,
  current: 1, size: 20
})
参数说明
containsFields模糊查询字段(转 contains)
equalsFields精确匹配字段(跳过清理过滤)
inFields数组字段(转 { in: [...] }
dateRangeFields日期范围(Schema 的 start/end → 目标字段 gte/lte)
ignoreFields忽略字段(如 keyword)
defaultWhere默认合并条件(优先级低于 where)

PrismaRepository — 泛型仓储基类

沉淀高频 CRUD 模板,自动集成软删除过滤和默认排序。继承即用:

typescript
import { Injectable } from '@nestjs/common'
import { PrismaRepository } from '@maxtan/nest-core'
import { PrismaService } from '../prisma/prisma.service'
import type { User } from '@prisma/client'

@Injectable()
export class UserRepository extends PrismaRepository<User> {
  constructor(private prisma: PrismaService) {
    super(prisma, 'user')
    // softDelete: false (默认,由中间件处理)
    // defaultOrderBy: { createdAt: 'desc' } (默认)
  }
}

事务安全

Repository 内部通过 usePrismaClient 动态获取模型委托,在 @PrismaTransactional 事务内自动参与事务,无需手动传递 tx。

内置方法:

方法说明
findById(id)按 ID 查单条
findByIds(ids)按 ID 数组批量查询
findOne(where)按条件查单条
findList(where)条件列表(不分页)
findPage({ where, current, size })分页查询(返回 Page)
findInBatches(handler, where?, options?)游标分批遍历,大表安全迭代
count(where)计数
exists(where)是否存在(使用 count() > 0,兼容复合主键)
create(data)创建
createMany(data[], skip?)批量创建(支持跳过重复)
updateById(id, data)按 ID 更新
updateMany(where, data)批量更新
upsert(where, create, update)存在则更新,否则创建
upsertMany(items, uniqueWhere, getUpdate?, options?)批量 Upsert(逐条 upsert,适合中小批量)
bulkUpsert(items, uniqueWhere, getUpdate?, options?)MySQL/MariaDB 原生批量 Upsert(高性能)
removeById(id)按 ID 软删除/物理删除
removeByIds(ids)批量软删除/物理删除
removeMany(where)按条件批量删除

findByIds — 按 ID 数组批量查询

typescript
// 按 ID 数组批量查询,自动处理软删除过滤
const users = await userRepo.findByIds(['id1', 'id2', 'id3'])

upsertMany — 批量 Upsert

消除业务层手写"查询 → diff → 分批写"模板。内部按 batchSize(默认 50)分批,批内并发,批间顺序执行,避免连接池打满。

性能说明

upsertMany便利封装,不是数据库原生 bulk upsert。当前实现会对数组中的每一项分别执行一次 upsert,只是做了分批并发控制。

因此它更适合:中小批量同步、管理后台导入、强调开发效率 的场景。

如果是大批量写入,优先考虑:

  • 纯新增:createMany(data, true)
  • 同一批记录更新同一份数据:updateMany(where, data)
  • 大批量异构 upsert:改用数据库原生 SQL(如 MySQL ON DUPLICATE KEY UPDATE
typescript
// 单字段唯一键(最常用)
await accountRepo.upsertMany(data, 'externalId')

// 复合唯一键
await accountRepo.upsertMany(
  data,
  (item) => ({ userId: item.userId, platform: item.platform })
)

// 自定义 update 字段(如不覆盖 createdAt)
await accountRepo.upsertMany(
  data,
  'externalId',
  (item) => ({ name: item.name, syncedAt: new Date() })
)

需要原子性时,在调用方加 @PrismaTransactional(),所有批次自动参与同一事务。事务场景建议调小 batchSize(如 20),防止触发 Prisma 默认 5s 事务超时:

typescript
@PrismaTransactional()
async syncAccounts(data: AccountDto[]) {
  await this.accountRepo.upsertMany(data, 'externalId', undefined, { batchSize: 20 })
}

bulkUpsert — MySQL/MariaDB 原生批量 Upsert

当数据量较大时,优先使用 bulkUpsert。它使用原生 SQL:INSERT ... ON DUPLICATE KEY UPDATE,通常比 upsertMany 的逐条 upsert() 快得多。

typescript
// 单字段唯一键
await accountRepo.bulkUpsert(data, 'externalId')

// 复合唯一键
await accountRepo.bulkUpsert(data, ['userId', 'platform'])

// 自定义更新字段
await accountRepo.bulkUpsert(
  data,
  'externalId',
  (item) => ({ name: item.name, updatedAt: new Date() }),
  { batchSize: 1000 }
)

使用限制

  • 仅适用于 MySQL / MariaDB
  • 走的是原生 SQL,不会触发 Prisma middlewares / extensions
  • 审计字段自动注入等 Prisma 层逻辑不会自动执行,需要你手动传值
  • 若模型存在 createdAt / updatedAt 字段且未显式传值,框架会自动补当前时间
  • getUpdate() 返回的字段也会写入 INSERT 数据,因此要确保这些字段在“新建记录”时同样可接受

findInBatches — 大表分批遍历

使用游标翻页(cursor-based)而非 offset,每批定位复杂度 O(1),适合全表遍历、数据迁移、批量导出等场景。不会一次性将全表加载进内存。

与 offset 分页的区别

findPage 使用 skip: N * size,在大表的深页会做全表扫描(O(N))。
findInBatches 使用游标定位,每批都是 O(1) 索引跳转,百万行性能恒定。

typescript
// 全表处理(每批默认 200 条)
const { count } = await userRepo.findInBatches(
  async (batch) => { await sendNotifications(batch) }
)
console.log(`处理完成: ${count} 条`)

// 带条件过滤 + 自定义批次大小
await userRepo.findInBatches(
  async (batch) => {
    await Promise.all(batch.map(item => this.migrate(item)))
  },
  { status: 0 },
  { batchSize: 500 }
)

cursorField 必须是唯一索引字段(默认 'id'),且排序必须包含该字段(默认按 cursorField 升序)。

自定义配置:

typescript
super(prisma, 'log', {
  softDelete: false,                  // 不启用 Repository 层软删除(推荐由中间件处理)
  defaultOrderBy: { timestamp: 'desc' },
  softDeleteField: 'removedAt'        // 自定义软删除字段名(仅 softDelete: true 时生效)
})

Prisma → Zod 辅助工具

从 Prisma DMMF 读取字段信息,生成 Zod schema 建议代码:

typescript
import { getModelFields, generateZodSuggestions, listPrismaModels } from '@maxtan/nest-core'

// 列出所有模型
listPrismaModels(prisma)
// => ['User', 'Post', 'Comment']

// 获取字段信息
const fields = getModelFields(prisma, 'User')
fields.forEach(f => console.log(f.name, f.zodSuggestion, f.dbType))

// 生成完整建议代码
console.log(generateZodSuggestions(prisma, 'User'))

更多详情参见 分层最佳实践


树形结构构建

parentId 扁平列表转换为嵌套树,适用于菜单、组织架构、分类等场景:

typescript
import { buildTree, flattenTree, findTreeNode, getTreePath } from '@maxtan/nest-core'

// 1. 基础用法 — 查询后构建树
const menus = await prisma.menu.findMany({
  where: { status: 1 },
  orderBy: { sort: 'asc' }
})
const tree = buildTree(menus)

// 2. 自定义字段名和根值
const deptTree = buildTree(departments, {
  idField: 'deptId',
  parentField: 'pid',
  rootValue: '0'       // parentId 为 '0' 视为根节点
})

// 3. 带排序(子节点也会递归排序)
const sorted = buildTree(categories, {
  sortBy: (a, b) => a.sort - b.sort
})

辅助函数:

函数说明
buildTree(items, options?)扁平 → 树(O(n),Map 单遍构建)
flattenTree(tree)树 → 扁平(附加 _depth 深度信息)
findTreeNode(tree, predicate)深度优先查找节点
getTreePath(tree, predicate)获取祖先路径(面包屑导航)
typescript
// 面包屑导航
const path = getTreePath(tree, n => n.id === currentId)
const breadcrumbs = path.map(n => n.name) // ['系统管理', '用户管理', '用户列表']

// 扁平导出(带缩进)
const flat = flattenTree(tree)
flat.forEach(item => console.log('  '.repeat(item._depth) + item.name))

字段选择工具

基于 DMMF 元数据动态生成 Prisma select 对象,避免手动列举所有字段:

prismaExclude — 排除字段

typescript
import { prismaExclude } from '@maxtan/nest-core'

// 排除密码和软删除字段
const users = await prisma.user.findMany({
  select: prismaExclude(prisma, 'User', ['password', 'deletedAt'])
})
// => select: { id: true, username: true, nickname: true, email: true, ... }

prismaSelect — 精确选择

typescript
import { prismaSelect } from '@maxtan/nest-core'

// 只选择指定字段
const users = await prisma.user.findMany({
  select: prismaSelect(prisma, 'User', ['id', 'username', 'nickname'])
})

// strict 模式:字段名错误时抛异常(开发环境推荐)
prismaSelect(prisma, 'User', ['id', 'typo'], { strict: true })
// => Error: Field "typo" not found in scalar fields of model "User"

prismaExcludeSets — 预定义字段集

typescript
import { prismaExcludeSets, PrismaFieldSets } from '@maxtan/nest-core'

// 排除审计字段 + 敏感字段
const select = prismaExcludeSets(prisma, 'User', ['AUDIT', 'SENSITIVE'])

// 排除审计字段 + 额外自定义字段
const select2 = prismaExcludeSets(prisma, 'User', ['AUDIT'], ['internalNote'])

预定义集合(PrismaFieldSets):

集合名包含字段
AUDITcreatedAt, createdBy, updatedAt, updatedBy, deletedAt
SOFT_DELETEdeletedAt
SENSITIVEpassword, salt, secret, token, refreshToken

提示prismaExclude / prismaSelect 依赖 DMMF 元数据,需传入原始 PrismaClient 实例(rawClient,非 tx 代理)。在 DAO 中建议初始化时缓存 select 对象。


完整 Service 示例(使用 DAO)

typescript
import { Injectable } from '@nestjs/common'
import { AppException, MSG } from '@maxtan/nest-core'
import { UserDao } from './user.dao'

@Injectable()
export class UserService {
  constructor(private readonly dao: UserDao) {}

  async list(dto: ListUser) {
    return this.dao.findPage({
      where: { status: dto.status },
      current: dto.current,
      size: dto.size
    })
  }

  async detail(id: string) {
    const user = await this.dao.findById(id)
    if (!user) throw new AppException(MSG.NOT_FOUND_DATA)
    return user
  }

  async remove(ids: string[]) {
    return this.dao.removeByIds(ids)
  }
}

相关文档