fix: preserve capture overrides on partial updates

This commit is contained in:
William Valentin
2026-02-02 21:27:21 -08:00
parent 6525a553ae
commit d0ad1caec5
2 changed files with 146 additions and 11 deletions

View File

@@ -11,7 +11,7 @@ const paramsSchema = z.object({
const bodySchema = z
.object({
captureTsUtcOverride: z.string().datetime().nullable().optional(),
captureOffsetMinutesOverride: z.coerce.number().int().nullable().optional(),
captureOffsetMinutesOverride: z.number().int().nullable().optional(),
})
.strict();
@@ -48,15 +48,23 @@ export async function handleSetCaptureOverride(input: {
};
}
const db = (input.db ?? getDb()) as DbLike;
const data = bodyParsed.data;
const captureTs = data.captureTsUtcOverride
? new Date(data.captureTsUtcOverride)
const hasCaptureTs = "captureTsUtcOverride" in data;
const hasCaptureOffset = "captureOffsetMinutesOverride" in data;
if (!hasCaptureTs && !hasCaptureOffset) {
return { status: 400, body: { error: "invalid_body" } };
}
const db = (input.db ?? getDb()) as DbLike;
const captureTs = hasCaptureTs
? data.captureTsUtcOverride
? new Date(data.captureTsUtcOverride)
: null
: null;
const captureOffset = hasCaptureOffset
? data.captureOffsetMinutesOverride ?? null
: null;
const captureOffset =
data.captureOffsetMinutesOverride !== undefined
? data.captureOffsetMinutesOverride
: null;
const rows = await db<
{
@@ -71,11 +79,21 @@ export async function handleSetCaptureOverride(input: {
capture_ts_utc_override,
capture_offset_minutes_override
)
values (${paramsParsed.data.id}, ${captureTs}, ${captureOffset})
values (
${paramsParsed.data.id},
${captureTs},
${captureOffset}
)
on conflict (asset_id)
do update set
capture_ts_utc_override = excluded.capture_ts_utc_override,
capture_offset_minutes_override = excluded.capture_offset_minutes_override
capture_ts_utc_override = case
when ${hasCaptureTs} then excluded.capture_ts_utc_override
else asset_overrides.capture_ts_utc_override
end,
capture_offset_minutes_override = case
when ${hasCaptureOffset} then excluded.capture_offset_minutes_override
else asset_overrides.capture_offset_minutes_override
end
returning asset_id, capture_ts_utc_override, capture_offset_minutes_override, created_at
`;

View File

@@ -1,4 +1,63 @@
import { test, expect } from "bun:test";
import type { getDb } from "@tline/db";
type DbRow = {
asset_id: string;
capture_ts_utc_override: string | null;
capture_offset_minutes_override: number | null;
created_at: string;
};
const createDbStub = (initial: DbRow) => {
let current = { ...initial };
const db = async <T>(
strings: TemplateStringsArray,
...values: unknown[]
): Promise<T> => {
const query = strings.join("");
if (query.includes("insert into asset_overrides")) {
const [assetId, captureTs, captureOffset, tsProvided, offsetProvided] =
values;
const hasFlags =
typeof tsProvided === "boolean" && typeof offsetProvided === "boolean";
const updateTs = hasFlags ? (tsProvided as boolean) : true;
const updateOffset = hasFlags ? (offsetProvided as boolean) : true;
if (updateTs) {
if (captureTs instanceof Date) {
current.capture_ts_utc_override = captureTs.toISOString();
} else if (captureTs === null) {
current.capture_ts_utc_override = null;
} else {
current.capture_ts_utc_override = String(captureTs ?? "");
}
}
if (updateOffset) {
current.capture_offset_minutes_override =
captureOffset as number | null;
}
return [
{
asset_id: String(assetId),
capture_ts_utc_override: current.capture_ts_utc_override,
capture_offset_minutes_override: current.capture_offset_minutes_override,
created_at: current.created_at,
},
] as T;
}
if (query.includes("insert into audit_log")) {
return [] as T;
}
throw new Error(`Unexpected query: ${query}`);
};
return db as unknown as ReturnType<typeof getDb>;
};
test("asset overrides POST rejects when missing admin token", async () => {
const { handleSetCaptureOverride } = await import(
@@ -26,3 +85,61 @@ test("asset overrides POST rejects invalid body", async () => {
expect(res.body).toMatchObject({ error: "invalid_body" });
expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true);
});
test("asset overrides POST rejects empty body", async () => {
const { handleSetCaptureOverride } = await import(
"../../app/api/assets/[id]/override-capture-ts/handlers"
);
const res = await handleSetCaptureOverride({
adminOk: true,
params: { id: "00000000-0000-4000-8000-000000000000" },
body: {},
});
expect(res.status).toBe(400);
expect(res.body).toMatchObject({ error: "invalid_body" });
});
test("asset overrides POST preserves omitted fields", async () => {
const { handleSetCaptureOverride } = await import(
"../../app/api/assets/[id]/override-capture-ts/handlers"
);
const db = createDbStub({
asset_id: "00000000-0000-4000-8000-000000000000",
capture_ts_utc_override: "2026-01-01T00:00:00.000Z",
capture_offset_minutes_override: 90,
created_at: "2026-02-01T00:00:00.000Z",
});
const res = await handleSetCaptureOverride({
adminOk: true,
params: { id: "00000000-0000-4000-8000-000000000000" },
body: { captureTsUtcOverride: "2026-02-01T00:00:00.000Z" },
db,
});
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
capture_ts_utc_override: "2026-02-01T00:00:00.000Z",
capture_offset_minutes_override: 90,
});
});
test("asset overrides POST allows explicit null clearing", async () => {
const { handleSetCaptureOverride } = await import(
"../../app/api/assets/[id]/override-capture-ts/handlers"
);
const db = createDbStub({
asset_id: "00000000-0000-4000-8000-000000000000",
capture_ts_utc_override: "2026-01-01T00:00:00.000Z",
capture_offset_minutes_override: 90,
created_at: "2026-02-01T00:00:00.000Z",
});
const res = await handleSetCaptureOverride({
adminOk: true,
params: { id: "00000000-0000-4000-8000-000000000000" },
body: { captureOffsetMinutesOverride: null },
db,
});
expect(res.status).toBe(200);
expect(res.body).toMatchObject({
capture_offset_minutes_override: null,
});
});