2026-09-17 19:14:41 +08:00
|
|
|
|
// services/api/test/reset.ts
|
|
|
|
|
|
// e2e 共用隔离工具:每个 e2e spec 的 beforeAll 调用 resetDb(),防止 spec 间
|
|
|
|
|
|
// 唯一约束冲突与脏数据。
|
2026-09-17 19:51:56 +08:00
|
|
|
|
// 实现:SET FOREIGN_KEY_CHECKS=0 包裹逐表 TRUNCATE(users 被 sessions/audit_logs
|
|
|
|
|
|
// 引用,即便子表已清空,存在 FK 约束本身即令 TRUNCATE 失败,必须关检查);
|
|
|
|
|
|
// 表序仍按 FK 安全顺序排列(子表在前),关检查失败时也不至于中途卡死。
|
|
|
|
|
|
// 顺序 await 在 Prisma 连接池上复用同一空闲连接,会话级 FOREIGN_KEY_CHECKS
|
|
|
|
|
|
// 对后续 TRUNCATE 生效;finally 恢复检查并断开,失败也不泄漏连接。
|
2026-09-17 22:42:13 +08:00
|
|
|
|
// Task 18 起 e2e 会向 MinIO 写入真实对象(staging/originals):同一函数内追加
|
|
|
|
|
|
// `photo-live/dev` 前缀对象清理(reset.ts 文件头 TODO 的落实——只清本前缀,
|
|
|
|
|
|
// packages/storage 的 test/<ts>-<uuid>/ 前缀对象不受影响,两包测试可并行)。
|
|
|
|
|
|
import {
|
|
|
|
|
|
DeleteObjectsCommand,
|
|
|
|
|
|
ListObjectsV2Command,
|
|
|
|
|
|
S3Client,
|
|
|
|
|
|
} from '@aws-sdk/client-s3';
|
2026-09-20 19:01:41 +08:00
|
|
|
|
import {
|
|
|
|
|
|
PrismaClient,
|
|
|
|
|
|
USAGE_THRESHOLDS_SINGLETON_ID,
|
2026-09-20 20:01:17 +08:00
|
|
|
|
USER_SYNC_STATE_SINGLETON_ID,
|
2026-09-20 19:01:41 +08:00
|
|
|
|
} from '@photo/db-cn';
|
2026-09-17 19:51:56 +08:00
|
|
|
|
|
|
|
|
|
|
export const RESET_TABLES: readonly string[] = [
|
2026-09-17 20:11:26 +08:00
|
|
|
|
// 照片与上传域 + outbox(Task 10):均为扁平字符串外键(无 FK 约束),
|
|
|
|
|
|
// 顺序按逻辑依赖排列(事件/作业/变体/会话先于 photos)。
|
|
|
|
|
|
'outbox_events',
|
2026-09-20 22:22:19 +08:00
|
|
|
|
// 展示位域(P4b Task 2):授权成员表先于展示位(建而未用,P5 激活)。
|
|
|
|
|
|
'embed_slot_members',
|
|
|
|
|
|
'embed_slots',
|
2026-09-17 20:11:26 +08:00
|
|
|
|
'photo_jobs',
|
|
|
|
|
|
'photo_variants',
|
|
|
|
|
|
'upload_sessions',
|
|
|
|
|
|
'photos',
|
2026-09-20 20:01:17 +08:00
|
|
|
|
// 用户同步域(P4a Task 7):坏行明细先于批次账本;单行状态重种在
|
|
|
|
|
|
// resetDb 尾部;映射表为扁平字符串外键(无 FK),先于 users 仅为逻辑可读。
|
|
|
|
|
|
// 注意:Redis 互斥锁(user-sync:lock)不在本函数清理范围——e2e spec 自行
|
|
|
|
|
|
// 清(仅用户同步 spec 触碰触发路径)。
|
|
|
|
|
|
'user_sync_errors',
|
|
|
|
|
|
'user_sync_runs',
|
|
|
|
|
|
'user_sync_state',
|
|
|
|
|
|
'external_user_mappings',
|
2026-09-20 19:01:41 +08:00
|
|
|
|
// 用量域(P4a Task 5):告警账本先于阈值单行(重种在 resetDb 尾部)。
|
|
|
|
|
|
'usage_alerts',
|
|
|
|
|
|
'usage_thresholds',
|
2026-09-20 17:53:44 +08:00
|
|
|
|
// 水印域(P4a Task 3):任务/配置账本先于 albums(watermarkVersion 等位列
|
|
|
|
|
|
// 随 albums 清零)。
|
|
|
|
|
|
'watermark_jobs',
|
|
|
|
|
|
'watermark_configs',
|
2026-09-17 20:05:09 +08:00
|
|
|
|
// 相册域(Task 9):子表(翻译/资产/成员)在前,albums 在后;相册域为扁平
|
|
|
|
|
|
// 字符串外键(无 FK 约束),此处顺序仅为逻辑可读,不依赖 FK 检查开关。
|
|
|
|
|
|
'category_translations',
|
|
|
|
|
|
'categories',
|
|
|
|
|
|
'album_assets',
|
|
|
|
|
|
'album_translations',
|
|
|
|
|
|
'album_members',
|
|
|
|
|
|
'albums',
|
|
|
|
|
|
// 身份域(Task 8)。
|
2026-09-17 19:51:56 +08:00
|
|
|
|
'audit_logs',
|
|
|
|
|
|
'sessions',
|
|
|
|
|
|
'users',
|
|
|
|
|
|
];
|
2026-09-17 19:14:41 +08:00
|
|
|
|
|
2026-09-17 22:42:13 +08:00
|
|
|
|
/** e2e 对象隔离边界:与 src/uploads/support.ts 的 OBJECT_KEY_ROOT 一致。 */
|
|
|
|
|
|
const RESET_OBJECT_PREFIX = 'photo-live/dev/';
|
|
|
|
|
|
|
|
|
|
|
|
/** 删除 MinIO 中 RESET_OBJECT_PREFIX 前缀的全部对象(分页列举 + 批量删除)。 */
|
|
|
|
|
|
async function resetMinioObjects(): Promise<void> {
|
|
|
|
|
|
const required = [
|
|
|
|
|
|
'S3_ENDPOINT',
|
|
|
|
|
|
'S3_REGION',
|
|
|
|
|
|
'S3_ACCESS_KEY',
|
|
|
|
|
|
'S3_SECRET_KEY',
|
|
|
|
|
|
'S3_BUCKET',
|
|
|
|
|
|
] as const;
|
|
|
|
|
|
const missing = required.filter((key) => !process.env[key]);
|
|
|
|
|
|
if (missing.length > 0) {
|
|
|
|
|
|
throw new Error(`resetMinioObjects: 缺少环境变量 ${missing.join(', ')}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const client = new S3Client({
|
|
|
|
|
|
endpoint: process.env.S3_ENDPOINT,
|
|
|
|
|
|
region: process.env.S3_REGION,
|
|
|
|
|
|
credentials: {
|
|
|
|
|
|
accessKeyId: process.env.S3_ACCESS_KEY as string,
|
|
|
|
|
|
secretAccessKey: process.env.S3_SECRET_KEY as string,
|
|
|
|
|
|
},
|
|
|
|
|
|
forcePathStyle: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
const bucket = process.env.S3_BUCKET as string;
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (;;) {
|
|
|
|
|
|
const listed = await client.send(
|
|
|
|
|
|
new ListObjectsV2Command({
|
|
|
|
|
|
Bucket: bucket,
|
|
|
|
|
|
Prefix: RESET_OBJECT_PREFIX,
|
|
|
|
|
|
MaxKeys: 1000,
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
const objects = (listed.Contents ?? []).filter(
|
|
|
|
|
|
(obj) => typeof obj.Key === 'string',
|
|
|
|
|
|
);
|
|
|
|
|
|
if (objects.length === 0) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
await client.send(
|
|
|
|
|
|
new DeleteObjectsCommand({
|
|
|
|
|
|
Bucket: bucket,
|
|
|
|
|
|
Delete: {
|
|
|
|
|
|
Objects: objects.map((obj) => ({ Key: obj.Key as string })),
|
|
|
|
|
|
Quiet: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!listed.IsTruncated) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
await client.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 19:14:41 +08:00
|
|
|
|
export async function resetDb(): Promise<void> {
|
2026-09-17 22:42:13 +08:00
|
|
|
|
await resetMinioObjects();
|
2026-09-17 19:51:56 +08:00
|
|
|
|
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}`);
|
|
|
|
|
|
}
|
2026-09-20 19:01:41 +08:00
|
|
|
|
// 用量阈值单行被 TRUNCATE 后重种(迁移同值种子;保证「单行恒存在」
|
|
|
|
|
|
// 不变量对后续 spec 成立——发布路径的评估检查点依赖它,缺行时兜底
|
|
|
|
|
|
// 视作双维停用,跨 spec 的阈值残留则被清除)。
|
|
|
|
|
|
await prisma.$executeRaw`INSERT INTO usage_thresholds (id, storageBytes, perAlbumBytes, updatedAt)
|
|
|
|
|
|
VALUES (${USAGE_THRESHOLDS_SINGLETON_ID}, NULL, NULL, CURRENT_TIMESTAMP(6))`;
|
2026-09-20 20:01:17 +08:00
|
|
|
|
// 用户同步单行状态同理重种(status 端点直读本行;缺行 = 从未运行)。
|
|
|
|
|
|
// 列名恒反引号:`cursor` 是 MySQL 保留字(`trigger` 同理,Prisma 客户端
|
|
|
|
|
|
// 查询自带反引号,raw SQL 必须显式)。
|
|
|
|
|
|
await prisma.$executeRaw`INSERT INTO user_sync_state (\`id\`, \`lastRunAt\`, \`lastSuccessAt\`, \`cursor\`, \`runCount\`, \`updatedAt\`)
|
|
|
|
|
|
VALUES (${USER_SYNC_STATE_SINGLETON_ID}, NULL, NULL, NULL, 0, CURRENT_TIMESTAMP(6))`;
|
2026-09-17 19:51:56 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
await prisma.$executeRawUnsafe('SET FOREIGN_KEY_CHECKS = 1');
|
|
|
|
|
|
await prisma.$disconnect();
|
2026-09-17 19:14:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|