/** * Query string parsing and Prisma `where` construction for list search * and filters. * * Design reference: docs/design/list-search-filters.md. * * Golden rule (§4.3 of the design doc): the query string SELECTS from a * finite whitelist of operators derived from the field's type. It never * describes a Prisma clause. No operator ever flows from the URL into the * `where` object as a key — the operator string from the URL is only ever * used to look up a fixed table; the table's value (never the URL's raw * string) becomes the Prisma operator key. */ import type { PrismaModel } from '../introspection/parser.js'; import type { Filter } from '../adapters/types.js'; export type FilterOp = 'equals' | 'contains' | 'startsWith' | 'gte' | 'lte' | 'isnull'; export interface ActiveFilter { field: string; op: FilterOp; /** Already coerced to the JS type Prisma expects for this field/op. */ value: unknown; /** Original string from the query string, kept to re-render the UI. */ raw: string; } export interface IgnoredFilter { /** Raw query param key, e.g. "f.passwordHash" or "f.nope__gte". */ param: string; reason: 'unknown-field' | 'not-filterable' | 'bad-operator' | 'bad-value'; } export interface ListQuery { q: string | null; searchFields: string[]; filters: ActiveFilter[]; ignored: IgnoredFilter[]; } /** * Field-name candidates for the default search heuristic (§2.1). Relation-label * resolution (handler.ts) keeps its own separate list — the two are not shared. */ export declare const DEFAULT_LABEL_FIELDS: string[]; /** * Fields eligible for the free-text search box. * * Explicit `searchFields` config always wins. Otherwise: String fields, * not sensitive, not relation/list/id, whose name is in `labelFields` * (same list used for relation labels — one heuristic, not two that could * drift apart). Empty result means "no search box rendered", never a * fallback that scans every String column. * * `hidden` excludes a field in EVERY case, config included (§3.5): a * field hidden from the list/form display must not remain a value- * confirmation oracle via `?q=`/`contains` just because a developer * explicitly listed it in `searchFields` — that's the exact §0.a leak * closed for sensitive-by-name fields, `hidden` is a second independent * source that must close the same way (§10). */ export declare function resolveSearchFields(model: PrismaModel, configured: string[] | undefined, labelFields?: string[], hidden?: Set): string[]; export interface DateRange { gte: Date; lt: Date; } /** * DateTime shortcuts, à la Django. Upper bound is always EXCLUSIVE (`lt`), * never `lte`: `lte 23:59:59.000` misses the last second's milliseconds, * a classic bug invisible in tests unless caught explicitly (§5.5). * * `now` is injectable so tests are deterministic (not "will break at * midnight UTC in CI"). */ export declare function resolveDateShortcut(raw: string, now?: () => Date): DateRange | undefined; /** * Parse `?q=` and `?f.*=` into a `ListQuery`. Pure function: no I/O, no * Prisma. `filterableFields` is the set of field names the caller allows * to be filtered (already validated against config/heuristics + the * shared sensitive-name predicate) — this function does not decide * *which* fields are filterable, only how to parse a value once a field * is known to be eligible. */ export declare function parseListQuery(searchParams: URLSearchParams, model: PrismaModel, enums: Map, searchFields: string[], filterableFields: Set, now?: () => Date): ListQuery; /** * Compose the final generic `Filter`: `and: [scope, ...filters, {or: search}]`. * NEVER a spread. Flat `{ tenantId: 1 }` scopes become `eq` leaves via * `normalizeScope`; nested Prisma where objects stay opaque for the Prisma * compiler. Drizzle's compiler throws on those opaques. */ export declare function buildWhere(query: ListQuery, scope: Record | Filter | undefined, caseInsensitiveSearch: boolean, model: PrismaModel): Filter | Record | undefined;