feat(shoppingList) Shopping lists (spec 001) #21

Merged
araemer merged 16 commits from 001-shopping-lists into stage 2026-10-03 11:43:41 +00:00
Owner

Implements spec 001 (shopping lists) in recipe-backend.

Approved at GATE B

  • docs/adr/2026-10-02-shopping-list-item-writes.md
  • docs/adr/2026-10-02-shopping-list-item-order.md (revised: the lock also covers share writes; untick and move re-read the item after locking)
  • docs/adr/2026-10-02-shopping-list-default-storage.md
  • docs/adr/2026-10-02-single-error-serialization-point.md

Approved at GATE C

  • docs/plans/001-shopping-lists.md

What's in it (by plan step)

  1. Error envelope: HttpError carries a required ErrorCode (generic default per status, per-status code unions). errorLoggerMiddleware is the only writer of error bodies ({ error, code }). Direct writes in auth/authorization middleware, AuthRestResource, ImageRestResource and the unknown-route fallback now throw. errorHandler.ts is deleted. Uuid.parse throws ValidationError. UserHandler uses USER_NOT_FOUND.
  2. GET /user/list-shareable: returns CompactUserDto (no email or role), registered before /:userId.
    3–4. Five migrations and entities: shopping_list, _share, _item, _item_reason, _default.
  3. Repositories: EntityManager-bound repositories plus ShoppingListTransactionHelper, with list row locks in id order. Item rows are written only via updateKeepingVersion or updateContentIfVersion. The plan's W1–W11 review grep finds only those two update paths.
  4. shoppingItemNameUtils: German plural duplicate match.
  5. Handlers: ShoppingListPermissionHelper, ShoppingListHandler, ShoppingListItemHandler.
  6. Mappers: four new mappers, plus CompactUserMapper.
  7. ShoppingListApplicationService: checks the JSON shape of request bodies and returns 400 before any handler call.
  8. Routers: /shopping-list (with GET /all first), /shopping-list-item, /shopping-list-share.
  9. Bruno: request files for every new route. Folder seq values are renumbered, which also fixes the duplicate seq: 8.
  10. docs/architecture.md: updated.

Deviations from the plan

  • npm run migration fixed along the way. It used typeorm-ts-node-commonjs on src/data-source.ts, which couldn't load the ESM sources, and the data source only finds compiled migrations under build/. The script now builds first and runs against build/data-source.js. The migrations for this feature had already been applied by the running dev server on startup.
  • Live verification signed its own tokens. The two-user check used short-lived tokens signed with the dev secret instead of logging in with the configured test users. The results are unaffected.

Judgment calls

  • Name must be a string. The service rejects a missing or non-string name (lists and items) with 400. Otherwise the handler's .trim() would crash and answer 500.
  • Version check on rename. Rename also requires version to be a positive integer, the same check as edit and change-permission. A non-number would otherwise reach the SQL and answer 500.
  • null optional fields. amount, unit and comment sent as null count as absent instead of being rejected.
  • List loading shared in one module. loadList, loadListWithPermission and lockListOrNotFound live in src/handlers/shoppingListHandlerUtils.ts and are used by both handlers. Permission resolution itself stays in ShoppingListPermissionHelper, which stays pure as the plan asked.
  • Purpose-suffixed names. Four types from the plan were renamed to follow the new naming rule in CLAUDE.md: ShoppingListAccessPolicy → ShoppingListPermissionHelper, ShoppingListTransactionRunner → ShoppingListTransactionHelper, ShoppingItemNameMatcher → shoppingItemNameUtils, and the shared loading helpers → shoppingListHandlerUtils. The plan keeps its original names and has a rename note.
  • Fewer reads with allowDuplicate. addItem reads the open and ticked items only when the duplicate check runs.
  • Bruno placeholder IDs. The all-zero placeholder ID in Bruno files now answers 400 instead of 500, because a malformed ID is rejected up front. The existing meal-plan files are affected the same way.

