@open-mercato/core 0.6.7-develop.6706.1.b3a4c759bb → 0.6.7-develop.6725.1.07a748cfc2

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.
Files changed (111) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/admin/nav.js +14 -4
  3. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +8 -19
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/lib/backendChrome.js +31 -14
  7. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  8. package/dist/modules/customers/api/utils.js +12 -18
  9. package/dist/modules/customers/api/utils.js.map +2 -2
  10. package/dist/modules/customers/cli.js +11 -4
  11. package/dist/modules/customers/cli.js.map +2 -2
  12. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +4 -2
  13. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  14. package/dist/modules/customers/components/formConfig.js +14 -10
  15. package/dist/modules/customers/components/formConfig.js.map +2 -2
  16. package/dist/modules/dashboards/api/widgets/data/batch/route.js +4 -2
  17. package/dist/modules/dashboards/api/widgets/data/batch/route.js.map +2 -2
  18. package/dist/modules/dashboards/api/widgets/data/route.js +20 -2
  19. package/dist/modules/dashboards/api/widgets/data/route.js.map +2 -2
  20. package/dist/modules/dashboards/lib/aggregations.js +76 -40
  21. package/dist/modules/dashboards/lib/aggregations.js.map +2 -2
  22. package/dist/modules/dashboards/lib/exactDecimal.js +69 -0
  23. package/dist/modules/dashboards/lib/exactDecimal.js.map +7 -0
  24. package/dist/modules/dashboards/services/widgetDataService.js +228 -5
  25. package/dist/modules/dashboards/services/widgetDataService.js.map +2 -2
  26. package/dist/modules/feature_toggles/components/FeatureGuard.js +3 -2
  27. package/dist/modules/feature_toggles/components/FeatureGuard.js.map +2 -2
  28. package/dist/modules/feature_toggles/components/hooks/staleTime.js +5 -0
  29. package/dist/modules/feature_toggles/components/hooks/staleTime.js.map +7 -0
  30. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js +5 -2
  31. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.js.map +2 -2
  32. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js +5 -2
  33. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagJson.js.map +2 -2
  34. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js +5 -2
  35. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagNumber.js.map +2 -2
  36. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js +5 -2
  37. package/dist/modules/feature_toggles/components/hooks/useFeatureFlagString.js.map +2 -2
  38. package/dist/modules/inbox_ops/api/proposals/route.js +16 -1
  39. package/dist/modules/inbox_ops/api/proposals/route.js.map +2 -2
  40. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js +32 -0
  41. package/dist/modules/inbox_ops/subscribers/search-reindex-proposal-created.js.map +7 -0
  42. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +0 -5
  43. package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
  44. package/dist/modules/integrations/widgets/injection/external-ids/widget.js +14 -0
  45. package/dist/modules/integrations/widgets/injection/external-ids/widget.js.map +7 -0
  46. package/dist/modules/messages/lib/searchLookup.js +8 -16
  47. package/dist/modules/messages/lib/searchLookup.js.map +2 -2
  48. package/dist/modules/query_index/api/openapi.js +13 -1
  49. package/dist/modules/query_index/api/openapi.js.map +2 -2
  50. package/dist/modules/query_index/api/status.js +165 -105
  51. package/dist/modules/query_index/api/status.js.map +2 -2
  52. package/dist/modules/query_index/components/QueryIndexesTable.js +128 -41
  53. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  54. package/dist/modules/query_index/lib/batch.js +11 -4
  55. package/dist/modules/query_index/lib/batch.js.map +2 -2
  56. package/dist/modules/query_index/lib/document.js +12 -5
  57. package/dist/modules/query_index/lib/document.js.map +2 -2
  58. package/dist/modules/query_index/lib/engine.js +36 -4
  59. package/dist/modules/query_index/lib/engine.js.map +2 -2
  60. package/dist/modules/query_index/lib/indexer.js +1 -1
  61. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  62. package/dist/modules/query_index/lib/search-tokens.js +7 -4
  63. package/dist/modules/query_index/lib/search-tokens.js.map +2 -2
  64. package/dist/modules/sales/api/documents/factory.js +14 -3
  65. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  66. package/dist/modules/sales/commands/documents.js +22 -1
  67. package/dist/modules/sales/commands/documents.js.map +2 -2
  68. package/dist/modules/sales/components/useSalesChannelsEnabled.js +2 -35
  69. package/dist/modules/sales/components/useSalesChannelsEnabled.js.map +2 -2
  70. package/dist/modules/sales/data/validators.js +25 -4
  71. package/dist/modules/sales/data/validators.js.map +2 -2
  72. package/package.json +7 -7
  73. package/src/modules/auth/api/admin/nav.ts +24 -4
  74. package/src/modules/auth/api/users/route.ts +8 -28
  75. package/src/modules/auth/lib/backendChrome.tsx +51 -13
  76. package/src/modules/customers/api/utils.ts +12 -31
  77. package/src/modules/customers/cli.ts +11 -4
  78. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +4 -0
  79. package/src/modules/customers/components/formConfig.tsx +12 -8
  80. package/src/modules/dashboards/api/widgets/data/batch/route.ts +4 -0
  81. package/src/modules/dashboards/api/widgets/data/route.ts +21 -0
  82. package/src/modules/dashboards/lib/aggregations.ts +122 -43
  83. package/src/modules/dashboards/lib/exactDecimal.ts +96 -0
  84. package/src/modules/dashboards/services/widgetDataService.ts +342 -4
  85. package/src/modules/feature_toggles/components/FeatureGuard.tsx +9 -2
  86. package/src/modules/feature_toggles/components/hooks/staleTime.ts +5 -0
  87. package/src/modules/feature_toggles/components/hooks/useFeatureFlagBoolean.ts +13 -1
  88. package/src/modules/feature_toggles/components/hooks/useFeatureFlagJson.ts +12 -3
  89. package/src/modules/feature_toggles/components/hooks/useFeatureFlagNumber.ts +10 -1
  90. package/src/modules/feature_toggles/components/hooks/useFeatureFlagString.ts +10 -1
  91. package/src/modules/inbox_ops/api/proposals/route.ts +20 -1
  92. package/src/modules/inbox_ops/subscribers/search-reindex-proposal-created.ts +39 -0
  93. package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +0 -5
  94. package/src/modules/integrations/widgets/injection/external-ids/widget.ts +13 -0
  95. package/src/modules/messages/lib/searchLookup.ts +11 -34
  96. package/src/modules/query_index/api/openapi.ts +12 -0
  97. package/src/modules/query_index/api/status.ts +238 -136
  98. package/src/modules/query_index/components/QueryIndexesTable.tsx +192 -46
  99. package/src/modules/query_index/i18n/de.json +5 -3
  100. package/src/modules/query_index/i18n/en.json +5 -3
  101. package/src/modules/query_index/i18n/es.json +5 -3
  102. package/src/modules/query_index/i18n/pl.json +5 -3
  103. package/src/modules/query_index/lib/batch.ts +11 -4
  104. package/src/modules/query_index/lib/document.ts +35 -4
  105. package/src/modules/query_index/lib/engine.ts +53 -5
  106. package/src/modules/query_index/lib/indexer.ts +1 -1
  107. package/src/modules/query_index/lib/search-tokens.ts +13 -4
  108. package/src/modules/sales/api/documents/factory.ts +28 -3
  109. package/src/modules/sales/commands/documents.ts +26 -2
  110. package/src/modules/sales/components/useSalesChannelsEnabled.ts +2 -48
  111. package/src/modules/sales/data/validators.ts +50 -5
