Task #293: DB CHECK constraint for executive_meetings.row_color (defense-in-depth for #288)

Mirrors the API-side row-colour whitelist at the database layer so any
out-of-band write path (manual psql, future bulk-import jobs, restored
backups) cannot smuggle an unrenderable colour past the Zod guard
introduced in #288.

Changes:

- lib/db/src/schema/executive-meetings.ts: export new shared constant
  EXECUTIVE_MEETING_ROW_COLOR_KEYS (red/amber/green/blue/violet/gray)
  + ExecutiveMeetingRowColor union type. Add a Drizzle check()
  constraint named `executive_meetings_row_color_palette_check` that
  allows NULL or any of the six keys. The CHECK uses sql.raw to inline
  the palette as quoted SQL literals (PG CHECK definitions are DDL and
  reject parameter placeholders); safe because the keys come from a
  hardcoded compile-time constant of single-word identifiers, never
  user input. Generating the literal list from the same constant
  guarantees the API guard and the DB constraint stay in sync.

- artifacts/api-server/src/routes/executive-meetings.ts: import
  EXECUTIVE_MEETING_ROW_COLOR_KEYS from @workspace/db and rewire
  rowColorSchema to z.enum(EXECUTIVE_MEETING_ROW_COLOR_KEYS).nullable().
  Deletes the local ROW_COLOR_KEYS const so the palette has exactly
  one source of truth. No behaviour change at runtime.

- artifacts/api-server/tests/executive-meetings-row-color.test.mjs:
  append a focused defense-in-depth test that bypasses the API and
  asserts (a) NULL is allowed, (b) each of the six palette keys
  round-trips on raw UPDATE, (c) off-palette UPDATEs reject with PG
  SQLSTATE 23514 referencing the constraint by name, (d) the row
  keeps its previous colour after the rejected updates, and (e) raw
  INSERT with an off-palette value is rejected the same way.

Migration applied via pnpm --filter @workspace/db push (no destructive
prompts; existing rows are NULL or valid keys from #288 so the
constraint installs cleanly). All 7 tests in the row-color file pass.
Architect review: APPROVED, no critical findings.

The single pre-existing Reorder test failure in the wider suite is
unrelated to this change (Reorder does not touch row_color).
This commit is contained in:
riyadhafraa
2026-05-01 15:41:21 +00:00
parent 4d497606d4
commit de7973f35b
3 changed files with 142 additions and 12 deletions
+38
View File
@@ -11,9 +11,28 @@ import {
boolean,
index,
uniqueIndex,
check,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { usersTable } from "./users";
// Single source of truth for the row-highlight palette used by the
// daily schedule overlay (#288). Imported by the API route's Zod
// whitelist AND pinned at the database layer via the CHECK constraint
// below (#293), so the same invariant survives any out-of-band write
// path (manual SQL, future bulk jobs, restored backups). NULL means
// "no tint" / default; any other value must be one of these keys.
export const EXECUTIVE_MEETING_ROW_COLOR_KEYS = [
"red",
"amber",
"green",
"blue",
"violet",
"gray",
] as const;
export type ExecutiveMeetingRowColor =
(typeof EXECUTIVE_MEETING_ROW_COLOR_KEYS)[number];
export const executiveMeetingsTable = pgTable(
"executive_meetings",
{
@@ -72,6 +91,25 @@ export const executiveMeetingsTable = pgTable(
t.meetingDate,
t.dailyNumber,
),
// Defense-in-depth (#293): mirror the API-side ROW_COLOR_KEYS
// whitelist at the DB layer so any out-of-band write path
// (manual SQL, future bulk jobs, restored backups) cannot smuggle
// an unrenderable colour past the Zod guard. NULL is the "no tint"
// sentinel, the six keys are the supported palette. Any other
// value raises a check_violation (PG SQLSTATE 23514) that the
// caller must explicitly handle.
// Literal-SQL form: Postgres CHECK constraint definitions are
// parsed as DDL and reject parameter placeholders ($1, $2, …),
// so the palette keys are inlined as quoted literals via
// `sql.raw`. This is safe because the keys come from a hardcoded
// compile-time constant of single-word identifiers (no user
// input ever reaches this string).
rowColorPaletteCheck: check(
"executive_meetings_row_color_palette_check",
sql`${t.rowColor} IS NULL OR ${t.rowColor} IN (${sql.raw(
EXECUTIVE_MEETING_ROW_COLOR_KEYS.map((k) => `'${k}'`).join(", "),
)})`,
),
}),
);