fix(18): make timezone seed idempotent under concurrent race (WR-02)
- Import sql from drizzle-orm in admin.ts
- Add onDuplicateKeyUpdate({ set: { value: sql\`value\` } }) to the
conditional INSERT in POST /config/timezone/seed so a concurrent seed
(or seed racing a PUT) cannot 500 on the app_config.key PK constraint
- Existing value is preserved per D-03 no-overwrite (no-op ODKU)
- seeded flag still reflects the pre-flight SELECT (winner: true, loser: false)
- Add tests: 403 access control, seeded:true on first seed, seeded:false
on second seed without throw (WR-02 idempotent race)
This commit is contained in:
@@ -23,7 +23,7 @@ import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { eq, sql } from 'drizzle-orm';
|
||||
import { db } from '../db/client.js';
|
||||
import { users, memberCredentials, calendars, appConfig } from '../db/schema.js';
|
||||
import { requireAdmin } from '../lib/requireAdmin.js';
|
||||
@@ -255,10 +255,19 @@ adminRouter.post('/config/timezone/seed', zValidator('json', timezoneSchema), as
|
||||
.limit(1);
|
||||
|
||||
const alreadySet = existing?.value != null;
|
||||
|
||||
// WR-02: Use onDuplicateKeyUpdate with a no-op (`set: { value: sql`value` }`)
|
||||
// so a concurrent seed or a seed racing a PUT cannot 500 on the PK constraint.
|
||||
// The no-op preserves the existing value (D-03 no-overwrite). We always INSERT
|
||||
// here and let the DB determine whether a row was inserted or not; `seeded` still
|
||||
// reflects the pre-flight SELECT so the caller gets the correct flag even in the
|
||||
// concurrent race (the winner observes alreadySet=false → seeded:true; the loser
|
||||
// observes alreadySet=true → seeded:false and the INSERT is a no-op).
|
||||
if (!alreadySet) {
|
||||
await db
|
||||
.insert(appConfig)
|
||||
.values({ key: 'household_timezone', value: timezone });
|
||||
.values({ key: 'household_timezone', value: timezone })
|
||||
.onDuplicateKeyUpdate({ set: { value: sql`value` } });
|
||||
}
|
||||
|
||||
return c.json({ ok: true, seeded: !alreadySet }, 200);
|
||||
|
||||
Reference in New Issue
Block a user