--- phase: "03" plan: "01" type: execute wave: 1 depends_on: [] files_modified: - src/db/schema.ts autonomous: true requirements: - CAT-01 - CAT-02 - ADMIN-03 must_haves: truths: - "quote_items.service_id is nullable in the database (free-form items can be inserted without a catalog reference)" - "quote_items.custom_label column exists in the database (free-form label storage)" - "TypeScript QuoteItem type reflects both changes (no compile errors when service_id is null or custom_label is set)" - "drizzle-kit push completes without errors against the live Neon database" artifacts: - path: "src/db/schema.ts" provides: "Updated quote_items table definition with nullable service_id and custom_label column" contains: "custom_label: text(\"custom_label\")" key_links: - from: "src/db/schema.ts quote_items.service_id" to: "Neon Postgres quote_items table" via: "drizzle-kit push" pattern: "service_id.*references.*service_catalog" --- Make the two schema changes required for free-form quote items — make `service_id` nullable and add `custom_label text` — then push the changes to the live Neon database. Purpose: All subsequent plans (Wave 2) reference `custom_label` and insert rows with `service_id = null`. Without this push, the DB will reject those inserts with a column-not-found or NOT NULL constraint error. Output: Updated `src/db/schema.ts` and a successful `drizzle-kit push` confirmation. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @/Users/simonecavalli/IAMCAVALLI/.planning/PROJECT.md @/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md ```typescript export const quote_items = pgTable("quote_items", { id: text("id").primaryKey().$defaultFn(() => nanoid()), client_id: text("client_id") .notNull() .references(() => clients.id, { onDelete: "cascade" }), service_id: text("service_id") .notNull() // <-- REMOVE .notNull() .references(() => service_catalog.id, { onDelete: "restrict" }), quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(), unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(), subtotal: numeric("subtotal", { precision: 10, scale: 2 }).notNull(), // custom_label missing — ADD after subtotal }); ``` ```typescript export const quote_items = pgTable("quote_items", { id: text("id").primaryKey().$defaultFn(() => nanoid()), client_id: text("client_id") .notNull() .references(() => clients.id, { onDelete: "cascade" }), service_id: text("service_id") .references(() => service_catalog.id, { onDelete: "restrict" }), // nullable — no .notNull() quantity: numeric("quantity", { precision: 10, scale: 2 }).notNull(), unit_price: numeric("unit_price", { precision: 10, scale: 2 }).notNull(), subtotal: numeric("subtotal", { precision: 10, scale: 2 }).notNull(), custom_label: text("custom_label"), // new field }); ``` ```typescript export const quoteItemsRelations = relations(quote_items, ({ one }) => ({ client: one(clients, { fields: [quote_items.client_id], references: [clients.id] }), service: one(service_catalog, { fields: [quote_items.service_id], references: [service_catalog.id], }), })); ``` ```typescript export type QuoteItem = typeof quote_items.$inferSelect; // QuoteItem.service_id will be: string | null // QuoteItem.custom_label will be: string | null ``` Task 1: Update quote_items schema — make service_id nullable and add custom_label - /Users/simonecavalli/IAMCAVALLI/src/db/schema.ts (full file — understand current definition before editing) src/db/schema.ts Edit `src/db/schema.ts` — two targeted changes to the `quote_items` table definition (lines 159-172): **Change 1 — Remove `.notNull()` from service_id (per D-03 from CONTEXT.md):** Before: ```typescript service_id: text("service_id") .notNull() .references(() => service_catalog.id, { onDelete: "restrict" }), ``` After: ```typescript service_id: text("service_id") .references(() => service_catalog.id, { onDelete: "restrict" }), ``` **Change 2 — Add custom_label field after the `subtotal` line:** ```typescript custom_label: text("custom_label"), ``` No other changes to the file. The `quoteItemsRelations` block does NOT need to change. After the edit, run `npx tsc --noEmit` to confirm zero TypeScript errors before pushing. cd /Users/simonecavalli/IAMCAVALLI && grep -v '^//' src/db/schema.ts | grep -c 'custom_label: text("custom_label")' Expected: 1 cd /Users/simonecavalli/IAMCAVALLI && grep -A3 'service_id: text("service_id")' src/db/schema.ts | grep -c 'notNull' Expected: 0 (notNull must be gone from service_id) cd /Users/simonecavalli/IAMCAVALLI && npx tsc --noEmit 2>&1 | head -20 Expected: no output (zero errors) `src/db/schema.ts` compiles with zero TypeScript errors. `service_id` has no `.notNull()`. `custom_label: text("custom_label")` is present in the quote_items table definition. Task 2: [BLOCKING] Push schema changes to Neon database - /Users/simonecavalli/IAMCAVALLI/.env.local (verify DATABASE_URL is set before running push) - /Users/simonecavalli/IAMCAVALLI/drizzle.config.ts (verify push config points to correct schema) — (no source files modified; runs drizzle-kit against live DB) Run drizzle-kit push with the .env.local DATABASE_URL loaded: ```bash cd /Users/simonecavalli/IAMCAVALLI set -a && source .env.local && set +a && npx drizzle-kit push ``` When prompted to confirm schema changes, accept all changes. The push will: 1. DROP NOT NULL constraint from `quote_items.service_id` 2. ADD COLUMN `custom_label text` to `quote_items` If the push fails with "column already exists" for `custom_label`, the column was already added in a prior run — this is safe to ignore. Verify the column exists by checking the push output or running a quick query. Do NOT skip this task. Wave 2 plans cannot execute correctly without the DB columns existing. cd /Users/simonecavalli/IAMCAVALLI && set -a && source .env.local && set +a && npx drizzle-kit push 2>&1 | tail -5 Expected: Output contains "No changes" or "Changes applied" — either confirms the schema is in sync. `drizzle-kit push` exits without error. The live Neon DB has `quote_items.service_id` as nullable and `quote_items.custom_label text` column present. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Schema file → Neon DB | drizzle-kit push executes DDL against the live database; misconfigured DATABASE_URL would push to wrong environment | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-03-01-01 | Tampering | drizzle-kit push | mitigate | Always load DATABASE_URL from `.env.local` (not hardcoded); verify `.env.local` exists before running push | | T-03-01-02 | Denial of Service | Neon DB DDL | accept | Schema changes are additive (ADD COLUMN, DROP NOT NULL) — no data loss risk; onDelete: "restrict" prevents orphaned quote_items | After both tasks complete: 1. `grep 'custom_label' src/db/schema.ts` returns the field definition 2. `grep -A3 'service_id: text' src/db/schema.ts` shows no `.notNull()` on service_id 3. `npx tsc --noEmit` exits 0 4. `npx drizzle-kit push` reports "No changes" (schema is in sync with DB) - `src/db/schema.ts` has nullable `service_id` and `custom_label: text("custom_label")` in quote_items - TypeScript compiles with zero errors - drizzle-kit push confirms schema is synced to Neon DB - Wave 2 plans can safely reference `custom_label` and insert rows with `service_id = null` After completion, create `.planning/phases/03-service-catalog-quote-builder/03-01-SUMMARY.md`