Files
clienthub/.planning/phases/11-catalog-database-view-ux-legacy-consolidation/11-02-SUMMARY.md
T
simone 62d5e97f39 docs(11-02): complete catalog query layer + server actions plan
- SUMMARY.md for Plan 02 (ServiceWithTags + 4 new server actions)
- STATE.md: advance to Plan 3/4, record metrics and decision
- REQUIREMENTS.md: mark OFFER-07/08/09 backend foundation complete
2026-06-13 15:47:07 +02:00

8.9 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
11-catalog-database-view-ux-legacy-consolidation 02 api
drizzle
postgres
server-actions
tags
services
catalog
inline-edit
phase plan provides
11-catalog-database-view-ux-legacy-consolidation 11-01 tags pgTable (polymorphic entity_type/entity_id/name) + Tag/NewTag types in src/db/schema.ts
ServiceWithTags type + getAllServices() returning Service & { tags: string[] }, left-joined with tags scoped to entity_type='services'
updateServiceField server action — single-field inline edit (name, description, category, unit_price, active) with per-field validation
addTagToService / removeTagFromService server actions — tag assignment scoped to entity_type='services', idempotent via onConflictDoNothing
quickAddService server action — creates a unit_price='0.00' service from just a name (D-12)
11-03
11-04
added patterns
leftJoin + in-memory Map aggregation for one-to-many tag rows (services -> tags), following the row-grouping pattern already used elsewhere in admin-queries.ts for quote_items/service_catalog joins
EditableField closed union + runtime EDITABLE_FIELDS.includes() check as a column-write whitelist for generic inline-edit actions (T-11-07)
entity_type hardcoded as a literal (never client-supplied) in tag actions to scope tag pools per entity (T-11-08, D-06)
created modified
src/lib/admin-queries.ts
src/app/admin/catalog/actions.ts
onConflictDoNothing() without an explicit target compiles and is used as-is — Drizzle's no-target form falls back to ON CONFLICT DO NOTHING (any conflict), which is sufficient given tags_entity_name_unique is the only unique constraint on the tags table (from Plan 01).
ServiceWithTags is now the canonical return type for admin catalog list views — Plans 03/04 import this type directly instead of Service
OFFER-07
OFFER-08
OFFER-09
12min 2026-06-13

Phase 11 Plan 2: Catalog Query Layer + Inline-Edit/Tag/Quick-Add Server Actions Summary

getAllServices() now returns each service's assigned tag names via a left-join on the new tags table, and four new admin-gated server actions (updateServiceField, addTagToService, removeTagFromService, quickAddService) provide the inline-edit, tag-assignment, and quick-add primitives for the database-view UX.

Performance

  • Duration: ~12 min
  • Started: 2026-06-13T13:36:00Z
  • Completed: 2026-06-13T13:40:07Z
  • Tasks: 2 of 2
  • Files modified: 2

