@open-mercato/shared 0.6.8-develop.7005.1.70a5747c3e → 0.6.8-develop.7012.1.1dbd6f5fbd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -123,7 +123,10 @@ index stores hashes of the plaintext, so it keeps matching. Issue #2990.
123
123
  predicate in that case.
124
124
  - `matched: true` with `ids: []` is a real empty result.
125
125
  - Queries that go through the query engine get this routing automatically; raw
126
- `em.find` / Kysely list routes must wire it themselves. When the fallback would run
126
+ `em.find` / Kysely list routes must wire it themselves. One carve-out: with
127
+ `OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS=true` (default false), a base-column
128
+ `like`/`ilike` on a **plaintext** column runs as exact SQL ILIKE instead of the token
129
+ rewrite — encrypted columns keep the token path either way. When the fallback would run
127
130
  `ILIKE` against an encrypted column, both query engines now log a warning
128
131
  (`lib/query/ciphertext-search-warning`) instead of degrading silently.
129
132
  - The `…WithDecryption` helpers log the same warning outside production when the `where`
@@ -11,6 +11,8 @@ import {
11
11
  isSearchFilterOp
12
12
  } from "../search/availability.js";
13
13
  import { tokenizeText } from "../search/tokenize.js";
14
+ import { fieldNameCandidates } from "./encrypted-sort.js";
15
+ import { isTenantDataEncryptionEnabled } from "../encryption/toggles.js";
14
16
  import { runBeforeQueryPipeline, runAfterQueryPipeline } from "./query-extension-runner.js";
