import { and, asc, desc, eq, ilike, isNull, or, sql, type SQL, } from 'drizzle-orm'; import type { PgTable, PgColumn } from 'drizzle-orm/pg-core'; import { db } from './index'; export interface BuildListQueryOptions { table: PgTable; portIdColumn: PgColumn; portId: string; idColumn: PgColumn; updatedAtColumn: PgColumn; filters?: SQL[]; sort?: { column: PgColumn; direction: 'asc' | 'desc' }; page: number; pageSize: number; searchColumns?: PgColumn[]; searchTerm?: string; includeArchived?: boolean; archivedAtColumn?: PgColumn; } export interface ListResult { data: T[]; total: number; } /** * Generic Drizzle paginated query builder with port-scoping. * * - Port scoping is always the first condition in the AND chain. * - `archivedAt IS NULL` by default (unless `includeArchived` is true). * - Deterministic secondary sort: `updatedAt DESC, id DESC`. */ export async function buildListQuery( opts: BuildListQueryOptions, ): Promise> { const { table, portIdColumn, portId, idColumn, updatedAtColumn, filters = [], sort, page, pageSize, searchColumns = [], searchTerm, includeArchived = false, archivedAtColumn, } = opts; const conditions: SQL[] = [eq(portIdColumn, portId)]; // Exclude archived by default if (!includeArchived && archivedAtColumn) { conditions.push(isNull(archivedAtColumn)); } // Full-text search across multiple columns via ILIKE if (searchTerm && searchColumns.length > 0) { const searchConditions = searchColumns.map((col) => ilike(col, `%${searchTerm}%`), ); conditions.push(or(...searchConditions)!); } // Append caller-supplied filters conditions.push(...filters); const where = and(...conditions); // Count total const countResult = await db .select({ count: sql`count(*)::int` }) .from(table) .where(where); const total = countResult[0]?.count ?? 0; // Build order by: user sort + deterministic secondary sort const orderClauses: SQL[] = []; if (sort) { orderClauses.push( sort.direction === 'asc' ? asc(sort.column) : desc(sort.column), ); } orderClauses.push(desc(updatedAtColumn), desc(idColumn)); // Fetch page const offset = (page - 1) * pageSize; const data = await db .select() .from(table) .where(where) .orderBy(...orderClauses) .limit(pageSize) .offset(offset); return { data: data as T[], total }; }