Accomplishments

  • getAllServices() rewritten to Promise<ServiceWithTags[]>: left-joins tags (scoped to entity_type="services" via eq + and), groups rows in-memory into { ...service, tags: string[] }, ordered by services.name asc, tags.name asc. Services with zero tags return tags: [] (left-join nulls filtered out).
  • getClientFullDetail and getProjectFullDetail (and their activeServices: Service[] fields) verified byte-for-byte unchanged — confirmed via git diff.
  • Added 4 new server actions to src/app/admin/catalog/actions.ts, all calling requireAdmin() first and revalidatePath("/admin/catalog") last:
    • updateServiceField(serviceId, fieldName, value) — generic single-field inline edit for name | description | category | unit_price | active, with a closed EDITABLE_FIELDS union + runtime whitelist check, per-field validation (required name, non-negative price stored with 2 decimals, boolean coercion for active).
    • addTagToService(serviceId, tagName) / removeTagFromService(serviceId, tagName) — tag add/remove hardcoded to entity_type="services", idempotent insert via onConflictDoNothing().
    • quickAddService(name) — creates a new services row with unit_price="0.00", active=true, from just a trimmed name (D-12).
  • npx tsc --noEmit passes with zero errors across the full project.
  • npx eslint on both modified files: 0 new warnings/errors (6 pre-existing unused-import warnings in admin-queries.ts, unrelated to this plan's changes, confirmed present before this plan too).

Task Commits

Each task was committed atomically:

  1. Task 1: Extend getAllServices() with tags join (ServiceWithTags) - f743410 (feat)
  2. Task 2: Add inline-edit, tag, and quick-add server actions - 445de85 (feat)

Plan metadata: (this commit, following SUMMARY)

Files Created/Modified

  • src/lib/admin-queries.ts - Added tags to schema import; added ServiceWithTags type (Service & { tags: string[] }); rewrote getAllServices() to left-join tags (scoped entity_type="services") and aggregate per-service tag arrays
  • src/app/admin/catalog/actions.ts - Added tags + and imports; appended updateServiceField, addTagToService, removeTagFromService, quickAddService server actions; existing createService/updateService/toggleServiceActive unchanged

Decisions Made

  • onConflictDoNothing() (no explicit target) was used as written in the plan — it compiled cleanly under the project's Drizzle version and is correct given the single unique index (tags_entity_name_unique) on the tags table. No change to the plan's suggested fallback was needed.

Deviations from Plan

None - plan executed exactly as written.

Issues Encountered

None.

DB-state note (carried from Plan 01, non-blocking for this plan)

The tags table exists only in src/db/schema.ts (committed in Plan 01) — it has not yet been physically created in the live dev/prod Postgres database (firewalled, pending manual SSH+docker exec migration apply by the user). This plan is code-only and verified via TypeScript types (npx tsc --noEmit passes against the Drizzle-inferred tags/services schema types). No live-DB queries were attempted or required for this plan's verification — getAllServices(), addTagToService, removeTagFromService etc. will function correctly once the Plan 01 migration (scripts/push-11-tags-migration.ts) is applied to the live DB. This is the same pending checkpoint documented in 11-01-SUMMARY.md and STATE.md Blockers — not a new issue introduced here.

Known Stubs

None - no UI or data-rendering stubs introduced by this plan (query layer + server actions only, consumed by Plans 03/04).

Threat Flags

None - this plan's new surface (updateServiceField, addTagToService, removeTagFromService, quickAddService) is fully covered by the threat model already documented in 11-02-PLAN.md (T-11-06 through T-11-10), all mitigated/accepted as designed:

  • T-11-06 (Elevation of Privilege): every new action calls await requireAdmin() first.
  • T-11-07 (Tampering via field whitelist): EDITABLE_FIELDS closed union + runtime .includes() check prevents arbitrary column writes.
  • T-11-08 (Tampering via entity_type): entity_type hardcoded to "services" literal in both tag actions, never client-supplied.
  • T-11-09 (SQL injection): all values pass through Drizzle's parameterized query builder.
  • T-11-10 (DoS via duplicate tags): onConflictDoNothing() on the (entity_type, entity_id, name) unique index makes addTagToService idempotent.

No new endpoints, auth paths, or trust-boundary changes beyond what was already reviewed in the plan's threat model.

User Setup Required

None - no new environment variables or external service configuration needed. (The pending tags table migration apply is tracked from Plan 01, not new to this plan.)

Next Phase Readiness

  • Query layer ready: ServiceWithTags type + getAllServices() are implemented and typecheck cleanly — Plan 03 (EditableCell/TagMultiSelect components) and Plan 04 (ServiceTable rewrite) can import ServiceWithTags directly with no further query-layer changes needed.
  • Server actions ready: updateServiceField, addTagToService, removeTagFromService, quickAddService are implemented, admin-gated, and exported — Plans 03/04 can wire these into UI components immediately.
  • NOT ready for live runtime verification: as with Plan 01, any runtime/integration test that queries the live tags table (e.g., confirming addTagToService actually persists a row) will fail until the Plan 01 migration is applied to the database. This does not block Plan 02's code-level completion (typecheck is the success bar per the DB-state note above).
  • Recommendation: Resolve the Plan 01 DB-execution checkpoint (apply scripts/push-11-tags-migration.ts via SSH+docker exec) before or in parallel with Plans 03/04, so that UI built against these actions can be verified end-to-end once ready.

Phase: 11-catalog-database-view-ux-legacy-consolidation Completed: 2026-06-13

Self-Check: PASSED

  • FOUND: src/lib/admin-queries.ts
  • FOUND: src/app/admin/catalog/actions.ts
  • FOUND: .planning/phases/11-catalog-database-view-ux-legacy-consolidation/11-02-SUMMARY.md
  • FOUND commit: f743410
  • FOUND commit: 445de85