import { asc, type Column, desc, type SQL } from "drizzle-orm"; import type { AllowedSorts, Sort } from "./sorts.js"; import { checkSort } from "./sorts.js"; /** * Per-field column map for sorts. Mirrors `AllowedSorts` one-for-one: every * field declared in `allowed` must have a matching column reference in * `columns`. Accepts either a real Drizzle `Column` or a SQL expression * (`sql...`) — useful for sorting by computed values like * `extract(epoch from completed_at - started_at)` that have no underlying * column. * * Same compile-time contract as `FilterColumns`: key presence is enforced; * data-type alignment is irrelevant for sort since every sortable type * works with `asc` / `desc`. */ export type SortColumns = { [K in keyof A]: Column | SQL; }; /** * Drizzle helper. Use this from a carte entry's `query` function to turn * an array of validated `Sort`s into Drizzle `SQL` order-by fragments ready * to spread into `.orderBy(...)`. * * const orderings = applyDrizzleSort( * { retryCount: jobs.retryCount, failedAt: jobs.failedAt }, * params.sorts ?? [], * allowedSorts, * ); * await db.select().from(jobs).orderBy(...orderings, desc(jobs.id)); * * Compose with your own default ordering as a tiebreaker — the LLM-supplied * sorts go first, your defaults last. * * Throws on any field/direction mismatch — defence in depth even though * `validatePlan` already gated the plan. */ export function applyDrizzleSort( columns: SortColumns, sorts: ReadonlyArray, allowed: A, ): SQL[] { return sorts.map((sort) => { const err = checkSort(sort, allowed); if (err) { throw new Error(`applyDrizzleSort: ${err}`); } const col = columns[sort.field as keyof A]; if (!col) { throw new Error( `applyDrizzleSort: column for field "${sort.field}" is missing from the columns map`, ); } // Both Column and SQL satisfy SQLWrapper which is what asc/desc accept. return sort.direction === "asc" ? asc(col) : desc(col); }); }