fix: preserve capture overrides on partial updates
This commit is contained in:
@@ -11,7 +11,7 @@ const paramsSchema = z.object({
|
|||||||
const bodySchema = z
|
const bodySchema = z
|
||||||
.object({
|
.object({
|
||||||
captureTsUtcOverride: z.string().datetime().nullable().optional(),
|
captureTsUtcOverride: z.string().datetime().nullable().optional(),
|
||||||
captureOffsetMinutesOverride: z.coerce.number().int().nullable().optional(),
|
captureOffsetMinutesOverride: z.number().int().nullable().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -48,15 +48,23 @@ export async function handleSetCaptureOverride(input: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = (input.db ?? getDb()) as DbLike;
|
|
||||||
const data = bodyParsed.data;
|
const data = bodyParsed.data;
|
||||||
const captureTs = data.captureTsUtcOverride
|
const hasCaptureTs = "captureTsUtcOverride" in data;
|
||||||
? new Date(data.captureTsUtcOverride)
|
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;
|
: null;
|
||||||
const captureOffset =
|
|
||||||
data.captureOffsetMinutesOverride !== undefined
|
|
||||||
? data.captureOffsetMinutesOverride
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const rows = await db<
|
const rows = await db<
|
||||||
{
|
{
|
||||||
@@ -71,11 +79,21 @@ export async function handleSetCaptureOverride(input: {
|
|||||||
capture_ts_utc_override,
|
capture_ts_utc_override,
|
||||||
capture_offset_minutes_override
|
capture_offset_minutes_override
|
||||||
)
|
)
|
||||||
values (${paramsParsed.data.id}, ${captureTs}, ${captureOffset})
|
values (
|
||||||
|
${paramsParsed.data.id},
|
||||||
|
${captureTs},
|
||||||
|
${captureOffset}
|
||||||
|
)
|
||||||
on conflict (asset_id)
|
on conflict (asset_id)
|
||||||
do update set
|
do update set
|
||||||
capture_ts_utc_override = excluded.capture_ts_utc_override,
|
capture_ts_utc_override = case
|
||||||
capture_offset_minutes_override = excluded.capture_offset_minutes_override
|
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
|
returning asset_id, capture_ts_utc_override, capture_offset_minutes_override, created_at
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,63 @@
|
|||||||
import { test, expect } from "bun:test";
|
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 () => {
|
test("asset overrides POST rejects when missing admin token", async () => {
|
||||||
const { handleSetCaptureOverride } = await import(
|
const { handleSetCaptureOverride } = await import(
|
||||||
@@ -26,3 +85,61 @@ test("asset overrides POST rejects invalid body", async () => {
|
|||||||
expect(res.body).toMatchObject({ error: "invalid_body" });
|
expect(res.body).toMatchObject({ error: "invalid_body" });
|
||||||
expect(Array.isArray((res.body as { issues?: unknown }).issues)).toBe(true);
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user