import { z } from "zod"; import { getDb } from "@tline/db"; export const runtime = "nodejs"; const querySchema = z .object({ start: z.string().datetime().optional(), end: z.string().datetime().optional(), mediaType: z.enum(["image", "video"]).optional(), status: z.enum(["new", "processing", "ready", "failed"]).optional(), limit: z.coerce.number().int().positive().max(200).default(60), cursor: z.string().uuid().optional(), cursorTs: z.string().datetime().optional(), }) .strict(); export async function GET(request: Request): Promise { const url = new URL(request.url); const parsed = querySchema.safeParse({ start: url.searchParams.get("start") ?? undefined, end: url.searchParams.get("end") ?? undefined, mediaType: url.searchParams.get("mediaType") ?? undefined, status: url.searchParams.get("status") ?? undefined, limit: url.searchParams.get("limit") ?? undefined, cursor: url.searchParams.get("cursor") ?? undefined, cursorTs: url.searchParams.get("cursorTs") ?? 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 db = getDb(); // Cursor pagination: (capture_ts_utc, id) > (cursorTs, cursor) const cursorTs = query.cursorTs ? new Date(query.cursorTs) : null; const cursorId = query.cursor ?? null; const rows = await db< { id: string; bucket: string; media_type: "image" | "video"; mime_type: string; active_key: string; capture_ts_utc: string | null; date_confidence: string | null; width: number | null; height: number | null; rotation: number | null; duration_seconds: number | null; thumb_small_key: string | null; thumb_med_key: string | null; poster_key: string | null; status: "new" | "processing" | "ready" | "failed"; error_message: string | null; }[] >` select id, bucket, media_type, mime_type, active_key, capture_ts_utc, date_confidence, width, height, rotation, duration_seconds, thumb_small_key, thumb_med_key, poster_key, status, error_message from assets where true and 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 (${query.mediaType ?? null}::media_type is null or media_type = ${query.mediaType ?? null}::media_type) and (${query.status ?? null}::asset_status is null or status = ${query.status ?? null}::asset_status) and ( ${cursorId}::uuid is null or ${cursorTs}::timestamptz is null or (capture_ts_utc, id) > (${cursorTs}::timestamptz, ${cursorId}::uuid) ) order by capture_ts_utc asc nulls last, id asc limit ${query.limit} `; const nextCursor = rows.length > 0 ? rows[rows.length - 1] : null; return Response.json({ start: start ? start.toISOString() : null, end: end ? end.toISOString() : null, items: rows, next: nextCursor && nextCursor.capture_ts_utc ? { cursor: nextCursor.id, cursorTs: nextCursor.capture_ts_utc } : null, }); }