feat(frame): duplicate a frame via POST /:id/duplicate #46

Merged
eliott merged 5 commits from feat/duplicate-frame into develop 2026-05-15 15:33:47 +02:00
Owner

Summary

  • New endpoint POST /frame/:id/duplicate - atomic transactional copy of a frame plus its optional frame_location row, into the same shoot.
  • Source frame row is locked with FOR UPDATE to serialize against concurrent deletes; shoot row is locked the same way to atomically compute MAX(number) + 1.
  • Rejects on a done shoot (400), other user's frame (404), unknown id (404). Returns {id} like the create endpoint.
  • Refactor: extracted getNextFrameNumber(tx, shootId) helper, used by create, update, and duplicate (3 call sites of the same COALESCE(MAX(...)) block collapsed into 1).

Test plan

  • 6 new integration tests in frame.int-spec.ts: happy path with location, no-location case, done-shoot 400, other-user 404, unknown 404, concurrent serialization.
  • Full API integration suite: 99/99 passing.
  • Lint clean.

Pairs with rollnote-front feat/duplicate-frame.

Generated with Claude Code (https://claude.com/claude-code)

## Summary - New endpoint `POST /frame/:id/duplicate` - atomic transactional copy of a frame plus its optional `frame_location` row, into the same shoot. - Source frame row is locked with `FOR UPDATE` to serialize against concurrent deletes; shoot row is locked the same way to atomically compute `MAX(number) + 1`. - Rejects on a done shoot (400), other user's frame (404), unknown id (404). Returns `{id}` like the create endpoint. - Refactor: extracted `getNextFrameNumber(tx, shootId)` helper, used by `create`, `update`, and `duplicate` (3 call sites of the same `COALESCE(MAX(...))` block collapsed into 1). ## Test plan - [x] 6 new integration tests in `frame.int-spec.ts`: happy path with location, no-location case, done-shoot 400, other-user 404, unknown 404, concurrent serialization. - [x] Full API integration suite: 99/99 passing. - [x] Lint clean. Pairs with rollnote-front feat/duplicate-frame. Generated with Claude Code (https://claude.com/claude-code)
feat(frame): duplicate a frame via POST /:id/duplicate
Some checks failed
tests / test (pull_request) Failing after 1m1s
eac6baca93
Adds a transactional duplicate endpoint that copies all source fields
plus the optional frame_location row, assigns the next number under
FOR UPDATE on the shoot, and rejects on a done shoot. Locks the source
frame row to serialize against concurrent deletes. Extracts the shared
MAX(number)+1 query into a private getNextFrameNumber helper used by
create, update, and duplicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(frame): extract helpers in duplicate-frame tests
All checks were successful
tests / test (pull_request) Successful in 2m33s
584f39e9fa
Pulls createFrameFor, duplicateFrameAs, and getFrameAs into local
helpers to remove the repeated request(...).post/get/set blocks across
the duplicate tests. Splits the big "copies all fields + location"
test into two focused tests (scalars + weather, location row) so the
single-test cyclomatic complexity drops below the fallow threshold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refactor(import): split commit and commitData into focused helpers
Some checks failed
tests / test (pull_request) Failing after 54s
e4809fcf71
Pulls the CreateNew branch of commit() into resolveTargetUserId,
extracts assertExportedEmailMatchesAuthedUser, wipeConnectedDataIfRequested,
and maybeDeleteConnectedAccount so commit() now reads as a four-line
orchestrator.

Splits the 335-line transaction body in commitData() into per-table
private methods (importIsos, importWeathers, importFilmFormats,
importFilms, importCameras, importLenses, importFilters, importFolders,
importShoots, importFrames, importFrameLocations) plus a shared
loadExistingForImport helper. The transaction body is now pure
orchestration.

Extracts parseShutterSeconds from parseShutterSpeedLabel and lifts the
v5-vs-legacy shutter-speed resolution into a free-standing
resolveShutterSpeed function. Introduces lookupOrNull to collapse the
repeated `row.x ? (map.get(row.x) ?? null) : null` pattern.

Behavior is unchanged: 100/100 integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refactor(api): drop remaining hotspots below fallow CRAP threshold
Some checks failed
tests / test (pull_request) Failing after 55s
babbec47dd
frame.service.ts:
  - extract loadOwnedFrameOrThrow, lockShootAndAssertOpen, and
    copyFrameLocation from duplicate(); arrow now reads as orchestration
  - extract prepareShootMove from update(); the shoot-move branch lives
    in its own helper alongside the unchanged early-exit path

import.service.ts:
  - split parseShutterSpeedLabel into parseFiniteShutter so the validity
    test no longer chains a single if
  - extract collectCreateNewErrors from validate() so the CreateNew
    branch's two checks no longer compound with the outer accountTarget
    guard
  - extract loadOwnedForCounts from computeCounts(); the five ternaries
    over targetUserId now live in one place with an early return
  - split resolveShutterSpeed into readV5Shutter and readLegacyShutter
    so the v5/legacy fork is a clean `?? fallback` at the call site

Behavior unchanged: 100/100 integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refactor(api): drop the last two CRAP=30 functions below threshold
All checks were successful
tests / test (pull_request) Successful in 2m32s
f2e9cd3777
fallow flags any function at CRAP >= 30, not just above. The previous
refactor left readV5Shutter and frame.service.ts's update() arrow
sitting at exactly 30; both are now lower.

  - readV5Shutter: extract hasV5ShutterFields so the v5 detection is
    its own predicate, dropping the inline `&& undefined` from the
    main function.
  - update(): replace the inline `frameDto.shoot != null && ... !==
    existing.shoot` with a shootChangeTarget helper that returns the
    target id or null.

Behavior unchanged: 100/100 integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eliott merged commit 14a7f82238 into develop 2026-05-15 15:33:47 +02:00
eliott deleted branch feat/duplicate-frame 2026-05-15 15:33:47 +02:00
Sign in to join this conversation.
No reviewers
No labels
run-ci
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
RollNote/rollnote-api!46
No description provided.