P1b Task 3: initAlbumAssetUpload / completeAlbumAssetUpload (tags
[assets]) over album_assets with in-row one-shot sessions.
- migration asset_state: album_assets + state (pending|effective),
expectedBytes, contentType (utf8mb4_0900_bin); pre-existing rows
backfilled to effective. Complete head-verifies bytes AND
contentType vs the init declaration (P1a Task 11 server-side
fallback), then conditionally flips pending->effective with the
winner writing the album.asset.add audit (resourceId=albumId,
metadata assetId/kind/locale). Effective rows re-complete
idempotently (re-signed URL, no re-verify, survives object
cleanup).
- object key is the final key (albums/{albumId}/assets/{assetId}.{ext});
the pending row is the logical staging - no copy step, keys embed
assetId so re-init can never overwrite another row's object.
- permissions: owner/admin only (collaborator 403, outsider 404);
uploadEnabled does NOT gate assets (switch governs photo uploads
per 4.9 - both endpoints carry no ALBUM_UPLOAD_DISABLED).
- public banner now locale-aware (req 4.7): effective rows resolved
as display-locale row -> global (locale NULL) row -> null, latest
wins per tier (same-kind re-upload replaces, old rows retained).
- e2e: test/assets.e2e-spec.ts (14 cases, real jpegs via sharp,
MinIO round-trips); public spec seed marked effective.
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>