From ba4349eda0315be2095556d9a41552c91ab82d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 13:26:07 +0200 Subject: [PATCH] refactor(db): extract the P2002 predicate into one shared helper The Prisma unique-constraint (P2002) narrowing predicate had been hand-copied into five call sites. Fold them into a single isP2002 in src/lib/prisma-errors.ts and import it everywhere (the two step-drain sites keep their local isUniqueConstraintViolation name via an import alias). Behaviour-identical; one place to chase a future P2002-semantics change instead of five. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../measurements/consolidate-legacy-steps.ts | 10 +------- .../measurements/dense-intraday-retention.ts | 14 +---------- src/lib/medications/intake-slot-dedup.ts | 11 +-------- src/lib/medications/scheduling/slot-upsert.ts | 10 +------- src/lib/prisma-errors.ts | 24 +++++++++++++++++++ src/lib/rollups/measurement-rollups.ts | 11 +-------- 6 files changed, 29 insertions(+), 51 deletions(-) create mode 100644 src/lib/prisma-errors.ts diff --git a/src/lib/measurements/consolidate-legacy-steps.ts b/src/lib/measurements/consolidate-legacy-steps.ts index e027bdabe..4a17a0d13 100644 --- a/src/lib/measurements/consolidate-legacy-steps.ts +++ b/src/lib/measurements/consolidate-legacy-steps.ts @@ -38,6 +38,7 @@ * `rollup-full-backfill` boot-time converging-backfill pattern. */ import type { PrismaClient } from "@/generated/prisma/client"; +import { isP2002 as isUniqueConstraintViolation } from "@/lib/prisma-errors"; import { dailyStatsExternalId } from "./apple-health-mapping"; import { @@ -100,15 +101,6 @@ export interface StepConsolidationSummary { }; } -/** True for a Prisma unique-constraint violation (P2002). */ -function isUniqueConstraintViolation(err: unknown): boolean { - return ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code?: unknown }).code === "P2002" - ); -} export interface StepConsolidationOptions { /** Limit the pass to a single user. Default = every user. */ diff --git a/src/lib/measurements/dense-intraday-retention.ts b/src/lib/measurements/dense-intraday-retention.ts index fd072a1f4..7b9fa25aa 100644 --- a/src/lib/measurements/dense-intraday-retention.ts +++ b/src/lib/measurements/dense-intraday-retention.ts @@ -41,6 +41,7 @@ * production standalone image strips `tsx`, so this can never be a CLI. */ import type { MeasurementType, PrismaClient } from "@/generated/prisma/client"; +import { isP2002 as isUniqueConstraintViolation } from "@/lib/prisma-errors"; import { dailyStatsExternalId, @@ -77,19 +78,6 @@ export const DENSE_INTRADAY_RETENTION_DAYS = 14; /** The daily-stats externalId prefix marks an already-collapsed row. */ const DAILY_STATS_PREFIX = "stats:"; -/** - * True for a Prisma unique-constraint violation (P2002). Mirrors the - * `consolidate-legacy-steps.ts` precedent verbatim so the two drains - * classify the same error the same way. - */ -function isUniqueConstraintViolation(err: unknown): boolean { - return ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code?: unknown }).code === "P2002" - ); -} export interface DenseIntradayRetentionSummary { dryRun: boolean; diff --git a/src/lib/medications/intake-slot-dedup.ts b/src/lib/medications/intake-slot-dedup.ts index 2c69498e0..fab3673be 100644 --- a/src/lib/medications/intake-slot-dedup.ts +++ b/src/lib/medications/intake-slot-dedup.ts @@ -41,6 +41,7 @@ * unregistered queue silently never drains. */ import { prisma } from "@/lib/db"; +import { isP2002 } from "@/lib/prisma-errors"; import { annotate } from "@/lib/logging/context"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { DEFAULT_TIMEZONE } from "@/lib/tz/format"; @@ -53,16 +54,6 @@ import { recomputeMedicationComplianceForEvent } from "@/lib/rollups/medication- export const INTAKE_SLOT_DEDUP_QUEUE = "intake-slot-dedup"; -/** Narrow a thrown Prisma error to the P2002 unique-constraint code. */ -function isP2002(err: unknown): boolean { - return ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code: unknown }).code === "P2002" - ); -} - /** * Serial concurrency — the populator walks a user's intake events and * writes per slot; concurrency-1 keeps it from crowding the request diff --git a/src/lib/medications/scheduling/slot-upsert.ts b/src/lib/medications/scheduling/slot-upsert.ts index 09d21612b..55cb3d6a8 100644 --- a/src/lib/medications/scheduling/slot-upsert.ts +++ b/src/lib/medications/scheduling/slot-upsert.ts @@ -17,21 +17,13 @@ import type { PrismaClient } from "@/generated/prisma/client"; import { prisma as defaultPrisma } from "@/lib/db"; +import { isP2002 } from "@/lib/prisma-errors"; import { resolveCanonicalSlotInstant } from "@/lib/medications/scheduling/resolve-slot-instant"; import type { WorkerScheduleRow } from "@/lib/medications/scheduling/worker-helpers"; import type { InjectionSiteKey } from "@/lib/medications/injection-sites"; type PrismaLike = Pick; -/** Narrow a thrown Prisma error to the P2002 unique-constraint code. */ -function isP2002(err: unknown): boolean { - return ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code: unknown }).code === "P2002" - ); -} /** The minimal intake-row shape the slot upsert reads + returns. */ export interface SlotIntakeRow { diff --git a/src/lib/prisma-errors.ts b/src/lib/prisma-errors.ts new file mode 100644 index 000000000..bf6096656 --- /dev/null +++ b/src/lib/prisma-errors.ts @@ -0,0 +1,24 @@ +/** + * Shared Prisma error narrowing helpers. + * + * The P2002 (unique-constraint violation) predicate was hand-copied into + * five call sites (rollup persist, intake-slot dedup, slot upsert, legacy + * step consolidation, dense-intraday retention). v1.11.2 folds them into + * this single helper so a future change to the P2002 semantics has one + * place to live instead of five. + * + * Deliberately structural (`typeof === "object"` + `"code" in err` + + * `code === "P2002"`) rather than `instanceof Prisma.PrismaClientKnownRequestError` + * so it survives client-bundling quirks while staying specific to the + * unique-constraint code. + */ + +/** Narrow a thrown Prisma error to the P2002 unique-constraint code. */ +export function isP2002(err: unknown): boolean { + return ( + typeof err === "object" && + err !== null && + "code" in err && + (err as { code: unknown }).code === "P2002" + ); +} diff --git a/src/lib/rollups/measurement-rollups.ts b/src/lib/rollups/measurement-rollups.ts index 6369483b6..012bada89 100644 --- a/src/lib/rollups/measurement-rollups.ts +++ b/src/lib/rollups/measurement-rollups.ts @@ -30,6 +30,7 @@ import { prisma } from "@/lib/db"; import { getGlobalBoss } from "@/lib/jobs/boss-instance"; import { annotate } from "@/lib/logging/context"; +import { isP2002 } from "@/lib/prisma-errors"; import { collapseRollupRowsBySource, loadUserSourcePriority, @@ -437,16 +438,6 @@ async function runRollupAggregate(input: { * with `ON CONFLICT DO UPDATE` so the round-trip count is bounded * regardless of fan-out width. */ -/** Narrow a thrown Prisma error to the P2002 unique-constraint code. */ -function isP2002(err: unknown): boolean { - return ( - typeof err === "object" && - err !== null && - "code" in err && - (err as { code: unknown }).code === "P2002" - ); -} - async function persistRollupRows( userId: string, granularity: RollupGranularity,