@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 0.6.7
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/AGENTS.md +6 -57
- package/build.mjs +4 -2
- package/dist/lib/auth/server.js +2 -15
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/dynamicLoader.js +43 -117
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +1 -5
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +11 -2
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +4 -63
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +0 -4
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/engine.js +11 -37
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +12 -3
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/db/pg-errors.js +0 -42
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +6 -48
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +0 -56
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +10 -2
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +17 -52
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/index.js +3 -47
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +1 -5
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/query/engine.js +40 -54
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +2 -7
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/config.js +2 -61
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenize.js +11 -23
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/index.js +1 -13
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +7 -12
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +1 -50
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +1 -8
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +0 -1
- package/dist/modules/registry.js.map +2 -2
- package/dist/security/enabledModulesRegistry.js +11 -56
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/jest.config.cjs +1 -2
- package/package.json +9 -19
- package/src/lib/ai/llm-provider.ts +0 -33
- package/src/lib/auth/__tests__/server.test.ts +0 -23
- package/src/lib/auth/server.ts +2 -23
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +8 -63
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +1 -21
- package/src/lib/bootstrap/dynamicLoader.ts +65 -213
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +0 -28
- package/src/lib/commands/command-bus.ts +1 -5
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +0 -298
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +0 -26
- package/src/lib/crud/enricher-runner.ts +11 -2
- package/src/lib/crud/factory.ts +1 -69
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +0 -3
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +3 -18
- package/src/lib/data/engine.ts +16 -50
- package/src/lib/db/__tests__/mikro.test.ts +25 -0
- package/src/lib/db/mikro.ts +38 -13
- package/src/lib/db/pg-errors.ts +0 -58
- package/src/lib/di/container.ts +8 -72
- package/src/lib/email/__tests__/send.test.ts +1 -37
- package/src/lib/email/send.ts +0 -79
- package/src/lib/encryption/__tests__/subscriber.test.ts +2 -54
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +0 -58
- package/src/lib/encryption/subscriber.ts +14 -2
- package/src/lib/encryption/tenantDataEncryptionService.ts +18 -64
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +2 -6
- package/src/lib/logger/__tests__/logger.test.ts +0 -77
- package/src/lib/logger/index.ts +1 -59
- package/src/lib/modules/registry.ts +0 -10
- package/src/lib/query/__tests__/engine.test.ts +13 -65
- package/src/lib/query/engine.ts +58 -75
- package/src/lib/query/types.ts +5 -4
- package/src/lib/redis/__tests__/connection.test.ts +0 -16
- package/src/lib/redis/connection.ts +7 -10
- package/src/lib/search/__tests__/config.test.ts +0 -177
- package/src/lib/search/config.ts +8 -106
- package/src/lib/search/tokenize.ts +11 -33
- package/src/lib/webhooks/__tests__/verify.test.ts +1 -20
- package/src/lib/webhooks/index.ts +1 -19
- package/src/lib/webhooks/verify.ts +9 -17
- package/src/modules/__tests__/registry.test.ts +2 -10
- package/src/modules/analytics.ts +0 -7
- package/src/modules/customer-auth.ts +0 -1
- package/src/modules/encryption.ts +0 -3
- package/src/modules/navigation/backendChrome.ts +0 -15
- package/src/modules/overrides.ts +0 -103
- package/src/modules/payment_gateways/types.ts +0 -41
- package/src/modules/registry.ts +0 -3
- package/src/security/enabledModulesRegistry.ts +12 -64
- package/dist/lib/ai/safety-identifier.js +0 -32
- package/dist/lib/ai/safety-identifier.js.map +0 -7
- package/dist/lib/auth/organizationScope.js +0 -34
- package/dist/lib/auth/organizationScope.js.map +0 -7
- package/dist/lib/bootstrap/clientOnlyModules.js +0 -55
- package/dist/lib/bootstrap/clientOnlyModules.js.map +0 -7
- package/dist/lib/data/consistency.js +0 -19
- package/dist/lib/data/consistency.js.map +0 -7
- package/dist/lib/db/entityMetadata.js +0 -26
- package/dist/lib/db/entityMetadata.js.map +0 -7
- package/dist/lib/logger/extension.js +0 -29
- package/dist/lib/logger/extension.js.map +0 -7
- package/dist/lib/modules/surfaceFingerprint.js +0 -47
- package/dist/lib/modules/surfaceFingerprint.js.map +0 -7
- package/dist/lib/query/ciphertext-search-warning.js +0 -45
- package/dist/lib/query/ciphertext-search-warning.js.map +0 -7
- package/dist/lib/search/auto-indexing.js +0 -14
- package/dist/lib/search/auto-indexing.js.map +0 -7
- package/dist/lib/search/availability.js +0 -107
- package/dist/lib/search/availability.js.map +0 -7
- package/dist/lib/search/tokenLookup.js +0 -46
- package/dist/lib/search/tokenLookup.js.map +0 -7
- package/dist/lib/telemetry/runtime.js +0 -35
- package/dist/lib/telemetry/runtime.js.map +0 -7
- package/dist/lib/webhooks/body.js +0 -57
- package/dist/lib/webhooks/body.js.map +0 -7
- package/dist/lib/webhooks/inbound-types.js +0 -1
- package/dist/lib/webhooks/inbound-types.js.map +0 -7
- package/dist/modules/widgets/extension-points.js +0 -96
- package/dist/modules/widgets/extension-points.js.map +0 -7
- package/dist/security/featurePolicy.js +0 -62
- package/dist/security/featurePolicy.js.map +0 -7
- package/scripts/versionSource.cjs +0 -65
- package/src/lib/__tests__/versionSource.test.ts +0 -46
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +0 -59
- package/src/lib/ai/__tests__/safety-identifier.test.ts +0 -71
- package/src/lib/ai/safety-identifier.ts +0 -73
- package/src/lib/auth/__tests__/organizationScope.test.ts +0 -107
- package/src/lib/auth/organizationScope.ts +0 -65
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +0 -189
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +0 -98
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +0 -211
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +0 -192
- package/src/lib/bootstrap/clientOnlyModules.ts +0 -85
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +0 -171
- package/src/lib/data/__tests__/consistency.test.ts +0 -38
- package/src/lib/data/consistency.ts +0 -17
- package/src/lib/db/__tests__/entityMetadata.test.ts +0 -38
- package/src/lib/db/__tests__/pg-errors.test.ts +0 -48
- package/src/lib/db/entityMetadata.ts +0 -43
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +0 -155
- package/src/lib/di/__tests__/container-app-di.test.ts +0 -173
- package/src/lib/di/__tests__/registrar-error-log.test.ts +0 -154
- package/src/lib/i18n/__tests__/config.test.ts +0 -8
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +0 -30
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +0 -29
- package/src/lib/logger/extension.ts +0 -59
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +0 -122
- package/src/lib/modules/surfaceFingerprint.ts +0 -87
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +0 -178
- package/src/lib/query/ciphertext-search-warning.ts +0 -95
- package/src/lib/search/__tests__/availability.test.ts +0 -211
- package/src/lib/search/__tests__/tokenLookup.test.ts +0 -206
- package/src/lib/search/__tests__/tokenize.test.ts +0 -49
- package/src/lib/search/auto-indexing.ts +0 -22
- package/src/lib/search/availability.ts +0 -232
- package/src/lib/search/tokenLookup.ts +0 -133
- package/src/lib/telemetry/runtime.ts +0 -72
- package/src/lib/webhooks/__tests__/body.test.ts +0 -95
- package/src/lib/webhooks/body.ts +0 -70
- package/src/lib/webhooks/inbound-types.ts +0 -125
- package/src/modules/__tests__/nav-group-order-override.test.ts +0 -183
- package/src/modules/payment_gateways/__tests__/types.test.ts +0 -37
- package/src/modules/widgets/__tests__/extension-points.test.ts +0 -101
- package/src/modules/widgets/extension-points.ts +0 -524
- package/src/security/__tests__/featurePolicy.test.ts +0 -166
- package/src/security/featurePolicy.ts +0 -89
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
clearSearchTokenPresenceCache,
|
|
3
|
-
createSearchTokenAvailability,
|
|
4
|
-
hasSearchFilter,
|
|
5
|
-
isSearchFilterOp,
|
|
6
|
-
type OrganizationScope,
|
|
7
|
-
type SearchTokenProbeQueryBuilder,
|
|
8
|
-
} from '../availability'
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
clearSearchTokenPresenceCache()
|
|
12
|
-
delete process.env.OM_SEARCH_TOKEN_PRESENCE_CACHE_MS
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
afterAll(() => {
|
|
16
|
-
delete process.env.OM_SEARCH_TOKEN_PRESENCE_CACHE_MS
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
type ProbeLog = { table: string; wheres: unknown[][] }
|
|
20
|
-
|
|
21
|
-
function createFakeDb(options?: { rowsByTable?: Record<string, object | undefined>; failTables?: string[] }) {
|
|
22
|
-
const probes: ProbeLog[] = []
|
|
23
|
-
const db = {
|
|
24
|
-
selectFrom(table: string) {
|
|
25
|
-
const log: ProbeLog = { table, wheres: [] }
|
|
26
|
-
probes.push(log)
|
|
27
|
-
const chain = {
|
|
28
|
-
select: () => chain,
|
|
29
|
-
where: (...args: unknown[]) => {
|
|
30
|
-
log.wheres.push(args)
|
|
31
|
-
return chain
|
|
32
|
-
},
|
|
33
|
-
limit: () => chain,
|
|
34
|
-
executeTakeFirst: async () => {
|
|
35
|
-
if (options?.failTables?.includes(table)) throw new Error(`probe failed for ${table}`)
|
|
36
|
-
return options?.rowsByTable?.[table]
|
|
37
|
-
},
|
|
38
|
-
}
|
|
39
|
-
return chain
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
return { db, probes }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function buildAvailability(options?: Parameters<typeof createFakeDb>[0] & { enabled?: boolean }) {
|
|
46
|
-
const { db, probes } = createFakeDb(options)
|
|
47
|
-
const logDebug = jest.fn()
|
|
48
|
-
const applyOrganizationScope = jest.fn((query: SearchTokenProbeQueryBuilder) => query)
|
|
49
|
-
const availability = createSearchTokenAvailability({
|
|
50
|
-
getDb: () => db,
|
|
51
|
-
getConfig: () => ({ enabled: options?.enabled ?? true }),
|
|
52
|
-
applyOrganizationScope,
|
|
53
|
-
logDebug,
|
|
54
|
-
})
|
|
55
|
-
return { availability, probes, logDebug, applyOrganizationScope }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const countProbes = (probes: ProbeLog[], table: string) => probes.filter((probe) => probe.table === table).length
|
|
59
|
-
|
|
60
|
-
describe('hasSearchFilter / isSearchFilterOp', () => {
|
|
61
|
-
test('recognizes like and ilike only', () => {
|
|
62
|
-
expect(isSearchFilterOp('like')).toBe(true)
|
|
63
|
-
expect(isSearchFilterOp('ilike')).toBe(true)
|
|
64
|
-
expect(isSearchFilterOp('eq')).toBe(false)
|
|
65
|
-
expect(hasSearchFilter([{ op: 'eq' }, { op: 'in' }])).toBe(false)
|
|
66
|
-
expect(hasSearchFilter([{ op: 'eq' }, { op: 'ilike' }])).toBe(true)
|
|
67
|
-
expect(hasSearchFilter([])).toBe(false)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('createSearchTokenAvailability', () => {
|
|
72
|
-
test('staticEnabled probes the table once per instance and rechecks config each call', async () => {
|
|
73
|
-
const { availability, probes } = buildAvailability({
|
|
74
|
-
rowsByTable: { 'information_schema.tables': { one: 1 } },
|
|
75
|
-
})
|
|
76
|
-
await expect(availability.staticEnabled()).resolves.toBe(true)
|
|
77
|
-
await expect(availability.staticEnabled()).resolves.toBe(true)
|
|
78
|
-
expect(countProbes(probes, 'information_schema.tables')).toBe(1)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
test('staticEnabled short-circuits on disabled config without probing', async () => {
|
|
82
|
-
const { availability, probes } = buildAvailability({ enabled: false })
|
|
83
|
-
await expect(availability.staticEnabled()).resolves.toBe(false)
|
|
84
|
-
expect(probes).toHaveLength(0)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('staticEnabled retries after a failed table probe instead of caching the rejection', async () => {
|
|
88
|
-
const options = { rowsByTable: { 'information_schema.tables': { one: 1 } }, failTables: ['information_schema.tables'] }
|
|
89
|
-
const { availability, probes } = buildAvailability(options)
|
|
90
|
-
await expect(availability.staticEnabled()).rejects.toThrow('probe failed')
|
|
91
|
-
options.failTables.length = 0
|
|
92
|
-
await expect(availability.staticEnabled()).resolves.toBe(true)
|
|
93
|
-
expect(countProbes(probes, 'information_schema.tables')).toBe(2)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('hasTokens memoizes per (entity, tenant, org) key', async () => {
|
|
97
|
-
const { availability, probes } = buildAvailability({ rowsByTable: { search_tokens: { one: 1 } } })
|
|
98
|
-
const orgScope: OrganizationScope = { ids: ['org1'], includeNull: false }
|
|
99
|
-
|
|
100
|
-
await expect(availability.hasTokens('example:todo', 't1', orgScope)).resolves.toBe(true)
|
|
101
|
-
await expect(availability.hasTokens('example:todo', 't1', orgScope)).resolves.toBe(true)
|
|
102
|
-
expect(countProbes(probes, 'search_tokens')).toBe(1)
|
|
103
|
-
|
|
104
|
-
await availability.hasTokens('example:todo', 't2', orgScope)
|
|
105
|
-
await availability.hasTokens('example:other', 't1', orgScope)
|
|
106
|
-
await availability.hasTokens('example:todo', 't1', { ids: ['org2'], includeNull: false })
|
|
107
|
-
expect(countProbes(probes, 'search_tokens')).toBe(4)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('hasTokens applies the injected organization scope', async () => {
|
|
111
|
-
const { availability, applyOrganizationScope } = buildAvailability()
|
|
112
|
-
const orgScope: OrganizationScope = { ids: ['org1'], includeNull: true }
|
|
113
|
-
await availability.hasTokens('example:todo', 't1', orgScope)
|
|
114
|
-
expect(applyOrganizationScope).toHaveBeenCalledWith(expect.anything(), 'search_tokens.organization_id', orgScope)
|
|
115
|
-
|
|
116
|
-
applyOrganizationScope.mockClear()
|
|
117
|
-
await availability.hasTokens('example:todo', 't1', null)
|
|
118
|
-
expect(applyOrganizationScope).not.toHaveBeenCalled()
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test('hasTokens resolves false and logs search:has-tokens-error on probe failure', async () => {
|
|
122
|
-
const { availability, logDebug } = buildAvailability({ failTables: ['search_tokens'] })
|
|
123
|
-
await expect(availability.hasTokens('example:todo', 't1', null)).resolves.toBe(false)
|
|
124
|
-
expect(logDebug).toHaveBeenCalledWith('search:has-tokens-error', expect.objectContaining({
|
|
125
|
-
entity: 'example:todo',
|
|
126
|
-
tenantId: 't1',
|
|
127
|
-
error: expect.stringContaining('probe failed'),
|
|
128
|
-
}))
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('anySourceHasTokens stops at the first source with tokens and logs each probed source', async () => {
|
|
132
|
-
const { availability, probes, logDebug } = buildAvailability({ rowsByTable: { search_tokens: { one: 1 } } })
|
|
133
|
-
const result = await availability.anySourceHasTokens(
|
|
134
|
-
[
|
|
135
|
-
{ entity: 'example:todo', recordIdColumn: 'b.id' },
|
|
136
|
-
{ entity: 'example:other', recordIdColumn: 'cfs0.entity_id' },
|
|
137
|
-
],
|
|
138
|
-
't1',
|
|
139
|
-
null,
|
|
140
|
-
)
|
|
141
|
-
expect(result).toBe(true)
|
|
142
|
-
expect(countProbes(probes, 'search_tokens')).toBe(1)
|
|
143
|
-
expect(logDebug).toHaveBeenCalledTimes(1)
|
|
144
|
-
expect(logDebug).toHaveBeenCalledWith('search:source-has-tokens', expect.objectContaining({
|
|
145
|
-
entity: 'example:todo',
|
|
146
|
-
recordIdColumn: 'b.id',
|
|
147
|
-
hasTokens: true,
|
|
148
|
-
}))
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test('process-level TTL cache serves later resolver instances without re-probing', async () => {
|
|
152
|
-
const first = buildAvailability({ rowsByTable: { search_tokens: { one: 1 } } })
|
|
153
|
-
await expect(first.availability.hasTokens('example:todo', 't1', null)).resolves.toBe(true)
|
|
154
|
-
expect(countProbes(first.probes, 'search_tokens')).toBe(1)
|
|
155
|
-
|
|
156
|
-
const second = buildAvailability()
|
|
157
|
-
await expect(second.availability.hasTokens('example:todo', 't1', null)).resolves.toBe(true)
|
|
158
|
-
expect(countProbes(second.probes, 'search_tokens')).toBe(0)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test('process-level TTL cache expires and can be disabled', async () => {
|
|
162
|
-
const nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1_000_000)
|
|
163
|
-
try {
|
|
164
|
-
const first = buildAvailability()
|
|
165
|
-
await first.availability.hasTokens('example:todo', 't1', null)
|
|
166
|
-
|
|
167
|
-
nowSpy.mockReturnValue(1_000_000 + 30_001)
|
|
168
|
-
const second = buildAvailability()
|
|
169
|
-
await second.availability.hasTokens('example:todo', 't1', null)
|
|
170
|
-
expect(countProbes(second.probes, 'search_tokens')).toBe(1)
|
|
171
|
-
|
|
172
|
-
process.env.OM_SEARCH_TOKEN_PRESENCE_CACHE_MS = '0'
|
|
173
|
-
const third = buildAvailability()
|
|
174
|
-
await third.availability.hasTokens('example:todo', 't1', null)
|
|
175
|
-
expect(countProbes(third.probes, 'search_tokens')).toBe(1)
|
|
176
|
-
} finally {
|
|
177
|
-
nowSpy.mockRestore()
|
|
178
|
-
}
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
test('error-driven false results are not TTL-cached', async () => {
|
|
182
|
-
const first = buildAvailability({ failTables: ['search_tokens'] })
|
|
183
|
-
await expect(first.availability.hasTokens('example:todo', 't1', null)).resolves.toBe(false)
|
|
184
|
-
|
|
185
|
-
const second = buildAvailability({ rowsByTable: { search_tokens: { one: 1 } } })
|
|
186
|
-
await expect(second.availability.hasTokens('example:todo', 't1', null)).resolves.toBe(true)
|
|
187
|
-
expect(countProbes(second.probes, 'search_tokens')).toBe(1)
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
test('probe uses index-friendly tenant predicates instead of IS NOT DISTINCT FROM', async () => {
|
|
191
|
-
const { availability, probes } = buildAvailability()
|
|
192
|
-
await availability.hasTokens('example:todo', 't1', null)
|
|
193
|
-
await availability.hasTokens('example:todo', null, null)
|
|
194
|
-
const [scoped, global] = probes.filter((probe) => probe.table === 'search_tokens')
|
|
195
|
-
expect(scoped.wheres).toContainEqual(['tenant_id', '=', 't1'])
|
|
196
|
-
expect(global.wheres).toContainEqual(['tenant_id', 'is', null])
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test('anySourceHasTokens sweeps all sources when none has tokens, sharing the memo with hasTokens', async () => {
|
|
200
|
-
const { availability, probes } = buildAvailability()
|
|
201
|
-
const sources = [
|
|
202
|
-
{ entity: 'example:todo', recordIdColumn: 'b.id' },
|
|
203
|
-
{ entity: 'example:other', recordIdColumn: 'cfs0.entity_id' },
|
|
204
|
-
]
|
|
205
|
-
await expect(availability.anySourceHasTokens(sources, 't1', null)).resolves.toBe(false)
|
|
206
|
-
expect(countProbes(probes, 'search_tokens')).toBe(2)
|
|
207
|
-
|
|
208
|
-
await expect(availability.hasTokens('example:todo', 't1', null)).resolves.toBe(false)
|
|
209
|
-
expect(countProbes(probes, 'search_tokens')).toBe(2)
|
|
210
|
-
})
|
|
211
|
-
})
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
findEntityIdsBySearchTokens,
|
|
3
|
-
findEntityIdsBySearchTokensCompat,
|
|
4
|
-
} from '../tokenLookup'
|
|
5
|
-
import { resolveSearchConfig } from '../config'
|
|
6
|
-
import { tokenizeText } from '../tokenize'
|
|
7
|
-
|
|
8
|
-
type KyselyCall = {
|
|
9
|
-
method: string
|
|
10
|
-
args: unknown[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function createKyselyMock(rows: Array<{ entity_id: unknown }>) {
|
|
14
|
-
const calls: KyselyCall[] = []
|
|
15
|
-
const tableNameRef: { value: string | null } = { value: null }
|
|
16
|
-
const builder: Record<string, unknown> = {}
|
|
17
|
-
const passthrough = (method: string) =>
|
|
18
|
-
(...args: unknown[]) => {
|
|
19
|
-
calls.push({ method, args })
|
|
20
|
-
return builder
|
|
21
|
-
}
|
|
22
|
-
builder.select = passthrough('select')
|
|
23
|
-
builder.where = passthrough('where')
|
|
24
|
-
builder.groupBy = passthrough('groupBy')
|
|
25
|
-
builder.having = passthrough('having')
|
|
26
|
-
builder.execute = jest.fn(async () => rows)
|
|
27
|
-
|
|
28
|
-
const db = {
|
|
29
|
-
selectFrom: (table: string) => {
|
|
30
|
-
tableNameRef.value = table
|
|
31
|
-
return builder
|
|
32
|
-
},
|
|
33
|
-
}
|
|
34
|
-
return { db: db as never, calls, tableNameRef, builder }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function compileSql(raw: unknown): string {
|
|
38
|
-
if (raw && typeof raw === 'object' && 'toOperationNode' in raw && typeof raw.toOperationNode === 'function') {
|
|
39
|
-
const node = raw.toOperationNode() as { sqlFragments?: unknown }
|
|
40
|
-
return Array.isArray(node.sqlFragments) ? node.sqlFragments.join(' ? ') : ''
|
|
41
|
-
}
|
|
42
|
-
if (raw && typeof raw === 'object' && 'sql' in raw) return String(raw.sql)
|
|
43
|
-
return String(raw)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function rawWhereFragments(calls: KyselyCall[]): string[] {
|
|
47
|
-
return calls
|
|
48
|
-
.filter((call) => call.method === 'where' && call.args.length === 1)
|
|
49
|
-
.map((call) => compileSql(call.args[0]))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
describe('findEntityIdsBySearchTokens', () => {
|
|
53
|
-
const baseInput = {
|
|
54
|
-
entityType: 'customers:customer_entity',
|
|
55
|
-
query: 'Hello',
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
it('skips the lookup for a blank query', async () => {
|
|
59
|
-
const { db, builder } = createKyselyMock([])
|
|
60
|
-
const result = await findEntityIdsBySearchTokens({ ...baseInput, db, query: ' ' })
|
|
61
|
-
expect(result).toEqual({ matched: false, reason: 'empty-query' })
|
|
62
|
-
expect(builder.execute).not.toHaveBeenCalled()
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('skips the lookup when token search is disabled', async () => {
|
|
66
|
-
const { db, builder } = createKyselyMock([])
|
|
67
|
-
const result = await findEntityIdsBySearchTokens({
|
|
68
|
-
...baseInput,
|
|
69
|
-
db,
|
|
70
|
-
config: { ...resolveSearchConfig(), enabled: false },
|
|
71
|
-
})
|
|
72
|
-
expect(result).toEqual({ matched: false, reason: 'search-disabled' })
|
|
73
|
-
expect(builder.execute).not.toHaveBeenCalled()
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('skips the lookup when the query yields no indexable tokens', async () => {
|
|
77
|
-
const { db, builder } = createKyselyMock([])
|
|
78
|
-
const result = await findEntityIdsBySearchTokens({ ...baseInput, db, query: '!' })
|
|
79
|
-
expect(result).toEqual({ matched: false, reason: 'no-tokens' })
|
|
80
|
-
expect(builder.execute).not.toHaveBeenCalled()
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('requires every query token to be present on the record', async () => {
|
|
84
|
-
const { db, calls } = createKyselyMock([{ entity_id: 'a' }])
|
|
85
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db, query: 'ada lovelace' })
|
|
86
|
-
|
|
87
|
-
const expectedHashes = tokenizeText('ada lovelace', resolveSearchConfig()).hashes
|
|
88
|
-
const hashFilter = calls.find((call) => call.args[0] === 'token_hash' && call.args[1] === 'in')
|
|
89
|
-
expect(hashFilter?.args[2]).toEqual(expectedHashes)
|
|
90
|
-
|
|
91
|
-
const having = calls.find((call) => call.method === 'having')
|
|
92
|
-
expect(compileSql(having?.args[0])).toContain('count(distinct token_hash) >=')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('returns the matched ids and drops non-string rows', async () => {
|
|
96
|
-
const { db, tableNameRef } = createKyselyMock([
|
|
97
|
-
{ entity_id: 'id-1' },
|
|
98
|
-
{ entity_id: null },
|
|
99
|
-
{ entity_id: 42 },
|
|
100
|
-
{ entity_id: '' },
|
|
101
|
-
{ entity_id: 'id-2' },
|
|
102
|
-
])
|
|
103
|
-
const result = await findEntityIdsBySearchTokens({ ...baseInput, db })
|
|
104
|
-
expect(result).toEqual({ matched: true, ids: ['id-1', 'id-2'] })
|
|
105
|
-
expect(tableNameRef.value).toBe('search_tokens')
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('reports an empty match instead of a skip when nothing matched', async () => {
|
|
109
|
-
const { db } = createKyselyMock([])
|
|
110
|
-
const result = await findEntityIdsBySearchTokens({ ...baseInput, db })
|
|
111
|
-
expect(result).toEqual({ matched: true, ids: [] })
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('narrows a single field with equality and multiple fields with IN', async () => {
|
|
115
|
-
const single = createKyselyMock([])
|
|
116
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db: single.db, fields: ['name'] })
|
|
117
|
-
expect(single.calls).toContainEqual({ method: 'where', args: ['field', '=', 'name'] })
|
|
118
|
-
|
|
119
|
-
const multi = createKyselyMock([])
|
|
120
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db: multi.db, fields: ['name', 'email'] })
|
|
121
|
-
expect(multi.calls).toContainEqual({ method: 'where', args: ['field', 'in', ['name', 'email']] })
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('omits the field predicate when no fields are supplied', async () => {
|
|
125
|
-
const { db, calls } = createKyselyMock([])
|
|
126
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db, fields: [] })
|
|
127
|
-
expect(calls.some((call) => call.args[0] === 'field')).toBe(false)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('omits tenant and organization predicates when the scope is absent', async () => {
|
|
131
|
-
const { db, calls } = createKyselyMock([])
|
|
132
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db })
|
|
133
|
-
expect(rawWhereFragments(calls)).toEqual([])
|
|
134
|
-
expect(calls.some((call) => call.args[0] === 'organization_id')).toBe(false)
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('emits a null-safe tenant predicate for an explicit null tenant', async () => {
|
|
138
|
-
const { db, calls } = createKyselyMock([])
|
|
139
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db, scope: { tenantId: null } })
|
|
140
|
-
expect(rawWhereFragments(calls).some((s) => s.includes('tenant_id is not distinct from'))).toBe(true)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('emits a null-safe organization predicate for an explicit null organization', async () => {
|
|
144
|
-
const { db, calls } = createKyselyMock([])
|
|
145
|
-
await findEntityIdsBySearchTokens({ ...baseInput, db, scope: { organizationId: null } })
|
|
146
|
-
expect(rawWhereFragments(calls).some((s) => s.includes('organization_id is not distinct from'))).toBe(true)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('prefers a concrete organization over the visible-organization list', async () => {
|
|
150
|
-
const { db, calls } = createKyselyMock([])
|
|
151
|
-
await findEntityIdsBySearchTokens({
|
|
152
|
-
...baseInput,
|
|
153
|
-
db,
|
|
154
|
-
scope: { organizationId: 'org-1', organizationIds: ['org-1', 'org-2'] },
|
|
155
|
-
})
|
|
156
|
-
expect(calls).toContainEqual({ method: 'where', args: ['organization_id', '=', 'org-1'] })
|
|
157
|
-
expect(calls.some((call) => call.args[1] === 'in' && call.args[0] === 'organization_id')).toBe(false)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('falls back to the visible-organization list when no organization is selected', async () => {
|
|
161
|
-
const { db, calls } = createKyselyMock([])
|
|
162
|
-
await findEntityIdsBySearchTokens({
|
|
163
|
-
...baseInput,
|
|
164
|
-
db,
|
|
165
|
-
scope: { organizationIds: ['org-1', 'org-2'] },
|
|
166
|
-
})
|
|
167
|
-
expect(calls).toContainEqual({ method: 'where', args: ['organization_id', 'in', ['org-1', 'org-2']] })
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
describe('findEntityIdsBySearchTokensCompat', () => {
|
|
172
|
-
it('returns null only for a blank query', async () => {
|
|
173
|
-
const { db } = createKyselyMock([])
|
|
174
|
-
await expect(findEntityIdsBySearchTokensCompat({
|
|
175
|
-
db,
|
|
176
|
-
entityType: 'customers:customer_entity',
|
|
177
|
-
query: ' ',
|
|
178
|
-
})).resolves.toBeNull()
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('returns an empty array for the remaining skip reasons', async () => {
|
|
182
|
-
const noTokens = createKyselyMock([])
|
|
183
|
-
await expect(findEntityIdsBySearchTokensCompat({
|
|
184
|
-
db: noTokens.db,
|
|
185
|
-
entityType: 'customers:customer_entity',
|
|
186
|
-
query: '!',
|
|
187
|
-
})).resolves.toEqual([])
|
|
188
|
-
|
|
189
|
-
const disabled = createKyselyMock([])
|
|
190
|
-
await expect(findEntityIdsBySearchTokensCompat({
|
|
191
|
-
db: disabled.db,
|
|
192
|
-
entityType: 'customers:customer_entity',
|
|
193
|
-
query: 'Hello',
|
|
194
|
-
config: { ...resolveSearchConfig(), enabled: false },
|
|
195
|
-
})).resolves.toEqual([])
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('returns the matched ids', async () => {
|
|
199
|
-
const { db } = createKyselyMock([{ entity_id: 'id-1' }])
|
|
200
|
-
await expect(findEntityIdsBySearchTokensCompat({
|
|
201
|
-
db,
|
|
202
|
-
entityType: 'customers:customer_entity',
|
|
203
|
-
query: 'Hello',
|
|
204
|
-
})).resolves.toEqual(['id-1'])
|
|
205
|
-
})
|
|
206
|
-
})
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { SearchConfig } from '../config'
|
|
2
|
-
import { tokenizeText } from '../tokenize'
|
|
3
|
-
|
|
4
|
-
const baseConfig: SearchConfig = {
|
|
5
|
-
enabled: true,
|
|
6
|
-
minTokenLength: 3,
|
|
7
|
-
enablePartials: true,
|
|
8
|
-
hashAlgorithm: 'sha256',
|
|
9
|
-
storeRawTokens: false,
|
|
10
|
-
blocklistedFields: [],
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
describe('tokenizeText limits', () => {
|
|
14
|
-
test('truncates oversized field text before tokenizing', () => {
|
|
15
|
-
const config = { ...baseConfig, enablePartials: false, maxFieldChars: 10 }
|
|
16
|
-
|
|
17
|
-
const { tokens } = tokenizeText('aaaa bbbb cccc', config)
|
|
18
|
-
|
|
19
|
-
expect(tokens).toEqual(['aaaa', 'bbbb'])
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('bounds prefix expansion while collecting tokens', () => {
|
|
23
|
-
const config = { ...baseConfig, maxFieldChars: 100, maxTokensPerField: 5 }
|
|
24
|
-
|
|
25
|
-
const { tokens, hashes } = tokenizeText('a'.repeat(100), config)
|
|
26
|
-
|
|
27
|
-
expect(tokens).toEqual(['aaa', 'aaaa', 'aaaaa', 'aaaaaa', 'aaaaaaa'])
|
|
28
|
-
expect(hashes).toHaveLength(5)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('applies safe defaults to legacy configs without limit fields', () => {
|
|
32
|
-
const { tokens } = tokenizeText('a'.repeat(50_000), baseConfig)
|
|
33
|
-
|
|
34
|
-
expect(tokens).toHaveLength(5_000)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('allows a limit to be disabled explicitly', () => {
|
|
38
|
-
const config = {
|
|
39
|
-
...baseConfig,
|
|
40
|
-
enablePartials: false,
|
|
41
|
-
maxFieldChars: 0,
|
|
42
|
-
maxTokensPerField: 0,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const { tokens } = tokenizeText('alpha beta gamma delta', config)
|
|
46
|
-
|
|
47
|
-
expect(tokens).toEqual(['alpha', 'beta', 'gamma', 'delta'])
|
|
48
|
-
})
|
|
49
|
-
})
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { parseBooleanToken } from '../boolean'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Module id backing the tenant-scoped vector auto-indexing switch. Kept as `vector`
|
|
5
|
-
* (rather than `search`) for backwards compatibility with rows written before the
|
|
6
|
-
* search module absorbed the vector settings.
|
|
7
|
-
*/
|
|
8
|
-
export const SEARCH_AUTO_INDEX_CONFIG_MODULE = 'vector'
|
|
9
|
-
export const SEARCH_AUTO_INDEX_CONFIG_KEY = 'auto_index_enabled'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Instance-wide kill switch for vector auto-indexing. Lives in `@open-mercato/shared`
|
|
13
|
-
* so consumers outside `@open-mercato/search` (for example the query index status page)
|
|
14
|
-
* can report vector coverage honestly without duplicating the env-var contract.
|
|
15
|
-
*/
|
|
16
|
-
export function envDisablesAutoIndexing(): boolean {
|
|
17
|
-
const raw =
|
|
18
|
-
process.env.OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING ??
|
|
19
|
-
process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING
|
|
20
|
-
if (!raw) return false
|
|
21
|
-
return parseBooleanToken(raw) === true
|
|
22
|
-
}
|