feat(shoppingList) Shopping list overview with item previews (spec 002) #25
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "002-shopping-list-overview-previews"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements spec 002 (shopping list overview with item previews) in recipe-backend.
docs/plans/002-shopping-list-overview-previews.mddocs/adr/2026-10-03-shopping-list-item-change-time.md,docs/adr/2026-10-03-shopping-list-overview-query.md(GATE B), and new in this PR:docs/adr/2026-10-03-database-backed-integration-tests.md(plan step 1.1)What was implemented
vitest.integration.config.ts;npm run test:integration;src/tests/integration/integrationDataSource.ts(schema per run in the database of.env.test, entities and migrations loaded from the.tssources withimport.meta.glob);npm testexcludes*.integration.test.ts;CLAUDE.mdand architecture §91780012800013-CreateShoppingListItemChangeTable.ts: table plus backfill withAT TIME ZONE current_setting('TimeZone')ShoppingListItemChangeEntity; one sentence onShoppingListEntityinsertItemChange,recordItemChange;findCompactForUserjoins the change table outside the union and orders by it;findOpenItemPreviews;ShoppingListItemPreviewProjection;deleteByIdanddeleteTickedOnListreturn the row countitemContentEqualsOPEN_ITEMS_PREVIEW_SIZE,ShoppingListOverviewRow,getOverview; mapper and serviceitemContentEquals; overview, mapper and service casesShoppingListItemChange.integration.test.ts, 45 tests: 9.1 recorded and list row untouched, 9.2 "written last" for every row of the plan's table plus the held-list-lock test, 9.3 tick × reorder (50 rounds) and all 16 pairs (20 rounds each), 9.4 the overview queriesdocs/architecture.md§4.2, 4.4–4.7, 5, 6, 7, 9Deviations from the plan
One, recorded in the plan's Deviations section:
ShoppingListItemHandlerno longer takes a list repository (2026-10-04, decided by the user in the session). The plan kept the constructor and the factory unchanged; the unusedlistRepositoryparameter is now removed, and the factory, the unit test and the integration test pass three arguments.Added after the review (on the user's request)
docs/howto-testing.md: running and writing tests, setting up the test database.docs/howto-locking.md: the locks, the lock order, the rules for the change row (including "at most once per transaction"), the steps for adding a write, and the known gap of plan judgment call 10.docs/architecture.md(header, §6, §9) andCLAUDE.md. Architecture §3 listssrc/tests/integration/.CLAUDE.md: the naming vocabulary gains…DataSource, and a short rule points shopping list writes to the locking how-to.WritePathconst; a 404 is now accepted only indeleteItem × untickItemanddeleteItem × moveItemrounds on the same item and inremoveTicked × untickItem(which has no "different items" variant, as the test now says).npm test606 passed,npm run test:integration45 passed three times in a row with the stricter rule.coverage/is in.gitignore. A coverage report was committed by mistake inafd6c04and removed in6630bba; both commits stay in the history.Finding for the reviewer
recordItemChangemust be called at most once per transaction. A secondUPDATEof a change row that the same transaction already wrote makes PostgreSQL re-check the foreign key, and that check takes a key-share lock on the list row, the same lock the ADR rules out for an upsert. Found while checking that the integration tests can fail: with a second call added toremoveTicked, the held-list-lock test ran into the lock timeout. The code calls it once per transaction everywhere (a move passes both lists in one call). The rule is now stated in the JSDoc ofrecordItemChangeand in architecture §4.6 and §6. The ADR does not state it; it is immutable, so if the rule should be in an ADR, that needs a decision.Judgment calls
listRepositoryconstructor parameter was removed.)tickItem × untickItemboth act on an open item (the untick then either finds it open or undoes the tick), anddeleteItem × untickItemboth act on a ticked item.integrationDataSource.tsloads.env.testwithoverride: true, soDB_*variables from the shell cannot point the tests at another database.src/entities/ShoppingListEntity.tsnamesShoppingListItemChangeEntityin the JSDoc sentence that plan step 3 asks for. It is a comment, not a write.items_changed_atistimestamp with time zone, that a rename with a version from before a partner's item changes succeeds, and that a previewed row carries amount and unit and no comment.Verification
npm run buildin../recipe-platform/api-contractnpm run buildnpm testnpm run test:integrationremoveTicked, and no record intickItem. Both failed the testspg_typeof(items_changed_at)istimestamp with time zone; 1 list, 1 change row; the backfilled time equals the newest itemupdate_dateof that list as wall-clock time. The runningstart:devserver applied the migration as soon as it was compiled, so the "before" values were read from the unchanged source columns afterwards, not noted in advance. The database had one list, not two or threeDUPLICATE, tick on a ticked item and edit with the same content do not; tick, untick, edit, delete, remove-ticked, reorder and move do (move: both lists); rename with a version from an old overview returns 201; a new list is first with an empty preview and count 0. The test lists were deleted afterwardsgit diff --stat stage(step 11.6)AbstractEntity.ts,AbstractRepository.tsor any recipe or meal plan filerecordItemChangeinShoppingListItemHandler.ts, 1 call ofinsertItemChangeinShoppingListHandler.ts, 2 definitions; no upsert or save inrecordItemChange;update(ShoppingListEntity)only inrenameIfVersion; onethis.itemRepositoryhit (first read ofmoveItem); no list lock in tick, edit, delete, remove-ticked. Grep 3: see judgment call 6Not verified: nothing was skipped. The repo defines no linter.
Notes
docs/architecture.mdand the class comment ofShoppingListRepository. Expect text conflicts with whichever merges second; the plan says how to resolve them.contract.ref) is on platformmain.🤖 Generated with Claude Code
WIP: feat(shoppingList) Shopping list overview with item previews (spec 002)to feat(shoppingList) Shopping list overview with item previews (spec 002)@ -14,1 +14,4 @@* that checks and bumps `version` itself (see ShoppingListRepository).** The time of the last change to the list's items lives in ShoppingListItemChangeEntity,* on purpose not on this row (docs/adr/2026-10-03-shopping-list-item-change-time.md).a very brief single sentence why here would be more comfortable than opening the docs to find out.
@ -76,0 +114,4 @@// list, and pushing keeps that order.const previewsByListId: Map<string, ShoppingListItemPreviewProjection[]> = new Map();for (const preview of previews) {const ofList: ShoppingListItemPreviewProjection[] = previewsByListId.get(preview.shopping_list_id) ?? [];naming I'd prefer previewsOfList
Review feedback addressed in
720329e:src/entities/ShoppingListEntity.ts(why the change time is not on the list row): the JSDoc now states the reason in place: writing the list row for an item change would bumpversionandupdate_date, which cover the name only, and could deadlock with the list row lock. The ADR reference stays. The other places that cite this ADR already state their reason next to the reference.src/handlers/ShoppingListHandler.ts(naming):ofListis renamed topreviewsOfList.npm run buildok,npm test606 passed,npm run test:integration45 passed.🤖 Generated with Claude Code