Verification

  • npm run build (contract and backend) and npm test: 557 of 557 passed (366 before).
  • Two users against the dev server, 29 of 29 checks passed:
    • DUPLICATE on "möhren" against "Möhre".
    • Both users tick the same item: tickedBy stays the first user and version doesn't change.
    • A stale edit answers 409 CONFLICT, and an edit without version answers 400.
    • A rename by a READ_WRITE user answers 403.
    • A move to a list without access answers 403 TARGET_SHOPPING_LIST_ACCESS_DENIED, and to an unknown list 404 TARGET_SHOPPING_LIST_NOT_FOUND.
    • After the share is removed, defaultListId is null and the list answers 403.
    • Unknown routes answer 404, requests without a token 401, and malformed IDs 400, all with { error, code }.
  • git diff --stat: no changes to AbstractRepository, the recipe or meal plan repositories, handlers or services.

Also adds a naming rule to CLAUDE.md: class and module names end in a purpose suffix.

Related: #20 (the AbstractRepository.update() race, out of scope here)

🤖 Generated with Claude Code

Implements spec 001 (shopping lists) in recipe-backend. - Spec: https://git.raemer.net/araemer/recipe-platform/src/branch/main/docs/features/001-shopping-lists.md - Plan: [`docs/plans/001-shopping-lists.md`](https://git.raemer.net/araemer/recipe-backend/src/branch/001-shopping-lists/docs/plans/001-shopping-lists.md) - Contract: `@recipe/contract` — `shopping-lists.ts`, `errors.ts`, `users.ts`, `shared.ts` ## Approved at GATE B - `docs/adr/2026-10-02-shopping-list-item-writes.md` - `docs/adr/2026-10-02-shopping-list-item-order.md` (revised: the lock also covers share writes; untick and move re-read the item after locking) - `docs/adr/2026-10-02-shopping-list-default-storage.md` - `docs/adr/2026-10-02-single-error-serialization-point.md` ## Approved at GATE C - `docs/plans/001-shopping-lists.md` ## What's in it (by plan step) 1. **Error envelope:** `HttpError` carries a required `ErrorCode` (generic default per status, per-status code unions). `errorLoggerMiddleware` is the only writer of error bodies (`{ error, code }`). Direct writes in auth/authorization middleware, `AuthRestResource`, `ImageRestResource` and the unknown-route fallback now throw. `errorHandler.ts` is deleted. `Uuid.parse` throws `ValidationError`. `UserHandler` uses `USER_NOT_FOUND`. 2. **`GET /user/list-shareable`:** returns `CompactUserDto` (no email or role), registered before `/:userId`. 3–4. **Five migrations and entities:** `shopping_list`, `_share`, `_item`, `_item_reason`, `_default`. 5. **Repositories:** EntityManager-bound repositories plus `ShoppingListTransactionHelper`, with list row locks in id order. Item rows are written only via `updateKeepingVersion` or `updateContentIfVersion`. The plan's W1–W11 review grep finds only those two update paths. 6. **`shoppingItemNameUtils`:** German plural duplicate match. 7. **Handlers:** `ShoppingListPermissionHelper`, `ShoppingListHandler`, `ShoppingListItemHandler`. 8. **Mappers:** four new mappers, plus `CompactUserMapper`. 9. **`ShoppingListApplicationService`:** checks the JSON shape of request bodies and returns 400 before any handler call. 10. **Routers:** `/shopping-list` (with `GET /all` first), `/shopping-list-item`, `/shopping-list-share`. 11. **Bruno:** request files for every new route. Folder `seq` values are renumbered, which also fixes the duplicate `seq: 8`. 12. **`docs/architecture.md`:** updated. ## Deviations from the plan - **`npm run migration` fixed along the way.** It used `typeorm-ts-node-commonjs` on `src/data-source.ts`, which couldn't load the ESM sources, and the data source only finds compiled migrations under `build/`. The script now builds first and runs against `build/data-source.js`. The migrations for this feature had already been applied by the running dev server on startup. - **Live verification signed its own tokens.** The two-user check used short-lived tokens signed with the dev secret instead of logging in with the configured test users. The results are unaffected. ## Judgment calls - **Name must be a string.** The service rejects a missing or non-string `name` (lists and items) with 400. Otherwise the handler's `.trim()` would crash and answer 500. - **Version check on rename.** Rename also requires `version` to be a positive integer, the same check as edit and change-permission. A non-number would otherwise reach the SQL and answer 500. - **`null` optional fields.** `amount`, `unit` and `comment` sent as `null` count as absent instead of being rejected. - **List loading shared in one module.** `loadList`, `loadListWithPermission` and `lockListOrNotFound` live in `src/handlers/shoppingListHandlerUtils.ts` and are used by both handlers. Permission resolution itself stays in `ShoppingListPermissionHelper`, which stays pure as the plan asked. - **Purpose-suffixed names.** Four types from the plan were renamed to follow the new naming rule in `CLAUDE.md`: `ShoppingListAccessPolicy` → `ShoppingListPermissionHelper`, `ShoppingListTransactionRunner` → `ShoppingListTransactionHelper`, `ShoppingItemNameMatcher` → `shoppingItemNameUtils`, and the shared loading helpers → `shoppingListHandlerUtils`. The plan keeps its original names and has a rename note. - **Fewer reads with `allowDuplicate`.** `addItem` reads the open and ticked items only when the duplicate check runs. - **Bruno placeholder IDs.** The all-zero placeholder ID in Bruno files now answers 400 instead of 500, because a malformed ID is rejected up front. The existing meal-plan files are affected the same way. ## Verification - `npm run build` (contract and backend) and `npm test`: 557 of 557 passed (366 before). - Two users against the dev server, 29 of 29 checks passed: - `DUPLICATE` on "möhren" against "Möhre". - Both users tick the same item: `tickedBy` stays the first user and `version` doesn't change. - A stale edit answers 409 `CONFLICT`, and an edit without `version` answers 400. - A rename by a READ_WRITE user answers 403. - A move to a list without access answers 403 `TARGET_SHOPPING_LIST_ACCESS_DENIED`, and to an unknown list 404 `TARGET_SHOPPING_LIST_NOT_FOUND`. - After the share is removed, `defaultListId` is `null` and the list answers 403. - Unknown routes answer 404, requests without a token 401, and malformed IDs 400, all with `{ error, code }`. - `git diff --stat`: no changes to `AbstractRepository`, the recipe or meal plan repositories, handlers or services. Also adds a naming rule to `CLAUDE.md`: class and module names end in a purpose suffix. Related: #20 (the `AbstractRepository.update()` race, out of scope here) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(adr) Add shopping list and error serialization ADRs for spec 001
All checks were successful
/ test (pull_request) Successful in 1m2s
f1274a1bb1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(adr) Extend the shopping list lock to share writes and concurrent moves (spec 001)
All checks were successful
/ test (pull_request) Successful in 1m2s
15c857a374
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(plan) Add shopping list implementation plan (spec 001)
All checks were successful
/ test (pull_request) Successful in 1m2s
58b761de1b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HttpError carries a required ErrorCode with a generic default per status;
errorLoggerMiddleware is the only writer of error bodies. Direct writes in
the auth/authorization middleware, AuthRestResource, ImageRestResource and
the unknown-route fallback become thrown/forwarded HttpErrors. Uuid.parse
throws ValidationError; UserHandler uses USER_NOT_FOUND. The unregistered
errorHandler.ts is deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Returns every user except the caller as CompactUserDto (no email, no
role) for share recipient pickers. Registered before GET /user/:userId.
Adds the one-way CompactUserMapper and the shared avatarUrlFor helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migrations and entities for shopping_list, shopping_list_share,
shopping_list_item, shopping_list_item_reason and shopping_list_default.
Item rows keep a sort_order and ticked state outside the content
version; reasons and defaults are plain tables without AbstractEntity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EntityManager-bound repositories (no AbstractRepository) with a
transaction runner for list row locks; item rows are written only via
targeted UPDATEs that pin or conditionally bump version. Adds the German
plural name matcher, ShoppingListAccessPolicy, ShoppingListHandler
(lists, shares, default) and ShoppingListItemHandler (items).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShoppingListApplicationService validates request shapes (400 before any
handler call) and maps to contract DTOs; one-way share/item/compact
mappers; three routers mounted under /shopping-list, /shopping-list-item
and /shopping-list-share. Bruno files follow in a separate commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three new resource folders plus UserRestResource/listShareable.bru.
Folder seq values are renumbered alphabetically, which also fixes the
duplicate seq 8 on Passkey/Rating.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(architecture) Document the shopping list domain and error envelope
All checks were successful
/ test (pull_request) Successful in 1m9s
da0778c2e7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
araemer changed title from WIP: feat(shoppingList) Shopping lists (spec 001) to feat(shoppingList) Shopping lists (spec 001) 2026-10-03 04:45:54 +00:00
refactor(shoppingList) Share list loading and locking between both handlers
All checks were successful
/ test (pull_request) Successful in 1m17s
a97844241d
loadList, loadListWithPermission and lockListOrNotFound move from private
copies in ShoppingListHandler and ShoppingListItemHandler into one
ShoppingListLoading module, together with the shared 404 message.
ShoppingListAccessPolicy stays pure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refactor(shoppingList) Rename helpers to purpose-suffixed names
All checks were successful
/ test (pull_request) Successful in 1m8s
0dd5f3d9b7
ShoppingListAccessPolicy -> ShoppingListPermissionHelper,
ShoppingListLoading -> shoppingListHandlerUtils,
ShoppingItemNameMatcher -> shoppingItemNameUtils,
ShoppingListTransactionRunner -> ShoppingListTransactionHelper.
The approved plan keeps its original names and gains a rename note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ -0,0 +18,4 @@
// -------------------------------------------------------------------------
describe("defaults", () => {
const cases: Array<{ name: string; error: HttpError; status: number; code: ErrorCode }> = [
{ name: "ValidationError", error: new ValidationError("bad"), status: 400, code: ErrorCode.VALIDATION_FAILED },
Author
Owner

should we be using the httpStatusCode enum here instead of plain numbers?

should we be using the httpStatusCode enum here instead of plain numbers?
araemer marked this conversation as resolved
@ -0,0 +50,4 @@
*/
export class ShoppingListHandler {
constructor(
private readonly lists: ShoppingListRepository,
Author
Owner

this naming isn't good. instead of lists and items use listRepository and itemRepository to make the role clear. And no two letter names like tx either. They only cause confusion

this naming isn't good. instead of lists and items use listRepository and itemRepository to make the role clear. And no two letter names like tx either. They only cause confusion
araemer marked this conversation as resolved
@ -0,0 +94,4 @@
if (!list) throw new NotFoundError(LIST_NOT_FOUND);
// Any access level may read
const permission: ShoppingListEffectivePermission = this.permissionHelper.resolve(list, userId);
Author
Owner

but what about no access level at all? shouldn't it return null? I don't see any treatment for that case.

but what about no access level at all? shouldn't it return null? I don't see any treatment for that case.
araemer marked this conversation as resolved
@ -0,0 +73,4 @@
const openIds: Set<string> = new Set(open.map((item: ShoppingListItemEntity): string => item.id!));
// Named open items first, in request order; a Set keeps the first occurrence only
const named: Set<string> = new Set(requested.filter((id: string): boolean => openIds.has(id)));
Author
Owner

I had to read this several time to understand openIds and named. Open seems to be unticked - worth a comment I think. and named seems to be open items mentioned in the request. We need to improve the names here.

I had to read this several time to understand openIds and named. Open seems to be unticked - worth a comment I think. and named seems to be open items mentioned in the request. We need to improve the names here.
araemer marked this conversation as resolved
@ -0,0 +96,4 @@
*/
export class ShoppingListItemHandler {
constructor(
private readonly lists: ShoppingListRepository,
Author
Owner

meaningful names please... listRepository, itemRepository, transaction or transactionHelper...

meaningful names please... listRepository, itemRepository, transaction or transactionHelper...
araemer marked this conversation as resolved
@ -0,0 +277,4 @@
const locked: Set<Uuid> = await lists.lockLists([sourceListId, targetListId]);
// Re-read under the lock; if another move changed its list, lock that one next
const item: ShoppingListItemEntity = await this.findItemOrNotFound(items, itemId);
Author
Owner

Hm, is this correct? If another move already changed the item's list, should we silently move it again or create a user-facing message "The item you are requesting has already moved to list A/another list (if the user doesn't have access to the traget list)

Hm, is this correct? If another move already changed the item's list, should we silently move it again or create a user-facing message "The item you are requesting has already moved to list A/another list (if the user doesn't have access to the traget list)
araemer marked this conversation as resolved
@ -0,0 +476,4 @@
}
/** Trims an optional text; empty or absent becomes null. */
private trimToNull(value: string | null | undefined): string | null {
Author
Owner

Wouldn't trimOrNull be better?

Wouldn't trimOrNull be better?
araemer marked this conversation as resolved
@ -0,0 +6,4 @@
* Raw user columns as a SQL projection returns them, e.g. the owner columns of the
* shopping list overview query.
*/
export interface CompactUserColumns {
Author
Owner

why columns and not projection here?

why columns and not projection here?
araemer marked this conversation as resolved
@ -0,0 +9,4 @@
* One-way: items are changed through dedicated request types (add, edit, tick, …), never
* by sending an item DTO back, so the reverse direction does not exist.
*
* Optional values are absent keys, not null, as the contract says: `amount`, `unit` and
Author
Owner

I don't understand that comment

I don't understand that comment
araemer marked this conversation as resolved
@ -25,3 +36,3 @@
dto.lastName = entity.lastName;
dto.role = entity.role;
dto.avatarUrl = entity.avatarKey ? `${IMAGE_BASE_ROUTE}/${entity.avatarKey}` : null;
dto.avatarUrl = avatarUrlFor(entity.avatarKey);
Author
Owner

The method name is strange. I'd prefer avatarUrlByKey

The method name is strange. I'd prefer avatarUrlByKey
araemer marked this conversation as resolved
@ -0,0 +147,4 @@
// ---------------------------------------------------------------------------
/**
* W1 — inserts a new, open item. The database sets `version` to 1.
Author
Owner

why w1, w2 and so on. Is it a reference to the plan? I'd remove the cryptic w somethings.

why w1, w2 and so on. Is it a reference to the plan? I'd remove the cryptic w somethings.
araemer marked this conversation as resolved
@ -0,0 +72,4 @@
.createQueryBuilder(ShoppingListEntity, LIST_ALIAS)
.select(`${LIST_ALIAS}.id`)
.where(`${LIST_ALIAS}.id = :id`, { id })
.setLock("pessimistic_write")
Author
Owner

I didn't find an ulock. Does typeORM handle that on it's own after write, did i miss it or did we forget it?

I didn't find an ulock. Does typeORM handle that on it's own after write, did i miss it or did we forget it?
araemer marked this conversation as resolved
@ -0,0 +6,4 @@
/** Repositories bound to one transaction's EntityManager. */
export interface ShoppingListTransactionRepositories {
/** Lists, shares and defaults, on the transaction's manager. */
lists: ShoppingListRepository;
Author
Owner

Names... listRepository, itemRepository

Names... listRepository, itemRepository
araemer marked this conversation as resolved
refactor(shoppingList) Address review feedback on names, comments and tests
All checks were successful
/ test (pull_request) Successful in 1m8s
7c298c7480
- Repository/transaction names: listRepository, itemRepository,
  transactionHelper; transaction callbacks take one named
  transactionRepositories parameter; em -> entityManager; no 1-2 letter
  lambda parameters (s, r).
- computeReorder: openItems/requestedItemIds/requestedOpenItemIds/
  remainingOpenItemIds, with "open = not ticked" spelled out.
- Renames: trimToNull -> trimOrNull, avatarUrlFor -> avatarUrlByKey,
  CompactUserColumns/fromColumns -> CompactUserFields/fromFields (JSDoc
  explains why it is not a projection).
- Comments: lockLists says locks end with the transaction; getList says
  resolve() answers 403; item mapper explains omitted optional fields;
  W1-W10 plan labels removed from the item repository.
- Tests use HttpStatusCode instead of literal status numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Review feedback addressed in 7c298c7. One entry per comment:

  • Unlock (ShoppingListRepository.ts:75): nothing was missing. PostgreSQL releases SELECT … FOR UPDATE locks when the transaction commits or rolls back, and every lock runs inside ShoppingListTransactionHelper.run(). The lockLists JSDoc now says so.
  • Names lists / items / tx (three comments): renamed to listRepository, itemRepository and transactionHelper, including the ShoppingListTransactionRepositories fields. Transaction callbacks take one named transactionRepositories parameter, so calls bound to the transaction read differently from this.listRepository. Also fixed: em → entityManager, and the lambda parameters s → share and r → reason.
  • HttpStatusCode in tests: all 7 new test files now use the enum instead of literal status numbers.
  • Columns vs projection: renamed to CompactUserFields / fromFields(). The JSDoc explains why it is not a projection: no query returns this shape, and callers fill it from their own projection's columns.
  • No access level in getList: already handled. resolve() throws 403 FORBIDDEN, which the contract requires and the app relies on. The misleading line comment is fixed.
  • computeReorder names: now openItems, requestedItemIds, openItemIds, requestedOpenItemIds and remainingOpenItemIds, with "open = not ticked" spelled out.
  • Concurrent move: kept as is, as agreed. It matches the contract's "last write wins, never 409" rule for non-content changes. A user-facing "already moved" message would need a contract change: the request would have to carry the expected source list, plus a new error code.
  • trimToNull → trimOrNull: done.
  • Item mapper comment: rewritten in plain words. The matching test title and a similar comment in CompactUserMapper were reworded too.
  • avatarUrlFor → avatarUrlByKey: done, including docs/architecture.md.
  • W1–W10 labels: removed from the item repository. The descriptions stay.

Verification:

  • npm run build and npm test: 554 of 554 pass.
  • Smoke test against the dev server with the configured test users: 15 of 15 checks pass. It covers every write that locks a list (add, reorder, untick, set-default, move, delete-share).

🤖 Generated with Claude Code

Review feedback addressed in 7c298c7. One entry per comment: - **Unlock (`ShoppingListRepository.ts:75`):** nothing was missing. PostgreSQL releases `SELECT … FOR UPDATE` locks when the transaction commits or rolls back, and every lock runs inside `ShoppingListTransactionHelper.run()`. The `lockLists` JSDoc now says so. - **Names `lists` / `items` / `tx` (three comments):** renamed to `listRepository`, `itemRepository` and `transactionHelper`, including the `ShoppingListTransactionRepositories` fields. Transaction callbacks take one named `transactionRepositories` parameter, so calls bound to the transaction read differently from `this.listRepository`. Also fixed: `em` → `entityManager`, and the lambda parameters `s` → `share` and `r` → `reason`. - **`HttpStatusCode` in tests:** all 7 new test files now use the enum instead of literal status numbers. - **Columns vs projection:** renamed to `CompactUserFields` / `fromFields()`. The JSDoc explains why it is not a projection: no query returns this shape, and callers fill it from their own projection's columns. - **No access level in `getList`:** already handled. `resolve()` throws 403 `FORBIDDEN`, which the contract requires and the app relies on. The misleading line comment is fixed. - **`computeReorder` names:** now `openItems`, `requestedItemIds`, `openItemIds`, `requestedOpenItemIds` and `remainingOpenItemIds`, with "open = not ticked" spelled out. - **Concurrent move:** kept as is, as agreed. It matches the contract's "last write wins, never 409" rule for non-content changes. A user-facing "already moved" message would need a contract change: the request would have to carry the expected source list, plus a new error code. - **`trimToNull` → `trimOrNull`:** done. - **Item mapper comment:** rewritten in plain words. The matching test title and a similar comment in `CompactUserMapper` were reworded too. - **`avatarUrlFor` → `avatarUrlByKey`:** done, including `docs/architecture.md`. - **W1–W10 labels:** removed from the item repository. The descriptions stay. Verification: - `npm run build` and `npm test`: 554 of 554 pass. - Smoke test against the dev server with the configured test users: 15 of 15 checks pass. It covers every write that locks a list (add, reorder, untick, set-default, move, delete-share). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The ts-node-commonjs runner could not load the ESM sources, and the data
source's migrations glob (<dir>/migrations/*.js) only matches in build/.
The script now builds first and runs typeorm against build/data-source.js.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refactor(shoppingList) Address implementation review findings
All checks were successful
/ test (pull_request) Successful in 1m8s
8c8a68ce4e
- JSDoc: @param/@returns on all ShoppingListApplicationService methods;
  createNewEntity and doMergeDtoIntoEntity documented.
- Explicit return types on the sendFile and transaction callbacks and on
  the service-test mocks.
- Overview SQL uses table-name constants and takes the owner permission
  from ShoppingListEffectivePermission.OWNER as a query parameter.
- TARGET_LIST_NOT_FOUND constant replaces a duplicated message.
- Tests: ShoppingListTransactionHelper unit test; leftover `repos`
  renamed; ResponseStub fields documented.
- Plan rename note also lists the field renames from PR review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
araemer merged commit d62a933982 into stage 2026-10-03 11:43:41 +00:00
Sign in to join this conversation.
No reviewers
No labels
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
araemer/recipe-backend!21
No description provided.