import type { TableMetadata } from "@tailor-platform/app-shell"; const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/; // Only `datetime` / `time` need expansion to RFC3339 — true `Date` fields // already match the GraphQL Date scalar's YYYY-MM-DD format the date filter // emits. Including "date" here corrupts Date filters // (e.g. receiptDate on the GR list, invoiceDate on the PI list). const DATETIME_FIELD_TYPES = new Set(["datetime", "time"]); /** * Normalize datetime filter values emitted by DataTable.Filters. * * The date filter editors DataTable.Filters uses emit bare "YYYY-MM-DD" * strings, but TailorDB's DateTime scalar requires a full RFC3339 datetime, so filters * on datetime fields silently match nothing. For each known datetime field in * the table metadata, rewrite bare date strings to ISO datetimes: * * - `eq: "YYYY-MM-DD"` → `gte: startOfDayLocal, lt: startOfNextDayLocal` * so "equals " covers the whole day in the user's local timezone. * - `gt`/`gte`/`lt`/`lte` expand to start-of-day UTC (lower) or end-of-day * UTC (upper) as before. */ export function normalizeCollectionQuery( query: Record> | undefined, tableMetadata: TableMetadata, ): Record> | undefined { if (!query) return query; const datetimeFields = new Set( tableMetadata.fields.filter((f) => DATETIME_FIELD_TYPES.has(f.type)).map((f) => f.name), ); const next: Record> = {}; for (const [field, ops] of Object.entries(query)) { if (!datetimeFields.has(field)) { next[field] = ops; continue; } next[field] = normalizeOps(ops); } return next; } function normalizeOps(ops: Record): Record { const out: Record = {}; for (const [op, value] of Object.entries(ops)) { // `eq` on a datetime field with a bare date string means "on this day". // Expand to a gte/lt range spanning the full day in the user's local timezone. if (op === "eq" && typeof value === "string" && DATE_ONLY_RE.test(value)) { const [year, month, day] = value.split("-").map(Number); out.gte = new Date(year, month - 1, day, 0, 0, 0, 0).toISOString(); out.lt = new Date(year, month - 1, day + 1, 0, 0, 0, 0).toISOString(); } else { out[op] = normalizeValue(op, value); } } return out; } function normalizeValue(op: string, value: unknown): unknown { if (Array.isArray(value)) { return value.map((v) => toIsoIfBareDate(op, v)); } return toIsoIfBareDate(op, value); } function toIsoIfBareDate(op: string, value: unknown): unknown { if (typeof value !== "string" || !DATE_ONLY_RE.test(value)) return value; // Upper-bound operators expand to end-of-day so the selected date is inclusive. const isUpperBound = op === "lt" || op === "lte"; return isUpperBound ? `${value}T23:59:59.999Z` : `${value}T00:00:00.000Z`; }