feat(api): shopping-list CRUD endpoints #14

Merged
admin-mrrm merged 2 commits from feat/shopping-list-crud into main 2026-04-15 09:26:09 +02:00
Owner

Summary

  • First feature module: REST CRUD for shopping lists + items, scoped per owner_sub (JWT sub)
  • Drizzle schema + migration 0000_aberrant_omega_red.sql (uuid PKs generated in JS, no PG extension needed)
  • Tiny ZodValidationPipe so controllers reuse the exact @mrrmlab/shared-types DTOs the clients will consume
  • shared-types refactored from source-only ESM to compiled CJS dist/ so NestJS can actually require it at runtime (separate commit)

Endpoints

Method Path
GET /shopping-lists list owned
GET /shopping-lists/:id with nested items
POST /shopping-lists create
PATCH /shopping-lists/:id rename
DELETE /shopping-lists/:id 204, cascades items
POST /shopping-lists/:id/items auto-position via max+1
PATCH /shopping-lists/:id/items/:itemId
DELETE /shopping-lists/:id/items/:itemId 204

Test plan

  • pnpm --filter @mrrmlab/shared-types build succeeds
  • pnpm --filter @mrrmlab/api build succeeds
  • drizzle-kit generate + drizzle-kit migrate apply cleanly to the local Termux Postgres
  • Create -> list -> add 2 items -> get nested -> patch checked -> delete -> 404 round-trip works against running API
  • 400 on empty title and empty PATCH body (Zod refine)
  • 404 on missing list/item
  • /healthz still 200 (public)

Closes #6

## Summary - First feature module: REST CRUD for shopping lists + items, scoped per `owner_sub` (JWT `sub`) - Drizzle schema + migration `0000_aberrant_omega_red.sql` (uuid PKs generated in JS, no PG extension needed) - Tiny `ZodValidationPipe` so controllers reuse the exact `@mrrmlab/shared-types` DTOs the clients will consume - `shared-types` refactored from source-only ESM to compiled CJS `dist/` so NestJS can actually require it at runtime (separate commit) ## Endpoints | Method | Path | | |---|---|---| | GET | `/shopping-lists` | list owned | | GET | `/shopping-lists/:id` | with nested items | | POST | `/shopping-lists` | create | | PATCH | `/shopping-lists/:id` | rename | | DELETE | `/shopping-lists/:id` | 204, cascades items | | POST | `/shopping-lists/:id/items` | auto-position via max+1 | | PATCH | `/shopping-lists/:id/items/:itemId` | | | DELETE | `/shopping-lists/:id/items/:itemId` | 204 | ## Test plan - [x] `pnpm --filter @mrrmlab/shared-types build` succeeds - [x] `pnpm --filter @mrrmlab/api build` succeeds - [x] `drizzle-kit generate` + `drizzle-kit migrate` apply cleanly to the local Termux Postgres - [x] Create -> list -> add 2 items -> get nested -> patch checked -> delete -> 404 round-trip works against running API - [x] 400 on empty title and empty PATCH body (Zod refine) - [x] 404 on missing list/item - [x] `/healthz` still 200 (public) Closes #6
The original "source-only" setup pointed `main`/`exports` at `./src/*.ts`
with `type: module` and `.js`-suffixed imports. tsc compiled the API
fine, but at runtime Node's ESM loader tried to resolve the sibling
`./common.js` next to `index.ts` and crashed with ERR_MODULE_NOT_FOUND
the moment the API tried to import a shared schema.

Build to `./dist` as CommonJS instead so both the NestJS API (CJS
runtime) and the bundlers (Vite, Metro) can consume it via the same
`main`/`exports` entries. tsBuildInfoFile lives inside `dist/` so
incremental builds stay consistent if `dist/` is wiped.

- package.json: drop `type: module`, point exports at `./dist/*`,
  add `build` script.
- src: drop `.js` suffixes from internal imports.
- tsconfig.build.json: emits CJS to `./dist`.
- .gitignore: ignore `dist/` and `*.tsbuildinfo`.
Adds the first feature module: a Drizzle-backed shopping list service
with a REST controller scoped per owner via the JWT subject.

Schema (drizzle/0000_*.sql):
- shopping_lists(id uuid pk, owner_sub text, title text, timestamps)
- shopping_list_items(id uuid pk, list_id fk on delete cascade,
  label text, checked bool, position int, timestamps)

UUIDs generated via crypto.randomUUID() in JS so we don't need a
Postgres extension (works on the Termux PG install).

Endpoints (all under JwtAuthGuard, owner_sub = JWT `sub` or "dev-user"
when the guard is in dev passthrough):
- GET    /shopping-lists
- GET    /shopping-lists/:id              -> list with nested items
- POST   /shopping-lists
- PATCH  /shopping-lists/:id
- DELETE /shopping-lists/:id              -> 204, cascades items
- POST   /shopping-lists/:id/items        -> auto-position via max+1
- PATCH  /shopping-lists/:id/items/:itemId
- DELETE /shopping-lists/:id/items/:itemId -> 204

Validation goes through a tiny ZodValidationPipe (src/common/) wrapped
around the DTO schemas from @mrrmlab/shared-types, so Nest controllers
share the exact same shapes the clients will use. Empty PATCH bodies
are rejected by the shared-types refine().

Smoke-tested locally end-to-end:
- create -> list -> add items -> get nested -> patch -> delete cascade
- 400 on empty title and empty patch
- 404 on missing list / item

Closes #6
Sign in to join this conversation.
No reviewers
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
admin-mrrm/mrrmlabapp!14
No description provided.