feat(api): identity and audit schema with mysql semantics tests

packages/db-cn:@photo/db-cn(prisma 5.22,api/worker 共享 client)+
schema(users/sessions/audit_logs,DateTime(6)、cuid、账号唯一)。
迁移 SQL 手工改写表级 collation 为 utf8mb4_0900_bin——Prisma 生成的 MySQL
DDL 固定 utf8mb4_unicode_ci 会覆盖库级 bin collation,账号/token 大小写
敏感语义依赖它(后续迁移需同样复核)。services/api:@Global PrismaModule
(PrismaService 封装 $connect/$disconnect,池上限仍由
DATABASE_URL_CN ?connection_limit=10 承担);reset.ts 落地 FK 包裹式
TRUNCATE;e2e 语义复核 4 例(UTC 往返/大小写敏感/audit Json+actor/
session FK Restrict P2003)。pnpm 10 未拦截 prisma engines(平台包内置),
无需 onlyBuiltDependencies。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 19:51:56 +08:00
parent 7d250ec342
commit 2801e4f158
14 changed files with 417 additions and 14 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@photo/db-cn",
"version": "0.1.0",
"private": true,
"description": "CN Prisma schema + shared client (api 与 worker 共用;MySQL 8.4 utf8mb4_0900_bin,迁移经根 db:* 脚本注入 .env)",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"generate": "prisma generate",
"migrate": "prisma migrate dev",
"build": "tsc -p tsconfig.json",
"typecheck": "tsc --noEmit",
"lint": "eslint ."
},
"dependencies": {
"@prisma/client": "^5.22.0"
},
"devDependencies": {
"prisma": "^5.22.0",
"typescript": "^5.6.0"
},
"prisma": {
"schema": "prisma/schema.prisma"
}
}
@@ -0,0 +1,50 @@
-- 注意:Prisma 生成的 MySQL DDL 固定写 utf8mb4_unicode_ci,会覆盖库级
-- utf8mb4_0900_bin;账号/token 的大小写敏感语义依赖 bin collation,故手工
-- 改写为 utf8mb4_0900_bin(后续迁移同样需要复核 CREATE TABLE 的 collation)。
-- CreateTable
CREATE TABLE `users` (
`id` VARCHAR(191) NOT NULL,
`account` VARCHAR(64) NOT NULL,
`passwordHash` VARCHAR(255) NOT NULL,
`displayName` VARCHAR(128) NOT NULL,
`role` VARCHAR(191) NOT NULL DEFAULT 'staff',
`sourceStatus` VARCHAR(191) NOT NULL DEFAULT 'local',
`localLocked` BOOLEAN NOT NULL DEFAULT false,
`credentialStatus` VARCHAR(191) NOT NULL DEFAULT 'active',
`locale` VARCHAR(16) NULL,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
UNIQUE INDEX `users_account_key`(`account`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `sessions` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`tokenHash` VARCHAR(64) NOT NULL,
`expiresAt` DATETIME(6) NOT NULL,
`revokedAt` DATETIME(6) NULL,
UNIQUE INDEX `sessions_tokenHash_key`(`tokenHash`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `audit_logs` (
`id` VARCHAR(191) NOT NULL,
`actorId` VARCHAR(191) NULL,
`action` VARCHAR(64) NOT NULL,
`resourceId` VARCHAR(64) NULL,
`metadata` JSON NULL,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- AddForeignKey
ALTER TABLE `sessions` ADD CONSTRAINT `sessions_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `audit_logs` ADD CONSTRAINT `audit_logs_actorId_fkey` FOREIGN KEY (`actorId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"
+48
View File
@@ -0,0 +1,48 @@
// packages/db-cn/prisma/schema.prisma —— CN 侧唯一 schema(Task 8 起步:身份与审计域)。
// 约定:MySQL 8.4、库级 collation utf8mb4_0900_bin(大小写敏感,账号/token 语义依赖它);
// 时间一律 @db.DateTime(6);ID cuid;连接池上限由 DATABASE_URL_CN 的 ?connection_limit=10
// 承担,不在应用侧另行放大。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL_CN")
}
model users {
id String @id @default(cuid())
account String @unique @db.VarChar(64)
passwordHash String @db.VarChar(255)
displayName String @db.VarChar(128)
role String @default("staff") // staff|admin(公司角色;相册角色另表)
sourceStatus String @default("local") // local|active|disabled(P4a 同步用)
localLocked Boolean @default(false)
credentialStatus String @default("active") // active|pending
locale String? @db.VarChar(16)
createdAt DateTime @default(now()) @db.DateTime(6)
sessions sessions[]
auditLogs audit_logs[]
}
model sessions {
id String @id @default(cuid())
userId String
tokenHash String @unique @db.VarChar(64)
// 长效会话期限(默认 30 天,AUTH_SESSION_TTL_SECONDS 可调;不做短时效
// access/refresh 双层——用户决策 2026-09-17)
expiresAt DateTime @db.DateTime(6)
revokedAt DateTime? @db.DateTime(6) // 注销/停用撤销
user users @relation(fields: [userId], references: [id])
}
model audit_logs {
id String @id @default(cuid())
actorId String?
action String @db.VarChar(64)
resourceId String? @db.VarChar(64)
metadata Json?
createdAt DateTime @default(now()) @db.DateTime(6)
actor users? @relation(fields: [actorId], references: [id])
}
+5
View File
@@ -0,0 +1,5 @@
// packages/db-cn/src/index.ts —— CN 侧唯一 Prisma client 出口。
// api(PrismaService)与 worker(Task 20)都从本包导入,schema/迁移只此一份。
// commonjs 包:消费方(api/worker)均为 CJS,tsc 直出无需打包。
export { PrismaClient } from '@prisma/client';
export type { Prisma } from '@prisma/client';
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "commonjs",
"moduleResolution": "node10",
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"]
}
+79 -3
View File
@@ -46,7 +46,20 @@ importers:
version: 7.13.0(typescript@5.9.3)
ts-jest:
specifier: ^29.2.0
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3)
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.9.3)
typescript:
specifier: ^5.6.0
version: 5.9.3
packages/db-cn:
dependencies:
'@prisma/client':
specifier: ^5.22.0
version: 5.22.0(prisma@5.22.0)
devDependencies:
prisma:
specifier: ^5.22.0
version: 5.22.0
typescript:
specifier: ^5.6.0
version: 5.9.3
@@ -65,6 +78,9 @@ importers:
'@photo/contracts':
specifier: workspace:*
version: link:../../packages/contracts
'@photo/db-cn':
specifier: workspace:*
version: link:../../packages/db-cn
nestjs-pino:
specifier: ^4.6.1
version: 4.6.1(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.3.1)(rxjs@7.8.2)
@@ -104,7 +120,7 @@ importers:
version: 7.2.2
ts-jest:
specifier: ^29.2.5
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3)
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.9.3)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.20.3)(typescript@5.9.3)
@@ -623,6 +639,30 @@ packages:
'@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@prisma/client@5.22.0':
resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
engines: {node: '>=16.13'}
peerDependencies:
prisma: '*'
peerDependenciesMeta:
prisma:
optional: true
'@prisma/debug@5.22.0':
resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==}
'@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2':
resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==}
'@prisma/engines@5.22.0':
resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==}
'@prisma/fetch-engine@5.22.0':
resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==}
'@prisma/get-platform@5.22.0':
resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==}
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
@@ -2260,6 +2300,11 @@ packages:
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
prisma@5.22.0:
resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==}
engines: {node: '>=16.13'}
hasBin: true
prismjs@1.30.0:
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
engines: {node: '>=6'}
@@ -3509,6 +3554,31 @@ snapshots:
'@pinojs/redact@0.4.0': {}
'@prisma/client@5.22.0(prisma@5.22.0)':
optionalDependencies:
prisma: 5.22.0
'@prisma/debug@5.22.0': {}
'@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {}
'@prisma/engines@5.22.0':
dependencies:
'@prisma/debug': 5.22.0
'@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
'@prisma/fetch-engine': 5.22.0
'@prisma/get-platform': 5.22.0
'@prisma/fetch-engine@5.22.0':
dependencies:
'@prisma/debug': 5.22.0
'@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
'@prisma/get-platform': 5.22.0
'@prisma/get-platform@5.22.0':
dependencies:
'@prisma/debug': 5.22.0
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
@@ -5414,6 +5484,12 @@ snapshots:
ansi-styles: 5.2.0
react-is: 18.3.1
prisma@5.22.0:
dependencies:
'@prisma/engines': 5.22.0
optionalDependencies:
fsevents: 2.3.3
prismjs@1.30.0: {}
process-warning@5.1.0: {}
@@ -5838,7 +5914,7 @@ snapshots:
dependencies:
typescript: 5.9.3
ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.3)(ts-node@10.9.2(@types/node@22.20.3)(typescript@5.9.3)))(typescript@5.9.3):
ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0)(typescript@5.9.3):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
+4
View File
@@ -5,6 +5,9 @@
// moduleNameMapper 直接指向其 TS 源码,由 ts-jest 以 CJS 编译(同
// packages/contracts/jest.config.cjs 的 .js -> $1 映射模式);运行时(node dist)
// 则经 require(esm) 消费其 dist(Node >= 22.12 支持)。
// @photo/db-cn 为 CJS 包(含 @prisma/client 再导出),同样映射到 TS 源码,
// 免依赖先 build dist;@prisma/client 及其生成的 .prisma/client 由 Node
// 解析规则从 packages/db-cn/node_modules 向上命中。
import type { Config } from 'jest';
const config: Config = {
@@ -30,6 +33,7 @@ const config: Config = {
},
moduleNameMapper: {
'^@photo/contracts$': '<rootDir>/../../packages/contracts/src/index.ts',
'^@photo/db-cn$': '<rootDir>/../../packages/db-cn/src/index.ts',
'^(\\.{1,2}/.*)\\.js$': '$1',
},
testMatch: ['<rootDir>/src/**/*.spec.ts'],
+1
View File
@@ -16,6 +16,7 @@
"@nestjs/core": "^10.4.6",
"@nestjs/platform-express": "^10.4.6",
"@photo/contracts": "workspace:*",
"@photo/db-cn": "workspace:*",
"nestjs-pino": "^4.6.1",
"pino": "^10.0.0",
"pino-http": "^11.0.0",
+6 -2
View File
@@ -1,6 +1,7 @@
// services/api/src/app-cn.module.ts
// 根模块:日志(nestjs-pino / pino-http)、request-id 中间件、全局异常过滤器、
// /v1/health。功能模块(auth/albums/uploads/photos/public)自 Task 12+ 起逐个加入。
// 根模块:Prisma(@Global,封装 @photo/db-cn)、日志(nestjs-pino / pino-http)、
// request-id 中间件、全局异常过滤器、/v1/health。
// 功能模块(auth/albums/uploads/photos/public)自 Task 12+ 起逐个加入。
// 过滤器经 APP_FILTER、中间件经 NestModule 在模块层注册——e2e 直接 import 本模块
// 即可获得与生产一致的管线(全局前缀除外,由入口设置)。
import { type MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
@@ -9,10 +10,13 @@ import { LoggerModule } from 'nestjs-pino';
import type { IncomingMessage, ServerResponse } from 'node:http';
import { AllExceptionsFilter } from './all-exceptions.filter';
import { HealthController } from './health.controller';
import { PrismaModule } from './prisma/prisma.module';
import { RequestIdMiddleware, resolveRequestId } from './request-id.middleware';
@Module({
imports: [
// @Global:功能模块(Task 12+)直接注入 PrismaService,无需 re-import。
PrismaModule,
LoggerModule.forRoot({
pinoHttp: {
// 与 RequestIdMiddleware 共用同一解析函数:优先透传 x-request-id,
+12
View File
@@ -0,0 +1,12 @@
// services/api/src/prisma/prisma.module.ts —— 封装 @photo/db-cn 供注入。
// @Global:Task 12+ 的功能模块(auth/albums/uploads/photos/public)直接注入
// PrismaService,无需逐模块 re-import;本模块只在 AppCnModule 挂载一次。
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
+23
View File
@@ -0,0 +1,23 @@
// services/api/src/prisma/prisma.service.ts —— @photo/db-cn 的可注入封装。
// 连接池上限由 DATABASE_URL_CN 的 ?connection_limit=10 承担,这里不另行放大;
// 不使用 enableShutdownHooks(Nest 10 经 onModuleDestroy 生命周期即可优雅断开)。
import {
Injectable,
type OnModuleDestroy,
type OnModuleInit,
} from '@nestjs/common';
import { PrismaClient } from '@photo/db-cn';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit(): Promise<void> {
await this.$connect();
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
}
}
+118
View File
@@ -0,0 +1,118 @@
// services/api/test/db.semantics.e2e-spec.ts —— Task 8 语义复核(真实 MySQL 8.4,
// 库级 utf8mb4_0900_bin;连接经 test/load-env.ts 载入根 .env 的 DATABASE_URL_CN)。
// 复核项是后续 auth/album 语义的依赖前提:
// 1) DateTime(6) 的 UTC 瞬间往返(写入+读回毫秒精度不丢、时区不改);
// 2) bin collation 下账号查找大小写敏感(token/account 语义同此);
// 3) audit_logs 的 Json 元数据落库与 actor 关系;
// 4) 必选关系默认 Restrict:删除仍持有 session 的用户抛 P2003——保留可审计性,
// 刻意不做 onDelete: Cascade(用户决策,见计划 Task 8)。
import { PrismaClient } from '@photo/db-cn';
import { resetDb } from './reset';
describe('db semantics (e2e)', () => {
let prisma: PrismaClient;
beforeAll(async () => {
await resetDb();
prisma = new PrismaClient();
});
afterAll(async () => {
await prisma.$disconnect();
});
it('DateTime(6) round-trips a UTC instant at millisecond precision', async () => {
const createdAt = new Date('2026-09-17T08:00:00.123Z');
const created = await prisma.users.create({
data: {
account: 'utc-roundtrip',
passwordHash: 'x',
displayName: 'UTC 往返',
createdAt,
},
select: { createdAt: true },
});
expect(created.createdAt.getTime()).toBe(createdAt.getTime());
const reread = await prisma.users.findUnique({
where: { account: 'utc-roundtrip' },
select: { createdAt: true },
});
expect(reread?.createdAt.getTime()).toBe(createdAt.getTime());
expect(reread?.createdAt.toISOString()).toBe('2026-09-17T08:00:00.123Z');
});
it('account lookup is case-sensitive under utf8mb4_0900_bin', async () => {
await prisma.users.create({
data: {
account: 'AdminUser',
passwordHash: 'x',
displayName: '大小写敏感',
},
});
const lowered = await prisma.users.findUnique({
where: { account: 'adminuser' },
});
expect(lowered).toBeNull();
const exact = await prisma.users.findUnique({
where: { account: 'AdminUser' },
});
expect(exact?.account).toBe('AdminUser');
});
it('stores audit log Json metadata and joins back to the actor', async () => {
const actor = await prisma.users.create({
data: {
account: 'audit-actor',
passwordHash: 'x',
displayName: '审计者',
},
});
const log = await prisma.audit_logs.create({
data: {
actorId: actor.id,
action: 'user.login',
resourceId: 'res-001',
metadata: { ip: '127.0.0.1', userAgent: 'e2e', attempts: 1 },
},
include: { actor: true },
});
expect(log.id).toBeTruthy();
expect(log.metadata).toEqual({ ip: '127.0.0.1', userAgent: 'e2e', attempts: 1 });
expect(log.actor?.id).toBe(actor.id);
expect(log.actor?.account).toBe('audit-actor');
// 反向关系:经 actor.auditLogs 能取回审计记录。
const viaActor = await prisma.users.findUnique({
where: { id: actor.id },
include: { auditLogs: true },
});
expect(viaActor?.auditLogs.map((l) => l.id)).toContain(log.id);
});
it('rejects deleting a user that still has a session (FK Restrict, no cascade)', async () => {
const user = await prisma.users.create({
data: {
account: 'restricted',
passwordHash: 'x',
displayName: '受保护删除',
},
});
await prisma.sessions.create({
data: {
userId: user.id,
tokenHash: 'a'.repeat(64),
expiresAt: new Date(Date.now() + 60_000),
},
});
// Prisma 对必选关系默认 onDelete: Restrict —— 删除被引用的 users 抛 P2003
// (外键约束失败)。这正是预期:会话/审计可追溯,不允许级联抹除。
await expect(
prisma.users.delete({ where: { id: user.id } }),
).rejects.toMatchObject({ code: 'P2003' });
});
});
+21 -9
View File
@@ -1,17 +1,29 @@
// services/api/test/reset.ts
// e2e 共用隔离工具:每个 e2e spec 的 beforeAll 调用 resetDb(),防止 spec 间
// 唯一约束冲突与脏数据。
// TODO(Task 8): 用 Prisma 实现 RESET_TABLES 中全部表的 truncate-if-exists
// (Task 8-10 逐步登记表名,届时空列表不再成立)。
// 实现:SET FOREIGN_KEY_CHECKS=0 包裹逐表 TRUNCATE(users 被 sessions/audit_logs
// 引用,即便子表已清空,存在 FK 约束本身即令 TRUNCATE 失败,必须关检查);
// 表序仍按 FK 安全顺序排列(子表在前),关检查失败时也不至于中途卡死。
// 顺序 await 在 Prisma 连接池上复用同一空闲连接,会话级 FOREIGN_KEY_CHECKS
// 对后续 TRUNCATE 生效;finally 恢复检查并断开,失败也不泄漏连接。
// TODO(Task 11): 增加 MinIO `photo-live/dev` 前缀对象清理(同一函数内追加)。
export const RESET_TABLES: readonly string[] = [];
import { PrismaClient } from '@photo/db-cn';
export const RESET_TABLES: readonly string[] = [
'audit_logs',
'sessions',
'users',
];
export async function resetDb(): Promise<void> {
if (RESET_TABLES.length === 0) {
// 尚未登记任何表(Task 8 前数据库无表):干净地跳过。
return;
const prisma = new PrismaClient();
try {
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 0');
for (const table of RESET_TABLES) {
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${table}`);
}
} finally {
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1');
await prisma.$disconnect();
}
throw new Error(
'resetDb: RESET_TABLES is non-empty but truncation is not implemented until Task 8',
);
}