Files
porthole/apps/web/app/api/moments/route.ts
2026-02-05 09:17:16 -08:00

70 lines
1.9 KiB
TypeScript

import { z } from "zod";
import { getDb } from "@tline/db";
import { clusterMoments } from "../../lib/moments";
export const runtime = "nodejs";
const querySchema = z
.object({
start: z.string().datetime().optional(),
end: z.string().datetime().optional(),
includeFailed: z.coerce.number().int().optional(),
limit: z.coerce.number().int().positive().max(2000).default(1000),
})
.strict();
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const parsed = querySchema.safeParse({
start: url.searchParams.get("start") ?? undefined,
end: url.searchParams.get("end") ?? undefined,
includeFailed: url.searchParams.get("includeFailed") ?? undefined,
limit: url.searchParams.get("limit") ?? undefined,
});
if (!parsed.success) {
return Response.json(
{ error: "invalid_query", issues: parsed.error.issues },
{ status: 400 },
);
}
const query = parsed.data;
const start = query.start ? new Date(query.start) : null;
const end = query.end ? new Date(query.end) : null;
const includeFailed = query.includeFailed === 1;
const db = getDb();
const rows = await db<
{
id: string;
capture_ts_utc: string | null;
}[]
>`
select id, capture_ts_utc
from assets
where capture_ts_utc is not null
and (${start}::timestamptz is null or capture_ts_utc >= ${start}::timestamptz)
and (${end}::timestamptz is null or capture_ts_utc < ${end}::timestamptz)
and (${includeFailed}::boolean is true or status <> 'failed')
order by capture_ts_utc asc, id asc
limit ${query.limit}
`;
const clusters = clusterMoments(
rows
.filter((row) => Boolean(row.capture_ts_utc))
.map((row) => ({
id: row.id,
capture_ts_utc: row.capture_ts_utc as string,
})),
);
return Response.json({
start: start ? start.toISOString() : null,
end: end ? end.toISOString() : null,
clusters,
});
}