15
17
  import {
16
18
  buildCustomFieldDefinitionIndexFromRows,
@@ -23,6 +25,28 @@ import { createLogger } from "../logger/index.js";
23
25
  const logger = createLogger("shared").child({ component: "query" });
24
26
  const DECRYPT_CONCURRENCY = 8;
25
27
  const entityTableCache = /* @__PURE__ */ new Map();
28
+ function isEncryptedLikeField(encrypted, field) {
29
+ return fieldNameCandidates(field).some((candidate) => encrypted.has(candidate));
30
+ }
31
+ const ENCRYPTED_LIKE_FIELDS_TTL_MS = 6e4;
32
+ const ENCRYPTED_LIKE_FIELDS_CACHE_CAP = 500;
33
+ const encryptedLikeFieldsCache = /* @__PURE__ */ new Map();
34
+ async function resolveEncryptedLikeFieldSet(read, entity, tenantId) {
35
+ const key = `${entity}|${tenantId ?? ""}`;
36
+ const hit = encryptedLikeFieldsCache.get(key);
37
+ if (hit && Date.now() - hit.at < ENCRYPTED_LIKE_FIELDS_TTL_MS) return hit.fields;
38
+ const names = await read();
39
+ const fields = /* @__PURE__ */ new Set();
40
+ for (const name of names ?? []) {
41
+ for (const candidate of fieldNameCandidates(String(name))) fields.add(candidate);
42
+ }
43
+ if (encryptedLikeFieldsCache.size >= ENCRYPTED_LIKE_FIELDS_CACHE_CAP) encryptedLikeFieldsCache.clear();
44
+ encryptedLikeFieldsCache.set(key, { at: Date.now(), fields });
45
+ return fields;
46
+ }
47
+ function clearEncryptedLikeFieldsCache() {
48
+ encryptedLikeFieldsCache.clear();
49
+ }
26
50
  const ENTITY_ID_PATTERN = /^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$/;
27
51
  const isValidEntityIdShape = (value) => ENTITY_ID_PATTERN.test(value);
28
52
  const pluralizeBaseName = (name) => {
@@ -232,6 +256,35 @@ class BasicQueryEngine {
232
256
  const searchEnabled = !skipAutoScope && await this.searchAvailability().staticEnabled();
233
257
  const hasSearchTokens = searchEnabled && searchFilters.length ? await this.searchAvailability().hasTokens(String(entity), opts.tenantId ?? null, orgScope) : false;
234
258
  const searchActive = searchEnabled && hasSearchTokens;
259
+ let encryptedLikeFields = null;
260
+ if (searchActive && searchConfig.useIlikeForNonEncryptedFields === true && searchFilters.some((filter) => !String(filter.field).startsWith("cf:"))) {
261
+ try {
262
+ const service = this.getEncryptionService();
263
+ const readEncryptedFieldNames = service?.getEncryptedFieldNames?.bind(service);
264
+ if (readEncryptedFieldNames) {
265
+ encryptedLikeFields = await resolveEncryptedLikeFieldSet(
266
+ () => readEncryptedFieldNames(
267
+ String(entity),
268
+ opts.tenantId ?? null,
269
+ null,
270
+ { ignoreRuntimeHealth: true }
271
+ ),
272
+ String(entity),
273
+ opts.tenantId ?? null
274
+ );
275
+ } else if (isTenantDataEncryptionEnabled()) {
276
+ encryptedLikeFields = null;
277
+ } else {
278
+ encryptedLikeFields = /* @__PURE__ */ new Set();
279
+ }
280
+ } catch (err) {
281
+ logger.warn("search: encrypted-field map unavailable; keeping the token rewrite for all columns", {
282
+ entity: String(entity),
283
+ error: err instanceof Error ? err.message : String(err)
284
+ });
285
+ encryptedLikeFields = null;
286
+ }
287
+ }
235
288
  if (searchFilters.length) {
236
289
  const fields = searchFilters.map((filter) => String(filter.field));
237
290
  this.logSearchDebug("search:init", {
@@ -293,7 +346,12 @@ class BasicQueryEngine {
293
346
  }
294
347
  const recordIdColumn = qualify("id");
295
348
  const applyFilterOp = (builder, column, op, value, fieldName) => {
296
- if ((op === "like" || op === "ilike") && searchActive && typeof value === "string" && fieldName && typeof column === "string") {
349
+ if ((op === "like" || op === "ilike") && searchActive && typeof value === "string" && fieldName && typeof column === "string" && // Plaintext columns keep exact SQL ILIKE -- see the encryptedLikeFields note above. cf:*
350
+ // filters never reach this path (they are applied by the custom-field branches), so this
351
+ // gate only decides base columns. Membership is tested across name-shape candidates --
352
+ // encryption maps may declare `displayName` while the filter carries the column name
353
+ // `display_name`, and a raw comparison would misread that ciphertext column as plaintext.
354
+ (encryptedLikeFields === null || isEncryptedLikeField(encryptedLikeFields, fieldName))) {
297
355
  const tokens = tokenizeText(String(value), searchConfig);
298
356
  const hashes = tokens.hashes;
299
357
  if (hashes.length) {
@@ -1126,7 +1184,10 @@ class BasicQueryEngine {
1126
1184
  export {
1127
1185
  BasicQueryEngine,
1128
1186
  ENTITY_ID_PATTERN,
1187
+ clearEncryptedLikeFieldsCache,
1188
+ isEncryptedLikeField,
1129
1189
  isValidEntityIdShape,
1190
+ resolveEncryptedLikeFieldSet,
1130
1191
  resolveEntityTableName,
1131
1192
  resolveRegisteredEntityTableName
1132
1193
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/query/engine.ts"],
4
- "sourcesContent": ["import type { QueryEngine, QueryOptions, QueryResult, QueryResultMeta, EncryptedSortRowCapWarning, QueryCustomFieldSource, QueryExtensionsConfig, Sort } from './types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql, type RawBuilder } from 'kysely'\nimport {\n applyJoinFilters,\n normalizeFilters,\n partitionFilters,\n resolveJoins,\n type BaseFilter,\n type NormalizedFilter,\n type ResolvedJoin,\n} from './join-utils'\nimport { resolveSearchConfig } from '../search/config'\nimport {\n createSearchTokenAvailability,\n isSearchFilterOp,\n type SearchTokenAvailability,\n type SearchTokenProbeDb,\n type SearchTokenProbeQueryBuilder,\n} from '../search/availability'\nimport { tokenizeText } from '../search/tokenize'\nimport { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from './query-extension-runner'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n resolveCfDefIndexOrgCandidates,\n type CustomFieldDefinitionRow,\n type ResolvedCustomFieldDefinitions,\n} from '../crud/custom-field-definition-index'\nimport { warnOnCiphertextLikeFallback } from './ciphertext-search-warning'\nimport { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from './encrypted-sort'\nimport { mapWithConcurrency } from './bounded-decrypt'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'query' })\n\nconst DECRYPT_CONCURRENCY = 8\n\ntype AnyDb = Kysely<any>\ntype AnyBuilder = any\n\nconst entityTableCache = new Map<string, string>()\n\ntype EncryptionResolver = () => {\n decryptEntityPayload?: (entityId: EntityId, payload: Record<string, unknown>, tenantId?: string | null, organizationId?: string | null) => Promise<Record<string, unknown>>\n getEncryptedFieldNames?: (entityId: EntityId, tenantId?: string | null, organizationId?: string | null) => Promise<readonly string[]>\n isEnabled?: () => boolean\n} | null\n\ntype ResolvedCustomFieldSource = {\n entityId: EntityId\n alias: string\n table: string\n recordIdExpr: RawBuilder<string>\n}\n\ntype ResultRow = Record<string, unknown>\n\n/**\n * Canonical `module:entity` entity-id shape (both segments snake_case,\n * starting with a lowercase letter). Used to validate caller-supplied entity\n * ids at security-sensitive boundaries before they reach table resolution.\n */\nexport const ENTITY_ID_PATTERN = /^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$/\n\nexport const isValidEntityIdShape = (value: string): boolean => ENTITY_ID_PATTERN.test(value)\n\nconst pluralizeBaseName = (name: string): string => {\n if (!name) return name\n if (name.endsWith('s')) return name\n if (name.endsWith('y')) return `${name.slice(0, -1)}ies`\n return `${name}s`\n}\n\n/**\n * Accepts a module-declared `EntityExtension.table` only when it is a bare SQL\n * identifier. The value is interpolated into a join clause, so anything else is\n * ignored in favour of the derived table name.\n */\nconst PLAIN_TABLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/\n\nconst isPlainTableIdentifier = (value: unknown): value is string =>\n typeof value === 'string' && PLAIN_TABLE_IDENTIFIER_PATTERN.test(value)\n\nconst toPascalCase = (value: string): string => {\n return value\n .split(/[_\\s]+/)\n .filter(Boolean)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('')\n}\n\nconst candidateClassNames = (rawName: string): string[] => {\n const base = toPascalCase(rawName)\n const candidates = new Set<string>()\n if (base) candidates.add(base)\n if (base && !base.endsWith('Entity')) candidates.add(`${base}Entity`)\n return Array.from(candidates)\n}\n\n/**\n * Resolve an entity id to a table name strictly via registered MikroORM metadata.\n *\n * Unlike {@link resolveEntityTableName}, this never falls back to a pluralized\n * guess: it returns `null` when no registered entity matches. Use it for\n * security-sensitive call sites (e.g. the reindexer) that must refuse to read\n * arbitrary, attacker-chosen tables that happen to exist in the schema.\n */\nexport function resolveRegisteredEntityTableName(\n em: EntityManager | undefined,\n entity: EntityId,\n): string | null {\n const parts = String(entity || '').split(':')\n const rawName = (parts[1] && parts[1].trim().length > 0) ? parts[1] : (parts[0] || '').trim()\n const metadata = (em as any)?.getMetadata?.()\n\n if (!metadata || !rawName) return null\n\n const candidates = candidateClassNames(rawName)\n for (const candidate of candidates) {\n try {\n const meta = metadata.find?.(candidate)\n if (meta?.tableName) {\n return String(meta.tableName)\n }\n } catch {}\n }\n\n // Secondary lookup: search ORM metadata by candidate table names\n const modulePrefix = parts[0] ?? ''\n const candidateTables = [\n `${modulePrefix}_${rawName}`,\n pluralizeBaseName(rawName),\n `${modulePrefix}_${pluralizeBaseName(rawName)}`,\n ]\n try {\n const allMeta: any[] = metadata.getAll?.() ?? []\n for (const meta of allMeta) {\n if (meta?.tableName && candidateTables.includes(String(meta.tableName))) {\n return String(meta.tableName)\n }\n }\n } catch {}\n\n return null\n}\n\nexport function resolveEntityTableName(em: EntityManager | undefined, entity: EntityId): string {\n if (entityTableCache.has(entity)) {\n return entityTableCache.get(entity)!\n }\n const parts = String(entity || '').split(':')\n const rawName = (parts[1] && parts[1].trim().length > 0) ? parts[1] : (parts[0] || '').trim()\n\n const registered = resolveRegisteredEntityTableName(em, entity)\n if (registered) {\n entityTableCache.set(entity, registered)\n return registered\n }\n\n const fallback = pluralizeBaseName(rawName || '')\n logger.warn('Could not resolve entity via ORM metadata; falling back to table name \u2014 ensure the entity ID segment matches the class name convention', { entity, fallback })\n entityTableCache.set(entity, fallback)\n return fallback\n}\n\nfunction buildFilterableCustomFieldJoins(\n sources: QueryCustomFieldSource[] | undefined,\n): Array<{\n alias: string\n table?: string\n entityId: EntityId\n from: { field: string }\n to: { field: string }\n type: 'left' | 'inner'\n}> {\n if (!sources || sources.length === 0) return []\n return sources.flatMap((source, index) => {\n if (!source.join) return []\n const alias = typeof source.alias === 'string' && source.alias.trim().length > 0\n ? source.alias.trim()\n : `cfs_${index}`\n return [{\n alias,\n table: source.table,\n entityId: source.entityId,\n from: { field: source.join.fromField },\n to: { field: source.join.toField },\n type: source.join.type === 'inner' ? 'inner' : 'left',\n }]\n })\n}\n\nfunction computeCustomFieldScore(cfg: Record<string, unknown>, kind: string, entityIndex: number) {\n const listVisibleScore = cfg.listVisible === false ? 0 : 1\n const formEditableScore = cfg.formEditable === false ? 0 : 1\n const filterableScore = cfg.filterable ? 1 : 0\n const kindScore = (() => {\n switch (kind) {\n case 'dictionary': return 8\n case 'relation': return 6\n case 'select': return 4\n case 'multiline': return 3\n case 'boolean':\n case 'integer':\n case 'float': return 2\n default: return 1\n }\n })()\n const optionsBonus = Array.isArray(cfg.options) && cfg.options.length ? 2 : 0\n const dictionaryBonus = typeof cfg.dictionaryId === 'string' && (cfg.dictionaryId as string).trim().length ? 5 : 0\n const base = (listVisibleScore * 16) + (formEditableScore * 8) + (filterableScore * 4) + kindScore + optionsBonus + dictionaryBonus\n const penalty = typeof cfg.priority === 'number' ? cfg.priority : 0\n return { base, penalty, entityIndex }\n}\n\n/**\n * BasicQueryEngine \u2014 Kysely-backed fallback query engine.\n *\n * Resolves base tables via MikroORM metadata, applies tenant/organization/\n * deleted_at scoping, handles custom field (cf:*) selection and filtering,\n * and performs entity-extension joins. Used as the fallback for\n * {@link HybridQueryEngine} when the query index is unavailable or incomplete.\n */\nexport class BasicQueryEngine implements QueryEngine {\n private columnCache = new Map<string, boolean>()\n private searchAliasSeq = 0\n private searchAvailabilityInstance: SearchTokenAvailability | null = null\n\n constructor(\n private em: EntityManager,\n private getDbFn?: () => AnyDb,\n private resolveEncryptionService?: EncryptionResolver,\n ) {}\n\n private getEncryptionService() {\n try {\n return this.resolveEncryptionService?.() ?? null\n } catch {\n return null\n }\n }\n\n private getDb(): AnyDb {\n if (this.getDbFn) return this.getDbFn()\n const emAny = this.em as any\n if (typeof emAny?.getKysely === 'function') return emAny.getKysely() as AnyDb\n throw new Error('BasicQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)')\n }\n\n private searchAvailability(): SearchTokenAvailability {\n if (!this.searchAvailabilityInstance) {\n this.searchAvailabilityInstance = createSearchTokenAvailability({\n getDb: () => this.getDb() as unknown as SearchTokenProbeDb,\n getConfig: resolveSearchConfig,\n applyOrganizationScope: (query, column, scope) => this.applyOrganizationScope(\n query as unknown as AnyBuilder,\n column,\n scope,\n ) as unknown as SearchTokenProbeQueryBuilder,\n logDebug: (event, payload) => this.logSearchDebug(event, payload),\n })\n }\n return this.searchAvailabilityInstance\n }\n\n async query<T = any>(entity: EntityId, opts: QueryOptions = {}): Promise<QueryResult<T>> {\n // --- UMES query extension: before-query pipeline ---\n const ext = opts.extensions\n let effectiveOpts = opts\n let extensionCtx: QueryExtensionContext | null = null\n const noop = { resolve: <R = unknown>(_name: string): R => { throw new Error('No DI context') } }\n\n if (ext) {\n extensionCtx = {\n entity: String(entity),\n engine: 'basic',\n tenantId: opts.tenantId ?? '',\n organizationId: opts.organizationId,\n userId: ext.userId,\n em: this.em,\n container: ext.container,\n userFeatures: ext.userFeatures,\n }\n const diCtx = ext.resolve ? { resolve: ext.resolve } : noop\n const beforeResult = await runBeforeQueryPipeline(opts, extensionCtx, diCtx)\n if (beforeResult.blocked) {\n throw new Error(beforeResult.errorMessage ?? 'Query blocked by extension subscriber')\n }\n effectiveOpts = beforeResult.query\n }\n // Strip extensions from effectiveOpts so they don't propagate to sub-queries\n const { extensions: _ext, ...coreOpts } = effectiveOpts\n opts = coreOpts\n\n // Heuristic: map '<module>:user' -> table 'users'\n const table = resolveEntityTableName(this.em, entity)\n const db = this.getDb()\n\n const qualify = (col: string) => `${table}.${col}`\n const orgScope = this.resolveOrganizationScope(opts)\n this.searchAliasSeq = 0\n // Require tenant scope for all queries\n if (!opts.tenantId) {\n throw new Error(\n 'QueryEngine: tenantId is now required for all queries (breaking change). ' +\n 'Please provide a tenantId in QueryOptions, e.g., query(entity, { tenantId: ... }). ' +\n 'See migration guide or documentation for details.'\n )\n }\n const skipAutoScope = opts.omitAutomaticTenantOrgScope === true\n\n const normalizedFilters = normalizeFilters(opts.filters)\n const resolvedJoins = resolveJoins(\n table,\n [...(opts.joins ?? []), ...buildFilterableCustomFieldJoins(opts.customFieldSources)],\n (entityId) => resolveEntityTableName(this.em, entityId as any),\n )\n const joinMap = new Map<string, ResolvedJoin>()\n const aliasTables = new Map<string, string>()\n aliasTables.set(table, table)\n aliasTables.set('base', table)\n for (const join of resolvedJoins) {\n joinMap.set(join.alias, join)\n aliasTables.set(join.alias, join.table)\n }\n const { baseFilters, joinFilters } = partitionFilters(table, normalizedFilters, joinMap)\n const cfFilters = normalizedFilters.filter((filter) => String(filter.field).startsWith('cf:'))\n // Custom-field leaves carrying an orGroup belong to an OR disjunct; applying them\n // one `.where()` at a time would AND them onto every disjunct (#5039).\n const regularCfFilters = cfFilters.filter((filter) => !filter.orGroup)\n const orGroupCfFilters = cfFilters.filter((filter) => filter.orGroup)\n const searchConfig = resolveSearchConfig()\n const searchFilters = [...baseFilters, ...cfFilters].filter((filter) => isSearchFilterOp(filter.op))\n // Callers that opt out of automatic tenant/org scoping own the full\n // visibility predicate. Search-token filtering has its own tenant/org\n // guards, so it must be disabled on this direct-query path as documented\n // by QueryOptions.omitAutomaticTenantOrgScope.\n const searchEnabled = !skipAutoScope && await this.searchAvailability().staticEnabled()\n // Probe `search_tokens` only when this query actually searches (#4723): every consumer of\n // `searchActive` sits behind a like/ilike guard, so on a plain list load the answer is never\n // read \u2014 and the probe is a `LIMIT 1` the planner can resolve as a seq scan over a large\n // `search_tokens`. The join path below already probes lazily for the same reason.\n const hasSearchTokens = searchEnabled && searchFilters.length\n ? await this.searchAvailability().hasTokens(String(entity), opts.tenantId ?? null, orgScope)\n : false\n const searchActive = searchEnabled && hasSearchTokens\n if (searchFilters.length) {\n const fields = searchFilters.map((filter) => String(filter.field))\n this.logSearchDebug('search:init', {\n entity: String(entity),\n table,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n fields,\n searchEnabled,\n hasSearchTokens,\n searchActive,\n searchConfig: {\n enabled: searchConfig.enabled,\n minTokenLength: searchConfig.minTokenLength,\n enablePartials: searchConfig.enablePartials,\n hashAlgorithm: searchConfig.hashAlgorithm,\n blocklistedFields: searchConfig.blocklistedFields,\n },\n })\n if (!searchEnabled) {\n this.logSearchDebug('search:disabled', { entity: String(entity), table })\n } else if (!hasSearchTokens) {\n this.logSearchDebug('search:no-search-tokens', {\n entity: String(entity),\n table,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n })\n }\n const fallbackFields = searchFilters\n .filter((filter) => !searchActive || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)\n .map((filter) => String(filter.field))\n if (fallbackFields.length) {\n await warnOnCiphertextLikeFallback({\n entity: String(entity),\n fields: fallbackFields,\n tenantId: opts.tenantId ?? null,\n // `searchEnabled` also folds in the missing-table and\n // omitAutomaticTenantOrgScope cases, which are \"no usable tokens\"\n // rather than \"the operator switched search off\".\n reason: searchActive\n ? 'no-indexable-tokens'\n : searchConfig.enabled ? 'no-search-tokens' : 'search-disabled',\n service: this.getEncryptionService(),\n })\n }\n }\n for (const [alias, joinedFilters] of joinFilters) {\n const filters = joinedFilters.filter((entry) => entry.op === 'like' || entry.op === 'ilike')\n if (!filters.length) continue\n const join = joinMap.get(alias)\n if (!join?.entityId) continue\n const hasJoinedTokens = searchEnabled\n ? await this.searchAvailability().hasTokens(join.entityId, opts.tenantId ?? null, orgScope)\n : false\n const fallbackFields = filters\n .filter((filter) => !hasJoinedTokens || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)\n .map((filter) => filter.column)\n if (!fallbackFields.length) continue\n await warnOnCiphertextLikeFallback({\n entity: join.entityId,\n fields: fallbackFields,\n tenantId: opts.tenantId ?? null,\n reason: hasJoinedTokens\n ? 'no-indexable-tokens'\n : searchConfig.enabled ? 'no-search-tokens' : 'search-disabled',\n service: this.getEncryptionService(),\n })\n }\n const recordIdColumn = qualify('id')\n\n const applyFilterOp = (builder: AnyBuilder, column: string | RawBuilder<unknown>, op: string, value: unknown, fieldName?: string): AnyBuilder => {\n if (\n (op === 'like' || op === 'ilike') &&\n searchActive &&\n typeof value === 'string' &&\n fieldName &&\n typeof column === 'string'\n ) {\n const tokens = tokenizeText(String(value), searchConfig)\n const hashes = tokens.hashes\n if (hashes.length) {\n const result = this.applySearchTokens(builder, {\n entity: String(entity),\n field: fieldName,\n hashes,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n tokens: tokens.tokens,\n })\n this.logSearchDebug('search:filter', {\n entity: String(entity),\n field: fieldName,\n tokens: tokens.tokens,\n hashes,\n applied: result.applied,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n })\n if (result.applied) return result.builder\n } else {\n this.logSearchDebug('search:skip-empty-hashes', {\n entity: String(entity),\n field: fieldName,\n value,\n })\n }\n }\n return this.applyColumnOp(builder, column, op, value)\n }\n\n // `eq` is accepted alongside `like`/`ilike` so that filters against\n // encrypted joined columns (whose ciphertext cannot be compared for\n // equality in SQL) can still resolve via tokenized search. Routing\n // only applies when `searchEnabled` is true AND the joined entity has\n // search tokens installed (`searchAvailable`); for non-searchable or\n // non-encrypted columns the caller still falls through to exact SQL\n // equality via `applyFilterOp`. Note that token match is approximate \u2014\n // callers needing strict equality on encrypted fields should filter on\n // the deterministic `*_hash` column instead.\n const applyJoinFilterOp = async (\n builder: AnyBuilder,\n filter: { column: string; op: string; value?: unknown },\n _qualified: string,\n join: ResolvedJoin,\n ): Promise<{ applied: boolean; builder: AnyBuilder }> => {\n if (!searchEnabled || !join.entityId) return { applied: false, builder }\n if (!['like', 'ilike'].includes(filter.op)) return { applied: false, builder }\n if (typeof filter.value !== 'string' || filter.value.trim().length === 0) return { applied: false, builder }\n\n const searchAvailable = await this.searchAvailability().hasTokens(join.entityId, opts.tenantId ?? null, orgScope)\n if (!searchAvailable) return { applied: false, builder }\n\n const tokens = tokenizeText(String(filter.value), searchConfig)\n if (!tokens.hashes.length) return { applied: false, builder }\n\n const result = this.applySearchTokens(builder, {\n entity: join.entityId,\n field: filter.column,\n hashes: tokens.hashes,\n recordIdColumn: `${join.alias}.id`,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n tokens: tokens.tokens,\n })\n return { applied: result.applied, builder: result.builder }\n }\n\n const regularBaseFilters = baseFilters.filter((f) => !f.orGroup)\n const orGroupFilters = baseFilters.filter((f) => f.orGroup)\n\n const applyAliasScopes = async (builder: AnyBuilder, aliasName: string): Promise<AnyBuilder> => {\n const targetTable = aliasTables.get(aliasName)\n if (!targetTable) return builder\n let next = builder\n if (!skipAutoScope && orgScope && await this.columnExists(targetTable, 'organization_id')) {\n next = this.applyOrganizationScope(next, `${aliasName}.organization_id`, orgScope)\n }\n if (!skipAutoScope && opts.tenantId && await this.columnExists(targetTable, 'tenant_id')) {\n next = next.where(`${aliasName}.tenant_id`, '=', opts.tenantId)\n }\n return next\n }\n\n const fallbackOrgId =\n opts.organizationId\n ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null)\n const encryptionService = this.getEncryptionService()\n const resolvedSorts: Sort[] = []\n for (const s of opts.sort || []) {\n if (s.field.startsWith('cf:')) {\n resolvedSorts.push(s)\n } else {\n const column = await this.resolveBaseColumn(table, s.field)\n if (column) resolvedSorts.push({ ...s, field: column })\n }\n }\n const encryptedSortFields = await resolveEncryptedSortFields(\n encryptionService,\n entity,\n resolvedSorts.filter((sort) => !sort.field.startsWith('cf:')).map((sort) => sort.field),\n opts.tenantId ?? null,\n fallbackOrgId,\n )\n const requiresPlaintextSort = encryptedSortFields.size > 0\n\n const tenantId = opts.tenantId\n const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_]/g, '_')\n\n type BuiltQuery = {\n builder: AnyBuilder\n hasJoinedAggregates: boolean\n cfJsonAliases: Set<string>\n cfMultiAliasByAlias: Map<string, string>\n resolvedCustomFieldDefinitions: ResolvedCustomFieldDefinitions | undefined\n }\n\n // Builds the fully-scoped query from a fresh root. `projection: 'full'` reproduces\n // today's complete selection (base fields + CF projections + extension joins).\n // `projection: 'sortKeys'` selects only `id` + the sort columns \u2014 the slim phase-1\n // candidate scan used when `requiresPlaintextSort`. Re-running the WHERE/JOIN logic\n // twice is cheap: every `columnExists` check is memoized on `this.columnCache`,\n // so the second pass hits no extra DB calls.\n const buildQuery = async (projection: 'full' | 'sortKeys'): Promise<BuiltQuery> => {\n const isSortKeysProjection = projection === 'sortKeys'\n let q: AnyBuilder = db.selectFrom(table as any)\n\n // Tenant/org/soft-delete scope\n if (!skipAutoScope && orgScope && await this.columnExists(table, 'organization_id')) {\n q = this.applyOrganizationScope(q, qualify('organization_id'), orgScope)\n }\n if (!skipAutoScope && await this.columnExists(table, 'tenant_id')) {\n q = q.where(qualify('tenant_id'), '=', opts.tenantId)\n }\n if (!opts.withDeleted && await this.columnExists(table, 'deleted_at')) {\n q = q.where(qualify('deleted_at'), 'is', null)\n }\n\n for (const filter of regularBaseFilters) {\n const fieldName = String(filter.field)\n let qualified = filter.qualified ?? null\n if (!qualified) {\n const column = await this.resolveBaseColumn(table, fieldName)\n if (!column) {\n q = this.applyIndexDocFilter(q, {\n entity: String(entity),\n field: fieldName,\n op: filter.op,\n value: filter.value,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n withDeleted: opts.withDeleted === true,\n searchActive,\n searchConfig,\n })\n continue\n }\n qualified = qualify(column)\n }\n q = applyFilterOp(q, qualified, filter.op, filter.value, fieldName)\n }\n\n // OR-grouped filters: AND within each group (one $or disjunct), OR between groups.\n // Resolution happens here (it needs async column lookups); the WHERE itself is\n // applied further down, once the cf:* value expressions exist \u2014 an OR group may\n // contain custom-field leaves whose SQL is only available then (#5039).\n type ResolvedOrClause =\n | { kind: 'column'; qualified: string; op: NormalizedFilter['op']; value: unknown }\n | { kind: 'doc'; field: string; op: NormalizedFilter['op']; value: unknown }\n | { kind: 'cf'; key: string; op: NormalizedFilter['op']; value: unknown }\n const resolvedGroupFilters: ResolvedOrClause[][] = []\n if (orGroupFilters.length > 0 || orGroupCfFilters.length > 0) {\n const groups = new Map<string, NormalizedFilter[]>()\n for (const f of [...orGroupFilters, ...orGroupCfFilters]) {\n const group = groups.get(f.orGroup!) ?? []\n group.push(f as NormalizedFilter)\n groups.set(f.orGroup!, group)\n }\n for (const [, groupFilters] of groups) {\n const resolved: ResolvedOrClause[] = []\n for (const filter of groupFilters) {\n const field = String(filter.field)\n if (field.startsWith('cf:')) {\n resolved.push({ kind: 'cf', key: field.slice(3), op: filter.op, value: filter.value })\n continue\n }\n const column = await this.resolveBaseColumn(table, field)\n if (column) {\n resolved.push({ kind: 'column', qualified: qualify(column), op: filter.op, value: filter.value })\n } else {\n // Field is not a base column \u2014 for custom-entity records it lives in\n // entity_indexes.doc. Build an EXISTS sub-filter so `$or` searches\n // across doc fields resolve instead of being silently dropped (#3229).\n resolved.push({ kind: 'doc', field, op: filter.op, value: filter.value })\n }\n }\n if (resolved.length > 0) resolvedGroupFilters.push(resolved)\n }\n }\n\n q = await applyJoinFilters({\n db,\n baseTable: table,\n builder: q,\n joinMap,\n joinFilters,\n aliasTables,\n qualifyBase: (column) => qualify(column),\n applyAliasScope: (builder, alias) => applyAliasScopes(builder, alias),\n applyFilterOp: (builder, column, op, value) => applyFilterOp(builder, column, op, value),\n applyJoinFilterOp,\n columnExists: (tbl, column) => this.columnExists(tbl, column),\n })\n\n // Selection (base columns only here; cf:* handled later)\n if (isSortKeysProjection) {\n q = q.select(sql.ref(qualify('id')).as('id'))\n if (await this.columnExists(table, 'tenant_id')) {\n q = q.select(sql.ref(qualify('tenant_id')).as('tenant_id'))\n }\n if (await this.columnExists(table, 'organization_id')) {\n q = q.select(sql.ref(qualify('organization_id')).as('organization_id'))\n }\n for (const s of resolvedSorts) {\n if (!s.field.startsWith('cf:')) q = q.select(sql.ref(qualify(s.field)).as(s.field))\n }\n } else if (opts.fields && opts.fields.length) {\n const cols = new Set(opts.fields.filter((f) => !f.startsWith('cf:')))\n if (requiresPlaintextSort) {\n for (const field of encryptedSortFields) cols.add(field)\n }\n for (const c of cols) {\n // Qualify and alias to base names to avoid ambiguity\n q = q.select(sql.ref(qualify(c)).as(c))\n }\n } else {\n // Default to selecting only base table columns to avoid ambiguity when joining\n q = q.select(sql`${sql.ref(table)}.*`.as('__all'))\n }\n\n // Resolve which custom fields to include\n const cfSourcesResult = this.configureCustomFieldSources(q, table, entity, db, opts, qualify)\n q = cfSourcesResult.builder\n const cfSources = cfSourcesResult.sources\n const entityIdToSource = new Map<string, ResolvedCustomFieldSource>()\n for (const source of cfSources) {\n entityIdToSource.set(String(source.entityId), source)\n }\n const requestedCustomFieldKeys = (!isSortKeysProjection && Array.isArray(opts.includeCustomFields))\n ? opts.includeCustomFields.map((key) => String(key))\n : []\n const cfKeys = new Set<string>()\n const keySource = new Map<string, ResolvedCustomFieldSource>()\n // Custom-field definition index threaded onto the result so the CRUD factory\n // can decorate list rows without reloading definitions from the DB (#2133).\n // Output-only \u2014 never resolved for the slim sortKeys projection.\n let resolvedCustomFieldDefinitions: ResolvedCustomFieldDefinitions | undefined\n // Explicit in fields/filters\n if (!isSortKeysProjection) {\n for (const f of (opts.fields || [])) {\n if (typeof f === 'string' && f.startsWith('cf:')) cfKeys.add(f.slice(3))\n }\n }\n for (const f of cfFilters) {\n if (typeof f.field === 'string' && f.field.startsWith('cf:')) cfKeys.add(f.field.slice(3))\n }\n if (!isSortKeysProjection && opts.includeCustomFields === true) {\n if (entityIdToSource.size > 0) {\n const entityIdList = Array.from(entityIdToSource.keys())\n const entityOrder = new Map<string, number>()\n entityIdList.forEach((id, idx) => entityOrder.set(id, idx))\n const rows = await db\n .selectFrom('custom_field_defs' as any)\n .select([\n 'key' as any,\n 'entity_id' as any,\n 'config_json' as any,\n 'kind' as any,\n 'organization_id' as any,\n 'tenant_id' as any,\n 'updated_at' as any,\n 'deleted_at' as any,\n ])\n .where('entity_id' as any, 'in', entityIdList)\n .where('is_active' as any, '=', true)\n .where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n .execute() as Array<{\n key: string\n entity_id: string\n config_json: unknown\n kind: string\n organization_id: string | null\n tenant_id: string | null\n updated_at: Date | string | number | null\n deleted_at: Date | string | number | null\n }>\n // Build the decoration index from the same rows, scoped exactly like the\n // factory's loadCustomFieldDefinitionIndex (tenant + is_active already\n // applied in SQL; org + soft-delete applied in the shared builder).\n const orgCandidates = resolveCfDefIndexOrgCandidates(opts.organizationIds, opts.organizationId ?? null)\n const definitionRows: CustomFieldDefinitionRow[] = rows.map((row) => ({\n key: String(row.key),\n entityId: String(row.entity_id),\n kind: row.kind == null ? null : String(row.kind),\n configJson: row.config_json,\n organizationId: row.organization_id == null ? null : String(row.organization_id),\n tenantId: row.tenant_id == null ? null : String(row.tenant_id),\n deletedAt: row.deleted_at ?? null,\n updatedAt: row.updated_at ?? null,\n }))\n resolvedCustomFieldDefinitions = {\n index: buildCustomFieldDefinitionIndexFromRows(definitionRows, { organizationIds: orgCandidates }),\n entityIds: entityIdList,\n tenantId: tenantId ?? null,\n organizationIds: orgCandidates,\n }\n type ScoredCustomFieldRow = {\n key: string\n entityId: string\n kind: string\n config: Record<string, unknown>\n }\n const sorted: ScoredCustomFieldRow[] = rows.map((row) => {\n const raw = row.config_json\n let cfg: Record<string, any> = {}\n if (raw && typeof raw === 'string') {\n try { cfg = JSON.parse(raw) } catch { cfg = {} }\n } else if (raw && typeof raw === 'object') {\n cfg = raw as Record<string, any>\n }\n return {\n key: String(row.key),\n entityId: String(row.entity_id),\n kind: String(row.kind || ''),\n config: cfg,\n }\n })\n sorted.sort((a, b) => {\n const ai = entityOrder.get(a.entityId) ?? Number.MAX_SAFE_INTEGER\n const bi = entityOrder.get(b.entityId) ?? Number.MAX_SAFE_INTEGER\n if (ai !== bi) return ai - bi\n return a.key.localeCompare(b.key)\n })\n const selectedSources = new Map<string, { source: ResolvedCustomFieldSource; score: number; penalty: number; entityIndex: number }>()\n for (const row of sorted) {\n const source = entityIdToSource.get(row.entityId)\n if (!source) continue\n const cfg = row.config || {}\n const entityIndex = entityOrder.get(row.entityId) ?? Number.MAX_SAFE_INTEGER\n const scores = computeCustomFieldScore(cfg, row.kind, entityIndex)\n const existing = selectedSources.get(row.key)\n if (!existing || scores.base > existing.score || (scores.base === existing.score && (scores.penalty < existing.penalty || (scores.penalty === existing.penalty && scores.entityIndex < existing.entityIndex)))) {\n selectedSources.set(row.key, { source, score: scores.base, penalty: scores.penalty, entityIndex: scores.entityIndex })\n }\n cfKeys.add(row.key)\n }\n for (const [key, entry] of selectedSources.entries()) {\n keySource.set(key, entry.source)\n }\n }\n } else if (!isSortKeysProjection && requestedCustomFieldKeys.length > 0) {\n for (const key of requestedCustomFieldKeys) cfKeys.add(key)\n }\n const unresolvedKeys = Array.from(cfKeys).filter((key) => !keySource.has(key))\n if (unresolvedKeys.length > 0 && entityIdToSource.size > 0) {\n const rows = await db\n .selectFrom('custom_field_defs' as any)\n .select(['key' as any, 'entity_id' as any])\n .where('entity_id' as any, 'in', Array.from(entityIdToSource.keys()))\n .where('key' as any, 'in', unresolvedKeys)\n .where('is_active' as any, '=', true)\n .where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n .execute() as Array<{ key: string; entity_id: string }>\n for (const row of rows) {\n const source = entityIdToSource.get(String(row.entity_id))\n if (!source) continue\n if (!keySource.has(row.key)) keySource.set(row.key, source)\n }\n }\n\n const cfValueExprByKey: Record<string, RawBuilder<string | null>> = {}\n const cfSelectedAliases: string[] = []\n const cfJsonAliases = new Set<string>()\n const cfMultiAliasByAlias = new Map<string, string>()\n for (const key of cfKeys) {\n const source = keySource.get(key)\n if (!source) continue\n const entityIdForKey = source.entityId\n const recordIdExpr = source.recordIdExpr\n const sourceAliasSafe = sanitize(source.alias || 'src')\n const keyAliasSafe = sanitize(key)\n const defAlias = `cfd_${sourceAliasSafe}_${keyAliasSafe}`\n const valAlias = `cfv_${sourceAliasSafe}_${keyAliasSafe}`\n // Join definitions for kind resolution\n q = q.leftJoin(`custom_field_defs as ${defAlias}` as any, (jb: any) =>\n jb.on(`${defAlias}.entity_id`, '=', String(entityIdForKey))\n .on(`${defAlias}.key`, '=', key)\n .on(`${defAlias}.is_active`, '=', true)\n .on((eb: any) => eb.or([\n eb(`${defAlias}.tenant_id`, '=', tenantId),\n eb(`${defAlias}.tenant_id`, 'is', null),\n ]))\n )\n // Join values with record match\n q = q.leftJoin(`custom_field_values as ${valAlias}` as any, (jb: any) =>\n jb.on(`${valAlias}.entity_id`, '=', String(entityIdForKey))\n .on(`${valAlias}.field_key`, '=', key)\n .onRef(`${valAlias}.record_id`, '=', recordIdExpr as any)\n .on((eb: any) => eb.or([\n eb(`${valAlias}.tenant_id`, '=', tenantId),\n eb(`${valAlias}.tenant_id`, 'is', null),\n ]))\n )\n // Force a common SQL type across branches to avoid Postgres CASE type conflicts\n const caseExpr = sql<string | null>`CASE ${sql.ref(`${defAlias}.kind`)}\n WHEN 'integer' THEN (${sql.ref(`${valAlias}.value_int`)})::text\n WHEN 'float' THEN (${sql.ref(`${valAlias}.value_float`)})::text\n WHEN 'boolean' THEN (${sql.ref(`${valAlias}.value_bool`)})::text\n WHEN 'multiline' THEN (${sql.ref(`${valAlias}.value_multiline`)})::text\n ELSE (${sql.ref(`${valAlias}.value_text`)})::text\n END`\n cfValueExprByKey[key] = caseExpr\n const alias = sanitize(`cf:${key}`)\n // Project as aggregated to avoid duplicates when multi values exist\n if (!isSortKeysProjection && ((opts.fields || []).includes(`cf:${key}`) || opts.includeCustomFields === true || (requestedCustomFieldKeys.length > 0 && requestedCustomFieldKeys.includes(key)))) {\n const multiAlias = `${alias}__is_multi`\n const isMultiExpr = sql<boolean>`bool_or(coalesce((${sql.ref(`${defAlias}.config_json`)}->>'multi')::boolean, false))`\n const aggregatedArray = sql<unknown>`array_remove(array_agg(DISTINCT ${caseExpr}), NULL)`\n const projExpr = sql<unknown>`CASE WHEN ${isMultiExpr}\n THEN to_jsonb(${aggregatedArray})\n ELSE to_jsonb(max(${caseExpr}))\n END`\n q = q.select(projExpr.as(alias))\n q = q.select(isMultiExpr.as(multiAlias))\n cfSelectedAliases.push(alias)\n cfJsonAliases.add(alias)\n cfMultiAliasByAlias.set(alias, multiAlias)\n }\n }\n\n // Apply cf:* filters (on raw expressions). OR-grouped ones are excluded here and\n // combined with their disjunct's other leaves right below.\n for (const f of regularCfFilters) {\n if (!f.field.startsWith('cf:')) continue\n const key = f.field.slice(3)\n const expr = cfValueExprByKey[key]\n if (!expr) continue\n if ((f.op === 'like' || f.op === 'ilike') && searchActive && typeof f.value === 'string') {\n const tokens = tokenizeText(String(f.value), searchConfig)\n const hashes = tokens.hashes\n if (hashes.length) {\n const result = this.applySearchTokens(q, {\n entity: String(entity),\n field: f.field,\n hashes,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n tokens: tokens.tokens,\n })\n this.logSearchDebug('search:cf-filter', {\n entity: String(entity),\n field: f.field,\n tokens: tokens.tokens,\n hashes,\n applied: result.applied,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n })\n if (result.applied) {\n q = result.builder\n continue\n }\n } else {\n this.logSearchDebug('search:cf-skip-empty-hashes', {\n entity: String(entity),\n field: f.field,\n value: f.value,\n })\n }\n }\n q = this.applyColumnOp(q, expr, f.op, f.value)\n }\n\n // OR groups are applied here, after the cf:* value expressions exist, so a\n // disjunct mixing base/doc and custom-field leaves is united rather than\n // intersected. A cf leaf whose key resolved no value expression yields no\n // predicate and is dropped; a disjunct left empty by that is dropped too,\n // because an empty AND would read as TRUE and widen the result.\n //\n // Known limitation, shared with the `doc` clause kind above: a leaf inside an OR\n // group compares against the stored value directly and does not route `like` /\n // `ilike` through the search-token index the way the ungrouped path does. On a\n // field covered by an encryption map such a leaf therefore compares against\n // ciphertext and will not match.\n const applicableGroupFilters = resolvedGroupFilters\n .map((group) => group.filter((rf) => rf.kind !== 'cf' || Boolean(cfValueExprByKey[rf.key])))\n .filter((group) => group.length > 0)\n if (applicableGroupFilters.length > 0) {\n q = q.where((eb: any) => {\n const disjuncts = applicableGroupFilters.map((group) => {\n const parts = group.map((rf) => {\n if (rf.kind === 'column') return this.buildColumnOpExpression(eb, rf.qualified, rf.op, rf.value)\n if (rf.kind === 'cf') return this.buildColumnOpExpression(eb, cfValueExprByKey[rf.key], rf.op, rf.value)\n return this.buildIndexDocOpExpression(eb, {\n entity: String(entity),\n field: rf.field,\n op: rf.op,\n value: rf.value,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n withDeleted: opts.withDeleted === true,\n })\n })\n return parts.length === 1 ? parts[0] : eb.and(parts)\n })\n return disjuncts.length === 1 ? disjuncts[0] : eb.or(disjuncts)\n })\n }\n\n // Entity extensions joins (no selection yet; enables future filters/projections)\n if (opts.includeExtensions) {\n const { getModules } = await import('@open-mercato/shared/lib/i18n/server')\n const allMods = getModules() as any[]\n const allExts = allMods.flatMap((m) => (m as any).entityExtensions || [])\n const exts = allExts.filter((e: any) => e.base === entity)\n const chosen = Array.isArray(opts.includeExtensions)\n ? exts.filter((e: any) => (opts.includeExtensions as string[]).includes(e.extension))\n : exts\n for (const e of chosen) {\n const [, extName] = (e.extension as string).split(':')\n // Uses the SAME derivation as every other table-name fallback in this file\n // (`pluralizeBaseName`, also called at the resolveEntityTableName sites above)\n // rather than a separate inline one. The inline version handled only `+s`, so\n // `example_customer_priority` derived `example_customer_prioritys` against the\n // real `example_customer_priorities`. Behaviour is unchanged for every name that\n // does not end in `y`; `table` below remains the escape hatch for irregular\n // plurals no guesser can win (`person` \u2192 `people`).\n const derivedTable = pluralizeBaseName(extName)\n const extTable = isPlainTableIdentifier(e.table) ? e.table : derivedTable\n const alias = `ext_${sanitize(extName)}`\n q = q.leftJoin(`${extTable} as ${alias}` as any, (jb: any) =>\n jb.onRef(`${alias}.${e.join.extensionKey}`, '=', `${table}.${e.join.baseKey}`)\n )\n }\n }\n\n // Sorting: base fields and cf:* (use aggregated alias for cf)\n for (const s of resolvedSorts) {\n if (s.field.startsWith('cf:')) {\n const key = s.field.slice(3)\n const alias = sanitize(`cf:${key}`)\n // Ensure included in projection to sort by\n if (!cfSelectedAliases.includes(alias)) {\n const expr = cfValueExprByKey[key]\n if (expr) {\n q = q.select(sql<string | null>`max(${expr})`.as(alias))\n cfSelectedAliases.push(alias)\n }\n }\n if (!requiresPlaintextSort) q = q.orderBy(alias, (s.dir ?? 'asc') as any)\n } else {\n if (!requiresPlaintextSort) q = q.orderBy(qualify(s.field), (s.dir ?? 'asc') as any)\n }\n }\n\n // Deduplicate if we joined CFs or extensions by grouping on base id\n const hasJoinedAggregates = (opts.includeExtensions && (Array.isArray(opts.includeExtensions) ? (opts.includeExtensions.length > 0) : true)) || Object.keys(cfValueExprByKey).length > 0\n if (hasJoinedAggregates) {\n q = q.groupBy(`${table}.id`)\n }\n\n return { builder: q, hasJoinedAggregates, cfJsonAliases, cfMultiAliasByAlias, resolvedCustomFieldDefinitions }\n }\n\n // Pagination\n const page = opts.page?.page ?? 1\n const pageSize = opts.page?.pageSize ?? 20\n\n const {\n builder: qFull,\n hasJoinedAggregates,\n cfJsonAliases,\n cfMultiAliasByAlias,\n resolvedCustomFieldDefinitions,\n } = await buildQuery('full')\n\n // `count(distinct base.id)` is only required when a join can multiply base rows\n // (CF/extension aggregates, explicit relation joins, or custom-field sources).\n // Without such joins base.id is the unique PK, so `count(*)` is equivalent and\n // lets Postgres skip the redundant DISTINCT sort/hash for an index-only count (#2227).\n const mayMultiplyBaseRows =\n hasJoinedAggregates ||\n (Array.isArray(opts.joins) && opts.joins.length > 0) ||\n (Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0)\n const countExpr = mayMultiplyBaseRows\n ? sql<string>`count(distinct ${sql.ref(`${table}.id`)})`\n : sql<string>`count(*)`\n const countBuilder = hasJoinedAggregates\n ? qFull.clearSelect().clearOrderBy().clearGroupBy().select(countExpr.as('count'))\n : qFull.clearSelect().clearOrderBy().select(countExpr.as('count'))\n const countRow = await countBuilder.executeTakeFirst() as { count: unknown } | undefined\n const total = Number((countRow as any)?.count ?? 0)\n\n const svc = encryptionService\n const decryptPayload =\n svc?.decryptEntityPayload?.bind(svc) as\n | ((\n entityId: EntityId,\n payload: Record<string, unknown>,\n tenantId: string | null,\n organizationId: string | null,\n ) => Promise<Record<string, unknown>>)\n | null\n\n const decryptRow = async (item: ResultRow): Promise<ResultRow> => {\n if (!decryptPayload) return item\n try {\n const decrypted = await decryptPayload(\n entity,\n item,\n (item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null) as string | null,\n (item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null) as string | null,\n )\n return { ...item, ...decrypted }\n } catch (err) {\n logger.error('Error decrypting entity payload', { err })\n return item\n }\n }\n\n const normalizeCfJsonAliases = (rows: ResultRow[]) => {\n if (cfJsonAliases.size === 0) return\n for (const row of rows) {\n for (const alias of cfJsonAliases) {\n const multiAlias = cfMultiAliasByAlias.get(alias)\n const isMulti = multiAlias ? Boolean(row[multiAlias]) : false\n let raw = row[alias]\n if (typeof raw === 'string') {\n try { raw = JSON.parse(raw) } catch { /* ignore malformed json */ }\n }\n if (isMulti) {\n if (raw == null) row[alias] = []\n else if (Array.isArray(raw)) row[alias] = raw\n else row[alias] = [raw]\n } else {\n if (Array.isArray(raw)) row[alias] = raw.length > 0 ? raw[0] : null\n else row[alias] = raw\n }\n if (multiAlias) delete row[multiAlias]\n }\n }\n }\n\n let pagedItems: ResultRow[]\n let encryptedSortRowCapWarning: EncryptedSortRowCapWarning | undefined\n\n if (requiresPlaintextSort) {\n // Phase 1: slim id+sort-columns candidate scan, bounded-concurrency decrypt,\n // in-memory sort over the full candidate set, then slice to the page's ids.\n const cap = resolveEncryptedSortMaxRows()\n let qSort = (await buildQuery('sortKeys')).builder\n if (cap !== null) {\n qSort = qSort.limit(cap).orderBy(qualify('id'), 'asc' as any)\n }\n const candidateRows = await qSort.execute() as ResultRow[]\n const decryptedCandidates = decryptPayload\n ? await mapWithConcurrency(candidateRows, DECRYPT_CONCURRENCY, decryptRow)\n : candidateRows\n const orderedCandidates = sortRowsInMemory(decryptedCandidates, resolvedSorts)\n const pageIds = orderedCandidates\n .slice((page - 1) * pageSize, page * pageSize)\n .map((row) => row.id)\n\n if (cap !== null && total > cap) {\n encryptedSortRowCapWarning = {\n entity,\n sortFields: resolvedSorts.map((s) => s.field),\n maxRows: cap,\n totalMatched: total,\n }\n }\n\n // Phase 2: fetch + decrypt full rows for just the page's ids, then reassemble\n // in phase-1's order (SQL `WHERE id IN (...)` order is unspecified \u2014 never\n // trust it for ordering).\n if (pageIds.length === 0) {\n pagedItems = []\n } else {\n const pageRows = await qFull.where(qualify('id'), 'in', pageIds).execute() as ResultRow[]\n normalizeCfJsonAliases(pageRows)\n const decryptedPageRows = decryptPayload\n ? await mapWithConcurrency(pageRows, DECRYPT_CONCURRENCY, decryptRow)\n : pageRows\n const byId = new Map(decryptedPageRows.map((row) => [String(row.id), row]))\n pagedItems = pageIds\n .map((id) => byId.get(String(id)))\n .filter((row): row is ResultRow => row != null)\n }\n } else {\n const dataQuery = qFull.limit(pageSize).offset((page - 1) * pageSize)\n const items = await dataQuery.execute() as ResultRow[]\n normalizeCfJsonAliases(items)\n pagedItems = decryptPayload\n ? await mapWithConcurrency(items, DECRYPT_CONCURRENCY, decryptRow)\n : items\n }\n\n let queryResult: QueryResult<T> = { items: pagedItems as unknown as T[], page, pageSize, total }\n\n if (encryptedSortRowCapWarning) {\n const meta: QueryResultMeta = { encryptedSortRowCapWarning }\n queryResult.meta = meta\n }\n\n // --- UMES query extension: after-query pipeline ---\n if (ext && extensionCtx) {\n const diCtx = ext.resolve ? { resolve: ext.resolve } : noop\n queryResult = await runAfterQueryPipeline(\n queryResult as QueryResult<Record<string, unknown>>,\n opts,\n extensionCtx,\n diCtx,\n ) as QueryResult<T>\n }\n\n // Attach after the extension pipeline so the field always survives even if a\n // subscriber replaces the whole result object.\n if (resolvedCustomFieldDefinitions) {\n queryResult.customFieldDefinitions = resolvedCustomFieldDefinitions\n }\n\n return queryResult\n }\n\n private applyColumnOp(builder: AnyBuilder, column: string | RawBuilder<unknown>, op: string, value: unknown): AnyBuilder {\n switch (op) {\n case 'eq':\n return value === null\n ? builder.where(column as any, 'is', null)\n : builder.where(column as any, '=', value as any)\n case 'ne':\n return value === null\n ? builder.where(column as any, 'is not', null)\n : builder.where(column as any, '!=', value as any)\n case 'gt':\n return builder.where(column as any, '>', value as any)\n case 'gte':\n return builder.where(column as any, '>=', value as any)\n case 'lt':\n return builder.where(column as any, '<', value as any)\n case 'lte':\n return builder.where(column as any, '<=', value as any)\n case 'in':\n return builder.where(column as any, 'in', Array.isArray(value) ? value : [value])\n case 'nin':\n return builder.where(column as any, 'not in', Array.isArray(value) ? value : [value])\n case 'like':\n return builder.where(column as any, 'like', value as any)\n case 'ilike':\n return builder.where(column as any, 'ilike', value as any)\n case 'exists':\n return value\n ? builder.where(column as any, 'is not', null)\n : builder.where(column as any, 'is', null)\n default:\n return builder\n }\n }\n\n private buildColumnOpExpression(eb: any, column: string | RawBuilder<unknown>, op: string, value: unknown): any {\n switch (op) {\n case 'eq': return value === null ? eb(column, 'is', null) : eb(column, '=', value)\n case 'ne': return value === null ? eb(column, 'is not', null) : eb(column, '!=', value)\n case 'gt': return eb(column, '>', value)\n case 'gte': return eb(column, '>=', value)\n case 'lt': return eb(column, '<', value)\n case 'lte': return eb(column, '<=', value)\n case 'in': return eb(column, 'in', Array.isArray(value) ? value : [value])\n case 'nin': return eb(column, 'not in', Array.isArray(value) ? value : [value])\n case 'like': return eb(column, 'like', value)\n case 'ilike': return eb(column, 'ilike', value)\n case 'exists': return value ? eb(column, 'is not', null) : eb(column, 'is', null)\n default: return eb.val(true)\n }\n }\n\n private async resolveBaseColumn(table: string, field: string): Promise<string | null> {\n if (await this.columnExists(table, field)) return field\n if (field === 'organization_id' && await this.columnExists(table, 'id')) return 'id'\n return null\n }\n\n private async columnExists(table: string, column: string): Promise<boolean> {\n const key = `${table}.${column}`\n if (this.columnCache.has(key)) {\n const cached = this.columnCache.get(key)\n if (cached === true) return true\n this.columnCache.delete(key)\n }\n const db = this.getDb()\n const exists = await db\n .selectFrom('information_schema.columns' as any)\n .select(sql<number>`1`.as('one'))\n .where('table_name' as any, '=', table)\n .where('column_name' as any, '=', column)\n .limit(1)\n .executeTakeFirst()\n const present = !!exists\n if (present) this.columnCache.set(key, true)\n else this.columnCache.delete(key)\n return present\n }\n\n private applySearchTokens(\n q: AnyBuilder,\n opts: {\n entity: string\n field: string\n hashes: string[]\n recordIdColumn: string\n tenantId?: string | null\n organizationScope?: { ids: string[]; includeNull: boolean } | null\n combineWith?: 'and' | 'or'\n tokens?: string[]\n }\n ): { applied: boolean; builder: AnyBuilder } {\n if (!opts.hashes.length) {\n this.logSearchDebug('search:skip-no-hashes', {\n entity: opts.entity,\n field: opts.field,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n })\n return { applied: false, builder: q }\n }\n const alias = `st_${this.searchAliasSeq++}`\n const engine = this\n this.logSearchDebug('search:apply-search-tokens', {\n entity: opts.entity,\n field: opts.field,\n alias,\n tokenCount: opts.hashes.length,\n tokens: opts.tokens,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n combineWith: opts.combineWith ?? 'and',\n })\n const buildSub = (eb: any) => {\n let sub: AnyBuilder = eb\n .selectFrom(`search_tokens as ${alias}`)\n .select(sql<number>`1`.as('one'))\n .where(`${alias}.entity_type`, '=', opts.entity)\n .where(`${alias}.field`, '=', opts.field)\n .where(sql<boolean>`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`)\n .where(`${alias}.token_hash`, 'in', opts.hashes)\n .groupBy([`${alias}.entity_id`, `${alias}.field`])\n .having(sql<boolean>`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`)\n if (opts.tenantId !== undefined) {\n sub = sub.where(sql<boolean>`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`)\n }\n if (opts.organizationScope) {\n sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope)\n }\n return sub\n }\n const combiner = opts.combineWith === 'or' ? 'or' : 'and'\n if (combiner === 'or') {\n // When OR combining, caller expects a raw predicate to include in eb.or([...]).\n // We keep the same semantics as the previous knex orWhereExists by mutating the outer builder with a WHERE EXISTS.\n // Return the mutated builder; callers that need per-predicate control should build the sub themselves.\n const next = q.where((eb: any) => eb.or([eb.exists(buildSub(eb))]))\n return { applied: true, builder: next }\n }\n const next = q.where((eb: any) => eb.exists(buildSub(eb)))\n return { applied: true, builder: next }\n }\n\n private applyIndexDocFilter(\n q: AnyBuilder,\n opts: {\n entity: string\n field: string\n op: NormalizedFilter['op']\n value: unknown\n recordIdColumn: string\n tenantId?: string | null\n organizationScope?: { ids: string[]; includeNull: boolean } | null\n withDeleted: boolean\n searchActive: boolean\n searchConfig: ReturnType<typeof resolveSearchConfig>\n }\n ): AnyBuilder {\n if ((opts.op === 'like' || opts.op === 'ilike') && opts.searchActive && typeof opts.value === 'string') {\n const tokens = tokenizeText(String(opts.value), opts.searchConfig)\n const hashes = tokens.hashes\n if (hashes.length) {\n const result = this.applySearchTokens(q, {\n entity: opts.entity,\n field: opts.field,\n hashes,\n recordIdColumn: opts.recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n tokens: tokens.tokens,\n })\n this.logSearchDebug('search:index-doc-filter', {\n entity: opts.entity,\n field: opts.field,\n tokens: tokens.tokens,\n hashes,\n applied: result.applied,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n })\n if (result.applied) return result.builder\n } else {\n this.logSearchDebug('search:index-doc-skip-empty-hashes', {\n entity: opts.entity,\n field: opts.field,\n value: opts.value,\n })\n }\n return q\n }\n\n return q.where((eb: any) => this.buildIndexDocOpExpression(eb, opts))\n }\n\n // Builds the entity_indexes EXISTS expression for a single doc-field operator,\n // shared by the regular doc-filter path (applyIndexDocFilter) and the OR-group\n // path so `$or` queries over custom-entity doc fields resolve instead of being\n // silently dropped (#3229).\n private buildIndexDocOpExpression(\n eb: any,\n opts: {\n entity: string\n field: string\n op: NormalizedFilter['op']\n value: unknown\n recordIdColumn: string\n tenantId?: string | null\n organizationScope?: { ids: string[]; includeNull: boolean } | null\n withDeleted: boolean\n }\n ): any {\n const alias = `ei_${this.searchAliasSeq++}`\n const engine = this\n return eb.exists((() => {\n let sub: AnyBuilder = eb\n .selectFrom(`entity_indexes as ${alias}`)\n .select(sql<number>`1`.as('one'))\n .where(`${alias}.entity_type`, '=', opts.entity)\n .where(sql<boolean>`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`)\n if (opts.tenantId !== undefined) {\n sub = sub.where(sql<boolean>`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`)\n }\n if (opts.organizationScope) {\n sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope)\n }\n if (!opts.withDeleted) {\n sub = sub.where(`${alias}.deleted_at`, 'is', null)\n }\n\n const textExpr = sql<string | null>`(${sql.ref(`${alias}.doc`)} ->> ${opts.field})`\n switch (opts.op) {\n case 'eq':\n sub = opts.value === null\n ? sub.where(sql<boolean>`${textExpr} is null`)\n : sub.where(sql<boolean>`${textExpr} = ${opts.value}`)\n break\n case 'ne':\n sub = opts.value === null\n ? sub.where(sql<boolean>`${textExpr} is not null`)\n : sub.where(sql<boolean>`${textExpr} <> ${opts.value}`)\n break\n case 'gt':\n case 'gte':\n case 'lt':\n case 'lte': {\n const operator = sql.raw(opts.op === 'gt' ? '>' : opts.op === 'gte' ? '>=' : opts.op === 'lt' ? '<' : '<=')\n sub = sub.where(sql<boolean>`${textExpr} ${operator} ${opts.value}`)\n break\n }\n case 'in': {\n const vals = Array.isArray(opts.value) ? opts.value : [opts.value]\n sub = sub.where(sql<boolean>`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`)\n break\n }\n case 'nin': {\n const vals = Array.isArray(opts.value) ? opts.value : [opts.value]\n sub = sub.where(sql<boolean>`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`)\n break\n }\n case 'like':\n sub = sub.where(sql<boolean>`${textExpr} like ${opts.value}`); break\n case 'ilike':\n sub = sub.where(sql<boolean>`${textExpr} ilike ${opts.value}`); break\n case 'exists':\n sub = opts.value\n ? sub.where(sql<boolean>`${textExpr} is not null`)\n : sub.where(sql<boolean>`${textExpr} is null`)\n break\n default:\n break\n }\n return sub\n })())\n }\n\n private configureCustomFieldSources(\n q: AnyBuilder,\n baseTable: string,\n baseEntity: EntityId,\n db: AnyDb,\n opts: QueryOptions,\n qualify: (column: string) => string,\n ): { builder: AnyBuilder; sources: ResolvedCustomFieldSource[] } {\n const sources: ResolvedCustomFieldSource[] = [\n {\n entityId: baseEntity,\n alias: 'base',\n table: baseTable,\n recordIdExpr: sql<string>`${sql.ref(`${baseTable}.id`)}::text`,\n },\n ]\n const extras: QueryCustomFieldSource[] = opts.customFieldSources ?? []\n let next = q\n extras.forEach((srcOpt, index) => {\n const joinTable = srcOpt.table ?? resolveEntityTableName(this.em, srcOpt.entityId)\n const alias = srcOpt.alias ?? `cfs_${index}`\n const join = srcOpt.join\n if (!join) {\n throw new Error(`QueryEngine: customFieldSources entry for ${String(srcOpt.entityId)} requires a join configuration`)\n }\n const joinFn = (join.type ?? 'left') === 'inner' ? 'innerJoin' : 'leftJoin'\n next = (next as any)[joinFn](`${joinTable} as ${alias}`, (jb: any) =>\n jb.onRef(`${alias}.${join.toField}`, '=', qualify(join.fromField)))\n const recordColumn = srcOpt.recordIdColumn ?? 'id'\n sources.push({\n entityId: srcOpt.entityId,\n alias,\n table: joinTable,\n recordIdExpr: sql<string>`${sql.ref(`${alias}.${recordColumn}`)}::text`,\n })\n })\n return { builder: next, sources }\n }\n\n private logSearchDebug(event: string, payload: Record<string, unknown>) {\n try {\n logger.debug(event, payload)\n } catch {}\n }\n\n private resolveOrganizationScope(opts: QueryOptions): { ids: string[]; includeNull: boolean } | null {\n if (opts.organizationIds !== undefined) {\n const raw = (opts.organizationIds ?? []).map((id) => (typeof id === 'string' ? id.trim() : id))\n const includeNull = raw.some((id) => id == null || id === '')\n const ids = raw.filter((id): id is string => typeof id === 'string' && id.length > 0)\n return { ids: Array.from(new Set(ids)), includeNull }\n }\n if (typeof opts.organizationId === 'string' && opts.organizationId.trim().length > 0) {\n return { ids: [opts.organizationId], includeNull: false }\n }\n return null\n }\n\n private applyOrganizationScope(q: AnyBuilder, column: string, scope: { ids: string[]; includeNull: boolean }): AnyBuilder {\n if (!scope) return q\n if (scope.ids.length === 0 && !scope.includeNull) {\n return q.where(sql<boolean>`1 = 0`)\n }\n return q.where((eb: any) => {\n const parts: any[] = []\n if (scope.ids.length > 0) parts.push(eb(column, 'in', scope.ids))\n if (scope.includeNull) parts.push(eb(column, 'is', null))\n if (parts.length === 1) return parts[0]\n return eb.or(parts)\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { QueryEngine, QueryOptions, QueryResult, QueryResultMeta, EncryptedSortRowCapWarning, QueryCustomFieldSource, QueryExtensionsConfig, Sort } from './types'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql, type RawBuilder } from 'kysely'\nimport {\n applyJoinFilters,\n normalizeFilters,\n partitionFilters,\n resolveJoins,\n type BaseFilter,\n type NormalizedFilter,\n type ResolvedJoin,\n} from './join-utils'\nimport { resolveSearchConfig } from '../search/config'\nimport {\n createSearchTokenAvailability,\n isSearchFilterOp,\n type SearchTokenAvailability,\n type SearchTokenProbeDb,\n type SearchTokenProbeQueryBuilder,\n} from '../search/availability'\nimport { tokenizeText } from '../search/tokenize'\nimport { fieldNameCandidates } from './encrypted-sort'\nimport { isTenantDataEncryptionEnabled } from '../encryption/toggles'\nimport { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from './query-extension-runner'\nimport {\n buildCustomFieldDefinitionIndexFromRows,\n resolveCfDefIndexOrgCandidates,\n type CustomFieldDefinitionRow,\n type ResolvedCustomFieldDefinitions,\n} from '../crud/custom-field-definition-index'\nimport { warnOnCiphertextLikeFallback } from './ciphertext-search-warning'\nimport { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from './encrypted-sort'\nimport { mapWithConcurrency } from './bounded-decrypt'\nimport { createLogger } from '../logger'\n\nconst logger = createLogger('shared').child({ component: 'query' })\n\nconst DECRYPT_CONCURRENCY = 8\n\ntype AnyDb = Kysely<any>\ntype AnyBuilder = any\n\nconst entityTableCache = new Map<string, string>()\n\ntype EncryptionResolver = () => {\n decryptEntityPayload?: (entityId: EntityId, payload: Record<string, unknown>, tenantId?: string | null, organizationId?: string | null) => Promise<Record<string, unknown>>\n getEncryptedFieldNames?: (entityId: EntityId, tenantId?: string | null, organizationId?: string | null, options?: { ignoreRuntimeHealth?: boolean }) => Promise<readonly string[]>\n isEnabled?: () => boolean\n} | null\n\n/**\n * Membership across name shapes: encryption maps may declare a field as `displayName` or\n * `display_name` (TenantDataEncryptionService resolves both), while query filters carry real\n * column names. Both sides expand through `fieldNameCandidates` at set-build and lookup time.\n */\nexport function isEncryptedLikeField(encrypted: ReadonlySet<string>, field: string): boolean {\n return fieldNameCandidates(field).some((candidate) => encrypted.has(candidate))\n}\n\n// The all-orgs union behind `getEncryptedFieldNames(..., organizationId: null)` is an UNCACHED\n// `encryption_maps` read. Encryption maps change on deploys, not per request, so a short TTL\n// removes the per-search round-trip without meaningfully delaying a map rollout.\nconst ENCRYPTED_LIKE_FIELDS_TTL_MS = 60_000\nconst ENCRYPTED_LIKE_FIELDS_CACHE_CAP = 500\nconst encryptedLikeFieldsCache = new Map<string, { at: number; fields: Set<string> }>()\n\nexport async function resolveEncryptedLikeFieldSet(\n read: () => Promise<readonly string[]>,\n entity: string,\n tenantId: string | null,\n): Promise<Set<string>> {\n const key = `${entity}|${tenantId ?? ''}`\n const hit = encryptedLikeFieldsCache.get(key)\n if (hit && Date.now() - hit.at < ENCRYPTED_LIKE_FIELDS_TTL_MS) return hit.fields\n const names = await read()\n const fields = new Set<string>()\n for (const name of names ?? []) {\n for (const candidate of fieldNameCandidates(String(name))) fields.add(candidate)\n }\n if (encryptedLikeFieldsCache.size >= ENCRYPTED_LIKE_FIELDS_CACHE_CAP) encryptedLikeFieldsCache.clear()\n encryptedLikeFieldsCache.set(key, { at: Date.now(), fields })\n return fields\n}\n\n/** Test-only: the TTL memo would otherwise leak state across specs. */\nexport function clearEncryptedLikeFieldsCache(): void {\n encryptedLikeFieldsCache.clear()\n}\n\ntype ResolvedCustomFieldSource = {\n entityId: EntityId\n alias: string\n table: string\n recordIdExpr: RawBuilder<string>\n}\n\ntype ResultRow = Record<string, unknown>\n\n/**\n * Canonical `module:entity` entity-id shape (both segments snake_case,\n * starting with a lowercase letter). Used to validate caller-supplied entity\n * ids at security-sensitive boundaries before they reach table resolution.\n */\nexport const ENTITY_ID_PATTERN = /^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$/\n\nexport const isValidEntityIdShape = (value: string): boolean => ENTITY_ID_PATTERN.test(value)\n\nconst pluralizeBaseName = (name: string): string => {\n if (!name) return name\n if (name.endsWith('s')) return name\n if (name.endsWith('y')) return `${name.slice(0, -1)}ies`\n return `${name}s`\n}\n\n/**\n * Accepts a module-declared `EntityExtension.table` only when it is a bare SQL\n * identifier. The value is interpolated into a join clause, so anything else is\n * ignored in favour of the derived table name.\n */\nconst PLAIN_TABLE_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/\n\nconst isPlainTableIdentifier = (value: unknown): value is string =>\n typeof value === 'string' && PLAIN_TABLE_IDENTIFIER_PATTERN.test(value)\n\nconst toPascalCase = (value: string): string => {\n return value\n .split(/[_\\s]+/)\n .filter(Boolean)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('')\n}\n\nconst candidateClassNames = (rawName: string): string[] => {\n const base = toPascalCase(rawName)\n const candidates = new Set<string>()\n if (base) candidates.add(base)\n if (base && !base.endsWith('Entity')) candidates.add(`${base}Entity`)\n return Array.from(candidates)\n}\n\n/**\n * Resolve an entity id to a table name strictly via registered MikroORM metadata.\n *\n * Unlike {@link resolveEntityTableName}, this never falls back to a pluralized\n * guess: it returns `null` when no registered entity matches. Use it for\n * security-sensitive call sites (e.g. the reindexer) that must refuse to read\n * arbitrary, attacker-chosen tables that happen to exist in the schema.\n */\nexport function resolveRegisteredEntityTableName(\n em: EntityManager | undefined,\n entity: EntityId,\n): string | null {\n const parts = String(entity || '').split(':')\n const rawName = (parts[1] && parts[1].trim().length > 0) ? parts[1] : (parts[0] || '').trim()\n const metadata = (em as any)?.getMetadata?.()\n\n if (!metadata || !rawName) return null\n\n const candidates = candidateClassNames(rawName)\n for (const candidate of candidates) {\n try {\n const meta = metadata.find?.(candidate)\n if (meta?.tableName) {\n return String(meta.tableName)\n }\n } catch {}\n }\n\n // Secondary lookup: search ORM metadata by candidate table names\n const modulePrefix = parts[0] ?? ''\n const candidateTables = [\n `${modulePrefix}_${rawName}`,\n pluralizeBaseName(rawName),\n `${modulePrefix}_${pluralizeBaseName(rawName)}`,\n ]\n try {\n const allMeta: any[] = metadata.getAll?.() ?? []\n for (const meta of allMeta) {\n if (meta?.tableName && candidateTables.includes(String(meta.tableName))) {\n return String(meta.tableName)\n }\n }\n } catch {}\n\n return null\n}\n\nexport function resolveEntityTableName(em: EntityManager | undefined, entity: EntityId): string {\n if (entityTableCache.has(entity)) {\n return entityTableCache.get(entity)!\n }\n const parts = String(entity || '').split(':')\n const rawName = (parts[1] && parts[1].trim().length > 0) ? parts[1] : (parts[0] || '').trim()\n\n const registered = resolveRegisteredEntityTableName(em, entity)\n if (registered) {\n entityTableCache.set(entity, registered)\n return registered\n }\n\n const fallback = pluralizeBaseName(rawName || '')\n logger.warn('Could not resolve entity via ORM metadata; falling back to table name \u2014 ensure the entity ID segment matches the class name convention', { entity, fallback })\n entityTableCache.set(entity, fallback)\n return fallback\n}\n\nfunction buildFilterableCustomFieldJoins(\n sources: QueryCustomFieldSource[] | undefined,\n): Array<{\n alias: string\n table?: string\n entityId: EntityId\n from: { field: string }\n to: { field: string }\n type: 'left' | 'inner'\n}> {\n if (!sources || sources.length === 0) return []\n return sources.flatMap((source, index) => {\n if (!source.join) return []\n const alias = typeof source.alias === 'string' && source.alias.trim().length > 0\n ? source.alias.trim()\n : `cfs_${index}`\n return [{\n alias,\n table: source.table,\n entityId: source.entityId,\n from: { field: source.join.fromField },\n to: { field: source.join.toField },\n type: source.join.type === 'inner' ? 'inner' : 'left',\n }]\n })\n}\n\nfunction computeCustomFieldScore(cfg: Record<string, unknown>, kind: string, entityIndex: number) {\n const listVisibleScore = cfg.listVisible === false ? 0 : 1\n const formEditableScore = cfg.formEditable === false ? 0 : 1\n const filterableScore = cfg.filterable ? 1 : 0\n const kindScore = (() => {\n switch (kind) {\n case 'dictionary': return 8\n case 'relation': return 6\n case 'select': return 4\n case 'multiline': return 3\n case 'boolean':\n case 'integer':\n case 'float': return 2\n default: return 1\n }\n })()\n const optionsBonus = Array.isArray(cfg.options) && cfg.options.length ? 2 : 0\n const dictionaryBonus = typeof cfg.dictionaryId === 'string' && (cfg.dictionaryId as string).trim().length ? 5 : 0\n const base = (listVisibleScore * 16) + (formEditableScore * 8) + (filterableScore * 4) + kindScore + optionsBonus + dictionaryBonus\n const penalty = typeof cfg.priority === 'number' ? cfg.priority : 0\n return { base, penalty, entityIndex }\n}\n\n/**\n * BasicQueryEngine \u2014 Kysely-backed fallback query engine.\n *\n * Resolves base tables via MikroORM metadata, applies tenant/organization/\n * deleted_at scoping, handles custom field (cf:*) selection and filtering,\n * and performs entity-extension joins. Used as the fallback for\n * {@link HybridQueryEngine} when the query index is unavailable or incomplete.\n */\nexport class BasicQueryEngine implements QueryEngine {\n private columnCache = new Map<string, boolean>()\n private searchAliasSeq = 0\n private searchAvailabilityInstance: SearchTokenAvailability | null = null\n\n constructor(\n private em: EntityManager,\n private getDbFn?: () => AnyDb,\n private resolveEncryptionService?: EncryptionResolver,\n ) {}\n\n private getEncryptionService() {\n try {\n return this.resolveEncryptionService?.() ?? null\n } catch {\n return null\n }\n }\n\n private getDb(): AnyDb {\n if (this.getDbFn) return this.getDbFn()\n const emAny = this.em as any\n if (typeof emAny?.getKysely === 'function') return emAny.getKysely() as AnyDb\n throw new Error('BasicQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)')\n }\n\n private searchAvailability(): SearchTokenAvailability {\n if (!this.searchAvailabilityInstance) {\n this.searchAvailabilityInstance = createSearchTokenAvailability({\n getDb: () => this.getDb() as unknown as SearchTokenProbeDb,\n getConfig: resolveSearchConfig,\n applyOrganizationScope: (query, column, scope) => this.applyOrganizationScope(\n query as unknown as AnyBuilder,\n column,\n scope,\n ) as unknown as SearchTokenProbeQueryBuilder,\n logDebug: (event, payload) => this.logSearchDebug(event, payload),\n })\n }\n return this.searchAvailabilityInstance\n }\n\n async query<T = any>(entity: EntityId, opts: QueryOptions = {}): Promise<QueryResult<T>> {\n // --- UMES query extension: before-query pipeline ---\n const ext = opts.extensions\n let effectiveOpts = opts\n let extensionCtx: QueryExtensionContext | null = null\n const noop = { resolve: <R = unknown>(_name: string): R => { throw new Error('No DI context') } }\n\n if (ext) {\n extensionCtx = {\n entity: String(entity),\n engine: 'basic',\n tenantId: opts.tenantId ?? '',\n organizationId: opts.organizationId,\n userId: ext.userId,\n em: this.em,\n container: ext.container,\n userFeatures: ext.userFeatures,\n }\n const diCtx = ext.resolve ? { resolve: ext.resolve } : noop\n const beforeResult = await runBeforeQueryPipeline(opts, extensionCtx, diCtx)\n if (beforeResult.blocked) {\n throw new Error(beforeResult.errorMessage ?? 'Query blocked by extension subscriber')\n }\n effectiveOpts = beforeResult.query\n }\n // Strip extensions from effectiveOpts so they don't propagate to sub-queries\n const { extensions: _ext, ...coreOpts } = effectiveOpts\n opts = coreOpts\n\n // Heuristic: map '<module>:user' -> table 'users'\n const table = resolveEntityTableName(this.em, entity)\n const db = this.getDb()\n\n const qualify = (col: string) => `${table}.${col}`\n const orgScope = this.resolveOrganizationScope(opts)\n this.searchAliasSeq = 0\n // Require tenant scope for all queries\n if (!opts.tenantId) {\n throw new Error(\n 'QueryEngine: tenantId is now required for all queries (breaking change). ' +\n 'Please provide a tenantId in QueryOptions, e.g., query(entity, { tenantId: ... }). ' +\n 'See migration guide or documentation for details.'\n )\n }\n const skipAutoScope = opts.omitAutomaticTenantOrgScope === true\n\n const normalizedFilters = normalizeFilters(opts.filters)\n const resolvedJoins = resolveJoins(\n table,\n [...(opts.joins ?? []), ...buildFilterableCustomFieldJoins(opts.customFieldSources)],\n (entityId) => resolveEntityTableName(this.em, entityId as any),\n )\n const joinMap = new Map<string, ResolvedJoin>()\n const aliasTables = new Map<string, string>()\n aliasTables.set(table, table)\n aliasTables.set('base', table)\n for (const join of resolvedJoins) {\n joinMap.set(join.alias, join)\n aliasTables.set(join.alias, join.table)\n }\n const { baseFilters, joinFilters } = partitionFilters(table, normalizedFilters, joinMap)\n const cfFilters = normalizedFilters.filter((filter) => String(filter.field).startsWith('cf:'))\n // Custom-field leaves carrying an orGroup belong to an OR disjunct; applying them\n // one `.where()` at a time would AND them onto every disjunct (#5039).\n const regularCfFilters = cfFilters.filter((filter) => !filter.orGroup)\n const orGroupCfFilters = cfFilters.filter((filter) => filter.orGroup)\n const searchConfig = resolveSearchConfig()\n const searchFilters = [...baseFilters, ...cfFilters].filter((filter) => isSearchFilterOp(filter.op))\n // Callers that opt out of automatic tenant/org scoping own the full\n // visibility predicate. Search-token filtering has its own tenant/org\n // guards, so it must be disabled on this direct-query path as documented\n // by QueryOptions.omitAutomaticTenantOrgScope.\n const searchEnabled = !skipAutoScope && await this.searchAvailability().staticEnabled()\n // Probe `search_tokens` only when this query actually searches (#4723): every consumer of\n // `searchActive` sits behind a like/ilike guard, so on a plain list load the answer is never\n // read \u2014 and the probe is a `LIMIT 1` the planner can resolve as a seq scan over a large\n // `search_tokens`. The join path below already probes lazily for the same reason.\n const hasSearchTokens = searchEnabled && searchFilters.length\n ? await this.searchAvailability().hasTokens(String(entity), opts.tenantId ?? null, orgScope)\n : false\n const searchActive = searchEnabled && hasSearchTokens\n // Opt-in via OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS (default false: the pre-existing\n // rewrite-everything behavior is kept). When enabled, base-column like/ilike is rerouted\n // through search tokens ONLY for encrypted columns, where\n // ILIKE against ciphertext cannot match. On a plaintext column SQL ILIKE is exact, and the token\n // rewrite silently changes the result set: tokenization splits on non-alphanumerics and drops\n // tokens shorter than minTokenLength, so a document-number search like \"ZK 1/2026\" degrades to\n // the tokens {202, 2026} and matches every record from that year instead of the one document.\n // `ignoreRuntimeHealth` asks the on-disk question -- a column holds ciphertext even while the\n // KMS is down -- so an outage keeps encrypted columns on the token path (#4622).\n // `organizationId: null` is deliberate, not an omission: the service then unions in every\n // organization's map (`fetchAllOrganizationFieldNames`), so a field any org encrypts stays on\n // the token path -- a wider set fails safe. Passing the request's org instead would silently\n // break encrypted-column search for orgs without their own map. That union is an UNCACHED\n // `encryption_maps` read, one extra round-trip per searched list request. `null` means\n // the encryption service could not answer at all; keep the pre-existing rewrite-everything\n // behavior then, because guessing \"plaintext\" would turn encrypted-column search into an\n // ILIKE-on-ciphertext that matches nothing.\n let encryptedLikeFields: Set<string> | null = null\n if (\n searchActive &&\n searchConfig.useIlikeForNonEncryptedFields === true &&\n searchFilters.some((filter) => !String(filter.field).startsWith('cf:'))\n ) {\n try {\n const service = this.getEncryptionService()\n const readEncryptedFieldNames = service?.getEncryptedFieldNames?.bind(service)\n if (readEncryptedFieldNames) {\n encryptedLikeFields = await resolveEncryptedLikeFieldSet(\n () => readEncryptedFieldNames(\n String(entity),\n opts.tenantId ?? null,\n null,\n { ignoreRuntimeHealth: true },\n ),\n String(entity),\n opts.tenantId ?? null,\n )\n } else if (isTenantDataEncryptionEnabled()) {\n // Encryption is on but the service is unreachable (a swallowed DI failure looks\n // exactly like \"no service\"): treat the map as UNKNOWN and keep the token rewrite,\n // rather than guessing \"plaintext\" and running ILIKE against ciphertext.\n encryptedLikeFields = null\n } else {\n // Encryption disabled: nothing is ciphertext at rest, exact ILIKE is always right.\n encryptedLikeFields = new Set()\n }\n } catch (err) {\n // The fallback is safe (the old rewrite-everything behavior), but taking it silently\n // would hide that the gate has stopped working.\n logger.warn('search: encrypted-field map unavailable; keeping the token rewrite for all columns', {\n entity: String(entity),\n error: err instanceof Error ? err.message : String(err),\n })\n encryptedLikeFields = null\n }\n }\n if (searchFilters.length) {\n const fields = searchFilters.map((filter) => String(filter.field))\n this.logSearchDebug('search:init', {\n entity: String(entity),\n table,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n fields,\n searchEnabled,\n hasSearchTokens,\n searchActive,\n searchConfig: {\n enabled: searchConfig.enabled,\n minTokenLength: searchConfig.minTokenLength,\n enablePartials: searchConfig.enablePartials,\n hashAlgorithm: searchConfig.hashAlgorithm,\n blocklistedFields: searchConfig.blocklistedFields,\n },\n })\n if (!searchEnabled) {\n this.logSearchDebug('search:disabled', { entity: String(entity), table })\n } else if (!hasSearchTokens) {\n this.logSearchDebug('search:no-search-tokens', {\n entity: String(entity),\n table,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n })\n }\n const fallbackFields = searchFilters\n .filter((filter) => !searchActive || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)\n .map((filter) => String(filter.field))\n if (fallbackFields.length) {\n await warnOnCiphertextLikeFallback({\n entity: String(entity),\n fields: fallbackFields,\n tenantId: opts.tenantId ?? null,\n // `searchEnabled` also folds in the missing-table and\n // omitAutomaticTenantOrgScope cases, which are \"no usable tokens\"\n // rather than \"the operator switched search off\".\n reason: searchActive\n ? 'no-indexable-tokens'\n : searchConfig.enabled ? 'no-search-tokens' : 'search-disabled',\n service: this.getEncryptionService(),\n })\n }\n }\n for (const [alias, joinedFilters] of joinFilters) {\n const filters = joinedFilters.filter((entry) => entry.op === 'like' || entry.op === 'ilike')\n if (!filters.length) continue\n const join = joinMap.get(alias)\n if (!join?.entityId) continue\n const hasJoinedTokens = searchEnabled\n ? await this.searchAvailability().hasTokens(join.entityId, opts.tenantId ?? null, orgScope)\n : false\n const fallbackFields = filters\n .filter((filter) => !hasJoinedTokens || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)\n .map((filter) => filter.column)\n if (!fallbackFields.length) continue\n await warnOnCiphertextLikeFallback({\n entity: join.entityId,\n fields: fallbackFields,\n tenantId: opts.tenantId ?? null,\n reason: hasJoinedTokens\n ? 'no-indexable-tokens'\n : searchConfig.enabled ? 'no-search-tokens' : 'search-disabled',\n service: this.getEncryptionService(),\n })\n }\n const recordIdColumn = qualify('id')\n\n const applyFilterOp = (builder: AnyBuilder, column: string | RawBuilder<unknown>, op: string, value: unknown, fieldName?: string): AnyBuilder => {\n if (\n (op === 'like' || op === 'ilike') &&\n searchActive &&\n typeof value === 'string' &&\n fieldName &&\n typeof column === 'string' &&\n // Plaintext columns keep exact SQL ILIKE -- see the encryptedLikeFields note above. cf:*\n // filters never reach this path (they are applied by the custom-field branches), so this\n // gate only decides base columns. Membership is tested across name-shape candidates --\n // encryption maps may declare `displayName` while the filter carries the column name\n // `display_name`, and a raw comparison would misread that ciphertext column as plaintext.\n (encryptedLikeFields === null || isEncryptedLikeField(encryptedLikeFields, fieldName))\n ) {\n const tokens = tokenizeText(String(value), searchConfig)\n const hashes = tokens.hashes\n if (hashes.length) {\n const result = this.applySearchTokens(builder, {\n entity: String(entity),\n field: fieldName,\n hashes,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n tokens: tokens.tokens,\n })\n this.logSearchDebug('search:filter', {\n entity: String(entity),\n field: fieldName,\n tokens: tokens.tokens,\n hashes,\n applied: result.applied,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n })\n if (result.applied) return result.builder\n } else {\n this.logSearchDebug('search:skip-empty-hashes', {\n entity: String(entity),\n field: fieldName,\n value,\n })\n }\n }\n return this.applyColumnOp(builder, column, op, value)\n }\n\n // `eq` is accepted alongside `like`/`ilike` so that filters against\n // encrypted joined columns (whose ciphertext cannot be compared for\n // equality in SQL) can still resolve via tokenized search. Routing\n // only applies when `searchEnabled` is true AND the joined entity has\n // search tokens installed (`searchAvailable`); for non-searchable or\n // non-encrypted columns the caller still falls through to exact SQL\n // equality via `applyFilterOp`. Note that token match is approximate \u2014\n // callers needing strict equality on encrypted fields should filter on\n // the deterministic `*_hash` column instead.\n const applyJoinFilterOp = async (\n builder: AnyBuilder,\n filter: { column: string; op: string; value?: unknown },\n _qualified: string,\n join: ResolvedJoin,\n ): Promise<{ applied: boolean; builder: AnyBuilder }> => {\n if (!searchEnabled || !join.entityId) return { applied: false, builder }\n if (!['like', 'ilike'].includes(filter.op)) return { applied: false, builder }\n if (typeof filter.value !== 'string' || filter.value.trim().length === 0) return { applied: false, builder }\n\n const searchAvailable = await this.searchAvailability().hasTokens(join.entityId, opts.tenantId ?? null, orgScope)\n if (!searchAvailable) return { applied: false, builder }\n\n const tokens = tokenizeText(String(filter.value), searchConfig)\n if (!tokens.hashes.length) return { applied: false, builder }\n\n const result = this.applySearchTokens(builder, {\n entity: join.entityId,\n field: filter.column,\n hashes: tokens.hashes,\n recordIdColumn: `${join.alias}.id`,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n tokens: tokens.tokens,\n })\n return { applied: result.applied, builder: result.builder }\n }\n\n const regularBaseFilters = baseFilters.filter((f) => !f.orGroup)\n const orGroupFilters = baseFilters.filter((f) => f.orGroup)\n\n const applyAliasScopes = async (builder: AnyBuilder, aliasName: string): Promise<AnyBuilder> => {\n const targetTable = aliasTables.get(aliasName)\n if (!targetTable) return builder\n let next = builder\n if (!skipAutoScope && orgScope && await this.columnExists(targetTable, 'organization_id')) {\n next = this.applyOrganizationScope(next, `${aliasName}.organization_id`, orgScope)\n }\n if (!skipAutoScope && opts.tenantId && await this.columnExists(targetTable, 'tenant_id')) {\n next = next.where(`${aliasName}.tenant_id`, '=', opts.tenantId)\n }\n return next\n }\n\n const fallbackOrgId =\n opts.organizationId\n ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null)\n const encryptionService = this.getEncryptionService()\n const resolvedSorts: Sort[] = []\n for (const s of opts.sort || []) {\n if (s.field.startsWith('cf:')) {\n resolvedSorts.push(s)\n } else {\n const column = await this.resolveBaseColumn(table, s.field)\n if (column) resolvedSorts.push({ ...s, field: column })\n }\n }\n const encryptedSortFields = await resolveEncryptedSortFields(\n encryptionService,\n entity,\n resolvedSorts.filter((sort) => !sort.field.startsWith('cf:')).map((sort) => sort.field),\n opts.tenantId ?? null,\n fallbackOrgId,\n )\n const requiresPlaintextSort = encryptedSortFields.size > 0\n\n const tenantId = opts.tenantId\n const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_]/g, '_')\n\n type BuiltQuery = {\n builder: AnyBuilder\n hasJoinedAggregates: boolean\n cfJsonAliases: Set<string>\n cfMultiAliasByAlias: Map<string, string>\n resolvedCustomFieldDefinitions: ResolvedCustomFieldDefinitions | undefined\n }\n\n // Builds the fully-scoped query from a fresh root. `projection: 'full'` reproduces\n // today's complete selection (base fields + CF projections + extension joins).\n // `projection: 'sortKeys'` selects only `id` + the sort columns \u2014 the slim phase-1\n // candidate scan used when `requiresPlaintextSort`. Re-running the WHERE/JOIN logic\n // twice is cheap: every `columnExists` check is memoized on `this.columnCache`,\n // so the second pass hits no extra DB calls.\n const buildQuery = async (projection: 'full' | 'sortKeys'): Promise<BuiltQuery> => {\n const isSortKeysProjection = projection === 'sortKeys'\n let q: AnyBuilder = db.selectFrom(table as any)\n\n // Tenant/org/soft-delete scope\n if (!skipAutoScope && orgScope && await this.columnExists(table, 'organization_id')) {\n q = this.applyOrganizationScope(q, qualify('organization_id'), orgScope)\n }\n if (!skipAutoScope && await this.columnExists(table, 'tenant_id')) {\n q = q.where(qualify('tenant_id'), '=', opts.tenantId)\n }\n if (!opts.withDeleted && await this.columnExists(table, 'deleted_at')) {\n q = q.where(qualify('deleted_at'), 'is', null)\n }\n\n for (const filter of regularBaseFilters) {\n const fieldName = String(filter.field)\n let qualified = filter.qualified ?? null\n if (!qualified) {\n const column = await this.resolveBaseColumn(table, fieldName)\n if (!column) {\n q = this.applyIndexDocFilter(q, {\n entity: String(entity),\n field: fieldName,\n op: filter.op,\n value: filter.value,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n withDeleted: opts.withDeleted === true,\n searchActive,\n searchConfig,\n })\n continue\n }\n qualified = qualify(column)\n }\n q = applyFilterOp(q, qualified, filter.op, filter.value, fieldName)\n }\n\n // OR-grouped filters: AND within each group (one $or disjunct), OR between groups.\n // Resolution happens here (it needs async column lookups); the WHERE itself is\n // applied further down, once the cf:* value expressions exist \u2014 an OR group may\n // contain custom-field leaves whose SQL is only available then (#5039).\n type ResolvedOrClause =\n | { kind: 'column'; qualified: string; op: NormalizedFilter['op']; value: unknown }\n | { kind: 'doc'; field: string; op: NormalizedFilter['op']; value: unknown }\n | { kind: 'cf'; key: string; op: NormalizedFilter['op']; value: unknown }\n const resolvedGroupFilters: ResolvedOrClause[][] = []\n if (orGroupFilters.length > 0 || orGroupCfFilters.length > 0) {\n const groups = new Map<string, NormalizedFilter[]>()\n for (const f of [...orGroupFilters, ...orGroupCfFilters]) {\n const group = groups.get(f.orGroup!) ?? []\n group.push(f as NormalizedFilter)\n groups.set(f.orGroup!, group)\n }\n for (const [, groupFilters] of groups) {\n const resolved: ResolvedOrClause[] = []\n for (const filter of groupFilters) {\n const field = String(filter.field)\n if (field.startsWith('cf:')) {\n resolved.push({ kind: 'cf', key: field.slice(3), op: filter.op, value: filter.value })\n continue\n }\n const column = await this.resolveBaseColumn(table, field)\n if (column) {\n resolved.push({ kind: 'column', qualified: qualify(column), op: filter.op, value: filter.value })\n } else {\n // Field is not a base column \u2014 for custom-entity records it lives in\n // entity_indexes.doc. Build an EXISTS sub-filter so `$or` searches\n // across doc fields resolve instead of being silently dropped (#3229).\n resolved.push({ kind: 'doc', field, op: filter.op, value: filter.value })\n }\n }\n if (resolved.length > 0) resolvedGroupFilters.push(resolved)\n }\n }\n\n q = await applyJoinFilters({\n db,\n baseTable: table,\n builder: q,\n joinMap,\n joinFilters,\n aliasTables,\n qualifyBase: (column) => qualify(column),\n applyAliasScope: (builder, alias) => applyAliasScopes(builder, alias),\n applyFilterOp: (builder, column, op, value) => applyFilterOp(builder, column, op, value),\n applyJoinFilterOp,\n columnExists: (tbl, column) => this.columnExists(tbl, column),\n })\n\n // Selection (base columns only here; cf:* handled later)\n if (isSortKeysProjection) {\n q = q.select(sql.ref(qualify('id')).as('id'))\n if (await this.columnExists(table, 'tenant_id')) {\n q = q.select(sql.ref(qualify('tenant_id')).as('tenant_id'))\n }\n if (await this.columnExists(table, 'organization_id')) {\n q = q.select(sql.ref(qualify('organization_id')).as('organization_id'))\n }\n for (const s of resolvedSorts) {\n if (!s.field.startsWith('cf:')) q = q.select(sql.ref(qualify(s.field)).as(s.field))\n }\n } else if (opts.fields && opts.fields.length) {\n const cols = new Set(opts.fields.filter((f) => !f.startsWith('cf:')))\n if (requiresPlaintextSort) {\n for (const field of encryptedSortFields) cols.add(field)\n }\n for (const c of cols) {\n // Qualify and alias to base names to avoid ambiguity\n q = q.select(sql.ref(qualify(c)).as(c))\n }\n } else {\n // Default to selecting only base table columns to avoid ambiguity when joining\n q = q.select(sql`${sql.ref(table)}.*`.as('__all'))\n }\n\n // Resolve which custom fields to include\n const cfSourcesResult = this.configureCustomFieldSources(q, table, entity, db, opts, qualify)\n q = cfSourcesResult.builder\n const cfSources = cfSourcesResult.sources\n const entityIdToSource = new Map<string, ResolvedCustomFieldSource>()\n for (const source of cfSources) {\n entityIdToSource.set(String(source.entityId), source)\n }\n const requestedCustomFieldKeys = (!isSortKeysProjection && Array.isArray(opts.includeCustomFields))\n ? opts.includeCustomFields.map((key) => String(key))\n : []\n const cfKeys = new Set<string>()\n const keySource = new Map<string, ResolvedCustomFieldSource>()\n // Custom-field definition index threaded onto the result so the CRUD factory\n // can decorate list rows without reloading definitions from the DB (#2133).\n // Output-only \u2014 never resolved for the slim sortKeys projection.\n let resolvedCustomFieldDefinitions: ResolvedCustomFieldDefinitions | undefined\n // Explicit in fields/filters\n if (!isSortKeysProjection) {\n for (const f of (opts.fields || [])) {\n if (typeof f === 'string' && f.startsWith('cf:')) cfKeys.add(f.slice(3))\n }\n }\n for (const f of cfFilters) {\n if (typeof f.field === 'string' && f.field.startsWith('cf:')) cfKeys.add(f.field.slice(3))\n }\n if (!isSortKeysProjection && opts.includeCustomFields === true) {\n if (entityIdToSource.size > 0) {\n const entityIdList = Array.from(entityIdToSource.keys())\n const entityOrder = new Map<string, number>()\n entityIdList.forEach((id, idx) => entityOrder.set(id, idx))\n const rows = await db\n .selectFrom('custom_field_defs' as any)\n .select([\n 'key' as any,\n 'entity_id' as any,\n 'config_json' as any,\n 'kind' as any,\n 'organization_id' as any,\n 'tenant_id' as any,\n 'updated_at' as any,\n 'deleted_at' as any,\n ])\n .where('entity_id' as any, 'in', entityIdList)\n .where('is_active' as any, '=', true)\n .where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n .execute() as Array<{\n key: string\n entity_id: string\n config_json: unknown\n kind: string\n organization_id: string | null\n tenant_id: string | null\n updated_at: Date | string | number | null\n deleted_at: Date | string | number | null\n }>\n // Build the decoration index from the same rows, scoped exactly like the\n // factory's loadCustomFieldDefinitionIndex (tenant + is_active already\n // applied in SQL; org + soft-delete applied in the shared builder).\n const orgCandidates = resolveCfDefIndexOrgCandidates(opts.organizationIds, opts.organizationId ?? null)\n const definitionRows: CustomFieldDefinitionRow[] = rows.map((row) => ({\n key: String(row.key),\n entityId: String(row.entity_id),\n kind: row.kind == null ? null : String(row.kind),\n configJson: row.config_json,\n organizationId: row.organization_id == null ? null : String(row.organization_id),\n tenantId: row.tenant_id == null ? null : String(row.tenant_id),\n deletedAt: row.deleted_at ?? null,\n updatedAt: row.updated_at ?? null,\n }))\n resolvedCustomFieldDefinitions = {\n index: buildCustomFieldDefinitionIndexFromRows(definitionRows, { organizationIds: orgCandidates }),\n entityIds: entityIdList,\n tenantId: tenantId ?? null,\n organizationIds: orgCandidates,\n }\n type ScoredCustomFieldRow = {\n key: string\n entityId: string\n kind: string\n config: Record<string, unknown>\n }\n const sorted: ScoredCustomFieldRow[] = rows.map((row) => {\n const raw = row.config_json\n let cfg: Record<string, any> = {}\n if (raw && typeof raw === 'string') {\n try { cfg = JSON.parse(raw) } catch { cfg = {} }\n } else if (raw && typeof raw === 'object') {\n cfg = raw as Record<string, any>\n }\n return {\n key: String(row.key),\n entityId: String(row.entity_id),\n kind: String(row.kind || ''),\n config: cfg,\n }\n })\n sorted.sort((a, b) => {\n const ai = entityOrder.get(a.entityId) ?? Number.MAX_SAFE_INTEGER\n const bi = entityOrder.get(b.entityId) ?? Number.MAX_SAFE_INTEGER\n if (ai !== bi) return ai - bi\n return a.key.localeCompare(b.key)\n })\n const selectedSources = new Map<string, { source: ResolvedCustomFieldSource; score: number; penalty: number; entityIndex: number }>()\n for (const row of sorted) {\n const source = entityIdToSource.get(row.entityId)\n if (!source) continue\n const cfg = row.config || {}\n const entityIndex = entityOrder.get(row.entityId) ?? Number.MAX_SAFE_INTEGER\n const scores = computeCustomFieldScore(cfg, row.kind, entityIndex)\n const existing = selectedSources.get(row.key)\n if (!existing || scores.base > existing.score || (scores.base === existing.score && (scores.penalty < existing.penalty || (scores.penalty === existing.penalty && scores.entityIndex < existing.entityIndex)))) {\n selectedSources.set(row.key, { source, score: scores.base, penalty: scores.penalty, entityIndex: scores.entityIndex })\n }\n cfKeys.add(row.key)\n }\n for (const [key, entry] of selectedSources.entries()) {\n keySource.set(key, entry.source)\n }\n }\n } else if (!isSortKeysProjection && requestedCustomFieldKeys.length > 0) {\n for (const key of requestedCustomFieldKeys) cfKeys.add(key)\n }\n const unresolvedKeys = Array.from(cfKeys).filter((key) => !keySource.has(key))\n if (unresolvedKeys.length > 0 && entityIdToSource.size > 0) {\n const rows = await db\n .selectFrom('custom_field_defs' as any)\n .select(['key' as any, 'entity_id' as any])\n .where('entity_id' as any, 'in', Array.from(entityIdToSource.keys()))\n .where('key' as any, 'in', unresolvedKeys)\n .where('is_active' as any, '=', true)\n .where((eb: any) => eb.or([\n eb('tenant_id' as any, '=', tenantId),\n eb('tenant_id' as any, 'is', null),\n ]))\n .execute() as Array<{ key: string; entity_id: string }>\n for (const row of rows) {\n const source = entityIdToSource.get(String(row.entity_id))\n if (!source) continue\n if (!keySource.has(row.key)) keySource.set(row.key, source)\n }\n }\n\n const cfValueExprByKey: Record<string, RawBuilder<string | null>> = {}\n const cfSelectedAliases: string[] = []\n const cfJsonAliases = new Set<string>()\n const cfMultiAliasByAlias = new Map<string, string>()\n for (const key of cfKeys) {\n const source = keySource.get(key)\n if (!source) continue\n const entityIdForKey = source.entityId\n const recordIdExpr = source.recordIdExpr\n const sourceAliasSafe = sanitize(source.alias || 'src')\n const keyAliasSafe = sanitize(key)\n const defAlias = `cfd_${sourceAliasSafe}_${keyAliasSafe}`\n const valAlias = `cfv_${sourceAliasSafe}_${keyAliasSafe}`\n // Join definitions for kind resolution\n q = q.leftJoin(`custom_field_defs as ${defAlias}` as any, (jb: any) =>\n jb.on(`${defAlias}.entity_id`, '=', String(entityIdForKey))\n .on(`${defAlias}.key`, '=', key)\n .on(`${defAlias}.is_active`, '=', true)\n .on((eb: any) => eb.or([\n eb(`${defAlias}.tenant_id`, '=', tenantId),\n eb(`${defAlias}.tenant_id`, 'is', null),\n ]))\n )\n // Join values with record match\n q = q.leftJoin(`custom_field_values as ${valAlias}` as any, (jb: any) =>\n jb.on(`${valAlias}.entity_id`, '=', String(entityIdForKey))\n .on(`${valAlias}.field_key`, '=', key)\n .onRef(`${valAlias}.record_id`, '=', recordIdExpr as any)\n .on((eb: any) => eb.or([\n eb(`${valAlias}.tenant_id`, '=', tenantId),\n eb(`${valAlias}.tenant_id`, 'is', null),\n ]))\n )\n // Force a common SQL type across branches to avoid Postgres CASE type conflicts\n const caseExpr = sql<string | null>`CASE ${sql.ref(`${defAlias}.kind`)}\n WHEN 'integer' THEN (${sql.ref(`${valAlias}.value_int`)})::text\n WHEN 'float' THEN (${sql.ref(`${valAlias}.value_float`)})::text\n WHEN 'boolean' THEN (${sql.ref(`${valAlias}.value_bool`)})::text\n WHEN 'multiline' THEN (${sql.ref(`${valAlias}.value_multiline`)})::text\n ELSE (${sql.ref(`${valAlias}.value_text`)})::text\n END`\n cfValueExprByKey[key] = caseExpr\n const alias = sanitize(`cf:${key}`)\n // Project as aggregated to avoid duplicates when multi values exist\n if (!isSortKeysProjection && ((opts.fields || []).includes(`cf:${key}`) || opts.includeCustomFields === true || (requestedCustomFieldKeys.length > 0 && requestedCustomFieldKeys.includes(key)))) {\n const multiAlias = `${alias}__is_multi`\n const isMultiExpr = sql<boolean>`bool_or(coalesce((${sql.ref(`${defAlias}.config_json`)}->>'multi')::boolean, false))`\n const aggregatedArray = sql<unknown>`array_remove(array_agg(DISTINCT ${caseExpr}), NULL)`\n const projExpr = sql<unknown>`CASE WHEN ${isMultiExpr}\n THEN to_jsonb(${aggregatedArray})\n ELSE to_jsonb(max(${caseExpr}))\n END`\n q = q.select(projExpr.as(alias))\n q = q.select(isMultiExpr.as(multiAlias))\n cfSelectedAliases.push(alias)\n cfJsonAliases.add(alias)\n cfMultiAliasByAlias.set(alias, multiAlias)\n }\n }\n\n // Apply cf:* filters (on raw expressions). OR-grouped ones are excluded here and\n // combined with their disjunct's other leaves right below.\n for (const f of regularCfFilters) {\n if (!f.field.startsWith('cf:')) continue\n const key = f.field.slice(3)\n const expr = cfValueExprByKey[key]\n if (!expr) continue\n if ((f.op === 'like' || f.op === 'ilike') && searchActive && typeof f.value === 'string') {\n const tokens = tokenizeText(String(f.value), searchConfig)\n const hashes = tokens.hashes\n if (hashes.length) {\n const result = this.applySearchTokens(q, {\n entity: String(entity),\n field: f.field,\n hashes,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n tokens: tokens.tokens,\n })\n this.logSearchDebug('search:cf-filter', {\n entity: String(entity),\n field: f.field,\n tokens: tokens.tokens,\n hashes,\n applied: result.applied,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n })\n if (result.applied) {\n q = result.builder\n continue\n }\n } else {\n this.logSearchDebug('search:cf-skip-empty-hashes', {\n entity: String(entity),\n field: f.field,\n value: f.value,\n })\n }\n }\n q = this.applyColumnOp(q, expr, f.op, f.value)\n }\n\n // OR groups are applied here, after the cf:* value expressions exist, so a\n // disjunct mixing base/doc and custom-field leaves is united rather than\n // intersected. A cf leaf whose key resolved no value expression yields no\n // predicate and is dropped; a disjunct left empty by that is dropped too,\n // because an empty AND would read as TRUE and widen the result.\n //\n // Known limitation, shared with the `doc` clause kind above: a leaf inside an OR\n // group compares against the stored value directly and does not route `like` /\n // `ilike` through the search-token index the way the ungrouped path does. On a\n // field covered by an encryption map such a leaf therefore compares against\n // ciphertext and will not match.\n const applicableGroupFilters = resolvedGroupFilters\n .map((group) => group.filter((rf) => rf.kind !== 'cf' || Boolean(cfValueExprByKey[rf.key])))\n .filter((group) => group.length > 0)\n if (applicableGroupFilters.length > 0) {\n q = q.where((eb: any) => {\n const disjuncts = applicableGroupFilters.map((group) => {\n const parts = group.map((rf) => {\n if (rf.kind === 'column') return this.buildColumnOpExpression(eb, rf.qualified, rf.op, rf.value)\n if (rf.kind === 'cf') return this.buildColumnOpExpression(eb, cfValueExprByKey[rf.key], rf.op, rf.value)\n return this.buildIndexDocOpExpression(eb, {\n entity: String(entity),\n field: rf.field,\n op: rf.op,\n value: rf.value,\n recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: orgScope,\n withDeleted: opts.withDeleted === true,\n })\n })\n return parts.length === 1 ? parts[0] : eb.and(parts)\n })\n return disjuncts.length === 1 ? disjuncts[0] : eb.or(disjuncts)\n })\n }\n\n // Entity extensions joins (no selection yet; enables future filters/projections)\n if (opts.includeExtensions) {\n const { getModules } = await import('@open-mercato/shared/lib/i18n/server')\n const allMods = getModules() as any[]\n const allExts = allMods.flatMap((m) => (m as any).entityExtensions || [])\n const exts = allExts.filter((e: any) => e.base === entity)\n const chosen = Array.isArray(opts.includeExtensions)\n ? exts.filter((e: any) => (opts.includeExtensions as string[]).includes(e.extension))\n : exts\n for (const e of chosen) {\n const [, extName] = (e.extension as string).split(':')\n // Uses the SAME derivation as every other table-name fallback in this file\n // (`pluralizeBaseName`, also called at the resolveEntityTableName sites above)\n // rather than a separate inline one. The inline version handled only `+s`, so\n // `example_customer_priority` derived `example_customer_prioritys` against the\n // real `example_customer_priorities`. Behaviour is unchanged for every name that\n // does not end in `y`; `table` below remains the escape hatch for irregular\n // plurals no guesser can win (`person` \u2192 `people`).\n const derivedTable = pluralizeBaseName(extName)\n const extTable = isPlainTableIdentifier(e.table) ? e.table : derivedTable\n const alias = `ext_${sanitize(extName)}`\n q = q.leftJoin(`${extTable} as ${alias}` as any, (jb: any) =>\n jb.onRef(`${alias}.${e.join.extensionKey}`, '=', `${table}.${e.join.baseKey}`)\n )\n }\n }\n\n // Sorting: base fields and cf:* (use aggregated alias for cf)\n for (const s of resolvedSorts) {\n if (s.field.startsWith('cf:')) {\n const key = s.field.slice(3)\n const alias = sanitize(`cf:${key}`)\n // Ensure included in projection to sort by\n if (!cfSelectedAliases.includes(alias)) {\n const expr = cfValueExprByKey[key]\n if (expr) {\n q = q.select(sql<string | null>`max(${expr})`.as(alias))\n cfSelectedAliases.push(alias)\n }\n }\n if (!requiresPlaintextSort) q = q.orderBy(alias, (s.dir ?? 'asc') as any)\n } else {\n if (!requiresPlaintextSort) q = q.orderBy(qualify(s.field), (s.dir ?? 'asc') as any)\n }\n }\n\n // Deduplicate if we joined CFs or extensions by grouping on base id\n const hasJoinedAggregates = (opts.includeExtensions && (Array.isArray(opts.includeExtensions) ? (opts.includeExtensions.length > 0) : true)) || Object.keys(cfValueExprByKey).length > 0\n if (hasJoinedAggregates) {\n q = q.groupBy(`${table}.id`)\n }\n\n return { builder: q, hasJoinedAggregates, cfJsonAliases, cfMultiAliasByAlias, resolvedCustomFieldDefinitions }\n }\n\n // Pagination\n const page = opts.page?.page ?? 1\n const pageSize = opts.page?.pageSize ?? 20\n\n const {\n builder: qFull,\n hasJoinedAggregates,\n cfJsonAliases,\n cfMultiAliasByAlias,\n resolvedCustomFieldDefinitions,\n } = await buildQuery('full')\n\n // `count(distinct base.id)` is only required when a join can multiply base rows\n // (CF/extension aggregates, explicit relation joins, or custom-field sources).\n // Without such joins base.id is the unique PK, so `count(*)` is equivalent and\n // lets Postgres skip the redundant DISTINCT sort/hash for an index-only count (#2227).\n const mayMultiplyBaseRows =\n hasJoinedAggregates ||\n (Array.isArray(opts.joins) && opts.joins.length > 0) ||\n (Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0)\n const countExpr = mayMultiplyBaseRows\n ? sql<string>`count(distinct ${sql.ref(`${table}.id`)})`\n : sql<string>`count(*)`\n const countBuilder = hasJoinedAggregates\n ? qFull.clearSelect().clearOrderBy().clearGroupBy().select(countExpr.as('count'))\n : qFull.clearSelect().clearOrderBy().select(countExpr.as('count'))\n const countRow = await countBuilder.executeTakeFirst() as { count: unknown } | undefined\n const total = Number((countRow as any)?.count ?? 0)\n\n const svc = encryptionService\n const decryptPayload =\n svc?.decryptEntityPayload?.bind(svc) as\n | ((\n entityId: EntityId,\n payload: Record<string, unknown>,\n tenantId: string | null,\n organizationId: string | null,\n ) => Promise<Record<string, unknown>>)\n | null\n\n const decryptRow = async (item: ResultRow): Promise<ResultRow> => {\n if (!decryptPayload) return item\n try {\n const decrypted = await decryptPayload(\n entity,\n item,\n (item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null) as string | null,\n (item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null) as string | null,\n )\n return { ...item, ...decrypted }\n } catch (err) {\n logger.error('Error decrypting entity payload', { err })\n return item\n }\n }\n\n const normalizeCfJsonAliases = (rows: ResultRow[]) => {\n if (cfJsonAliases.size === 0) return\n for (const row of rows) {\n for (const alias of cfJsonAliases) {\n const multiAlias = cfMultiAliasByAlias.get(alias)\n const isMulti = multiAlias ? Boolean(row[multiAlias]) : false\n let raw = row[alias]\n if (typeof raw === 'string') {\n try { raw = JSON.parse(raw) } catch { /* ignore malformed json */ }\n }\n if (isMulti) {\n if (raw == null) row[alias] = []\n else if (Array.isArray(raw)) row[alias] = raw\n else row[alias] = [raw]\n } else {\n if (Array.isArray(raw)) row[alias] = raw.length > 0 ? raw[0] : null\n else row[alias] = raw\n }\n if (multiAlias) delete row[multiAlias]\n }\n }\n }\n\n let pagedItems: ResultRow[]\n let encryptedSortRowCapWarning: EncryptedSortRowCapWarning | undefined\n\n if (requiresPlaintextSort) {\n // Phase 1: slim id+sort-columns candidate scan, bounded-concurrency decrypt,\n // in-memory sort over the full candidate set, then slice to the page's ids.\n const cap = resolveEncryptedSortMaxRows()\n let qSort = (await buildQuery('sortKeys')).builder\n if (cap !== null) {\n qSort = qSort.limit(cap).orderBy(qualify('id'), 'asc' as any)\n }\n const candidateRows = await qSort.execute() as ResultRow[]\n const decryptedCandidates = decryptPayload\n ? await mapWithConcurrency(candidateRows, DECRYPT_CONCURRENCY, decryptRow)\n : candidateRows\n const orderedCandidates = sortRowsInMemory(decryptedCandidates, resolvedSorts)\n const pageIds = orderedCandidates\n .slice((page - 1) * pageSize, page * pageSize)\n .map((row) => row.id)\n\n if (cap !== null && total > cap) {\n encryptedSortRowCapWarning = {\n entity,\n sortFields: resolvedSorts.map((s) => s.field),\n maxRows: cap,\n totalMatched: total,\n }\n }\n\n // Phase 2: fetch + decrypt full rows for just the page's ids, then reassemble\n // in phase-1's order (SQL `WHERE id IN (...)` order is unspecified \u2014 never\n // trust it for ordering).\n if (pageIds.length === 0) {\n pagedItems = []\n } else {\n const pageRows = await qFull.where(qualify('id'), 'in', pageIds).execute() as ResultRow[]\n normalizeCfJsonAliases(pageRows)\n const decryptedPageRows = decryptPayload\n ? await mapWithConcurrency(pageRows, DECRYPT_CONCURRENCY, decryptRow)\n : pageRows\n const byId = new Map(decryptedPageRows.map((row) => [String(row.id), row]))\n pagedItems = pageIds\n .map((id) => byId.get(String(id)))\n .filter((row): row is ResultRow => row != null)\n }\n } else {\n const dataQuery = qFull.limit(pageSize).offset((page - 1) * pageSize)\n const items = await dataQuery.execute() as ResultRow[]\n normalizeCfJsonAliases(items)\n pagedItems = decryptPayload\n ? await mapWithConcurrency(items, DECRYPT_CONCURRENCY, decryptRow)\n : items\n }\n\n let queryResult: QueryResult<T> = { items: pagedItems as unknown as T[], page, pageSize, total }\n\n if (encryptedSortRowCapWarning) {\n const meta: QueryResultMeta = { encryptedSortRowCapWarning }\n queryResult.meta = meta\n }\n\n // --- UMES query extension: after-query pipeline ---\n if (ext && extensionCtx) {\n const diCtx = ext.resolve ? { resolve: ext.resolve } : noop\n queryResult = await runAfterQueryPipeline(\n queryResult as QueryResult<Record<string, unknown>>,\n opts,\n extensionCtx,\n diCtx,\n ) as QueryResult<T>\n }\n\n // Attach after the extension pipeline so the field always survives even if a\n // subscriber replaces the whole result object.\n if (resolvedCustomFieldDefinitions) {\n queryResult.customFieldDefinitions = resolvedCustomFieldDefinitions\n }\n\n return queryResult\n }\n\n private applyColumnOp(builder: AnyBuilder, column: string | RawBuilder<unknown>, op: string, value: unknown): AnyBuilder {\n switch (op) {\n case 'eq':\n return value === null\n ? builder.where(column as any, 'is', null)\n : builder.where(column as any, '=', value as any)\n case 'ne':\n return value === null\n ? builder.where(column as any, 'is not', null)\n : builder.where(column as any, '!=', value as any)\n case 'gt':\n return builder.where(column as any, '>', value as any)\n case 'gte':\n return builder.where(column as any, '>=', value as any)\n case 'lt':\n return builder.where(column as any, '<', value as any)\n case 'lte':\n return builder.where(column as any, '<=', value as any)\n case 'in':\n return builder.where(column as any, 'in', Array.isArray(value) ? value : [value])\n case 'nin':\n return builder.where(column as any, 'not in', Array.isArray(value) ? value : [value])\n case 'like':\n return builder.where(column as any, 'like', value as any)\n case 'ilike':\n return builder.where(column as any, 'ilike', value as any)\n case 'exists':\n return value\n ? builder.where(column as any, 'is not', null)\n : builder.where(column as any, 'is', null)\n default:\n return builder\n }\n }\n\n private buildColumnOpExpression(eb: any, column: string | RawBuilder<unknown>, op: string, value: unknown): any {\n switch (op) {\n case 'eq': return value === null ? eb(column, 'is', null) : eb(column, '=', value)\n case 'ne': return value === null ? eb(column, 'is not', null) : eb(column, '!=', value)\n case 'gt': return eb(column, '>', value)\n case 'gte': return eb(column, '>=', value)\n case 'lt': return eb(column, '<', value)\n case 'lte': return eb(column, '<=', value)\n case 'in': return eb(column, 'in', Array.isArray(value) ? value : [value])\n case 'nin': return eb(column, 'not in', Array.isArray(value) ? value : [value])\n case 'like': return eb(column, 'like', value)\n case 'ilike': return eb(column, 'ilike', value)\n case 'exists': return value ? eb(column, 'is not', null) : eb(column, 'is', null)\n default: return eb.val(true)\n }\n }\n\n private async resolveBaseColumn(table: string, field: string): Promise<string | null> {\n if (await this.columnExists(table, field)) return field\n if (field === 'organization_id' && await this.columnExists(table, 'id')) return 'id'\n return null\n }\n\n private async columnExists(table: string, column: string): Promise<boolean> {\n const key = `${table}.${column}`\n if (this.columnCache.has(key)) {\n const cached = this.columnCache.get(key)\n if (cached === true) return true\n this.columnCache.delete(key)\n }\n const db = this.getDb()\n const exists = await db\n .selectFrom('information_schema.columns' as any)\n .select(sql<number>`1`.as('one'))\n .where('table_name' as any, '=', table)\n .where('column_name' as any, '=', column)\n .limit(1)\n .executeTakeFirst()\n const present = !!exists\n if (present) this.columnCache.set(key, true)\n else this.columnCache.delete(key)\n return present\n }\n\n private applySearchTokens(\n q: AnyBuilder,\n opts: {\n entity: string\n field: string\n hashes: string[]\n recordIdColumn: string\n tenantId?: string | null\n organizationScope?: { ids: string[]; includeNull: boolean } | null\n combineWith?: 'and' | 'or'\n tokens?: string[]\n }\n ): { applied: boolean; builder: AnyBuilder } {\n if (!opts.hashes.length) {\n this.logSearchDebug('search:skip-no-hashes', {\n entity: opts.entity,\n field: opts.field,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n })\n return { applied: false, builder: q }\n }\n const alias = `st_${this.searchAliasSeq++}`\n const engine = this\n this.logSearchDebug('search:apply-search-tokens', {\n entity: opts.entity,\n field: opts.field,\n alias,\n tokenCount: opts.hashes.length,\n tokens: opts.tokens,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n combineWith: opts.combineWith ?? 'and',\n })\n const buildSub = (eb: any) => {\n let sub: AnyBuilder = eb\n .selectFrom(`search_tokens as ${alias}`)\n .select(sql<number>`1`.as('one'))\n .where(`${alias}.entity_type`, '=', opts.entity)\n .where(`${alias}.field`, '=', opts.field)\n .where(sql<boolean>`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`)\n .where(`${alias}.token_hash`, 'in', opts.hashes)\n .groupBy([`${alias}.entity_id`, `${alias}.field`])\n .having(sql<boolean>`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`)\n if (opts.tenantId !== undefined) {\n sub = sub.where(sql<boolean>`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`)\n }\n if (opts.organizationScope) {\n sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope)\n }\n return sub\n }\n const combiner = opts.combineWith === 'or' ? 'or' : 'and'\n if (combiner === 'or') {\n // When OR combining, caller expects a raw predicate to include in eb.or([...]).\n // We keep the same semantics as the previous knex orWhereExists by mutating the outer builder with a WHERE EXISTS.\n // Return the mutated builder; callers that need per-predicate control should build the sub themselves.\n const next = q.where((eb: any) => eb.or([eb.exists(buildSub(eb))]))\n return { applied: true, builder: next }\n }\n const next = q.where((eb: any) => eb.exists(buildSub(eb)))\n return { applied: true, builder: next }\n }\n\n private applyIndexDocFilter(\n q: AnyBuilder,\n opts: {\n entity: string\n field: string\n op: NormalizedFilter['op']\n value: unknown\n recordIdColumn: string\n tenantId?: string | null\n organizationScope?: { ids: string[]; includeNull: boolean } | null\n withDeleted: boolean\n searchActive: boolean\n searchConfig: ReturnType<typeof resolveSearchConfig>\n }\n ): AnyBuilder {\n if ((opts.op === 'like' || opts.op === 'ilike') && opts.searchActive && typeof opts.value === 'string') {\n const tokens = tokenizeText(String(opts.value), opts.searchConfig)\n const hashes = tokens.hashes\n if (hashes.length) {\n const result = this.applySearchTokens(q, {\n entity: opts.entity,\n field: opts.field,\n hashes,\n recordIdColumn: opts.recordIdColumn,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n tokens: tokens.tokens,\n })\n this.logSearchDebug('search:index-doc-filter', {\n entity: opts.entity,\n field: opts.field,\n tokens: tokens.tokens,\n hashes,\n applied: result.applied,\n tenantId: opts.tenantId ?? null,\n organizationScope: opts.organizationScope,\n })\n if (result.applied) return result.builder\n } else {\n this.logSearchDebug('search:index-doc-skip-empty-hashes', {\n entity: opts.entity,\n field: opts.field,\n value: opts.value,\n })\n }\n return q\n }\n\n return q.where((eb: any) => this.buildIndexDocOpExpression(eb, opts))\n }\n\n // Builds the entity_indexes EXISTS expression for a single doc-field operator,\n // shared by the regular doc-filter path (applyIndexDocFilter) and the OR-group\n // path so `$or` queries over custom-entity doc fields resolve instead of being\n // silently dropped (#3229).\n private buildIndexDocOpExpression(\n eb: any,\n opts: {\n entity: string\n field: string\n op: NormalizedFilter['op']\n value: unknown\n recordIdColumn: string\n tenantId?: string | null\n organizationScope?: { ids: string[]; includeNull: boolean } | null\n withDeleted: boolean\n }\n ): any {\n const alias = `ei_${this.searchAliasSeq++}`\n const engine = this\n return eb.exists((() => {\n let sub: AnyBuilder = eb\n .selectFrom(`entity_indexes as ${alias}`)\n .select(sql<number>`1`.as('one'))\n .where(`${alias}.entity_type`, '=', opts.entity)\n .where(sql<boolean>`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`)\n if (opts.tenantId !== undefined) {\n sub = sub.where(sql<boolean>`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`)\n }\n if (opts.organizationScope) {\n sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope)\n }\n if (!opts.withDeleted) {\n sub = sub.where(`${alias}.deleted_at`, 'is', null)\n }\n\n const textExpr = sql<string | null>`(${sql.ref(`${alias}.doc`)} ->> ${opts.field})`\n switch (opts.op) {\n case 'eq':\n sub = opts.value === null\n ? sub.where(sql<boolean>`${textExpr} is null`)\n : sub.where(sql<boolean>`${textExpr} = ${opts.value}`)\n break\n case 'ne':\n sub = opts.value === null\n ? sub.where(sql<boolean>`${textExpr} is not null`)\n : sub.where(sql<boolean>`${textExpr} <> ${opts.value}`)\n break\n case 'gt':\n case 'gte':\n case 'lt':\n case 'lte': {\n const operator = sql.raw(opts.op === 'gt' ? '>' : opts.op === 'gte' ? '>=' : opts.op === 'lt' ? '<' : '<=')\n sub = sub.where(sql<boolean>`${textExpr} ${operator} ${opts.value}`)\n break\n }\n case 'in': {\n const vals = Array.isArray(opts.value) ? opts.value : [opts.value]\n sub = sub.where(sql<boolean>`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`)\n break\n }\n case 'nin': {\n const vals = Array.isArray(opts.value) ? opts.value : [opts.value]\n sub = sub.where(sql<boolean>`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`)\n break\n }\n case 'like':\n sub = sub.where(sql<boolean>`${textExpr} like ${opts.value}`); break\n case 'ilike':\n sub = sub.where(sql<boolean>`${textExpr} ilike ${opts.value}`); break\n case 'exists':\n sub = opts.value\n ? sub.where(sql<boolean>`${textExpr} is not null`)\n : sub.where(sql<boolean>`${textExpr} is null`)\n break\n default:\n break\n }\n return sub\n })())\n }\n\n private configureCustomFieldSources(\n q: AnyBuilder,\n baseTable: string,\n baseEntity: EntityId,\n db: AnyDb,\n opts: QueryOptions,\n qualify: (column: string) => string,\n ): { builder: AnyBuilder; sources: ResolvedCustomFieldSource[] } {\n const sources: ResolvedCustomFieldSource[] = [\n {\n entityId: baseEntity,\n alias: 'base',\n table: baseTable,\n recordIdExpr: sql<string>`${sql.ref(`${baseTable}.id`)}::text`,\n },\n ]\n const extras: QueryCustomFieldSource[] = opts.customFieldSources ?? []\n let next = q\n extras.forEach((srcOpt, index) => {\n const joinTable = srcOpt.table ?? resolveEntityTableName(this.em, srcOpt.entityId)\n const alias = srcOpt.alias ?? `cfs_${index}`\n const join = srcOpt.join\n if (!join) {\n throw new Error(`QueryEngine: customFieldSources entry for ${String(srcOpt.entityId)} requires a join configuration`)\n }\n const joinFn = (join.type ?? 'left') === 'inner' ? 'innerJoin' : 'leftJoin'\n next = (next as any)[joinFn](`${joinTable} as ${alias}`, (jb: any) =>\n jb.onRef(`${alias}.${join.toField}`, '=', qualify(join.fromField)))\n const recordColumn = srcOpt.recordIdColumn ?? 'id'\n sources.push({\n entityId: srcOpt.entityId,\n alias,\n table: joinTable,\n recordIdExpr: sql<string>`${sql.ref(`${alias}.${recordColumn}`)}::text`,\n })\n })\n return { builder: next, sources }\n }\n\n private logSearchDebug(event: string, payload: Record<string, unknown>) {\n try {\n logger.debug(event, payload)\n } catch {}\n }\n\n private resolveOrganizationScope(opts: QueryOptions): { ids: string[]; includeNull: boolean } | null {\n if (opts.organizationIds !== undefined) {\n const raw = (opts.organizationIds ?? []).map((id) => (typeof id === 'string' ? id.trim() : id))\n const includeNull = raw.some((id) => id == null || id === '')\n const ids = raw.filter((id): id is string => typeof id === 'string' && id.length > 0)\n return { ids: Array.from(new Set(ids)), includeNull }\n }\n if (typeof opts.organizationId === 'string' && opts.organizationId.trim().length > 0) {\n return { ids: [opts.organizationId], includeNull: false }\n }\n return null\n }\n\n private applyOrganizationScope(q: AnyBuilder, column: string, scope: { ids: string[]; includeNull: boolean }): AnyBuilder {\n if (!scope) return q\n if (scope.ids.length === 0 && !scope.includeNull) {\n return q.where(sql<boolean>`1 = 0`)\n }\n return q.where((eb: any) => {\n const parts: any[] = []\n if (scope.ids.length > 0) parts.push(eb(column, 'in', scope.ids))\n if (scope.includeNull) parts.push(eb(column, 'is', null))\n if (parts.length === 1) return parts[0]\n return eb.or(parts)\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["resolvedCustomFieldDefinitions", "cfJsonAliases", "cfMultiAliasByAlias", "hasJoinedAggregates", "next"]
7
7
  }
@@ -63,6 +63,7 @@ function resolveSearchConfig() {
63
63
  enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
64
64
  hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
65
65
  storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
66
+ useIlikeForNonEncryptedFields: parseBoolean(process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS, false),
66
67
  blocklistedFields: blocklist.global,
67
68
  entityBlocklistedFields: blocklist.byEntity,
68
69
  maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/search/config.ts"],
4
- "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\n\nexport type SearchConfig = {\n enabled: boolean\n minTokenLength: number\n enablePartials: boolean\n hashAlgorithm: 'sha256' | 'sha1' | 'md5'\n storeRawTokens: boolean\n blocklistedFields: string[]\n entityBlocklistedFields?: Record<string, string[]>\n maxFieldChars?: number\n maxTokensPerField?: number\n maxTokensPerRecord?: number\n}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\nexport const DEFAULT_SEARCH_MAX_FIELD_CHARS = 20_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD = 5_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD = 20_000\n\nexport type SearchTokenLimits = {\n maxFieldChars: number\n maxTokensPerField: number\n maxTokensPerRecord: number\n}\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\n\nconst ENTITY_BLOCKLIST_SEPARATOR = '@'\n\nfunction parseBoolean(raw: string | undefined, fallback: boolean): boolean {\n return parseBooleanWithDefault(raw, fallback)\n}\n\nfunction parseNumber(raw: string | undefined, fallback: number, min = 1): number {\n return parseNumberWithDefault(raw, fallback, { integer: true, min })\n}\n\nexport function resolveSearchTokenLimits(config: SearchConfig): SearchTokenLimits {\n const resolveLimit = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback\n if (!Number.isFinite(value) || value < 0) return fallback\n return Math.trunc(value)\n }\n return {\n maxFieldChars: resolveLimit(config.maxFieldChars, DEFAULT_SEARCH_MAX_FIELD_CHARS),\n maxTokensPerField: resolveLimit(config.maxTokensPerField, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD),\n maxTokensPerRecord: resolveLimit(config.maxTokensPerRecord, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD),\n }\n}\n\nfunction parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {\n const value = (raw ?? '').trim().toLowerCase()\n if (value === 'sha1') return 'sha1'\n if (value === 'md5') return 'md5'\n return 'sha256'\n}\n\n/**\n * Parses `OM_SEARCH_FIELD_BLOCKLIST` into a global list plus per-entity-type lists.\n *\n * Why: a deployment often needs to keep one large free-text column out of the token\n * index (e-mail bodies on `customers:customer_interaction`) while still indexing the\n * same-named column elsewhere. A flat global list cannot express that.\n *\n * How to apply: entries are comma-separated; an entry may carry an optional\n * `entityType@` prefix \u2014 `body` blocks the field everywhere, while\n * `customers:customer_interaction@body` blocks it only for that entity type. Entries\n * whose field part is empty are ignored so malformed env input cannot break indexing.\n */\nfunction parseFieldBlocklist(raw: string | undefined): {\n global: string[]\n byEntity: Record<string, string[]>\n} {\n const global: string[] = []\n const byEntity = new Map<string, string[]>()\n\n for (const rawEntry of parseCommaSeparatedList(raw)) {\n const entry = rawEntry.toLowerCase()\n const separatorIndex = entry.indexOf(ENTITY_BLOCKLIST_SEPARATOR)\n const entityType = separatorIndex >= 0 ? entry.slice(0, separatorIndex).trim() : ''\n const field = separatorIndex >= 0 ? entry.slice(separatorIndex + 1).trim() : entry\n if (!field.length) continue\n\n if (!entityType.length) {\n if (!global.includes(field)) global.push(field)\n continue\n }\n\n const scoped = byEntity.get(entityType) ?? []\n if (!scoped.includes(field)) scoped.push(field)\n byEntity.set(entityType, scoped)\n }\n\n for (const fallback of DEFAULT_BLOCKLIST) {\n if (!global.includes(fallback)) global.push(fallback)\n }\n\n const scopedBlocklist = Object.create(null) as Record<string, string[]>\n for (const [entityType, fields] of byEntity) scopedBlocklist[entityType] = fields\n\n return { global, byEntity: scopedBlocklist }\n}\n\nexport function resolveSearchConfig(): SearchConfig {\n const blocklist = parseFieldBlocklist(process.env.OM_SEARCH_FIELD_BLOCKLIST)\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: resolveSearchMinTokenLength(),\n enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),\n hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),\n storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),\n blocklistedFields: blocklist.global,\n entityBlocklistedFields: blocklist.byEntity,\n maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),\n maxTokensPerField: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_FIELD, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD, 0),\n maxTokensPerRecord: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_RECORD, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD, 0),\n }\n}\n\n/**\n * Single matcher for \"should this field be kept out of the search index?\".\n *\n * Why: the per-field token path and the `search_text` aggregate previously each\n * decided this on their own, and the aggregate simply never consulted the config \u2014\n * so a blocklisted column's text came back into the index under the aggregate's\n * field name (#4624). Both paths now share this function so they cannot drift.\n *\n * How to apply: pass the document's field name and the entity type being indexed;\n * `entityType` may be omitted when unknown, in which case only global entries apply.\n * Matching keeps the historical substring semantics (`fieldName.includes(pattern)`).\n */\nexport function isSearchFieldBlocklisted(\n field: string,\n entityType: string | null | undefined,\n config: SearchConfig,\n): boolean {\n const lower = field.toLowerCase()\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return true\n if (!entityType) return false\n const scoped = config.entityBlocklistedFields?.[entityType.trim().toLowerCase()]\n if (!Array.isArray(scoped) || !scoped.length) return false\n return scoped.some((blocked) => lower.includes(blocked))\n}\n\n/**\n * Browser-safe accessor for the minimum search token length.\n *\n * Why: client components (e.g. global search dialog) must mirror the server-side\n * tokenizer's `minTokenLength` so the UI gates the request before hitting an\n * empty result set. Pulling the value through this single helper keeps the env\n * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.\n *\n * How to apply: call from anywhere \u2014 server, client (when the host app exposes\n * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.\n */\nexport function resolveSearchMinTokenLength(): number {\n return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { parseNumberWithDefault } from '@open-mercato/shared/lib/number'\nimport { parseCommaSeparatedList } from '@open-mercato/shared/lib/string'\n\nexport type SearchConfig = {\n enabled: boolean\n minTokenLength: number\n enablePartials: boolean\n hashAlgorithm: 'sha256' | 'sha1' | 'md5'\n storeRawTokens: boolean\n /**\n * When true, a like/ilike on a PLAINTEXT base column runs as exact SQL ILIKE instead of being\n * rewritten into an approximate search-token match; encrypted columns always keep the token\n * path (ILIKE against ciphertext cannot match). Off by default: token matching can be faster\n * than an unanchored ILIKE, which may need a full scan without a trigram index \u2014 but it is\n * approximate (fragments under minTokenLength vanish, so `ZK 1/2026` degrades to its year and\n * an all-short term drops the predicate). Flip it on when list search must be exact.\n */\n useIlikeForNonEncryptedFields?: boolean\n blocklistedFields: string[]\n entityBlocklistedFields?: Record<string, string[]>\n maxFieldChars?: number\n maxTokensPerField?: number\n maxTokensPerRecord?: number\n}\n\nexport const DEFAULT_SEARCH_MIN_TOKEN_LENGTH = 3\nexport const DEFAULT_SEARCH_MAX_FIELD_CHARS = 20_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD = 5_000\nexport const DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD = 20_000\n\nexport type SearchTokenLimits = {\n maxFieldChars: number\n maxTokensPerField: number\n maxTokensPerRecord: number\n}\n\nconst DEFAULT_BLOCKLIST = ['password', 'token', 'secret', 'hash']\n\nconst ENTITY_BLOCKLIST_SEPARATOR = '@'\n\nfunction parseBoolean(raw: string | undefined, fallback: boolean): boolean {\n return parseBooleanWithDefault(raw, fallback)\n}\n\nfunction parseNumber(raw: string | undefined, fallback: number, min = 1): number {\n return parseNumberWithDefault(raw, fallback, { integer: true, min })\n}\n\nexport function resolveSearchTokenLimits(config: SearchConfig): SearchTokenLimits {\n const resolveLimit = (value: number | undefined, fallback: number): number => {\n if (value === undefined) return fallback\n if (!Number.isFinite(value) || value < 0) return fallback\n return Math.trunc(value)\n }\n return {\n maxFieldChars: resolveLimit(config.maxFieldChars, DEFAULT_SEARCH_MAX_FIELD_CHARS),\n maxTokensPerField: resolveLimit(config.maxTokensPerField, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD),\n maxTokensPerRecord: resolveLimit(config.maxTokensPerRecord, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD),\n }\n}\n\nfunction parseHashAlgorithm(raw: string | undefined): 'sha256' | 'sha1' | 'md5' {\n const value = (raw ?? '').trim().toLowerCase()\n if (value === 'sha1') return 'sha1'\n if (value === 'md5') return 'md5'\n return 'sha256'\n}\n\n/**\n * Parses `OM_SEARCH_FIELD_BLOCKLIST` into a global list plus per-entity-type lists.\n *\n * Why: a deployment often needs to keep one large free-text column out of the token\n * index (e-mail bodies on `customers:customer_interaction`) while still indexing the\n * same-named column elsewhere. A flat global list cannot express that.\n *\n * How to apply: entries are comma-separated; an entry may carry an optional\n * `entityType@` prefix \u2014 `body` blocks the field everywhere, while\n * `customers:customer_interaction@body` blocks it only for that entity type. Entries\n * whose field part is empty are ignored so malformed env input cannot break indexing.\n */\nfunction parseFieldBlocklist(raw: string | undefined): {\n global: string[]\n byEntity: Record<string, string[]>\n} {\n const global: string[] = []\n const byEntity = new Map<string, string[]>()\n\n for (const rawEntry of parseCommaSeparatedList(raw)) {\n const entry = rawEntry.toLowerCase()\n const separatorIndex = entry.indexOf(ENTITY_BLOCKLIST_SEPARATOR)\n const entityType = separatorIndex >= 0 ? entry.slice(0, separatorIndex).trim() : ''\n const field = separatorIndex >= 0 ? entry.slice(separatorIndex + 1).trim() : entry\n if (!field.length) continue\n\n if (!entityType.length) {\n if (!global.includes(field)) global.push(field)\n continue\n }\n\n const scoped = byEntity.get(entityType) ?? []\n if (!scoped.includes(field)) scoped.push(field)\n byEntity.set(entityType, scoped)\n }\n\n for (const fallback of DEFAULT_BLOCKLIST) {\n if (!global.includes(fallback)) global.push(fallback)\n }\n\n const scopedBlocklist = Object.create(null) as Record<string, string[]>\n for (const [entityType, fields] of byEntity) scopedBlocklist[entityType] = fields\n\n return { global, byEntity: scopedBlocklist }\n}\n\nexport function resolveSearchConfig(): SearchConfig {\n const blocklist = parseFieldBlocklist(process.env.OM_SEARCH_FIELD_BLOCKLIST)\n return {\n enabled: parseBoolean(process.env.OM_SEARCH_ENABLED, true),\n minTokenLength: resolveSearchMinTokenLength(),\n enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),\n hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),\n storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),\n useIlikeForNonEncryptedFields: parseBoolean(process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS, false),\n blocklistedFields: blocklist.global,\n entityBlocklistedFields: blocklist.byEntity,\n maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),\n maxTokensPerField: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_FIELD, DEFAULT_SEARCH_MAX_TOKENS_PER_FIELD, 0),\n maxTokensPerRecord: parseNumber(process.env.OM_SEARCH_MAX_TOKENS_PER_RECORD, DEFAULT_SEARCH_MAX_TOKENS_PER_RECORD, 0),\n }\n}\n\n/**\n * Single matcher for \"should this field be kept out of the search index?\".\n *\n * Why: the per-field token path and the `search_text` aggregate previously each\n * decided this on their own, and the aggregate simply never consulted the config \u2014\n * so a blocklisted column's text came back into the index under the aggregate's\n * field name (#4624). Both paths now share this function so they cannot drift.\n *\n * How to apply: pass the document's field name and the entity type being indexed;\n * `entityType` may be omitted when unknown, in which case only global entries apply.\n * Matching keeps the historical substring semantics (`fieldName.includes(pattern)`).\n */\nexport function isSearchFieldBlocklisted(\n field: string,\n entityType: string | null | undefined,\n config: SearchConfig,\n): boolean {\n const lower = field.toLowerCase()\n if (config.blocklistedFields.some((blocked) => lower.includes(blocked))) return true\n if (!entityType) return false\n const scoped = config.entityBlocklistedFields?.[entityType.trim().toLowerCase()]\n if (!Array.isArray(scoped) || !scoped.length) return false\n return scoped.some((blocked) => lower.includes(blocked))\n}\n\n/**\n * Browser-safe accessor for the minimum search token length.\n *\n * Why: client components (e.g. global search dialog) must mirror the server-side\n * tokenizer's `minTokenLength` so the UI gates the request before hitting an\n * empty result set. Pulling the value through this single helper keeps the env\n * contract (`OM_SEARCH_MIN_LEN`) authoritative on both sides.\n *\n * How to apply: call from anywhere \u2014 server, client (when the host app exposes\n * `OM_SEARCH_MIN_LEN` through `next.config.ts`'s `env` block), or tests.\n */\nexport function resolveSearchMinTokenLength(): number {\n return parseNumber(process.env.OM_SEARCH_MIN_LEN, DEFAULT_SEARCH_MIN_TOKEN_LENGTH, 1)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.8-develop.7005.1.70a5747c3e";
1
+ const APP_VERSION = "0.6.8-develop.7012.1.1dbd6f5fbd";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.7005.1.70a5747c3e';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.7012.1.1dbd6f5fbd';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.8-develop.7005.1.70a5747c3e",
3
+ "version": "0.6.8-develop.7012.1.1dbd6f5fbd",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -105,7 +105,7 @@
105
105
  "@mikro-orm/core": "^7.1.8",
106
106
  "@mikro-orm/decorators": "^7.1.8",
107
107
  "@mikro-orm/postgresql": "^7.1.8",
108
- "@open-mercato/cache": "0.6.8-develop.7005.1.70a5747c3e",
108
+ "@open-mercato/cache": "0.6.8-develop.7012.1.1dbd6f5fbd",
109
109
  "@types/sanitize-html": "^2.16.1",
110
110
  "dotenv": "^17.4.2",
111
111
  "pino": "^10.3.1",
@@ -2,11 +2,13 @@ import { BasicQueryEngine } from '../engine'
2
2
  import { SortDir } from '../types'
3
3
  import { registerModules } from '../../i18n/server'
4
4
  import { clearSearchTokenPresenceCache } from '../../search/availability'
5
+ import { clearEncryptedLikeFieldsCache } from '../engine'
5
6
 
6
7
  // The token-presence answer is cached process-wide (TTL); without clearing it,
7
8
  // probe-count assertions would observe hits from earlier tests in this file.
8
9
  beforeEach(() => {
9
10
  clearSearchTokenPresenceCache()
11
+ clearEncryptedLikeFieldsCache()
10
12
  })
11
13
 
12
14
  // Mock modules with one entity extension
@@ -1134,3 +1136,158 @@ describe('BasicQueryEngine entity-extension joins', () => {
1134
1136
  expect(join.aliasObj).toEqual({ ext_example_session_note: 'example_session_notes' })
1135
1137
  })
1136
1138
  })
1139
+
1140
+ describe('BasicQueryEngine like/ilike routing by column encryption', () => {
1141
+ // The gate is opt-in: OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS defaults to false and the
1142
+ // legacy rewrite-everything behavior stays. These cases flip it on; the last one pins the
1143
+ // default off.
1144
+ beforeEach(() => {
1145
+ process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS = 'true'
1146
+ })
1147
+ afterEach(() => {
1148
+ delete process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS
1149
+ })
1150
+
1151
+ // The token rewrite exists because ILIKE against ciphertext cannot match. On a plaintext
1152
+ // column SQL ILIKE is exact, and the rewrite silently changes the result set: tokenization
1153
+ // splits on non-alphanumerics and drops tokens shorter than minTokenLength, so a
1154
+ // document-number search like "ZK 1/2026" degrades to the tokens {202, 2026} and matches
1155
+ // every record from that year instead of the one document.
1156
+ const fakeDbWithTokens = () => createFakeKysely({
1157
+ customer_entities: [],
1158
+ search_tokens: [{ one: 1 }],
1159
+ 'information_schema.tables': [{ table_name: 'search_tokens' }],
1160
+ 'information_schema.columns': [
1161
+ { table_name: 'customer_entities', column_name: 'tenant_id' },
1162
+ { table_name: 'customer_entities', column_name: 'display_name' },
1163
+ ],
1164
+ })
1165
+
1166
+ test('a plaintext base column keeps exact SQL ILIKE even when tokens are available', async () => {
1167
+ const fakeDb = fakeDbWithTokens()
1168
+ const engine = new BasicQueryEngine(
1169
+ {} as any,
1170
+ () => fakeDb as any,
1171
+ () => ({ getEncryptedFieldNames: async () => [] }) as any,
1172
+ )
1173
+ const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
1174
+
1175
+ await engine.query('customers:customer_entity', {
1176
+ tenantId: 't1',
1177
+ fields: ['id'],
1178
+ filters: { display_name: { $ilike: '%ZK 1/2026%' } },
1179
+ page: { page: 1, pageSize: 10 },
1180
+ })
1181
+
1182
+ expect(applySearchTokensSpy).not.toHaveBeenCalled()
1183
+ const baseCall = fakeDb._calls.find((b: any) => b._ops.table === 'customer_entities')
1184
+ expect(baseCall).toBeTruthy()
1185
+ const ilikeWhere = baseCall._ops.wheres.some(
1186
+ (w: any) => Array.isArray(w) && String(w[0]).includes('display_name') && w[1] === 'ilike' && w[2] === '%ZK 1/2026%',
1187
+ )
1188
+ expect(ilikeWhere).toBe(true)
1189
+ })
1190
+
1191
+ test('an encrypted base column still routes through search tokens', async () => {
1192
+ const engine = new BasicQueryEngine(
1193
+ {} as any,
1194
+ () => fakeDbWithTokens() as any,
1195
+ () => ({ getEncryptedFieldNames: async () => ['display_name'] }) as any,
1196
+ )
1197
+ const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
1198
+
1199
+ await engine.query('customers:customer_entity', {
1200
+ tenantId: 't1',
1201
+ fields: ['id'],
1202
+ filters: { display_name: { $ilike: '%avision%' } },
1203
+ page: { page: 1, pageSize: 10 },
1204
+ })
1205
+
1206
+ expect(applySearchTokensSpy).toHaveBeenCalled()
1207
+ })
1208
+
1209
+ test('no service + encryption disabled: nothing is ciphertext, ILIKE stays exact', async () => {
1210
+ process.env.TENANT_DATA_ENCRYPTION = 'no'
1211
+ try {
1212
+ const fakeDb = fakeDbWithTokens()
1213
+ const engine = new BasicQueryEngine({} as any, () => fakeDb as any)
1214
+ const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
1215
+
1216
+ await engine.query('customers:customer_entity', {
1217
+ tenantId: 't1',
1218
+ fields: ['id'],
1219
+ filters: { display_name: { $ilike: '%avision%' } },
1220
+ page: { page: 1, pageSize: 10 },
1221
+ })
1222
+
1223
+ expect(applySearchTokensSpy).not.toHaveBeenCalled()
1224
+ const baseCall = fakeDb._calls.find((b: any) => b._ops.table === 'customer_entities')
1225
+ const ilikeWhere = baseCall._ops.wheres.some(
1226
+ (w: any) => Array.isArray(w) && String(w[0]).includes('display_name') && w[1] === 'ilike' && w[2] === '%avision%',
1227
+ )
1228
+ expect(ilikeWhere).toBe(true)
1229
+ } finally {
1230
+ delete process.env.TENANT_DATA_ENCRYPTION
1231
+ }
1232
+ })
1233
+
1234
+ test('no service + encryption enabled: the map is unknown, the token rewrite is kept', async () => {
1235
+ // A swallowed DI failure looks exactly like "no service". Guessing "plaintext" here would
1236
+ // run ILIKE against ciphertext (zero rows, silently) -- so the gate stays inert instead.
1237
+ const engine = new BasicQueryEngine({} as any, () => fakeDbWithTokens() as any)
1238
+ const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
1239
+
1240
+ await engine.query('customers:customer_entity', {
1241
+ tenantId: 't1',
1242
+ fields: ['id'],
1243
+ filters: { display_name: { $ilike: '%avision%' } },
1244
+ page: { page: 1, pageSize: 10 },
1245
+ })
1246
+
1247
+ expect(applySearchTokensSpy).toHaveBeenCalled()
1248
+ })
1249
+
1250
+ test('a camelCase encryption-map entry still routes its column through tokens', async () => {
1251
+ // Encryption maps may declare `displayName` while the filter carries the column name
1252
+ // `display_name` (TenantDataEncryptionService resolves both). A raw name comparison would
1253
+ // misread that ciphertext column as plaintext and run ILIKE against ciphertext -- zero rows,
1254
+ // silently. The gate must match across name shapes.
1255
+ const engine = new BasicQueryEngine(
1256
+ {} as any,
1257
+ () => fakeDbWithTokens() as any,
1258
+ () => ({ getEncryptedFieldNames: async () => ['displayName'] }) as any,
1259
+ )
1260
+ const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
1261
+
1262
+ await engine.query('customers:customer_entity', {
1263
+ tenantId: 't1',
1264
+ fields: ['id'],
1265
+ filters: { display_name: { $ilike: '%avision%' } },
1266
+ page: { page: 1, pageSize: 10 },
1267
+ })
1268
+
1269
+ expect(applySearchTokensSpy).toHaveBeenCalled()
1270
+ })
1271
+
1272
+ test('with the flag off (default) the token rewrite is kept even for plaintext columns', () => {
1273
+ delete process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS
1274
+ const fakeDb = fakeDbWithTokens()
1275
+ const engine = new BasicQueryEngine(
1276
+ {} as any,
1277
+ () => fakeDb as any,
1278
+ () => ({ getEncryptedFieldNames: async () => [] }) as any,
1279
+ )
1280
+ const applySearchTokensSpy = jest.spyOn(engine as any, 'applySearchTokens')
1281
+
1282
+ return engine
1283
+ .query('customers:customer_entity', {
1284
+ tenantId: 't1',
1285
+ fields: ['id'],
1286
+ filters: { display_name: { $ilike: '%avision%' } },
1287
+ page: { page: 1, pageSize: 10 },
1288
+ })
1289
+ .then(() => {
1290
+ expect(applySearchTokensSpy).toHaveBeenCalled()
1291
+ })
1292
+ })
1293
+ })
@@ -20,6 +20,8 @@ import {
20
20
  type SearchTokenProbeQueryBuilder,
21
21
  } from '../search/availability'
22
22
  import { tokenizeText } from '../search/tokenize'
23
+ import { fieldNameCandidates } from './encrypted-sort'
24
+ import { isTenantDataEncryptionEnabled } from '../encryption/toggles'
23
25
  import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from './query-extension-runner'
24
26
  import {
25
27
  buildCustomFieldDefinitionIndexFromRows,
@@ -43,10 +45,49 @@ const entityTableCache = new Map<string, string>()
43
45
 
44
46
  type EncryptionResolver = () => {
45
47
  decryptEntityPayload?: (entityId: EntityId, payload: Record<string, unknown>, tenantId?: string | null, organizationId?: string | null) => Promise<Record<string, unknown>>
46
- getEncryptedFieldNames?: (entityId: EntityId, tenantId?: string | null, organizationId?: string | null) => Promise<readonly string[]>
48
+ getEncryptedFieldNames?: (entityId: EntityId, tenantId?: string | null, organizationId?: string | null, options?: { ignoreRuntimeHealth?: boolean }) => Promise<readonly string[]>
47
49
  isEnabled?: () => boolean
48
50
  } | null
49
51
 
52
+ /**
53
+ * Membership across name shapes: encryption maps may declare a field as `displayName` or
54
+ * `display_name` (TenantDataEncryptionService resolves both), while query filters carry real
55
+ * column names. Both sides expand through `fieldNameCandidates` at set-build and lookup time.
56
+ */
57
+ export function isEncryptedLikeField(encrypted: ReadonlySet<string>, field: string): boolean {
58
+ return fieldNameCandidates(field).some((candidate) => encrypted.has(candidate))
59
+ }
60
+
61
+ // The all-orgs union behind `getEncryptedFieldNames(..., organizationId: null)` is an UNCACHED
62
+ // `encryption_maps` read. Encryption maps change on deploys, not per request, so a short TTL
63
+ // removes the per-search round-trip without meaningfully delaying a map rollout.
64
+ const ENCRYPTED_LIKE_FIELDS_TTL_MS = 60_000
65
+ const ENCRYPTED_LIKE_FIELDS_CACHE_CAP = 500
66
+ const encryptedLikeFieldsCache = new Map<string, { at: number; fields: Set<string> }>()
67
+
68
+ export async function resolveEncryptedLikeFieldSet(
69
+ read: () => Promise<readonly string[]>,
70
+ entity: string,
71
+ tenantId: string | null,
72
+ ): Promise<Set<string>> {
73
+ const key = `${entity}|${tenantId ?? ''}`
74
+ const hit = encryptedLikeFieldsCache.get(key)
75
+ if (hit && Date.now() - hit.at < ENCRYPTED_LIKE_FIELDS_TTL_MS) return hit.fields
76
+ const names = await read()
77
+ const fields = new Set<string>()
78
+ for (const name of names ?? []) {
79
+ for (const candidate of fieldNameCandidates(String(name))) fields.add(candidate)
80
+ }
81
+ if (encryptedLikeFieldsCache.size >= ENCRYPTED_LIKE_FIELDS_CACHE_CAP) encryptedLikeFieldsCache.clear()
82
+ encryptedLikeFieldsCache.set(key, { at: Date.now(), fields })
83
+ return fields
84
+ }
85
+
86
+ /** Test-only: the TTL memo would otherwise leak state across specs. */
87
+ export function clearEncryptedLikeFieldsCache(): void {
88
+ encryptedLikeFieldsCache.clear()
89
+ }
90
+
50
91
  type ResolvedCustomFieldSource = {
51
92
  entityId: EntityId
52
93
  alias: string
@@ -345,6 +386,62 @@ export class BasicQueryEngine implements QueryEngine {
345
386
  ? await this.searchAvailability().hasTokens(String(entity), opts.tenantId ?? null, orgScope)
346
387
  : false
347
388
  const searchActive = searchEnabled && hasSearchTokens
389
+ // Opt-in via OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS (default false: the pre-existing
390
+ // rewrite-everything behavior is kept). When enabled, base-column like/ilike is rerouted
391
+ // through search tokens ONLY for encrypted columns, where
392
+ // ILIKE against ciphertext cannot match. On a plaintext column SQL ILIKE is exact, and the token
393
+ // rewrite silently changes the result set: tokenization splits on non-alphanumerics and drops
394
+ // tokens shorter than minTokenLength, so a document-number search like "ZK 1/2026" degrades to
395
+ // the tokens {202, 2026} and matches every record from that year instead of the one document.
396
+ // `ignoreRuntimeHealth` asks the on-disk question -- a column holds ciphertext even while the
397
+ // KMS is down -- so an outage keeps encrypted columns on the token path (#4622).
398
+ // `organizationId: null` is deliberate, not an omission: the service then unions in every
399
+ // organization's map (`fetchAllOrganizationFieldNames`), so a field any org encrypts stays on
400
+ // the token path -- a wider set fails safe. Passing the request's org instead would silently
401
+ // break encrypted-column search for orgs without their own map. That union is an UNCACHED
402
+ // `encryption_maps` read, one extra round-trip per searched list request. `null` means
403
+ // the encryption service could not answer at all; keep the pre-existing rewrite-everything
404
+ // behavior then, because guessing "plaintext" would turn encrypted-column search into an
405
+ // ILIKE-on-ciphertext that matches nothing.
406
+ let encryptedLikeFields: Set<string> | null = null
407
+ if (
408
+ searchActive &&
409
+ searchConfig.useIlikeForNonEncryptedFields === true &&
410
+ searchFilters.some((filter) => !String(filter.field).startsWith('cf:'))
411
+ ) {
412
+ try {
413
+ const service = this.getEncryptionService()
414
+ const readEncryptedFieldNames = service?.getEncryptedFieldNames?.bind(service)
415
+ if (readEncryptedFieldNames) {
416
+ encryptedLikeFields = await resolveEncryptedLikeFieldSet(
417
+ () => readEncryptedFieldNames(
418
+ String(entity),
419
+ opts.tenantId ?? null,
420
+ null,
421
+ { ignoreRuntimeHealth: true },
422
+ ),
423
+ String(entity),
424
+ opts.tenantId ?? null,
425
+ )
426
+ } else if (isTenantDataEncryptionEnabled()) {
427
+ // Encryption is on but the service is unreachable (a swallowed DI failure looks
428
+ // exactly like "no service"): treat the map as UNKNOWN and keep the token rewrite,
429
+ // rather than guessing "plaintext" and running ILIKE against ciphertext.
430
+ encryptedLikeFields = null
431
+ } else {
432
+ // Encryption disabled: nothing is ciphertext at rest, exact ILIKE is always right.
433
+ encryptedLikeFields = new Set()
434
+ }
435
+ } catch (err) {
436
+ // The fallback is safe (the old rewrite-everything behavior), but taking it silently
437
+ // would hide that the gate has stopped working.
438
+ logger.warn('search: encrypted-field map unavailable; keeping the token rewrite for all columns', {
439
+ entity: String(entity),
440
+ error: err instanceof Error ? err.message : String(err),
441
+ })
442
+ encryptedLikeFields = null
443
+ }
444
+ }
348
445
  if (searchFilters.length) {
349
446
  const fields = searchFilters.map((filter) => String(filter.field))
350
447
  this.logSearchDebug('search:init', {
@@ -422,7 +519,13 @@ export class BasicQueryEngine implements QueryEngine {
422
519
  searchActive &&
423
520
  typeof value === 'string' &&
424
521
  fieldName &&
425
- typeof column === 'string'
522
+ typeof column === 'string' &&
523
+ // Plaintext columns keep exact SQL ILIKE -- see the encryptedLikeFields note above. cf:*
524
+ // filters never reach this path (they are applied by the custom-field branches), so this
525
+ // gate only decides base columns. Membership is tested across name-shape candidates --
526
+ // encryption maps may declare `displayName` while the filter carries the column name
527
+ // `display_name`, and a raw comparison would misread that ciphertext column as plaintext.
528
+ (encryptedLikeFields === null || isEncryptedLikeField(encryptedLikeFields, fieldName))
426
529
  ) {
427
530
  const tokens = tokenizeText(String(value), searchConfig)
428
531
  const hashes = tokens.hashes
@@ -8,6 +8,15 @@ export type SearchConfig = {
8
8
  enablePartials: boolean
9
9
  hashAlgorithm: 'sha256' | 'sha1' | 'md5'
10
10
  storeRawTokens: boolean
11
+ /**
12
+ * When true, a like/ilike on a PLAINTEXT base column runs as exact SQL ILIKE instead of being
13
+ * rewritten into an approximate search-token match; encrypted columns always keep the token
14
+ * path (ILIKE against ciphertext cannot match). Off by default: token matching can be faster
15
+ * than an unanchored ILIKE, which may need a full scan without a trigram index — but it is
16
+ * approximate (fragments under minTokenLength vanish, so `ZK 1/2026` degrades to its year and
17
+ * an all-short term drops the predicate). Flip it on when list search must be exact.
18
+ */
19
+ useIlikeForNonEncryptedFields?: boolean
11
20
  blocklistedFields: string[]
12
21
  entityBlocklistedFields?: Record<string, string[]>
13
22
  maxFieldChars?: number
@@ -112,6 +121,7 @@ export function resolveSearchConfig(): SearchConfig {
112
121
  enablePartials: parseBoolean(process.env.OM_SEARCH_ENABLE_PARTIAL, true),
113
122
  hashAlgorithm: parseHashAlgorithm(process.env.OM_SEARCH_HASH_ALGO),
114
123
  storeRawTokens: parseBoolean(process.env.OM_SEARCH_STORE_RAW_TOKENS, false),
124
+ useIlikeForNonEncryptedFields: parseBoolean(process.env.OM_SEARCH_USE_ILIKE_FOR_NON_ENCRYPTED_FIELDS, false),
115
125
  blocklistedFields: blocklist.global,
116
126
  entityBlocklistedFields: blocklist.byEntity,
117
127
  maxFieldChars: parseNumber(process.env.OM_SEARCH_MAX_FIELD_CHARS, DEFAULT_SEARCH_MAX_FIELD_CHARS, 0),