import { YonexusError } from '../core/models/errors'; import type { Identity, QueryFilter, QueryInput, QueryOptions, YonexusSchema } from "../core/models/types"; const DEFAULT_LIMIT = 20; const MAX_LIMIT = 100; const regexCache = new Map(); const containsCache = new Map(); function getRegex(pattern: string): RegExp { const cached = regexCache.get(pattern); if (cached) return cached; const created = new RegExp(pattern); regexCache.set(pattern, created); return created; } function normalizeNeedle(value: string): string { const cached = containsCache.get(value); if (cached) return cached; const normalized = value.toLowerCase(); containsCache.set(value, normalized); return normalized; } function isQueryable(field: string, schema: YonexusSchema): boolean { return Boolean(schema[field]?.queryable); } function matchFilter(identity: Identity, filter: QueryFilter): boolean { const raw = identity.meta[filter.field] ?? ""; switch (filter.op) { case "eq": return raw === filter.value; case "contains": return raw.toLowerCase().includes(normalizeNeedle(filter.value)); case "regex": { const re = getRegex(filter.value); return re.test(raw); } default: return false; } } function normalizeOptions(options?: QueryOptions): Required { const limit = Math.min(Math.max(1, options?.limit ?? DEFAULT_LIMIT), MAX_LIMIT); const offset = Math.max(0, options?.offset ?? 0); return { limit, offset }; } function sortFilters(filters: QueryFilter[]): QueryFilter[] { const weight = (f: QueryFilter): number => { if (f.op === 'eq') return 1; if (f.op === 'contains') return 2; return 3; }; return [...filters].sort((a, b) => weight(a) - weight(b)); } export function queryIdentities(identities: Identity[], input: QueryInput, schema: YonexusSchema): Identity[] { for (const filter of input.filters) { if (!isQueryable(filter.field, schema)) { throw new YonexusError('FIELD_NOT_QUERYABLE', `field_not_queryable: ${filter.field}`, { field: filter.field }); } } const orderedFilters = sortFilters(input.filters); const filtered = identities.filter((identity) => orderedFilters.every((f) => matchFilter(identity, f))); const { limit, offset } = normalizeOptions(input.options); return filtered.slice(offset, offset + limit); }