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:
@@ -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(", "),
|
||||
)})`,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user