POST /v1/uploads (Task 18, A03): @@unique(userId, clientTaskId) idempotency
anchor — same params return the original session (201, no duplicate photo);
param drift (album/bytes/contentType/extension) -> 409 UPLOAD_SESSION_CONFLICT.
photos.objectKey is set to the FINAL originals key at init while the session
holds the staging key; IDs come from an app-side cuid-like generator so both
self-referencing keys are written in one transaction. Credential is a real
MinIO presigned PUT (SIGNED_URL_TTL_SECONDS, lazily read). Permission order:
album invisible 404 -> member 403 -> uploadEnabled 409 -> size/ext 400 ->
category ownership 400. No audit on init (audit anchors on complete).
Pre-step for Task 19: QUEUE_PHOTO_PROCESS contract constant + lazy-safe
BullMQ queue producer (Redis down at boot must not crash the app; enqueue
failure only logs — sweep compensates). e2e reset now also wipes the MinIO
photo-live/dev prefix; jest maps @photo/storage to TS source.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Task 15: POST/GET /v1/albums, GET /v1/albums/{id}.
- create: single transaction writes albums + album_translations +
album_members(owner) + audit_logs(album.create); publicToken 24-char
[0-9a-z] with one P2002 retry; duplicate-locale input rejected as 400.
- dto: class-validator per contract AlbumCreateRequest, incl. cross-field
rules (defaultLocale must be covered by translations; endAt>startAt when
present; timezone string-only per P1a scope).
- get: PermissionsService.canViewAlbum gate -> 404 ALBUM_NOT_FOUND for
non-member/nonexistent alike (anti-enumeration); admin bypass; joins
categories (empty until Task 17).
- list: admin sees all, members see joined albums; stable createdAt DESC,
id DESC keyset pagination via new shared/cursor.ts (encode/decode +
typed where-builder, designed for Task 23 photo list reuse).
- e2e: services/api/test/albums.e2e-spec.ts (12 cases, seeded sessions).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Task 14 (A02 foundation): PermissionsService with fresh-DB-per-call
canViewAlbum/canManageAlbum/canUploadToAlbum/canManagePhoto over the
album_members @@unique([albumId,userId]) compound key; admin bypass,
owner/collaborator matrix per requirements doc section 2, removed
members lose access on the very next call (no caching). Boolean-only
returns; callers map 404/403. AlbumsModule shell wired into app module
(controllers arrive in Task 15). jest maxWorkers=1: DB-backed specs
share one MySQL database and reset it via TRUNCATE, so parallel spec
files wipe each other's seed data.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Task 10: photos/photo_variants/upload_sessions/photo_jobs/outbox_events
on the CN schema (flat string FKs, no Prisma relations); migration
20260917120827_photo_upload_domain rewritten to utf8mb4_0900_bin per the
固化 collation recipe; reset.ts table order extended; one semantics e2e
case for photo_jobs @@unique([photoId, type]) and the upload_sessions
A03 idempotency anchor @@unique([userId, clientTaskId]).
Co-Authored-By: Claude Code <noreply@anthropic.com>