@@ -1,3 +1,7 @@
1
+ import {
2
+ isSearchFieldBlocklisted,
3
+ resolveSearchConfig
4
+ } from "@open-mercato/shared/lib/search/config";
1
5
  const AGGREGATE_SEARCH_FIELD = "search_text";
2
6
  function normalizeScopeValue(value) {
3
7
  if (value === void 0 || value === null || value === "") return null;
@@ -11,11 +15,12 @@ function normalizeValue(value) {
11
15
  if (value === void 0) return null;
12
16
  return value;
13
17
  }
14
- function collectAggregateSearchValues(field, value) {
18
+ function collectAggregateSearchValues(field, value, entityType, config) {
15
19
  const lower = field.toLowerCase();
16
20
  if (lower === AGGREGATE_SEARCH_FIELD || lower === "id" || lower.endsWith("_id") || lower.endsWith(".id") || lower.endsWith("_at") || ["created_at", "updated_at", "deleted_at", "tenant_id", "organization_id"].includes(lower)) {
17
21
  return [];
18
22
  }
23
+ if (isSearchFieldBlocklisted(field, entityType, config)) return [];
19
24
  if (typeof value === "string") {
20
25
  const trimmed = value.trim();
21
26
  return trimmed.length > 0 ? [trimmed] : [];
@@ -25,11 +30,13 @@ function collectAggregateSearchValues(field, value) {
25
30
  }
26
31
  return [];
27
32
  }
28
- function attachAggregateSearchField(doc) {
33
+ function attachAggregateSearchField(doc, options = {}) {
34
+ const config = options.config ?? resolveSearchConfig();
35
+ const entityType = options.entityType ?? null;
29
36
  const parts = [];
30
37
  const seen = /* @__PURE__ */ new Set();
31
38
  for (const [field, value] of Object.entries(doc)) {
32
- const values = collectAggregateSearchValues(field, value);
39
+ const values = collectAggregateSearchValues(field, value, entityType, config);
33
40
  for (const entry of values) {
34
41
  const key = entry.toLowerCase();
35
42
  if (seen.has(key)) continue;
@@ -42,7 +49,7 @@ function attachAggregateSearchField(doc) {
42
49
  }
43
50
  return doc;
44
51
  }
45
- function buildIndexDocument(baseRow, customFieldValues = [], scope = {}) {
52
+ function buildIndexDocument(baseRow, customFieldValues = [], scope = {}, options = {}) {
46
53
  const doc = {};
47
54
  for (const [key, value] of Object.entries(baseRow)) {
48
55
  doc[key] = value;
@@ -75,7 +82,7 @@ function buildIndexDocument(baseRow, customFieldValues = [], scope = {}) {
75
82
  doc[key] = values;
76
83
  }
77
84
  }
78
- return attachAggregateSearchField(doc);
85
+ return attachAggregateSearchField(doc, options);
79
86
  }
80
87
  export {
81
88
  AGGREGATE_SEARCH_FIELD,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/query_index/lib/document.ts"],
4
- "sourcesContent": ["export type IndexDocumentScope = {\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport type IndexCustomFieldValue = {\n key: string\n value: unknown\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport const AGGREGATE_SEARCH_FIELD = 'search_text'\n\nfunction normalizeScopeValue(value: string | null | undefined): string | null {\n if (value === undefined || value === null || value === '') return null\n return value\n}\n\nfunction isScopedValueVisible(\n scopeValue: string | null,\n fieldValue: string | null,\n): boolean {\n if (scopeValue === null) return fieldValue === null\n return fieldValue === null || fieldValue === scopeValue\n}\n\nfunction normalizeValue(value: unknown): unknown {\n if (value === undefined) return null\n return value\n}\n\nfunction collectAggregateSearchValues(field: string, value: unknown): string[] {\n const lower = field.toLowerCase()\n if (\n lower === AGGREGATE_SEARCH_FIELD\n || lower === 'id'\n || lower.endsWith('_id')\n || lower.endsWith('.id')\n || lower.endsWith('_at')\n || ['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)\n ) {\n return []\n }\n\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? [trimmed] : []\n }\n\n if (Array.isArray(value)) {\n return value\n .filter((entry): entry is string => typeof entry === 'string')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n }\n\n return []\n}\n\nexport function attachAggregateSearchField(doc: Record<string, unknown>): Record<string, unknown> {\n const parts: string[] = []\n const seen = new Set<string>()\n\n for (const [field, value] of Object.entries(doc)) {\n const values = collectAggregateSearchValues(field, value)\n for (const entry of values) {\n const key = entry.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n parts.push(entry)\n }\n }\n\n if (parts.length > 0) {\n doc[AGGREGATE_SEARCH_FIELD] = parts.join('\\n')\n }\n\n return doc\n}\n\nexport function buildIndexDocument(\n baseRow: Record<string, unknown>,\n customFieldValues: Iterable<IndexCustomFieldValue> = [],\n scope: IndexDocumentScope = {},\n): Record<string, unknown> {\n const doc: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(baseRow)) {\n doc[key] = value\n }\n\n const scopeOrg = normalizeScopeValue(scope.organizationId ?? null)\n const scopeTenant = normalizeScopeValue(scope.tenantId ?? null)\n\n const grouped = new Map<string, unknown[]>()\n for (const field of customFieldValues) {\n const org = normalizeScopeValue(field.organizationId ?? null)\n const tenant = normalizeScopeValue(field.tenantId ?? null)\n\n if (!isScopedValueVisible(scopeOrg, org)) continue\n if (!isScopedValueVisible(scopeTenant, tenant)) continue\n\n const bucketKey = `cf:${field.key}`\n let bucket = grouped.get(bucketKey)\n if (!bucket) {\n bucket = []\n grouped.set(bucketKey, bucket)\n }\n\n const { value } = field\n if (Array.isArray(value)) {\n for (const entry of value) bucket.push(normalizeValue(entry))\n } else {\n bucket.push(normalizeValue(value))\n }\n }\n\n for (const [key, values] of grouped.entries()) {\n if (values.length === 1) {\n doc[key] = values[0]\n } else if (values.length > 1) {\n doc[key] = values\n }\n }\n\n return attachAggregateSearchField(doc)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n isSearchFieldBlocklisted,\n resolveSearchConfig,\n type SearchConfig,\n} from '@open-mercato/shared/lib/search/config'\n\nexport type IndexDocumentScope = {\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport type IndexCustomFieldValue = {\n key: string\n value: unknown\n organizationId?: string | null\n tenantId?: string | null\n}\n\nexport const AGGREGATE_SEARCH_FIELD = 'search_text'\n\n/**\n * Controls which fields may contribute text to the `search_text` aggregate.\n *\n * Both properties are optional so existing callers keep working unchanged: an\n * omitted `config` is resolved from the environment, and an omitted `entityType`\n * means only the global blocklist entries apply.\n */\nexport type AggregateSearchOptions = {\n entityType?: string | null\n config?: SearchConfig\n}\n\nfunction normalizeScopeValue(value: string | null | undefined): string | null {\n if (value === undefined || value === null || value === '') return null\n return value\n}\n\nfunction isScopedValueVisible(\n scopeValue: string | null,\n fieldValue: string | null,\n): boolean {\n if (scopeValue === null) return fieldValue === null\n return fieldValue === null || fieldValue === scopeValue\n}\n\nfunction normalizeValue(value: unknown): unknown {\n if (value === undefined) return null\n return value\n}\n\nfunction collectAggregateSearchValues(\n field: string,\n value: unknown,\n entityType: string | null,\n config: SearchConfig,\n): string[] {\n const lower = field.toLowerCase()\n if (\n lower === AGGREGATE_SEARCH_FIELD\n || lower === 'id'\n || lower.endsWith('_id')\n || lower.endsWith('.id')\n || lower.endsWith('_at')\n || ['created_at', 'updated_at', 'deleted_at', 'tenant_id', 'organization_id'].includes(lower)\n ) {\n return []\n }\n\n if (isSearchFieldBlocklisted(field, entityType, config)) return []\n\n if (typeof value === 'string') {\n const trimmed = value.trim()\n return trimmed.length > 0 ? [trimmed] : []\n }\n\n if (Array.isArray(value)) {\n return value\n .filter((entry): entry is string => typeof entry === 'string')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0)\n }\n\n return []\n}\n\nexport function attachAggregateSearchField(\n doc: Record<string, unknown>,\n options: AggregateSearchOptions = {},\n): Record<string, unknown> {\n const config = options.config ?? resolveSearchConfig()\n const entityType = options.entityType ?? null\n const parts: string[] = []\n const seen = new Set<string>()\n\n for (const [field, value] of Object.entries(doc)) {\n const values = collectAggregateSearchValues(field, value, entityType, config)\n for (const entry of values) {\n const key = entry.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n parts.push(entry)\n }\n }\n\n if (parts.length > 0) {\n doc[AGGREGATE_SEARCH_FIELD] = parts.join('\\n')\n }\n\n return doc\n}\n\nexport function buildIndexDocument(\n baseRow: Record<string, unknown>,\n customFieldValues: Iterable<IndexCustomFieldValue> = [],\n scope: IndexDocumentScope = {},\n options: AggregateSearchOptions = {},\n): Record<string, unknown> {\n const doc: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(baseRow)) {\n doc[key] = value\n }\n\n const scopeOrg = normalizeScopeValue(scope.organizationId ?? null)\n const scopeTenant = normalizeScopeValue(scope.tenantId ?? null)\n\n const grouped = new Map<string, unknown[]>()\n for (const field of customFieldValues) {\n const org = normalizeScopeValue(field.organizationId ?? null)\n const tenant = normalizeScopeValue(field.tenantId ?? null)\n\n if (!isScopedValueVisible(scopeOrg, org)) continue\n if (!isScopedValueVisible(scopeTenant, tenant)) continue\n\n const bucketKey = `cf:${field.key}`\n let bucket = grouped.get(bucketKey)\n if (!bucket) {\n bucket = []\n grouped.set(bucketKey, bucket)\n }\n\n const { value } = field\n if (Array.isArray(value)) {\n for (const entry of value) bucket.push(normalizeValue(entry))\n } else {\n bucket.push(normalizeValue(value))\n }\n }\n\n for (const [key, values] of grouped.entries()) {\n if (values.length === 1) {\n doc[key] = values[0]\n } else if (values.length > 1) {\n doc[key] = values\n }\n }\n\n return attachAggregateSearchField(doc, options)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -15,6 +15,7 @@ import {
15
15
  import { resolveSearchConfig } from "@open-mercato/shared/lib/search/config";
16
16
  import { tokenizeText } from "@open-mercato/shared/lib/search/tokenize";
17
17
  import { runBeforeQueryPipeline, runAfterQueryPipeline } from "@open-mercato/shared/lib/query/query-extension-runner";
18
+ import { warnOnCiphertextLikeFallback } from "@open-mercato/shared/lib/query/ciphertext-search-warning";
18
19
  import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from "@open-mercato/shared/lib/query/encrypted-sort";
19
20
  import { mapWithConcurrency } from "@open-mercato/shared/lib/query/bounded-decrypt";
20
21
  import { createLogger } from "@open-mercato/shared/lib/logger";
@@ -316,10 +317,10 @@ class HybridQueryEngine {
316
317
  }
317
318
  }
318
319
  const searchSources = indexSources.map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn })).filter((src) => src.recordIdColumn && src.entity);
319
- const hasSearchTokens = searchEnabled && searchSources.length ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope) : false;
320
+ const searchFilters = normalizedFilters.filter((filter) => filter.op === "like" || filter.op === "ilike");
321
+ const hasSearchTokens = searchEnabled && searchSources.length && searchFilters.length ? await this.searchSourcesHaveTokens(searchSources, opts.tenantId ?? null, orgScope) : false;
320
322
  const searchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens };
321
323
  const joinSearchAvailability = /* @__PURE__ */ new Map();
322
- const searchFilters = normalizeFilters(opts.filters).filter((filter) => filter.op === "like" || filter.op === "ilike");
323
324
  if (searchFilters.length) {
324
325
  this.logSearchDebug("search:init", {
325
326
  entity,
@@ -346,6 +347,36 @@ class HybridQueryEngine {
346
347
  organizationScope: orgScope,
347
348
  searchSources
348
349
  });
350
+ const baseSearchFilters = [...baseFilters, ...cfFilters].filter((filter) => filter.op === "like" || filter.op === "ilike");
351
+ const fallbackFields = baseSearchFilters.filter((filter) => !searchRuntime.enabled || typeof filter.value !== "string" || tokenizeText(filter.value, searchConfig).hashes.length === 0).map((filter) => String(filter.field));
352
+ if (fallbackFields.length) {
353
+ await warnOnCiphertextLikeFallback({
354
+ entity: String(entity),
355
+ fields: fallbackFields,
356
+ tenantId: opts.tenantId ?? null,
357
+ // `searchEnabled` also folds in the missing-table case, which is
358
+ // "no usable tokens" rather than "the operator switched search off".
359
+ reason: searchRuntime.enabled ? "no-indexable-tokens" : searchConfig.enabled ? "no-search-tokens" : "search-disabled",
360
+ service: this.getEncryptionService()
361
+ });
362
+ }
363
+ }
364
+ for (const [alias, joinedFilters] of joinFilters) {
365
+ const filters = joinedFilters.filter((entry) => entry.op === "like" || entry.op === "ilike");
366
+ if (!filters.length) continue;
367
+ const join = joinMap.get(alias);
368
+ if (!join?.entityId) continue;
369
+ const hasJoinedTokens = searchEnabled ? await this.hasSearchTokens(String(join.entityId), opts.tenantId ?? null, orgScope) : false;
370
+ joinSearchAvailability.set(String(join.entityId), hasJoinedTokens);
371
+ const fallbackFields = filters.filter((filter) => !hasJoinedTokens || typeof filter.value !== "string" || tokenizeText(filter.value, searchConfig).hashes.length === 0).map((filter) => filter.column);
372
+ if (!fallbackFields.length) continue;
373
+ await warnOnCiphertextLikeFallback({
374
+ entity: String(join.entityId),
375
+ fields: fallbackFields,
376
+ tenantId: opts.tenantId ?? null,
377
+ reason: hasJoinedTokens ? "no-indexable-tokens" : searchConfig.enabled ? "no-search-tokens" : "search-disabled",
378
+ service: this.getEncryptionService()
379
+ });
349
380
  }
350
381
  const hasNonBaseSearchSource = searchSources.some(
351
382
  (src) => src.entity !== String(entity) || src.recordIdColumn !== "b.id"
@@ -1356,9 +1387,11 @@ class HybridQueryEngine {
1356
1387
  const alias = "ce";
1357
1388
  const orgScope = this.resolveOrganizationScope(opts);
1358
1389
  if (!opts.tenantId) throw new Error("QueryEngine: tenantId is required");
1390
+ const normalizedFilters = normalizeFilters(opts.filters);
1391
+ const hasSearchFilter = normalizedFilters.some((filter) => filter.op === "like" || filter.op === "ilike");
1359
1392
  const searchConfig = resolveSearchConfig();
1360
1393
  const searchEnabled = searchConfig.enabled && await this.tableExists("search_tokens");
1361
- const hasSearchTokens = searchEnabled ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope) : false;
1394
+ const hasSearchTokens = searchEnabled && hasSearchFilter ? await this.hasSearchTokens(entity, opts.tenantId ?? null, orgScope) : false;
1362
1395
  const searchRuntime = {
1363
1396
  enabled: searchEnabled && hasSearchTokens,
1364
1397
  config: searchConfig,
@@ -1366,7 +1399,6 @@ class HybridQueryEngine {
1366
1399
  tenantId: opts.tenantId ?? null,
1367
1400
  mintAlias: createSearchAliasMinter()
1368
1401
  };
1369
- const normalizedFilters = normalizeFilters(opts.filters);
1370
1402
  const applyScope = (q) => {
1371
1403
  let next = q.where(`${alias}.entity_type`, "=", entity).where(`${alias}.tenant_id`, "=", opts.tenantId);
1372
1404
  if (orgScope) {