@open-mercato/core 0.6.7-develop.6785.1.1dd7cfac55 → 0.6.7-develop.6795.1.8a3f27921c
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities.ids.generated.js +1 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/modules/auth/backend/settings/page.js +14 -9
- package/dist/modules/auth/backend/settings/page.js.map +2 -2
- package/dist/modules/customers/cli.js +17 -7
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +229 -216
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/search.js +50 -29
- package/dist/modules/customers/search.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/[id]/route.js +22 -9
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/route.js +13 -4
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +7 -2
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +7 -2
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +8 -3
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +7 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/route.js +8 -3
- package/dist/modules/data_sync/api/runs/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/runs.js +7 -2
- package/dist/modules/data_sync/api/runs.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +19 -6
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +13 -4
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/api/validate.js +9 -4
- package/dist/modules/data_sync/api/validate.js.map +2 -2
- package/dist/modules/design_system/acl.js +9 -0
- package/dist/modules/design_system/acl.js.map +7 -0
- package/dist/modules/design_system/backend/design-system/page.js +11 -0
- package/dist/modules/design_system/backend/design-system/page.js.map +7 -0
- package/dist/modules/design_system/backend/design-system/page.meta.js +15 -0
- package/dist/modules/design_system/backend/design-system/page.meta.js.map +7 -0
- package/dist/modules/design_system/gallery/components/CodeSnippet.js +52 -0
- package/dist/modules/design_system/gallery/components/CodeSnippet.js.map +7 -0
- package/dist/modules/design_system/gallery/components/EntryCard.js +63 -0
- package/dist/modules/design_system/gallery/components/EntryCard.js.map +7 -0
- package/dist/modules/design_system/gallery/components/GalleryShell.js +163 -0
- package/dist/modules/design_system/gallery/components/GalleryShell.js.map +7 -0
- package/dist/modules/design_system/gallery/components/VariantPreview.js +9 -0
- package/dist/modules/design_system/gallery/components/VariantPreview.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/banners.js +186 -0
- package/dist/modules/design_system/gallery/entries/banners.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/buttons.js +361 -0
- package/dist/modules/design_system/gallery/entries/buttons.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/charts.js +457 -0
- package/dist/modules/design_system/gallery/entries/charts.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/dates.js +306 -0
- package/dist/modules/design_system/gallery/entries/dates.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/detail.js +358 -0
- package/dist/modules/design_system/gallery/entries/detail.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/display.js +785 -0
- package/dist/modules/design_system/gallery/entries/display.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/feedback.js +658 -0
- package/dist/modules/design_system/gallery/entries/feedback.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/filters.js +505 -0
- package/dist/modules/design_system/gallery/entries/filters.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/foundations.js +453 -0
- package/dist/modules/design_system/gallery/entries/foundations.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/icons.js +140 -0
- package/dist/modules/design_system/gallery/entries/icons.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/inputs.js +1217 -0
- package/dist/modules/design_system/gallery/entries/inputs.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/messages.js +334 -0
- package/dist/modules/design_system/gallery/entries/messages.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/navigation.js +475 -0
- package/dist/modules/design_system/gallery/entries/navigation.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/notifications.js +257 -0
- package/dist/modules/design_system/gallery/entries/notifications.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/overlays.js +576 -0
- package/dist/modules/design_system/gallery/entries/overlays.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/scaffolding.js +378 -0
- package/dist/modules/design_system/gallery/entries/scaffolding.js.map +7 -0
- package/dist/modules/design_system/gallery/entries/schedule.js +297 -0
- package/dist/modules/design_system/gallery/entries/schedule.js.map +7 -0
- package/dist/modules/design_system/gallery/registry.js +139 -0
- package/dist/modules/design_system/gallery/registry.js.map +7 -0
- package/dist/modules/design_system/gallery/types.js +1 -0
- package/dist/modules/design_system/gallery/types.js.map +7 -0
- package/dist/modules/design_system/index.js +9 -0
- package/dist/modules/design_system/index.js.map +7 -0
- package/dist/modules/design_system/setup.js +14 -0
- package/dist/modules/design_system/setup.js.map +7 -0
- package/dist/modules/inbox_ops/search.js +15 -4
- package/dist/modules/inbox_ops/search.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +11 -5
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/health/route.js +6 -3
- package/dist/modules/integrations/api/[id]/health/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +6 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +6 -3
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +6 -3
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/logs/route.js +6 -3
- package/dist/modules/integrations/api/logs/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +6 -3
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/api/umes-read.js +2 -2
- package/dist/modules/integrations/api/umes-read.js.map +2 -2
- package/dist/modules/integrations/lib/organization-scope.js +2 -9
- package/dist/modules/integrations/lib/organization-scope.js.map +2 -2
- package/dist/modules/messages/search.js +10 -5
- package/dist/modules/messages/search.js.map +2 -2
- package/dist/modules/query_index/data/entities.js +2 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +32 -50
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js +17 -0
- package/dist/modules/query_index/migrations/Migration20260731105052_query_index.js.map +7 -0
- package/generated/entities.ids.generated.ts +1 -0
- package/package.json +16 -16
- package/src/modules/auth/backend/settings/page.tsx +17 -8
- package/src/modules/customers/cli.ts +17 -7
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +24 -13
- package/src/modules/customers/i18n/de.json +17 -0
- package/src/modules/customers/i18n/en.json +17 -0
- package/src/modules/customers/i18n/es.json +17 -0
- package/src/modules/customers/i18n/pl.json +17 -0
- package/src/modules/customers/search.ts +59 -27
- package/src/modules/data_sync/AGENTS.md +1 -0
- package/src/modules/data_sync/api/mappings/[id]/route.ts +22 -9
- package/src/modules/data_sync/api/mappings/route.ts +13 -4
- package/src/modules/data_sync/api/options.ts +7 -2
- package/src/modules/data_sync/api/run.ts +7 -2
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +8 -3
- package/src/modules/data_sync/api/runs/[id]/retry.ts +7 -2
- package/src/modules/data_sync/api/runs/[id]/route.ts +8 -3
- package/src/modules/data_sync/api/runs.ts +7 -2
- package/src/modules/data_sync/api/schedules/[id]/route.ts +19 -6
- package/src/modules/data_sync/api/schedules/route.ts +13 -4
- package/src/modules/data_sync/api/validate.ts +9 -4
- package/src/modules/design_system/AGENTS.md +34 -0
- package/src/modules/design_system/acl.ts +5 -0
- package/src/modules/design_system/backend/design-system/page.meta.ts +11 -0
- package/src/modules/design_system/backend/design-system/page.tsx +14 -0
- package/src/modules/design_system/gallery/components/CodeSnippet.tsx +55 -0
- package/src/modules/design_system/gallery/components/EntryCard.tsx +95 -0
- package/src/modules/design_system/gallery/components/GalleryShell.tsx +206 -0
- package/src/modules/design_system/gallery/components/VariantPreview.tsx +15 -0
- package/src/modules/design_system/gallery/entries/banners.tsx +226 -0
- package/src/modules/design_system/gallery/entries/buttons.tsx +422 -0
- package/src/modules/design_system/gallery/entries/charts.tsx +518 -0
- package/src/modules/design_system/gallery/entries/dates.tsx +325 -0
- package/src/modules/design_system/gallery/entries/detail.tsx +403 -0
- package/src/modules/design_system/gallery/entries/display.tsx +893 -0
- package/src/modules/design_system/gallery/entries/feedback.tsx +746 -0
- package/src/modules/design_system/gallery/entries/filters.tsx +570 -0
- package/src/modules/design_system/gallery/entries/foundations.tsx +529 -0
- package/src/modules/design_system/gallery/entries/icons.tsx +168 -0
- package/src/modules/design_system/gallery/entries/inputs.tsx +1516 -0
- package/src/modules/design_system/gallery/entries/messages.tsx +339 -0
- package/src/modules/design_system/gallery/entries/navigation.tsx +573 -0
- package/src/modules/design_system/gallery/entries/notifications.tsx +285 -0
- package/src/modules/design_system/gallery/entries/overlays.tsx +677 -0
- package/src/modules/design_system/gallery/entries/scaffolding.tsx +430 -0
- package/src/modules/design_system/gallery/entries/schedule.tsx +307 -0
- package/src/modules/design_system/gallery/registry.ts +152 -0
- package/src/modules/design_system/gallery/types.ts +32 -0
- package/src/modules/design_system/i18n/.hardcoded-allowlist.json +71 -0
- package/src/modules/design_system/i18n/de.json +46 -0
- package/src/modules/design_system/i18n/en.json +46 -0
- package/src/modules/design_system/i18n/es.json +46 -0
- package/src/modules/design_system/i18n/pl.json +46 -0
- package/src/modules/design_system/index.ts +7 -0
- package/src/modules/design_system/setup.ts +12 -0
- package/src/modules/inbox_ops/i18n/de.json +3 -0
- package/src/modules/inbox_ops/i18n/en.json +3 -0
- package/src/modules/inbox_ops/i18n/es.json +3 -0
- package/src/modules/inbox_ops/i18n/pl.json +3 -0
- package/src/modules/inbox_ops/search.ts +20 -4
- package/src/modules/integrations/api/[id]/credentials/route.ts +11 -5
- package/src/modules/integrations/api/[id]/health/route.ts +6 -3
- package/src/modules/integrations/api/[id]/route.ts +6 -3
- package/src/modules/integrations/api/[id]/state/route.ts +6 -3
- package/src/modules/integrations/api/[id]/version/route.ts +6 -3
- package/src/modules/integrations/api/logs/route.ts +6 -3
- package/src/modules/integrations/api/route.ts +6 -3
- package/src/modules/integrations/api/umes-read.ts +2 -2
- package/src/modules/integrations/lib/organization-scope.ts +5 -25
- package/src/modules/messages/i18n/de.json +2 -0
- package/src/modules/messages/i18n/en.json +2 -0
- package/src/modules/messages/i18n/es.json +2 -0
- package/src/modules/messages/i18n/pl.json +2 -0
- package/src/modules/messages/search.ts +11 -5
- package/src/modules/query_index/data/entities.ts +7 -0
- package/src/modules/query_index/lib/engine.ts +40 -63
- package/src/modules/query_index/migrations/.snapshot-open-mercato.json +12 -0
- package/src/modules/query_index/migrations/Migration20260731105052_query_index.ts +35 -0
|
@@ -20,6 +20,14 @@ import {
|
|
|
20
20
|
type ResolvedJoin,
|
|
21
21
|
} from '@open-mercato/shared/lib/query/join-utils'
|
|
22
22
|
import { resolveSearchConfig, type SearchConfig } from '@open-mercato/shared/lib/search/config'
|
|
23
|
+
import {
|
|
24
|
+
createSearchTokenAvailability,
|
|
25
|
+
isSearchFilterOp,
|
|
26
|
+
hasSearchFilter,
|
|
27
|
+
type SearchTokenAvailability,
|
|
28
|
+
type SearchTokenProbeDb,
|
|
29
|
+
type SearchTokenProbeQueryBuilder,
|
|
30
|
+
} from '@open-mercato/shared/lib/search/availability'
|
|
23
31
|
import { tokenizeText } from '@open-mercato/shared/lib/search/tokenize'
|
|
24
32
|
import { runBeforeQueryPipeline, runAfterQueryPipeline, type QueryExtensionContext } from '@open-mercato/shared/lib/query/query-extension-runner'
|
|
25
33
|
import { warnOnCiphertextLikeFallback } from '@open-mercato/shared/lib/query/ciphertext-search-warning'
|
|
@@ -146,6 +154,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
146
154
|
private autoReindexEnabled: boolean | null = null
|
|
147
155
|
private coverageOptimizationEnabled: boolean | null = null
|
|
148
156
|
private pendingCoverageRefreshKeys = new Set<string>()
|
|
157
|
+
private searchAvailabilityInstance: SearchTokenAvailability | null = null
|
|
149
158
|
|
|
150
159
|
constructor(
|
|
151
160
|
private em: EntityManager,
|
|
@@ -174,6 +183,22 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
174
183
|
throw new Error('HybridQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)')
|
|
175
184
|
}
|
|
176
185
|
|
|
186
|
+
private searchAvailability(): SearchTokenAvailability {
|
|
187
|
+
if (!this.searchAvailabilityInstance) {
|
|
188
|
+
this.searchAvailabilityInstance = createSearchTokenAvailability({
|
|
189
|
+
getDb: () => this.getDb() as unknown as SearchTokenProbeDb,
|
|
190
|
+
getConfig: resolveSearchConfig,
|
|
191
|
+
applyOrganizationScope: (query, column, scope) => this.applyOrganizationScope(
|
|
192
|
+
query as unknown as AnyBuilder,
|
|
193
|
+
column,
|
|
194
|
+
scope,
|
|
195
|
+
) as unknown as SearchTokenProbeQueryBuilder,
|
|
196
|
+
logDebug: (event, payload) => this.logSearchDebug(event, payload),
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
return this.searchAvailabilityInstance
|
|
200
|
+
}
|
|
201
|
+
|
|
177
202
|
async query<T = unknown>(entity: EntityId, opts: QueryOptions = {}): Promise<QueryResult<T>> {
|
|
178
203
|
const ext: QueryExtensionsConfig | undefined = opts.extensions
|
|
179
204
|
let hybridExtCtx: QueryExtensionContext | null = null
|
|
@@ -251,7 +276,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
251
276
|
profiler.mark('query:base_table_resolved')
|
|
252
277
|
const searchConfig = resolveSearchConfig()
|
|
253
278
|
const orgScope = this.resolveOrganizationScope(opts)
|
|
254
|
-
const searchEnabled =
|
|
279
|
+
const searchEnabled = await this.searchAvailability().staticEnabled()
|
|
255
280
|
|
|
256
281
|
const baseExists = await profiler.measure('base_table_exists', () => this.tableExists(baseTable))
|
|
257
282
|
if (!baseExists) {
|
|
@@ -416,16 +441,19 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
416
441
|
const searchSources: SearchTokenSource[] = indexSources
|
|
417
442
|
.map((src) => ({ entity: String(src.entityId), recordIdColumn: src.recordIdColumn }))
|
|
418
443
|
.filter((src) => src.recordIdColumn && src.entity)
|
|
419
|
-
const searchFilters = normalizedFilters.filter((filter) => filter.op
|
|
420
|
-
|
|
444
|
+
const searchFilters = normalizedFilters.filter((filter) => isSearchFilterOp(filter.op))
|
|
445
|
+
const sourceSearchFilters = [
|
|
446
|
+
...baseFilters,
|
|
447
|
+
...normalizedFilters.filter((filter) => String(filter.field).startsWith('cf:')),
|
|
448
|
+
].filter((filter) => isSearchFilterOp(filter.op))
|
|
449
|
+
// Probe `search_tokens` only when this query actually searches (#4723). Every consumer of
|
|
421
450
|
// `searchRuntime.enabled` already sits behind a like/ilike guard, so on a plain list load the
|
|
422
451
|
// answer is unused — and the probe is a `LIMIT 1` the planner can resolve as a seq scan over a
|
|
423
452
|
// large `search_tokens`. The join path below already probes lazily for the same reason.
|
|
424
|
-
const hasSearchTokens = searchEnabled && searchSources.length &&
|
|
425
|
-
? await this.
|
|
453
|
+
const hasSearchTokens = searchEnabled && searchSources.length && sourceSearchFilters.length
|
|
454
|
+
? await this.searchAvailability().anySourceHasTokens(searchSources, opts.tenantId ?? null, orgScope)
|
|
426
455
|
: false
|
|
427
456
|
const searchRuntime: SearchRuntime = { ...searchRuntimeBase, searchSources, enabled: searchEnabled && hasSearchTokens }
|
|
428
|
-
const joinSearchAvailability = new Map<string, boolean>()
|
|
429
457
|
if (searchFilters.length) {
|
|
430
458
|
this.logSearchDebug('search:init', {
|
|
431
459
|
entity,
|
|
@@ -476,9 +504,8 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
476
504
|
const join = joinMap.get(alias)
|
|
477
505
|
if (!join?.entityId) continue
|
|
478
506
|
const hasJoinedTokens = searchEnabled
|
|
479
|
-
? await this.
|
|
507
|
+
? await this.searchAvailability().hasTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
|
|
480
508
|
: false
|
|
481
|
-
joinSearchAvailability.set(String(join.entityId), hasJoinedTokens)
|
|
482
509
|
const fallbackFields = filters
|
|
483
510
|
.filter((filter) => !hasJoinedTokens || typeof filter.value !== 'string' || tokenizeText(filter.value, searchConfig).hashes.length === 0)
|
|
484
511
|
.map((filter) => filter.column)
|
|
@@ -784,11 +811,7 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
784
811
|
if (!['like', 'ilike'].includes(filter.op)) return false
|
|
785
812
|
if (typeof filter.value !== 'string' || filter.value.trim().length === 0) return false
|
|
786
813
|
|
|
787
|
-
|
|
788
|
-
if (searchAvailable === undefined) {
|
|
789
|
-
searchAvailable = await this.hasSearchTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
|
|
790
|
-
joinSearchAvailability.set(join.entityId, searchAvailable)
|
|
791
|
-
}
|
|
814
|
+
const searchAvailable = await this.searchAvailability().hasTokens(String(join.entityId), opts.tenantId ?? null, orgScope)
|
|
792
815
|
if (!searchAvailable) return false
|
|
793
816
|
|
|
794
817
|
const tokens = tokenizeText(String(filter.value), searchConfig)
|
|
@@ -1683,13 +1706,11 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1683
1706
|
if (!opts.tenantId) throw new Error('QueryEngine: tenantId is required')
|
|
1684
1707
|
|
|
1685
1708
|
const normalizedFilters = normalizeFilters(opts.filters)
|
|
1686
|
-
const hasSearchFilter = normalizedFilters.some((filter) => filter.op === 'like' || filter.op === 'ilike')
|
|
1687
|
-
|
|
1688
1709
|
const searchConfig = resolveSearchConfig()
|
|
1689
|
-
const searchEnabled =
|
|
1690
|
-
// Same gate as `query()
|
|
1691
|
-
const hasSearchTokens = searchEnabled && hasSearchFilter
|
|
1692
|
-
? await this.
|
|
1710
|
+
const searchEnabled = await this.searchAvailability().staticEnabled()
|
|
1711
|
+
// Same gate as `query()` (#4723): without a like/ilike filter the probe's answer is never read.
|
|
1712
|
+
const hasSearchTokens = searchEnabled && hasSearchFilter(normalizedFilters)
|
|
1713
|
+
? await this.searchAvailability().hasTokens(entity, opts.tenantId ?? null, orgScope)
|
|
1693
1714
|
: false
|
|
1694
1715
|
const searchRuntime: SearchRuntime = {
|
|
1695
1716
|
enabled: searchEnabled && hasSearchTokens,
|
|
@@ -1829,50 +1850,6 @@ export class HybridQueryEngine implements QueryEngine {
|
|
|
1829
1850
|
return !!exists
|
|
1830
1851
|
}
|
|
1831
1852
|
|
|
1832
|
-
private async hasSearchTokens(
|
|
1833
|
-
entity: string,
|
|
1834
|
-
tenantId: string | null,
|
|
1835
|
-
orgScope?: { ids: string[]; includeNull: boolean } | null
|
|
1836
|
-
): Promise<boolean> {
|
|
1837
|
-
try {
|
|
1838
|
-
const db = this.getDb() as any
|
|
1839
|
-
let query = db
|
|
1840
|
-
.selectFrom('search_tokens')
|
|
1841
|
-
.select(sql<number>`1`.as('one'))
|
|
1842
|
-
.where('entity_type', '=', entity)
|
|
1843
|
-
if (tenantId !== undefined) {
|
|
1844
|
-
query = query.where(sql<boolean>`tenant_id is not distinct from ${tenantId}`)
|
|
1845
|
-
}
|
|
1846
|
-
if (orgScope) {
|
|
1847
|
-
query = this.applyOrganizationScope(query, 'search_tokens.organization_id', orgScope)
|
|
1848
|
-
}
|
|
1849
|
-
const row = await query.limit(1).executeTakeFirst()
|
|
1850
|
-
return !!row
|
|
1851
|
-
} catch (err) {
|
|
1852
|
-
this.logSearchDebug('search:has-tokens-error', {
|
|
1853
|
-
entity, tenantId, organizationScope: orgScope,
|
|
1854
|
-
error: err instanceof Error ? err.message : String(err),
|
|
1855
|
-
})
|
|
1856
|
-
return false
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
private async searchSourcesHaveTokens(
|
|
1861
|
-
sources: SearchTokenSource[],
|
|
1862
|
-
tenantId: string | null,
|
|
1863
|
-
orgScope?: { ids: string[]; includeNull: boolean } | null
|
|
1864
|
-
): Promise<boolean> {
|
|
1865
|
-
for (const source of sources) {
|
|
1866
|
-
const ok = await this.hasSearchTokens(source.entity, tenantId, orgScope)
|
|
1867
|
-
this.logSearchDebug('search:source-has-tokens', {
|
|
1868
|
-
entity: source.entity, recordIdColumn: source.recordIdColumn,
|
|
1869
|
-
tenantId, organizationScope: orgScope, hasTokens: ok,
|
|
1870
|
-
})
|
|
1871
|
-
if (ok) return true
|
|
1872
|
-
}
|
|
1873
|
-
return false
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
1853
|
private async resolveAvailableCustomFieldKeys(entityIds: string[], tenantId: string | null): Promise<string[]> {
|
|
1877
1854
|
if (!entityIds.length) return []
|
|
1878
1855
|
const cacheKey = this.customFieldKeysCacheKey(entityIds, tenantId)
|
|
@@ -1365,6 +1365,18 @@
|
|
|
1365
1365
|
"primary": false,
|
|
1366
1366
|
"unique": false
|
|
1367
1367
|
},
|
|
1368
|
+
{
|
|
1369
|
+
"columnNames": [
|
|
1370
|
+
"entity_type",
|
|
1371
|
+
"tenant_id",
|
|
1372
|
+
"organization_id"
|
|
1373
|
+
],
|
|
1374
|
+
"composite": true,
|
|
1375
|
+
"constraint": false,
|
|
1376
|
+
"keyName": "search_tokens_presence_idx",
|
|
1377
|
+
"primary": false,
|
|
1378
|
+
"unique": false
|
|
1379
|
+
},
|
|
1368
1380
|
{
|
|
1369
1381
|
"columnNames": [
|
|
1370
1382
|
"id"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Availability-probe index (.ai/specs/2026-07-31-search-token-probe-index.md).
|
|
4
|
+
// The query engines gate like/ilike routing on "does (entity_type, tenant, org)
|
|
5
|
+
// have any search tokens?". Neither existing index serves that probe as an
|
|
6
|
+
// access path — lookup_idx buries tenant_id behind field/token_hash — so on a
|
|
7
|
+
// large search_tokens the planner degraded the probe to a seq scan and the MISS
|
|
8
|
+
// answer had to exhaust every entry for the entity type (p95 12.5 s in
|
|
9
|
+
// production, #4723). With this (entity_type, tenant_id, organization_id)
|
|
10
|
+
// prefix and the index-usable `= / IS NULL` predicates the availability
|
|
11
|
+
// resolver emits, both the hit AND the miss become a B-tree prefix seek.
|
|
12
|
+
//
|
|
13
|
+
// search_tokens is high-churn, so the index is built CONCURRENTLY to avoid
|
|
14
|
+
// blocking writes during the build. CREATE INDEX CONCURRENTLY cannot run
|
|
15
|
+
// inside a transaction, hence isTransactional() => false; the migration runner
|
|
16
|
+
// applies migrations one-by-one, so this opt-out is safe (same pattern as
|
|
17
|
+
// Migration20260611103000_query_index). Drop first so retrying a failed
|
|
18
|
+
// concurrent build removes PostgreSQL's invalid index stub instead of letting
|
|
19
|
+
// IF NOT EXISTS silently accept it.
|
|
20
|
+
export class Migration20260731105052_query_index extends Migration {
|
|
21
|
+
|
|
22
|
+
override isTransactional(): boolean {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override up(): void | Promise<void> {
|
|
27
|
+
this.addSql(`drop index concurrently if exists "search_tokens_presence_idx";`);
|
|
28
|
+
this.addSql(`create index concurrently "search_tokens_presence_idx" on "search_tokens" ("entity_type", "tenant_id", "organization_id");`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override down(): void | Promise<void> {
|
|
32
|
+
this.addSql(`drop index concurrently if exists "search_tokens_presence_idx";`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|