115 lines
3.4 KiB
TypeScript
115 lines
3.4 KiB
TypeScript
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<Response> {
|
|
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,
|
|
});
|
|
}
|