mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(db): Migration 0060 backfills onboardingStep + flips to NOT NULL
Migration 0057 shipped `onboarding_step INTEGER DEFAULT 0` as nullable so the additive DDL avoided a long table rewrite during the W14b deploy. The wizard mount code and the POST route then carried defensive null-handling branches (read-side `?? 0`, write-side `OR step IS NULL`) to keep legacy pre-W14b rows working. That left a tri-state surface the route had to widen on every step-0 advance. Backfill every NULL row to 0 (the same value the read-path already substitutes), then `ALTER COLUMN ... SET NOT NULL`. schema.prisma drops the `?` so the Prisma type-surface mirrors the DB. The conditional update WHERE in `/api/onboarding/step` collapses to the strict-equality form on every step — the legacy null branch becomes unreachable code and is removed. Fix-O / senior-M1.
This commit is contained in:
1 parent
a238ae48d1
commit
3f7a8ce6a6
4 files changed
+46
-32
No files matched your search
@@ -0,0 +1,25 @@
|
||||
-- v1.4.25 W21 Fix-O — Backfill `onboarding_step` and flip column to NOT NULL.
|
||||
--
|
||||
-- Migration 0057 shipped `onboarding_step INTEGER DEFAULT 0` — nullable on
|
||||
-- purpose so the additive DDL didn't risk a long table-rewrite during the
|
||||
-- W14b deploy. The wizard mount code (`src/app/onboarding/page.tsx`) treats
|
||||
-- `null` as `0`, the POST route (`src/app/api/onboarding/step/route.ts`)
|
||||
-- widens its conditional-update WHERE to `OR onboardingStep IS NULL` when
|
||||
-- the caller is at step 0 — all defensive against the tri-state surface.
|
||||
--
|
||||
-- This migration closes that tri-state: every legacy pre-W14b row that
|
||||
-- still carries a NULL is backfilled to 0 (the same value the read-path
|
||||
-- already substitutes), then the column is flipped to NOT NULL. New
|
||||
-- inserts continue to land at the DEFAULT 0 — schema.prisma now mirrors
|
||||
-- the DB shape: `onboardingStep Int @default(0)` without the `?`.
|
||||
--
|
||||
-- Idempotent — re-running on a database that's already been flipped is a
|
||||
-- no-op for the UPDATE (no rows match) and the ALTER COLUMN tolerates
|
||||
-- SET NOT NULL on an already-NOT NULL column.
|
||||
|
||||
UPDATE "users"
|
||||
SET "onboarding_step" = 0
|
||||
WHERE "onboarding_step" IS NULL;
|
||||
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "onboarding_step" SET NOT NULL;
|
||||
@@ -98,7 +98,7 @@ model User {
|
||||
// 4 = done (triggers `onboardingCompletedAt` flip on the same
|
||||
// round-trip — the column stays at 4 for completed users)
|
||||
// Null = legacy pre-W14b user; the wizard treats null as 0 on mount.
|
||||
onboardingStep Int? @default(0) @map("onboarding_step")
|
||||
onboardingStep Int @default(0) @map("onboarding_step")
|
||||
// v1.4.15 Phase B5: tracks whether the user has finished (or
|
||||
// explicitly dismissed) the spotlight tour that overlays the
|
||||
// dashboard on first arrival. Distinct from the wizard at
|
||||
|
||||
@@ -208,18 +208,19 @@ describe("POST /api/onboarding/step — concurrent write race", () => {
|
||||
id: "user-1",
|
||||
onboardingCompletedAt: null,
|
||||
});
|
||||
// Either the literal { onboardingStep: 2 } form (current > 0) or
|
||||
// the legacy null-OR-zero form when current === 0. Here current
|
||||
// is 2 so we expect the strict-equality form.
|
||||
// Strict-equality precondition — Migration 0060 dropped the
|
||||
// legacy nullable path so every advance uses `{ in: [current] }`.
|
||||
expect(whereArg.onboardingStep).toEqual({ in: [2] });
|
||||
});
|
||||
|
||||
it("widens the precondition to null OR 0 when the row is a legacy pre-W14b user", async () => {
|
||||
// Legacy accounts may have `onboarding_step IS NULL`. The
|
||||
// conditional update must still claim them on step 1.
|
||||
it("advances a step-0 row to step 1 using the strict-equality precondition", async () => {
|
||||
// Migration 0060 (v1.4.25 W21 Fix-O) backfilled NULL → 0 and
|
||||
// flipped `onboarding_step` to NOT NULL, so the conditional
|
||||
// update always uses the strict-equality form — there is no
|
||||
// legacy null branch left for the route to widen.
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
onboardingStep: null,
|
||||
onboardingStep: 0,
|
||||
onboardingCompletedAt: null,
|
||||
} as never);
|
||||
vi.mocked(prisma.user.updateMany).mockResolvedValue({ count: 1 } as never);
|
||||
@@ -232,12 +233,7 @@ describe("POST /api/onboarding/step — concurrent write race", () => {
|
||||
expect(res.status).toBe(200);
|
||||
const whereArg = vi.mocked(prisma.user.updateMany).mock.calls[0][0]
|
||||
.where as Record<string, unknown>;
|
||||
expect(whereArg.OR).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ onboardingStep: 0 },
|
||||
{ onboardingStep: null },
|
||||
]),
|
||||
);
|
||||
expect(whereArg.onboardingStep).toEqual({ in: [0] });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("Onboarding already completed", 409);
|
||||
}
|
||||
|
||||
const current = fresh.onboardingStep ?? 0;
|
||||
const current = fresh.onboardingStep;
|
||||
if (step !== current + 1) {
|
||||
annotate({
|
||||
action: { name: "onboarding.step" },
|
||||
@@ -116,24 +116,17 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// both reading `onboardingStep = 2` and both POSTing `step: 3`
|
||||
// would otherwise both succeed (the read-then-write race); with
|
||||
// this guard exactly one update sees `count = 1`, the second sees
|
||||
// `count = 0` and returns 409. Legacy pre-W14b accounts may have
|
||||
// `onboarding_step IS NULL`, so the `current === 0` branch widens
|
||||
// the match to null OR 0 — the wizard treats both as "not
|
||||
// started". `onboardingCompletedAt: null` also catches a race with
|
||||
// `POST /api/onboarding/complete`.
|
||||
// `count = 0` and returns 409. `onboardingCompletedAt: null` also
|
||||
// catches a race with `POST /api/onboarding/complete`. Migration
|
||||
// 0060 (v1.4.25 W21) backfilled NULL → 0 and flipped the column
|
||||
// to NOT NULL, so the legacy null branch this code carried before
|
||||
// is dropped — every row now matches the strict-equality form.
|
||||
const claimed = await prisma.user.updateMany({
|
||||
where:
|
||||
current === 0
|
||||
? {
|
||||
id: user.id,
|
||||
onboardingCompletedAt: null,
|
||||
OR: [{ onboardingStep: 0 }, { onboardingStep: null }],
|
||||
}
|
||||
: {
|
||||
id: user.id,
|
||||
onboardingCompletedAt: null,
|
||||
onboardingStep: { in: [current] },
|
||||
},
|
||||
where: {
|
||||
id: user.id,
|
||||
onboardingCompletedAt: null,
|
||||
onboardingStep: { in: [current] },
|
||||
},
|
||||
data: {
|
||||
onboardingStep: step,
|
||||
...(completing ? { onboardingCompletedAt: new Date() } : {}),
|
||||
|
||||
Reference in new issue
Block a user