feat(api): album domain schema
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -362,24 +362,21 @@ model audit_logs {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: 迁移**
|
- [x] **Step 2: 迁移**(完成:20260917114313_init_identity 已应用。**重要发现 A**:Prisma 生成的 MySQL DDL 每表硬编码 `utf8mb4_unicode_ci`,覆盖库级 `utf8mb4_0900_bin`——已手工改 migration.sql 为 bin 校对并经 information_schema 验证;**Task 9/10 每张新表必须重复:`--create-only` 生成→改 COLLATE→应用**。**重要发现 B**:`pnpm db:migrate -- --name x` 会转发字面 `--` 使 prisma 交互挂起——正确语法 `pnpm db:migrate --name xxx`)
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm db:migrate -- --name init_identity
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: migration SQL 生成并应用。
|
Expected: migration SQL 生成并应用。
|
||||||
|
|
||||||
- [ ] **Step 3: 写语义复核 E2E 测试**(TDD:先写测试)——UTC 往返(写入+读回同为 UTC 瞬间)、`utf8mb4_0900_bin` 下 `public_token` 大小写敏感查询、audit 插入。运行 `pnpm --filter @photo/api-cn test:e2e`,Expected: PASS。
|
- [x] **Step 3: 写语义复核 E2E 测试**(完成:7/7 通过——UTC DateTime(6) 毫秒往返、bin 校对大小写敏感查找、audit Json+actor 关系、带会话删用户被 P2003 拒绝;reset.ts 已实现真实 truncate)
|
||||||
- [ ] **Step 4: Commit** `feat(api): identity and audit schema with mysql semantics tests`
|
- [x] **Step 4: Commit** `feat(api): identity and audit schema with mysql semantics tests`(完成:2801e4f)
|
||||||
|
|
||||||
### Task 9: Prisma cn schema —— 相册域
|
### Task 9: Prisma cn schema —— 相册域
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `packages/db-cn/prisma/schema.prisma`
|
- Modify: `packages/db-cn/prisma/schema.prisma`
|
||||||
|
|
||||||
- [ ] **Step 1: 模型**:`albums`(publicToken `@unique @db.VarChar(32)`、ownerId、startAt/endAt、timezone、status active|offline、defaultLocale、uploadEnabled/publicEnabled/downloadEnabled/originalDownloadEnabled 默认 true/true/true/false、watermarkVersion 默认 0)、`album_translations`(`@@unique([albumId, locale])`,title/description/venue/address)、`album_members`(`@@unique([albumId, userId])`,role owner|collaborator,status)、`categories`(albumId、sortOrder)+ `category_translations`(`@@unique([categoryId, locale])`)、`album_assets`(kind banner|cover|logo、locale?、objectKey)。
|
- [x] **Step 1: 模型**(完成:六模型入 schema,相册域全量扁平字符串外键、无 Prisma relation;Task 10 照片域沿用):`albums`(publicToken `@unique @db.VarChar(32)`、ownerId、startAt/endAt、timezone、status active|offline、defaultLocale、uploadEnabled/publicEnabled/downloadEnabled/originalDownloadEnabled 默认 true/true/true/false、watermarkVersion 默认 0)、`album_translations`(`@@unique([albumId, locale])`,title/description/venue/address)、`album_members`(`@@unique([albumId, userId])`,role owner|collaborator,status)、`categories`(albumId、sortOrder)+ `category_translations`(`@@unique([categoryId, locale])`)、`album_assets`(kind banner|cover|logo、locale?、objectKey)。
|
||||||
- [ ] **Step 2: 迁移** `--name album_domain`;Step 3: Commit `feat(api): album domain schema`
|
- [x] **Step 2: 迁移**(完成:20260917120158_album_domain。流程固化:`pnpm db:migrate --create-only --name album_domain` 生成→sed 改 6 表 COLLATE→`pnpm db:migrate`(无参,非交互应用 pending)→information_schema 验证 10 张域表全 utf8mb4_0900_bin。补充发现 C:`pnpm db:migrate --create-only --name x` 参数可直传,无需进包目录)
|
||||||
|
- [x] **Step 3: 语义冒烟 + Commit**(完成:reset.ts 增 6 表(翻译/资产/成员在前、albums 在后);db.semantics.e2e-spec.ts 增相册用例——(albumId, locale) 复合唯一双向命中 + 同键重复与 publicToken 重复均 P2002;e2e 8/8、lint/typecheck/test 全绿)
|
||||||
|
|
||||||
### Task 10: Prisma cn schema —— 照片与上传域 + outbox
|
### Task 10: Prisma cn schema —— 照片与上传域 + outbox
|
||||||
|
|
||||||
@@ -459,7 +456,7 @@ model outbox_events {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: 迁移** `--name photo_upload_domain`;Step 3: Commit `feat(api): photo, upload, outbox schema`
|
- [ ] **Step 2: 迁移** `pnpm db:migrate --name photo_upload_domain`(同 Task 9:`--create-only` → 逐表改 COLLATE `utf8mb4_0900_bin` → 应用 → information_schema 验证);Step 3: Commit `feat(api): photo, upload, outbox schema`
|
||||||
|
|
||||||
### Task 11: StoragePort 接口 + MinIO 适配器
|
### Task 11: StoragePort 接口 + MinIO 适配器
|
||||||
|
|
||||||
@@ -496,6 +493,8 @@ export interface StoragePort {
|
|||||||
- Create: `services/api/src/auth/password.ts`、`services/api/scripts/seed-admin.ts`
|
- Create: `services/api/src/auth/password.ts`、`services/api/scripts/seed-admin.ts`
|
||||||
- Test: `services/api/test/password.spec.ts`
|
- Test: `services/api/test/password.spec.ts`
|
||||||
|
|
||||||
|
> Task 7 质量审查遗留(本任务一并落地):① `ApiException extends HttpException` 携带 `{code: ErrorCode, message, httpStatus}`,异常过滤器优先识别(状态码表无法表达同状态多错误码的契约空间)+ 未映射状态分支单测;② 5xx/INTERNAL_ERROR 强制通用文案(堵住 InternalServerErrorException(message) 泄露);③ pino redact 增 cookie/set-cookie;④ x-request-id 回显加长度上限(128);⑤ LOG_LEVEL 非法值启动期明确报错;⑥ 抽出 `API_PREFIX='v1'` 与 `RequestWithId` 共享类型;⑦ jest ts-jest 改 `tsconfig: 'tsconfig.json'` 复用;⑧ 增加 `start:dev`(tsc --watch + node --watch 或 nodemon 任一)。计划文本笔误修正:request-id 生成为 UUID v4(非 cuid)。
|
||||||
|
|
||||||
- [ ] **Step 1: 失败测试**:hash/verify(@node-rs/argon2,memoryCost 默认)、错误口令 verify=false。
|
- [ ] **Step 1: 失败测试**:hash/verify(@node-rs/argon2,memoryCost 默认)、错误口令 verify=false。
|
||||||
- [ ] **Step 2: 实现 + seed 脚本**(读 env `ADMIN_ACCOUNT/ADMIN_PASSWORD`,不存在则插入 admin 角色,存在则跳过——幂等)。
|
- [ ] **Step 2: 实现 + seed 脚本**(读 env `ADMIN_ACCOUNT/ADMIN_PASSWORD`,不存在则插入 admin 角色,存在则跳过——幂等)。
|
||||||
- [ ] **Step 3: 测试通过;Commit** `feat(api): argon2 password and idempotent admin seed`
|
- [ ] **Step 3: 测试通过;Commit** `feat(api): argon2 password and idempotent admin seed`
|
||||||
@@ -645,7 +644,7 @@ export interface StoragePort {
|
|||||||
- 每任务 TDD 节奏不可跳步:先失败测试→实现→通过→提交;集成/E2E 测试统一跑在 compose 依赖上(`docker compose up -d` 前置)。
|
- 每任务 TDD 节奏不可跳步:先失败测试→实现→通过→提交;集成/E2E 测试统一跑在 compose 依赖上(`docker compose up -d` 前置)。
|
||||||
- e2e 隔离:每个 e2e spec `beforeAll` 调 `test/reset.ts`(truncate cn 全表 + 清 MinIO `photo-live/dev` 前缀);worker/storage 测试沿用同约定(各自包内复制 reset 工具或引用)。
|
- e2e 隔离:每个 e2e spec `beforeAll` 调 `test/reset.ts`(truncate cn 全表 + 清 MinIO `photo-live/dev` 前缀);worker/storage 测试沿用同约定(各自包内复制 reset 工具或引用)。
|
||||||
- 基础设施注意:minio 镜像统一用 `quay.io/minio/*`(Docker Hub 官方仓库已删除,P2 ov 侧同样);跑 prisma/集成测试前先确认宿主端口实际指向 photo-live 容器(`docker port photo-live-mysql-cn-1`),Docker Desktop 重启可能拉起其他项目容器抢占 3306/6379(已对其设 restart=no,但新冲突可能来自其他来源)。
|
- 基础设施注意:minio 镜像统一用 `quay.io/minio/*`(Docker Hub 官方仓库已删除,P2 ov 侧同样);跑 prisma/集成测试前先确认宿主端口实际指向 photo-live 容器(`docker port photo-live-mysql-cn-1`),Docker Desktop 重启可能拉起其他项目容器抢占 3306/6379(已对其设 restart=no,但新冲突可能来自其他来源)。
|
||||||
- env:命令一律在仓库根执行;根 `.env` 为唯一 env 文件,脚本经 dotenv-cli 注入、测试经 `test/load-env.ts` 载入(Task 7 约定;`services/worker/test` 与 `packages/storage/test` 复制同约定 load-env.ts——两者到仓库根同为三级向上,路径一致);新增 env 变量必须同步 `.env.example`;密钥不入库。
|
- env:命令一律在仓库根执行;根 `.env` 为唯一 env 文件,脚本经 dotenv-cli 注入、测试经 `test/load-env.ts` 载入(Task 7 约定;注意路径深度:`services/api/test`、`services/worker/test` 为三级向上(`../../../.env`),`packages/storage/test` 为**两级**向上(`../../.env`)——Task 11 复制时按包调整,或改为向上查找 pnpm-workspace.yaml 定位仓库根);新增 env 变量必须同步 `.env.example`;密钥不入库。
|
||||||
- 发现契约需变更时:先改 `packages/contracts` 并重新生成类型、在任务提交信息中登记,再改消费端(契约单一事实源)。
|
- 发现契约需变更时:先改 `packages/contracts` 并重新生成类型、在任务提交信息中登记,再改消费端(契约单一事实源)。
|
||||||
- 与总纲冲突处以总纲为准并回写修订;HEIC、MySQL 行值比较等 D12 复核项结论写入验收记录。
|
- 与总纲冲突处以总纲为准并回写修订;HEIC、MySQL 行值比较等 D12 复核项结论写入验收记录。
|
||||||
- P1a 完成后:编写 P1b 计划(viewer 基础页 + admin 最小页 + **宣传图/封面/Logo 上传链路(album_assets 的 presign+确认,复用照片上传机制,R09 依赖)** + **GET /albums/{albumId}/photos 管理端照片列表**(含未发布/隐藏态,照片管理入口)+ **用户账号查询接口**(邀请协作者时按 account 搜索)+ Playwright 冒烟)。
|
- P1a 完成后:编写 P1b 计划(viewer 基础页 + admin 最小页 + **宣传图/封面/Logo 上传链路(album_assets 的 presign+确认,复用照片上传机制,R09 依赖)** + **GET /albums/{albumId}/photos 管理端照片列表**(含未发布/隐藏态,照片管理入口)+ **用户账号查询接口**(邀请协作者时按 account 搜索)+ Playwright 冒烟)。
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
-- 同 20260917114313_init_identity:Prisma 生成的 MySQL DDL 固定写
|
||||||
|
-- utf8mb4_unicode_ci,会覆盖库级 utf8mb4_0900_bin;相册域(publicToken、
|
||||||
|
-- locale 键、objectKey)同样依赖 bin 校对的大小写敏感语义,故逐表改写为
|
||||||
|
-- utf8mb4_0900_bin。
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `albums` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`publicToken` VARCHAR(32) NOT NULL,
|
||||||
|
`ownerId` VARCHAR(64) NOT NULL,
|
||||||
|
`startAt` DATETIME(6) NOT NULL,
|
||||||
|
`endAt` DATETIME(6) NULL,
|
||||||
|
`timezone` VARCHAR(64) NOT NULL DEFAULT 'Asia/Shanghai',
|
||||||
|
`status` VARCHAR(191) NOT NULL DEFAULT 'active',
|
||||||
|
`defaultLocale` VARCHAR(16) NOT NULL,
|
||||||
|
`uploadEnabled` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`publicEnabled` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`downloadEnabled` BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
`originalDownloadEnabled` BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
`watermarkVersion` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
`updatedAt` DATETIME(6) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `albums_publicToken_key`(`publicToken`),
|
||||||
|
INDEX `albums_ownerId_idx`(`ownerId`),
|
||||||
|
INDEX `albums_status_publicEnabled_idx`(`status`, `publicEnabled`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `album_translations` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`albumId` VARCHAR(64) NOT NULL,
|
||||||
|
`locale` VARCHAR(16) NOT NULL,
|
||||||
|
`title` VARCHAR(255) NOT NULL,
|
||||||
|
`description` TEXT NULL,
|
||||||
|
`venue` VARCHAR(255) NULL,
|
||||||
|
`address` VARCHAR(255) NULL,
|
||||||
|
|
||||||
|
INDEX `album_translations_albumId_idx`(`albumId`),
|
||||||
|
UNIQUE INDEX `album_translations_albumId_locale_key`(`albumId`, `locale`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `album_members` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`albumId` VARCHAR(64) NOT NULL,
|
||||||
|
`userId` VARCHAR(64) NOT NULL,
|
||||||
|
`role` VARCHAR(191) NOT NULL DEFAULT 'collaborator',
|
||||||
|
`status` VARCHAR(191) NOT NULL DEFAULT 'active',
|
||||||
|
`addedAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
|
||||||
|
INDEX `album_members_userId_idx`(`userId`),
|
||||||
|
UNIQUE INDEX `album_members_albumId_userId_key`(`albumId`, `userId`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `categories` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`albumId` VARCHAR(64) NOT NULL,
|
||||||
|
`sortOrder` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
|
||||||
|
INDEX `categories_albumId_sortOrder_idx`(`albumId`, `sortOrder`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `category_translations` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`categoryId` VARCHAR(64) NOT NULL,
|
||||||
|
`locale` VARCHAR(16) NOT NULL,
|
||||||
|
`name` VARCHAR(128) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `category_translations_categoryId_locale_key`(`categoryId`, `locale`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `album_assets` (
|
||||||
|
`id` VARCHAR(191) NOT NULL,
|
||||||
|
`albumId` VARCHAR(64) NOT NULL,
|
||||||
|
`kind` VARCHAR(16) NOT NULL,
|
||||||
|
`locale` VARCHAR(16) NULL,
|
||||||
|
`objectKey` VARCHAR(255) NOT NULL,
|
||||||
|
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
|
||||||
|
INDEX `album_assets_albumId_kind_idx`(`albumId`, `kind`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
|
||||||
@@ -46,3 +46,83 @@ model audit_logs {
|
|||||||
createdAt DateTime @default(now()) @db.DateTime(6)
|
createdAt DateTime @default(now()) @db.DateTime(6)
|
||||||
actor users? @relation(fields: [actorId], references: [id])
|
actor users? @relation(fields: [actorId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// —— 相册域(Task 9)——
|
||||||
|
// 约定:与身份域不同,相册域一律使用扁平字符串外键(albumId/ownerId/userId/
|
||||||
|
// categoryId 为纯字符串,不建 Prisma relation/FK 约束)——Task 10 的照片域同此。
|
||||||
|
// 原因:跨域删除语义(用户注销→相册归属、相册下线→照片保留)由应用层显式
|
||||||
|
// 决策,不做数据库级联;也避免 TRUNCATE 隔离工具为绕 FK 检查承担更多顺序负担。
|
||||||
|
model albums {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
publicToken String @unique @db.VarChar(32)
|
||||||
|
ownerId String @db.VarChar(64)
|
||||||
|
startAt DateTime @db.DateTime(6)
|
||||||
|
endAt DateTime? @db.DateTime(6)
|
||||||
|
timezone String @default("Asia/Shanghai") @db.VarChar(64)
|
||||||
|
status String @default("active") // active|offline
|
||||||
|
defaultLocale String @db.VarChar(16)
|
||||||
|
uploadEnabled Boolean @default(true)
|
||||||
|
publicEnabled Boolean @default(true)
|
||||||
|
downloadEnabled Boolean @default(true)
|
||||||
|
originalDownloadEnabled Boolean @default(false)
|
||||||
|
watermarkVersion Int @default(0)
|
||||||
|
createdAt DateTime @default(now()) @db.DateTime(6)
|
||||||
|
updatedAt DateTime @updatedAt @db.DateTime(6)
|
||||||
|
|
||||||
|
@@index([ownerId])
|
||||||
|
@@index([status, publicEnabled])
|
||||||
|
}
|
||||||
|
|
||||||
|
model album_translations {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
albumId String @db.VarChar(64)
|
||||||
|
locale String @db.VarChar(16)
|
||||||
|
title String @db.VarChar(255)
|
||||||
|
description String? @db.Text
|
||||||
|
venue String? @db.VarChar(255)
|
||||||
|
address String? @db.VarChar(255)
|
||||||
|
|
||||||
|
@@unique([albumId, locale])
|
||||||
|
@@index([albumId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model album_members {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
albumId String @db.VarChar(64)
|
||||||
|
userId String @db.VarChar(64)
|
||||||
|
role String @default("collaborator") // owner|collaborator
|
||||||
|
status String @default("active")
|
||||||
|
addedAt DateTime @default(now()) @db.DateTime(6)
|
||||||
|
|
||||||
|
@@unique([albumId, userId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model categories {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
albumId String @db.VarChar(64)
|
||||||
|
sortOrder Int @default(0)
|
||||||
|
createdAt DateTime @default(now()) @db.DateTime(6)
|
||||||
|
|
||||||
|
@@index([albumId, sortOrder])
|
||||||
|
}
|
||||||
|
|
||||||
|
model category_translations {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
categoryId String @db.VarChar(64)
|
||||||
|
locale String @db.VarChar(16)
|
||||||
|
name String @db.VarChar(128)
|
||||||
|
|
||||||
|
@@unique([categoryId, locale])
|
||||||
|
}
|
||||||
|
|
||||||
|
model album_assets {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
albumId String @db.VarChar(64)
|
||||||
|
kind String @db.VarChar(16) // banner|cover|logo
|
||||||
|
locale String? @db.VarChar(16)
|
||||||
|
objectKey String @db.VarChar(255)
|
||||||
|
createdAt DateTime @default(now()) @db.DateTime(6)
|
||||||
|
|
||||||
|
@@index([albumId, kind])
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
// 3) audit_logs 的 Json 元数据落库与 actor 关系;
|
// 3) audit_logs 的 Json 元数据落库与 actor 关系;
|
||||||
// 4) 必选关系默认 Restrict:删除仍持有 session 的用户抛 P2003——保留可审计性,
|
// 4) 必选关系默认 Restrict:删除仍持有 session 的用户抛 P2003——保留可审计性,
|
||||||
// 刻意不做 onDelete: Cascade(用户决策,见计划 Task 8)。
|
// 刻意不做 onDelete: Cascade(用户决策,见计划 Task 8)。
|
||||||
|
// 5) 相册域(Task 9 增补):多语言翻译按 @@unique([albumId, locale]) 双向
|
||||||
|
// 精确命中、publicToken 唯一约束拒绝重复(P2002)——相册域为扁平字符串
|
||||||
|
// 外键(无 Prisma relation),完整 CRUD 语义由 Task 15 展开。
|
||||||
import { PrismaClient } from '@photo/db-cn';
|
import { PrismaClient } from '@photo/db-cn';
|
||||||
import { resetDb } from './reset';
|
import { resetDb } from './reset';
|
||||||
|
|
||||||
@@ -115,4 +118,50 @@ describe('db semantics (e2e)', () => {
|
|||||||
prisma.users.delete({ where: { id: user.id } }),
|
prisma.users.delete({ where: { id: user.id } }),
|
||||||
).rejects.toMatchObject({ code: 'P2003' });
|
).rejects.toMatchObject({ code: 'P2003' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('album translations resolve by (albumId, locale) both ways and publicToken is unique (Task 9)', async () => {
|
||||||
|
const album = await prisma.albums.create({
|
||||||
|
data: {
|
||||||
|
publicToken: 'album-tok-e2e-01',
|
||||||
|
ownerId: 'owner-flat-fk', // 相册域扁平字符串外键,无 FK 约束
|
||||||
|
startAt: new Date('2026-10-01T00:00:00Z'),
|
||||||
|
defaultLocale: 'zh-CN',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prisma.album_translations.create({
|
||||||
|
data: { albumId: album.id, locale: 'zh-CN', title: '婚礼相册' },
|
||||||
|
});
|
||||||
|
await prisma.album_translations.create({
|
||||||
|
data: { albumId: album.id, locale: 'en', title: 'Wedding Album' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 复合唯一键两个方向都能精确命中(bin 校对:locale 大小写敏感)。
|
||||||
|
const zh = await prisma.album_translations.findUnique({
|
||||||
|
where: { albumId_locale: { albumId: album.id, locale: 'zh-CN' } },
|
||||||
|
});
|
||||||
|
expect(zh?.title).toBe('婚礼相册');
|
||||||
|
const en = await prisma.album_translations.findUnique({
|
||||||
|
where: { albumId_locale: { albumId: album.id, locale: 'en' } },
|
||||||
|
});
|
||||||
|
expect(en?.title).toBe('Wedding Album');
|
||||||
|
|
||||||
|
// 同一 (albumId, locale) 重复写入被唯一约束拒绝(P2002)。
|
||||||
|
await expect(
|
||||||
|
prisma.album_translations.create({
|
||||||
|
data: { albumId: album.id, locale: 'en', title: 'Dup' },
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({ code: 'P2002' });
|
||||||
|
|
||||||
|
// publicToken 全局唯一:第二个相册复用同 token 抛 P2002。
|
||||||
|
await expect(
|
||||||
|
prisma.albums.create({
|
||||||
|
data: {
|
||||||
|
publicToken: 'album-tok-e2e-01',
|
||||||
|
ownerId: 'owner-flat-fk',
|
||||||
|
startAt: new Date('2026-11-01T00:00:00Z'),
|
||||||
|
defaultLocale: 'zh-CN',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({ code: 'P2002' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
import { PrismaClient } from '@photo/db-cn';
|
import { PrismaClient } from '@photo/db-cn';
|
||||||
|
|
||||||
export const RESET_TABLES: readonly string[] = [
|
export const RESET_TABLES: readonly string[] = [
|
||||||
|
// 相册域(Task 9):子表(翻译/资产/成员)在前,albums 在后;相册域为扁平
|
||||||
|
// 字符串外键(无 FK 约束),此处顺序仅为逻辑可读,不依赖 FK 检查开关。
|
||||||
|
'category_translations',
|
||||||
|
'categories',
|
||||||
|
'album_assets',
|
||||||
|
'album_translations',
|
||||||
|
'album_members',
|
||||||
|
'albums',
|
||||||
|
// 身份域(Task 8)。
|
||||||
'audit_logs',
|
'audit_logs',
|
||||||
'sessions',
|
'sessions',
|
||||||
'users',
|
'users',
|
||||||
|
|||||||
Reference in New Issue
Block a user