@open-mercato/shared 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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 +57 -6
- package/build.mjs +2 -4
- package/dist/lib/ai/safety-identifier.js +32 -0
- package/dist/lib/ai/safety-identifier.js.map +7 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
- package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +117 -43
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +5 -1
- 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 +2 -11
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +72 -9
- 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 +4 -0
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/consistency.js +19 -0
- package/dist/lib/data/consistency.js.map +7 -0
- package/dist/lib/data/engine.js +37 -11
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +48 -6
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +56 -0
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
- 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/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/modules/surfaceFingerprint.js +47 -0
- package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
- package/dist/lib/query/ciphertext-search-warning.js +45 -0
- package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
- package/dist/lib/query/engine.js +54 -40
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/auto-indexing.js +14 -0
- package/dist/lib/search/auto-indexing.js.map +7 -0
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +61 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenLookup.js +46 -0
- package/dist/lib/search/tokenLookup.js.map +7 -0
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +57 -0
- package/dist/lib/webhooks/body.js.map +7 -0
- package/dist/lib/webhooks/inbound-types.js +1 -0
- package/dist/lib/webhooks/inbound-types.js.map +7 -0
- package/dist/lib/webhooks/index.js +13 -1
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +12 -7
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +50 -1
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +8 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/extension-points.js +96 -0
- package/dist/modules/widgets/extension-points.js.map +7 -0
- package/dist/security/enabledModulesRegistry.js +56 -11
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/dist/security/featurePolicy.js +62 -0
- package/dist/security/featurePolicy.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/scripts/versionSource.cjs +65 -0
- package/src/lib/__tests__/versionSource.test.ts +46 -0
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
- package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
- package/src/lib/ai/llm-provider.ts +33 -0
- package/src/lib/ai/safety-identifier.ts +73 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
- package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +213 -65
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
- package/src/lib/commands/command-bus.ts +5 -1
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
- package/src/lib/crud/enricher-runner.ts +2 -11
- package/src/lib/crud/factory.ts +82 -6
- 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 +3 -0
- package/src/lib/data/__tests__/consistency.test.ts +38 -0
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
- package/src/lib/data/consistency.ts +17 -0
- package/src/lib/data/engine.ts +50 -16
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
- package/src/lib/di/container.ts +72 -8
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/modules/surfaceFingerprint.ts +87 -0
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/ciphertext-search-warning.ts +95 -0
- package/src/lib/query/engine.ts +75 -58
- package/src/lib/query/types.ts +4 -5
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +177 -0
- package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/auto-indexing.ts +22 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +106 -8
- package/src/lib/search/tokenLookup.ts +133 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/telemetry/runtime.ts +72 -0
- package/src/lib/webhooks/__tests__/body.test.ts +95 -0
- package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
- package/src/lib/webhooks/body.ts +70 -0
- package/src/lib/webhooks/inbound-types.ts +125 -0
- package/src/lib/webhooks/index.ts +19 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/analytics.ts +7 -0
- package/src/modules/customer-auth.ts +1 -0
- package/src/modules/encryption.ts +3 -0
- package/src/modules/navigation/backendChrome.ts +15 -0
- package/src/modules/overrides.ts +103 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +41 -0
- package/src/modules/registry.ts +3 -0
- package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
- package/src/modules/widgets/extension-points.ts +524 -0
- package/src/security/__tests__/featurePolicy.test.ts +166 -0
- package/src/security/enabledModulesRegistry.ts +64 -12
- package/src/security/featurePolicy.ts +89 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createLogger } from "../logger/index.js";
|
|
2
|
+
const logger = createLogger("shared").child({ component: "query" });
|
|
3
|
+
const WARN_CACHE_CAP = 5e3;
|
|
4
|
+
const warned = /* @__PURE__ */ new Set();
|
|
5
|
+
const warnKey = (entity, tenantId, field) => `${entity}|${tenantId ?? "null"}|${field}`;
|
|
6
|
+
const normalizeColumnName = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
7
|
+
function resetCiphertextLikeWarnCache() {
|
|
8
|
+
warned.clear();
|
|
9
|
+
}
|
|
10
|
+
async function warnOnCiphertextLikeFallback(params) {
|
|
11
|
+
const { entity, tenantId, reason, service } = params;
|
|
12
|
+
if (!service || service.isEnabled?.() === false) return;
|
|
13
|
+
if (typeof service.getEncryptedFieldNames !== "function") return;
|
|
14
|
+
const candidates = params.fields.filter((field) => typeof field === "string" && !field.startsWith("cf:")).map((field) => normalizeColumnName(field)).filter((field, index, all) => field.length > 0 && all.indexOf(field) === index).filter((field) => !warned.has(warnKey(entity, tenantId, field)));
|
|
15
|
+
if (!candidates.length) return;
|
|
16
|
+
try {
|
|
17
|
+
const encrypted = new Set(
|
|
18
|
+
(await service.getEncryptedFieldNames(entity, tenantId, null)).map(
|
|
19
|
+
(field) => normalizeColumnName(String(field))
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
for (const field of candidates) {
|
|
23
|
+
if (!encrypted.has(field)) continue;
|
|
24
|
+
if (warned.size >= WARN_CACHE_CAP) warned.clear();
|
|
25
|
+
warned.add(warnKey(entity, tenantId, field));
|
|
26
|
+
logger.warn("Text search filter cannot match an encrypted column", {
|
|
27
|
+
entity,
|
|
28
|
+
field,
|
|
29
|
+
reason,
|
|
30
|
+
hint: reason === "search-disabled" ? "OM_SEARCH_ENABLED is off, so the filter runs as ILIKE against ciphertext and matches nothing." : reason === "no-indexable-tokens" ? "The filter value produced no search tokens, so it runs as ILIKE against ciphertext and matches nothing. Use at least OM_SEARCH_MIN_LEN indexable characters, or filter on the deterministic hash column." : "No search_tokens exist for this entity and scope, so the filter runs as ILIKE against ciphertext and matches nothing. Index the entity through query_index and reindex, or filter on the deterministic hash column."
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.debug("Ciphertext search warning check failed", {
|
|
35
|
+
entity,
|
|
36
|
+
err: err instanceof Error ? err.message : String(err)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
normalizeColumnName,
|
|
42
|
+
resetCiphertextLikeWarnCache,
|
|
43
|
+
warnOnCiphertextLikeFallback
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=ciphertext-search-warning.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/query/ciphertext-search-warning.ts"],
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '../logger'\nimport type { EntityId } from '../../modules/entities'\n\nconst logger = createLogger('shared').child({ component: 'query' })\n\n/**\n * Minimal slice of `TenantDataEncryptionService` needed to decide whether a\n * text filter targets an encrypted column.\n */\nexport type CiphertextWarningEncryptionService = {\n getEncryptedFieldNames?: (\n entityId: EntityId,\n tenantId?: string | null,\n organizationId?: string | null,\n ) => Promise<readonly string[]>\n isEnabled?: () => boolean\n} | null | undefined\n\nexport type CiphertextLikeFallbackReason = 'no-indexable-tokens' | 'no-search-tokens' | 'search-disabled'\n\n// One warning per entity/tenant/field per process \u2014 the fallback repeats on\n// every request, and an operator only needs to learn about it once.\nconst WARN_CACHE_CAP = 5000\nconst warned = new Set<string>()\n\nconst warnKey = (entity: string, tenantId: string | null, field: string): string =>\n `${entity}|${tenantId ?? 'null'}|${field}`\n\nexport const normalizeColumnName = (value: string): string =>\n value.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()\n\n/** Test seam: the warn-once cache is process-global by design. */\nexport function resetCiphertextLikeWarnCache(): void {\n warned.clear()\n}\n\n/**\n * Warn when a `like`/`ilike` filter is about to run against a column that an\n * encryption map covers while the token index is unavailable.\n *\n * Without this the query degrades silently: the predicate compares a plaintext\n * pattern against ciphertext, matches nothing, and the endpoint returns an\n * empty page that is indistinguishable from a genuine no-result. Issue #2990.\n *\n * Never throws and never blocks the query \u2014 a failure here is logged at debug\n * level and the caller proceeds unchanged.\n */\nexport async function warnOnCiphertextLikeFallback(params: {\n entity: string\n fields: readonly string[]\n tenantId: string | null\n reason: CiphertextLikeFallbackReason\n service: CiphertextWarningEncryptionService\n}): Promise<void> {\n const { entity, tenantId, reason, service } = params\n if (!service || service.isEnabled?.() === false) return\n if (typeof service.getEncryptedFieldNames !== 'function') return\n\n const candidates = params.fields\n .filter((field) => typeof field === 'string' && !field.startsWith('cf:'))\n .map((field) => normalizeColumnName(field))\n .filter((field, index, all) => field.length > 0 && all.indexOf(field) === index)\n .filter((field) => !warned.has(warnKey(entity, tenantId, field)))\n if (!candidates.length) return\n\n try {\n // `organizationId: null` resolves the global map plus every per-organization\n // override, so a field encrypted for any organization is reported.\n const encrypted = new Set(\n (await service.getEncryptedFieldNames(entity, tenantId, null)).map((field) =>\n normalizeColumnName(String(field)),\n ),\n )\n for (const field of candidates) {\n if (!encrypted.has(field)) continue\n if (warned.size >= WARN_CACHE_CAP) warned.clear()\n warned.add(warnKey(entity, tenantId, field))\n logger.warn('Text search filter cannot match an encrypted column', {\n entity,\n field,\n reason,\n hint: reason === 'search-disabled'\n ? 'OM_SEARCH_ENABLED is off, so the filter runs as ILIKE against ciphertext and matches nothing.'\n : reason === 'no-indexable-tokens'\n ? 'The filter value produced no search tokens, so it runs as ILIKE against ciphertext and matches nothing. Use at least OM_SEARCH_MIN_LEN indexable characters, or filter on the deterministic hash column.'\n : 'No search_tokens exist for this entity and scope, so the filter runs as ILIKE against ciphertext and matches nothing. Index the entity through query_index and reindex, or filter on the deterministic hash column.',\n })\n }\n } catch (err) {\n logger.debug('Ciphertext search warning check failed', {\n entity,\n err: err instanceof Error ? err.message : String(err),\n })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAG7B,MAAM,SAAS,aAAa,QAAQ,EAAE,MAAM,EAAE,WAAW,QAAQ,CAAC;AAmBlE,MAAM,iBAAiB;AACvB,MAAM,SAAS,oBAAI,IAAY;AAE/B,MAAM,UAAU,CAAC,QAAgB,UAAyB,UACxD,GAAG,MAAM,IAAI,YAAY,MAAM,IAAI,KAAK;AAEnC,MAAM,sBAAsB,CAAC,UAClC,MAAM,QAAQ,sBAAsB,OAAO,EAAE,YAAY;AAGpD,SAAS,+BAAqC;AACnD,SAAO,MAAM;AACf;AAaA,eAAsB,6BAA6B,QAMjC;AAChB,QAAM,EAAE,QAAQ,UAAU,QAAQ,QAAQ,IAAI;AAC9C,MAAI,CAAC,WAAW,QAAQ,YAAY,MAAM,MAAO;AACjD,MAAI,OAAO,QAAQ,2BAA2B,WAAY;AAE1D,QAAM,aAAa,OAAO,OACvB,OAAO,CAAC,UAAU,OAAO,UAAU,YAAY,CAAC,MAAM,WAAW,KAAK,CAAC,EACvE,IAAI,CAAC,UAAU,oBAAoB,KAAK,CAAC,EACzC,OAAO,CAAC,OAAO,OAAO,QAAQ,MAAM,SAAS,KAAK,IAAI,QAAQ,KAAK,MAAM,KAAK,EAC9E,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,QAAQ,QAAQ,UAAU,KAAK,CAAC,CAAC;AAClE,MAAI,CAAC,WAAW,OAAQ;AAExB,MAAI;AAGF,UAAM,YAAY,IAAI;AAAA,OACnB,MAAM,QAAQ,uBAAuB,QAAQ,UAAU,IAAI,GAAG;AAAA,QAAI,CAAC,UAClE,oBAAoB,OAAO,KAAK,CAAC;AAAA,MACnC;AAAA,IACF;AACA,eAAW,SAAS,YAAY;AAC9B,UAAI,CAAC,UAAU,IAAI,KAAK,EAAG;AAC3B,UAAI,OAAO,QAAQ,eAAgB,QAAO,MAAM;AAChD,aAAO,IAAI,QAAQ,QAAQ,UAAU,KAAK,CAAC;AAC3C,aAAO,KAAK,uDAAuD;AAAA,QACjE;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM,WAAW,oBACb,kGACA,WAAW,wBACT,6MACA;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF,SAAS,KAAK;AACZ,WAAO,MAAM,0CAA0C;AAAA,MACrD;AAAA,MACA,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IACtD,CAAC;AAAA,EACH;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/lib/query/engine.js
CHANGED
|
@@ -6,12 +6,17 @@ import {
|
|
|
6
6
|
resolveJoins
|
|
7
7
|
} from "./join-utils.js";
|
|
8
8
|
import { resolveSearchConfig } from "../search/config.js";
|
|
9
|
+
import {
|
|
10
|
+
createSearchTokenAvailability,
|
|
11
|
+
isSearchFilterOp
|
|
12
|
+
} from "../search/availability.js";
|
|
9
13
|
import { tokenizeText } from "../search/tokenize.js";
|
|
10
14
|
import { runBeforeQueryPipeline, runAfterQueryPipeline } from "./query-extension-runner.js";
|
|
11
15
|
import {
|
|
12
16
|
buildCustomFieldDefinitionIndexFromRows,
|
|
13
17
|
resolveCfDefIndexOrgCandidates
|
|
14
18
|
} from "../crud/custom-field-definition-index.js";
|
|
19
|
+
import { warnOnCiphertextLikeFallback } from "./ciphertext-search-warning.js";
|
|
15
20
|
import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from "./encrypted-sort.js";
|
|
16
21
|
import { mapWithConcurrency } from "./bounded-decrypt.js";
|
|
17
22
|
import { createLogger } from "../logger/index.js";
|
|
@@ -133,8 +138,8 @@ class BasicQueryEngine {
|
|
|
133
138
|
this.getDbFn = getDbFn;
|
|
134
139
|
this.resolveEncryptionService = resolveEncryptionService;
|
|
135
140
|
this.columnCache = /* @__PURE__ */ new Map();
|
|
136
|
-
this.tableCache = /* @__PURE__ */ new Map();
|
|
137
141
|
this.searchAliasSeq = 0;
|
|
142
|
+
this.searchAvailabilityInstance = null;
|
|
138
143
|
}
|
|
139
144
|
getEncryptionService() {
|
|
140
145
|
try {
|
|
@@ -149,6 +154,21 @@ class BasicQueryEngine {
|
|
|
149
154
|
if (typeof emAny?.getKysely === "function") return emAny.getKysely();
|
|
150
155
|
throw new Error("BasicQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)");
|
|
151
156
|
}
|
|
157
|
+
searchAvailability() {
|
|
158
|
+
if (!this.searchAvailabilityInstance) {
|
|
159
|
+
this.searchAvailabilityInstance = createSearchTokenAvailability({
|
|
160
|
+
getDb: () => this.getDb(),
|
|
161
|
+
getConfig: resolveSearchConfig,
|
|
162
|
+
applyOrganizationScope: (query, column, scope) => this.applyOrganizationScope(
|
|
163
|
+
query,
|
|
164
|
+
column,
|
|
165
|
+
scope
|
|
166
|
+
),
|
|
167
|
+
logDebug: (event, payload) => this.logSearchDebug(event, payload)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return this.searchAvailabilityInstance;
|
|
171
|
+
}
|
|
152
172
|
async query(entity, opts = {}) {
|
|
153
173
|
const ext = opts.extensions;
|
|
154
174
|
let effectiveOpts = opts;
|
|
@@ -204,11 +224,10 @@ class BasicQueryEngine {
|
|
|
204
224
|
const { baseFilters, joinFilters } = partitionFilters(table, normalizedFilters, joinMap);
|
|
205
225
|
const cfFilters = normalizedFilters.filter((filter) => String(filter.field).startsWith("cf:"));
|
|
206
226
|
const searchConfig = resolveSearchConfig();
|
|
207
|
-
const
|
|
208
|
-
const
|
|
227
|
+
const searchFilters = [...baseFilters, ...cfFilters].filter((filter) => isSearchFilterOp(filter.op));
|
|
228
|
+
const searchEnabled = !skipAutoScope && await this.searchAvailability().staticEnabled();
|
|
229
|
+
const hasSearchTokens = searchEnabled && searchFilters.length ? await this.searchAvailability().hasTokens(String(entity), opts.tenantId ?? null, orgScope) : false;
|
|
209
230
|
const searchActive = searchEnabled && hasSearchTokens;
|
|
210
|
-
const joinSearchAvailability = /* @__PURE__ */ new Map();
|
|
211
|
-
const searchFilters = [...baseFilters, ...cfFilters].filter((filter) => filter.op === "like" || filter.op === "ilike");
|
|
212
231
|
if (searchFilters.length) {
|
|
213
232
|
const fields = searchFilters.map((filter) => String(filter.field));
|
|
214
233
|
this.logSearchDebug("search:init", {
|
|
@@ -238,6 +257,35 @@ class BasicQueryEngine {
|
|
|
238
257
|
organizationScope: orgScope
|
|
239
258
|
});
|
|
240
259
|
}
|
|
260
|
+
const fallbackFields = searchFilters.filter((filter) => !searchActive || typeof filter.value !== "string" || tokenizeText(filter.value, searchConfig).hashes.length === 0).map((filter) => String(filter.field));
|
|
261
|
+
if (fallbackFields.length) {
|
|
262
|
+
await warnOnCiphertextLikeFallback({
|
|
263
|
+
entity: String(entity),
|
|
264
|
+
fields: fallbackFields,
|
|
265
|
+
tenantId: opts.tenantId ?? null,
|
|
266
|
+
// `searchEnabled` also folds in the missing-table and
|
|
267
|
+
// omitAutomaticTenantOrgScope cases, which are "no usable tokens"
|
|
268
|
+
// rather than "the operator switched search off".
|
|
269
|
+
reason: searchActive ? "no-indexable-tokens" : searchConfig.enabled ? "no-search-tokens" : "search-disabled",
|
|
270
|
+
service: this.getEncryptionService()
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const [alias, joinedFilters] of joinFilters) {
|
|
275
|
+
const filters = joinedFilters.filter((entry) => entry.op === "like" || entry.op === "ilike");
|
|
276
|
+
if (!filters.length) continue;
|
|
277
|
+
const join = joinMap.get(alias);
|
|
278
|
+
if (!join?.entityId) continue;
|
|
279
|
+
const hasJoinedTokens = searchEnabled ? await this.searchAvailability().hasTokens(join.entityId, opts.tenantId ?? null, orgScope) : false;
|
|
280
|
+
const fallbackFields = filters.filter((filter) => !hasJoinedTokens || typeof filter.value !== "string" || tokenizeText(filter.value, searchConfig).hashes.length === 0).map((filter) => filter.column);
|
|
281
|
+
if (!fallbackFields.length) continue;
|
|
282
|
+
await warnOnCiphertextLikeFallback({
|
|
283
|
+
entity: join.entityId,
|
|
284
|
+
fields: fallbackFields,
|
|
285
|
+
tenantId: opts.tenantId ?? null,
|
|
286
|
+
reason: hasJoinedTokens ? "no-indexable-tokens" : searchConfig.enabled ? "no-search-tokens" : "search-disabled",
|
|
287
|
+
service: this.getEncryptionService()
|
|
288
|
+
});
|
|
241
289
|
}
|
|
242
290
|
const recordIdColumn = qualify("id");
|
|
243
291
|
const applyFilterOp = (builder, column, op, value, fieldName) => {
|
|
@@ -278,11 +326,7 @@ class BasicQueryEngine {
|
|
|
278
326
|
if (!searchEnabled || !join.entityId) return { applied: false, builder };
|
|
279
327
|
if (!["like", "ilike"].includes(filter.op)) return { applied: false, builder };
|
|
280
328
|
if (typeof filter.value !== "string" || filter.value.trim().length === 0) return { applied: false, builder };
|
|
281
|
-
|
|
282
|
-
if (searchAvailable === void 0) {
|
|
283
|
-
searchAvailable = await this.hasSearchTokens(join.entityId, opts.tenantId ?? null, orgScope);
|
|
284
|
-
joinSearchAvailability.set(join.entityId, searchAvailable);
|
|
285
|
-
}
|
|
329
|
+
const searchAvailable = await this.searchAvailability().hasTokens(join.entityId, opts.tenantId ?? null, orgScope);
|
|
286
330
|
if (!searchAvailable) return { applied: false, builder };
|
|
287
331
|
const tokens = tokenizeText(String(filter.value), searchConfig);
|
|
288
332
|
if (!tokens.hashes.length) return { applied: false, builder };
|
|
@@ -867,36 +911,6 @@ class BasicQueryEngine {
|
|
|
867
911
|
else this.columnCache.delete(key);
|
|
868
912
|
return present;
|
|
869
913
|
}
|
|
870
|
-
async tableExists(table) {
|
|
871
|
-
if (this.tableCache.has(table)) return this.tableCache.get(table) ?? false;
|
|
872
|
-
const db = this.getDb();
|
|
873
|
-
const exists = await db.selectFrom("information_schema.tables").select(sql`1`.as("one")).where("table_name", "=", table).limit(1).executeTakeFirst();
|
|
874
|
-
const present = !!exists;
|
|
875
|
-
this.tableCache.set(table, present);
|
|
876
|
-
return present;
|
|
877
|
-
}
|
|
878
|
-
async hasSearchTokens(entity, tenantId, orgScope) {
|
|
879
|
-
try {
|
|
880
|
-
const db = this.getDb();
|
|
881
|
-
let query = db.selectFrom("search_tokens").select(sql`1`.as("one")).where("entity_type", "=", entity).limit(1);
|
|
882
|
-
if (tenantId !== void 0) {
|
|
883
|
-
query = query.where(sql`tenant_id is not distinct from ${tenantId}`);
|
|
884
|
-
}
|
|
885
|
-
if (orgScope) {
|
|
886
|
-
query = this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
|
|
887
|
-
}
|
|
888
|
-
const row = await query.executeTakeFirst();
|
|
889
|
-
return !!row;
|
|
890
|
-
} catch (err) {
|
|
891
|
-
this.logSearchDebug("search:has-tokens-error", {
|
|
892
|
-
entity,
|
|
893
|
-
tenantId,
|
|
894
|
-
organizationScope: orgScope,
|
|
895
|
-
error: err instanceof Error ? err.message : String(err)
|
|
896
|
-
});
|
|
897
|
-
return false;
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
914
|
applySearchTokens(q, opts) {
|
|
901
915
|
if (!opts.hashes.length) {
|
|
902
916
|
this.logSearchDebug("search:skip-no-hashes", {
|