fix: persist birth time declarations
This commit is contained in:
@@ -11,6 +11,12 @@ type ProfilePatchPayload = {
|
||||
name?: unknown;
|
||||
birth_date?: unknown;
|
||||
birth_time?: unknown;
|
||||
reported_birth_time?: unknown;
|
||||
birth_time_source?: unknown;
|
||||
birth_time_period?: unknown;
|
||||
birth_time_clue?: unknown;
|
||||
uncertainty_before_minutes?: unknown;
|
||||
uncertainty_after_minutes?: unknown;
|
||||
country_code?: unknown;
|
||||
province_code?: unknown;
|
||||
city_code?: unknown;
|
||||
@@ -20,6 +26,9 @@ type ProfilePatchPayload = {
|
||||
timezone_offset?: unknown;
|
||||
};
|
||||
|
||||
const birthTimeSources = ["hospital_record", "family_exact", "approximate", "period_only", "unknown", "legacy_import"] as const;
|
||||
const birthTimePeriods = ["early_morning", "morning", "afternoon", "evening", "late_night"] as const;
|
||||
|
||||
function nullableString(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : null;
|
||||
}
|
||||
@@ -28,6 +37,14 @@ function nullableNumber(value: unknown) {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function nullableInteger(value: unknown) {
|
||||
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
||||
}
|
||||
|
||||
function nullableChoice(value: unknown, choices: readonly string[]) {
|
||||
return typeof value === "string" && choices.includes(value) ? value : null;
|
||||
}
|
||||
|
||||
function isMissingProfileColumn(error: { code?: string; message?: string } | null) {
|
||||
const message = error?.message?.toLowerCase() ?? "";
|
||||
return error?.code === "PGRST204"
|
||||
@@ -88,6 +105,12 @@ export async function PATCH(request: Request) {
|
||||
name: nullableString(payload.name),
|
||||
birth_date: nullableString(payload.birth_date),
|
||||
birth_time: nullableString(payload.birth_time),
|
||||
reported_birth_time: nullableString(payload.reported_birth_time),
|
||||
birth_time_source: nullableChoice(payload.birth_time_source, birthTimeSources),
|
||||
birth_time_period: nullableChoice(payload.birth_time_period, birthTimePeriods),
|
||||
birth_time_clue: nullableString(payload.birth_time_clue),
|
||||
uncertainty_before_minutes: nullableInteger(payload.uncertainty_before_minutes),
|
||||
uncertainty_after_minutes: nullableInteger(payload.uncertainty_after_minutes),
|
||||
country_code: nullableString(payload.country_code),
|
||||
province_code: nullableString(payload.province_code),
|
||||
city_code: nullableString(payload.city_code),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
begin;
|
||||
|
||||
grant insert (
|
||||
reported_birth_time,
|
||||
birth_time_source,
|
||||
birth_time_period,
|
||||
birth_time_clue,
|
||||
uncertainty_before_minutes,
|
||||
uncertainty_after_minutes
|
||||
) on table public.profiles to service_role;
|
||||
|
||||
grant select (
|
||||
reported_birth_time,
|
||||
birth_time_source,
|
||||
birth_time_period,
|
||||
birth_time_clue,
|
||||
uncertainty_before_minutes,
|
||||
uncertainty_after_minutes
|
||||
) on table public.profiles to service_role;
|
||||
|
||||
commit;
|
||||
@@ -20,6 +20,42 @@ test("account route can fall back when coordinate columns are not deployed", ()
|
||||
assert.match(source, /PGRST204|42703|schema cache|column/i);
|
||||
});
|
||||
|
||||
test("account route persists the birth-time declaration before assessment", () => {
|
||||
// Given: the birthday step submits a reported time and its source metadata.
|
||||
const source = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8");
|
||||
const declarationColumns = [
|
||||
"reported_birth_time",
|
||||
"birth_time_source",
|
||||
"birth_time_period",
|
||||
"birth_time_clue",
|
||||
"uncertainty_before_minutes",
|
||||
"uncertainty_after_minutes",
|
||||
] as const;
|
||||
|
||||
// When: /api/account builds the profile upsert.
|
||||
// Then: every declaration field must cross the route boundary instead of being dropped.
|
||||
for (const column of declarationColumns) {
|
||||
assert.match(source, new RegExp(`${column}:\\s*nullable`), `${column} is not persisted`);
|
||||
}
|
||||
assert.match(source, /birthTimeSources\s*=\s*\[[\s\S]*"legacy_import"/);
|
||||
});
|
||||
|
||||
test("service role can insert and read birth-time declaration columns during profile upsert", () => {
|
||||
// Given: onboarding creates the profile before the birthday declaration is known.
|
||||
const migration = readFileSync(
|
||||
new URL(
|
||||
"../supabase/migrations/20260718103000_profile_birth_time_declaration_grants.sql",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// When: the existing profile is upserted after the birthday step.
|
||||
// Then: PostgREST may insert and read all declaration columns used by that upsert.
|
||||
assert.match(migration, /grant\s+insert\s*\([\s\S]*reported_birth_time[\s\S]*birth_time_source[\s\S]*uncertainty_after_minutes[\s\S]*\)\s*on\s+table\s+public\.profiles\s+to\s+service_role/i);
|
||||
assert.match(migration, /grant\s+select\s*\([\s\S]*reported_birth_time[\s\S]*birth_time_source[\s\S]*uncertainty_after_minutes[\s\S]*\)\s*on\s+table\s+public\.profiles\s+to\s+service_role/i);
|
||||
});
|
||||
|
||||
test("service role can read every column used by account profile upserts", () => {
|
||||
// Given: the least-privilege grant omitted two columns submitted by /api/account.
|
||||
const migration = readFileSync(
|
||||
|
||||
Reference in New Issue
Block a user