/** * @factory/database-engine/query — the record query pipeline (TASK-0066). * * `runRecordQuery` is the pure pipeline: base set (unarchived unless * `includeArchived`) → filter → sort → (group | paginate). The API owns the * DB fetch (base set + EAV in one indexed round-trip) and calls this with the * assembled `RecordRow`s; the client calls the same function over its cached * rows for optimistic UI — one code path, no drift (TASK-0067 R3). */ import type { FilterNode, SortSpec, GroupSpec, RecordQuery } from "@factory/shared/dto"; import type { RecordRow, PropertyInfo, PropertyValue } from "../values/types.js"; import { applyFilter, propertiesMap } from "../filter/index.js"; import { sortRecords, type SortKey } from "./sort.js"; import { groupRecords, type GroupMeta } from "./group.js"; import { paginateRows, type Paged } from "./paginate.js"; export interface QueryInput { records: RecordRow[]; properties: PropertyInfo[]; filter?: FilterNode | null; sort?: SortSpec[]; group?: GroupSpec[]; includeArchived?: boolean; visiblePropertyIds?: string[]; cursor?: string | null; pageSize?: number; now?: number; actorUserId?: string; userTimezone?: string; } export interface QueryResult { records: RecordRow[]; groups: GroupMeta[] | null; cursor: string | null; total: number; } export function runRecordQuery(input: QueryInput): QueryResult { const props = propertiesMap(input.properties); const now = input.now ?? Date.now(); const ctx = { now, actorUserId: input.actorUserId, userTimezone: input.userTimezone ?? "UTC" }; // 1. base set (unarchived unless flag) let rows = input.records; if (!input.includeArchived) rows = rows.filter((r) => !r.archivedAt); // 2. filter rows = applyFilter(rows, input.filter ?? null, props, ctx); // 3. sort (default: position ASC) const sort: readonly SortKey[] = input.sort && input.sort.length > 0 ? (input.sort as SortKey[]) : []; rows = sortRecords(rows, sort, props, true); // visible-property mask (view-level column visibility) if (input.visiblePropertyIds && input.visiblePropertyIds.length > 0) { const visible = new Set(input.visiblePropertyIds); rows = rows.map((r) => { const values: Record = {}; for (const [k, v] of Object.entries(r.values)) if (visible.has(k)) values[k] = v; const masked: RecordRow = { ...r, values }; return masked; }); } // 4a. grouped views (board/calendar/timeline): groups + per-group records if (input.group && input.group.length > 0) { const groups = groupRecords(rows, input.group, props); return { records: rows, groups, cursor: null, total: rows.length }; } // 4b. flat views: keyset pagination const paged: Paged = paginateRows( rows, input.cursor ?? undefined, input.pageSize ?? 50, ); return { records: paged.items, groups: null, cursor: paged.cursor, total: paged.total }; } export * from "./sort.js"; export * from "./group.js"; export * from "./paginate.js"; export * from "./pivot.js";