@open-mercato/shared 0.6.6-develop.5757.1.f5cc26cf92 → 0.6.6-develop.5876.1.6a45c370ac
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/lib/query/bounded-decrypt.js +18 -0
- package/dist/lib/query/bounded-decrypt.js.map +7 -0
- package/dist/lib/query/encrypted-sort.js +8 -0
- package/dist/lib/query/encrypted-sort.js.map +2 -2
- package/dist/lib/query/engine.js +389 -336
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/query/__tests__/bounded-decrypt.test.ts +49 -0
- package/src/lib/query/__tests__/engine.test.ts +159 -4
- package/src/lib/query/bounded-decrypt.ts +26 -0
- package/src/lib/query/encrypted-sort.ts +13 -0
- package/src/lib/query/engine.ts +496 -406
- package/src/lib/query/types.ts +8 -0
package/dist/lib/query/engine.js
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
buildCustomFieldDefinitionIndexFromRows,
|
|
13
13
|
resolveCfDefIndexOrgCandidates
|
|
14
14
|
} from "../crud/custom-field-definition-index.js";
|
|
15
|
-
import { resolveEncryptedSortFields, sortRowsInMemory } from "./encrypted-sort.js";
|
|
15
|
+
import { resolveEncryptedSortFields, resolveEncryptedSortMaxRows, sortRowsInMemory } from "./encrypted-sort.js";
|
|
16
|
+
import { mapWithConcurrency } from "./bounded-decrypt.js";
|
|
17
|
+
const DECRYPT_CONCURRENCY = 8;
|
|
16
18
|
const entityTableCache = /* @__PURE__ */ new Map();
|
|
17
19
|
const ENTITY_ID_PATTERN = /^[a-z][a-z0-9_]*:[a-z][a-z0-9_]*$/;
|
|
18
20
|
const isValidEntityIdShape = (value) => ENTITY_ID_PATTERN.test(value);
|
|
@@ -176,7 +178,6 @@ class BasicQueryEngine {
|
|
|
176
178
|
opts = coreOpts;
|
|
177
179
|
const table = resolveEntityTableName(this.em, entity);
|
|
178
180
|
const db = this.getDb();
|
|
179
|
-
let q = db.selectFrom(table);
|
|
180
181
|
const qualify = (col) => `${table}.${col}`;
|
|
181
182
|
const orgScope = this.resolveOrganizationScope(opts);
|
|
182
183
|
this.searchAliasSeq = 0;
|
|
@@ -186,15 +187,6 @@ class BasicQueryEngine {
|
|
|
186
187
|
);
|
|
187
188
|
}
|
|
188
189
|
const skipAutoScope = opts.omitAutomaticTenantOrgScope === true;
|
|
189
|
-
if (!skipAutoScope && orgScope && await this.columnExists(table, "organization_id")) {
|
|
190
|
-
q = this.applyOrganizationScope(q, qualify("organization_id"), orgScope);
|
|
191
|
-
}
|
|
192
|
-
if (!skipAutoScope && await this.columnExists(table, "tenant_id")) {
|
|
193
|
-
q = q.where(qualify("tenant_id"), "=", opts.tenantId);
|
|
194
|
-
}
|
|
195
|
-
if (!opts.withDeleted && await this.columnExists(table, "deleted_at")) {
|
|
196
|
-
q = q.where(qualify("deleted_at"), "is", null);
|
|
197
|
-
}
|
|
198
190
|
const normalizedFilters = normalizeFilters(opts.filters);
|
|
199
191
|
const resolvedJoins = resolveJoins(
|
|
200
192
|
table,
|
|
@@ -307,62 +299,6 @@ class BasicQueryEngine {
|
|
|
307
299
|
};
|
|
308
300
|
const regularBaseFilters = baseFilters.filter((f) => !f.orGroup);
|
|
309
301
|
const orGroupFilters = baseFilters.filter((f) => f.orGroup);
|
|
310
|
-
for (const filter of regularBaseFilters) {
|
|
311
|
-
const fieldName = String(filter.field);
|
|
312
|
-
let qualified = filter.qualified ?? null;
|
|
313
|
-
if (!qualified) {
|
|
314
|
-
const column = await this.resolveBaseColumn(table, fieldName);
|
|
315
|
-
if (!column) {
|
|
316
|
-
q = this.applyIndexDocFilter(q, {
|
|
317
|
-
entity: String(entity),
|
|
318
|
-
field: fieldName,
|
|
319
|
-
op: filter.op,
|
|
320
|
-
value: filter.value,
|
|
321
|
-
recordIdColumn,
|
|
322
|
-
tenantId: opts.tenantId ?? null,
|
|
323
|
-
organizationScope: orgScope,
|
|
324
|
-
withDeleted: opts.withDeleted === true,
|
|
325
|
-
searchActive,
|
|
326
|
-
searchConfig
|
|
327
|
-
});
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
qualified = qualify(column);
|
|
331
|
-
}
|
|
332
|
-
q = applyFilterOp(q, qualified, filter.op, filter.value, fieldName);
|
|
333
|
-
}
|
|
334
|
-
if (orGroupFilters.length > 0) {
|
|
335
|
-
const groups = /* @__PURE__ */ new Map();
|
|
336
|
-
for (const f of orGroupFilters) {
|
|
337
|
-
const group = groups.get(f.orGroup) ?? [];
|
|
338
|
-
group.push(f);
|
|
339
|
-
groups.set(f.orGroup, group);
|
|
340
|
-
}
|
|
341
|
-
const resolvedGroupFilters = [];
|
|
342
|
-
for (const [, groupFilters] of groups) {
|
|
343
|
-
const resolved = [];
|
|
344
|
-
for (const filter of groupFilters) {
|
|
345
|
-
const column = await this.resolveBaseColumn(table, String(filter.field));
|
|
346
|
-
if (column) {
|
|
347
|
-
resolved.push({
|
|
348
|
-
qualified: qualify(column),
|
|
349
|
-
op: filter.op,
|
|
350
|
-
value: filter.value,
|
|
351
|
-
fieldName: String(filter.field)
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (resolved.length > 0) resolvedGroupFilters.push(resolved);
|
|
356
|
-
}
|
|
357
|
-
if (resolvedGroupFilters.length > 0) {
|
|
358
|
-
q = q.where((eb) => eb.or(
|
|
359
|
-
resolvedGroupFilters.map((group) => {
|
|
360
|
-
const parts = group.map((rf) => this.buildColumnOpExpression(eb, rf.qualified, rf.op, rf.value));
|
|
361
|
-
return parts.length === 1 ? parts[0] : eb.and(parts);
|
|
362
|
-
})
|
|
363
|
-
));
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
302
|
const applyAliasScopes = async (builder, aliasName) => {
|
|
367
303
|
const targetTable = aliasTables.get(aliasName);
|
|
368
304
|
if (!targetTable) return builder;
|
|
@@ -375,19 +311,6 @@ class BasicQueryEngine {
|
|
|
375
311
|
}
|
|
376
312
|
return next;
|
|
377
313
|
};
|
|
378
|
-
q = await applyJoinFilters({
|
|
379
|
-
db,
|
|
380
|
-
baseTable: table,
|
|
381
|
-
builder: q,
|
|
382
|
-
joinMap,
|
|
383
|
-
joinFilters,
|
|
384
|
-
aliasTables,
|
|
385
|
-
qualifyBase: (column) => qualify(column),
|
|
386
|
-
applyAliasScope: (builder, alias) => applyAliasScopes(builder, alias),
|
|
387
|
-
applyFilterOp: (builder, column, op, value) => applyFilterOp(builder, column, op, value),
|
|
388
|
-
applyJoinFilterOp,
|
|
389
|
-
columnExists: (tbl, column) => this.columnExists(tbl, column)
|
|
390
|
-
});
|
|
391
314
|
const fallbackOrgId = opts.organizationId ?? (Array.isArray(opts.organizationIds) && opts.organizationIds.length === 1 ? opts.organizationIds[0] : null);
|
|
392
315
|
const encryptionService = this.getEncryptionService();
|
|
393
316
|
const resolvedSorts = [];
|
|
@@ -407,267 +330,380 @@ class BasicQueryEngine {
|
|
|
407
330
|
fallbackOrgId
|
|
408
331
|
);
|
|
409
332
|
const requiresPlaintextSort = encryptedSortFields.size > 0;
|
|
410
|
-
if (opts.fields && opts.fields.length) {
|
|
411
|
-
const cols = new Set(opts.fields.filter((f) => !f.startsWith("cf:")));
|
|
412
|
-
if (requiresPlaintextSort) {
|
|
413
|
-
for (const field of encryptedSortFields) cols.add(field);
|
|
414
|
-
}
|
|
415
|
-
for (const c of cols) {
|
|
416
|
-
q = q.select(sql.ref(qualify(c)).as(c));
|
|
417
|
-
}
|
|
418
|
-
} else {
|
|
419
|
-
q = q.select(sql`${sql.ref(table)}.*`.as("__all"));
|
|
420
|
-
}
|
|
421
333
|
const tenantId = opts.tenantId;
|
|
422
334
|
const sanitize = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (raw && typeof raw === "string") {
|
|
479
|
-
try {
|
|
480
|
-
cfg = JSON.parse(raw);
|
|
481
|
-
} catch {
|
|
482
|
-
cfg = {};
|
|
335
|
+
const buildQuery = async (projection) => {
|
|
336
|
+
const isSortKeysProjection = projection === "sortKeys";
|
|
337
|
+
let q = db.selectFrom(table);
|
|
338
|
+
if (!skipAutoScope && orgScope && await this.columnExists(table, "organization_id")) {
|
|
339
|
+
q = this.applyOrganizationScope(q, qualify("organization_id"), orgScope);
|
|
340
|
+
}
|
|
341
|
+
if (!skipAutoScope && await this.columnExists(table, "tenant_id")) {
|
|
342
|
+
q = q.where(qualify("tenant_id"), "=", opts.tenantId);
|
|
343
|
+
}
|
|
344
|
+
if (!opts.withDeleted && await this.columnExists(table, "deleted_at")) {
|
|
345
|
+
q = q.where(qualify("deleted_at"), "is", null);
|
|
346
|
+
}
|
|
347
|
+
for (const filter of regularBaseFilters) {
|
|
348
|
+
const fieldName = String(filter.field);
|
|
349
|
+
let qualified = filter.qualified ?? null;
|
|
350
|
+
if (!qualified) {
|
|
351
|
+
const column = await this.resolveBaseColumn(table, fieldName);
|
|
352
|
+
if (!column) {
|
|
353
|
+
q = this.applyIndexDocFilter(q, {
|
|
354
|
+
entity: String(entity),
|
|
355
|
+
field: fieldName,
|
|
356
|
+
op: filter.op,
|
|
357
|
+
value: filter.value,
|
|
358
|
+
recordIdColumn,
|
|
359
|
+
tenantId: opts.tenantId ?? null,
|
|
360
|
+
organizationScope: orgScope,
|
|
361
|
+
withDeleted: opts.withDeleted === true,
|
|
362
|
+
searchActive,
|
|
363
|
+
searchConfig
|
|
364
|
+
});
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
qualified = qualify(column);
|
|
368
|
+
}
|
|
369
|
+
q = applyFilterOp(q, qualified, filter.op, filter.value, fieldName);
|
|
370
|
+
}
|
|
371
|
+
if (orGroupFilters.length > 0) {
|
|
372
|
+
const groups = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const f of orGroupFilters) {
|
|
374
|
+
const group = groups.get(f.orGroup) ?? [];
|
|
375
|
+
group.push(f);
|
|
376
|
+
groups.set(f.orGroup, group);
|
|
377
|
+
}
|
|
378
|
+
const resolvedGroupFilters = [];
|
|
379
|
+
for (const [, groupFilters] of groups) {
|
|
380
|
+
const resolved = [];
|
|
381
|
+
for (const filter of groupFilters) {
|
|
382
|
+
const column = await this.resolveBaseColumn(table, String(filter.field));
|
|
383
|
+
if (column) {
|
|
384
|
+
resolved.push({
|
|
385
|
+
qualified: qualify(column),
|
|
386
|
+
op: filter.op,
|
|
387
|
+
value: filter.value,
|
|
388
|
+
fieldName: String(filter.field)
|
|
389
|
+
});
|
|
483
390
|
}
|
|
484
|
-
} else if (raw && typeof raw === "object") {
|
|
485
|
-
cfg = raw;
|
|
486
391
|
}
|
|
487
|
-
|
|
392
|
+
if (resolved.length > 0) resolvedGroupFilters.push(resolved);
|
|
393
|
+
}
|
|
394
|
+
if (resolvedGroupFilters.length > 0) {
|
|
395
|
+
q = q.where((eb) => eb.or(
|
|
396
|
+
resolvedGroupFilters.map((group) => {
|
|
397
|
+
const parts = group.map((rf) => this.buildColumnOpExpression(eb, rf.qualified, rf.op, rf.value));
|
|
398
|
+
return parts.length === 1 ? parts[0] : eb.and(parts);
|
|
399
|
+
})
|
|
400
|
+
));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
q = await applyJoinFilters({
|
|
404
|
+
db,
|
|
405
|
+
baseTable: table,
|
|
406
|
+
builder: q,
|
|
407
|
+
joinMap,
|
|
408
|
+
joinFilters,
|
|
409
|
+
aliasTables,
|
|
410
|
+
qualifyBase: (column) => qualify(column),
|
|
411
|
+
applyAliasScope: (builder, alias) => applyAliasScopes(builder, alias),
|
|
412
|
+
applyFilterOp: (builder, column, op, value) => applyFilterOp(builder, column, op, value),
|
|
413
|
+
applyJoinFilterOp,
|
|
414
|
+
columnExists: (tbl, column) => this.columnExists(tbl, column)
|
|
415
|
+
});
|
|
416
|
+
if (isSortKeysProjection) {
|
|
417
|
+
q = q.select(sql.ref(qualify("id")).as("id"));
|
|
418
|
+
for (const s of resolvedSorts) {
|
|
419
|
+
if (!s.field.startsWith("cf:")) q = q.select(sql.ref(qualify(s.field)).as(s.field));
|
|
420
|
+
}
|
|
421
|
+
} else if (opts.fields && opts.fields.length) {
|
|
422
|
+
const cols = new Set(opts.fields.filter((f) => !f.startsWith("cf:")));
|
|
423
|
+
if (requiresPlaintextSort) {
|
|
424
|
+
for (const field of encryptedSortFields) cols.add(field);
|
|
425
|
+
}
|
|
426
|
+
for (const c of cols) {
|
|
427
|
+
q = q.select(sql.ref(qualify(c)).as(c));
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
q = q.select(sql`${sql.ref(table)}.*`.as("__all"));
|
|
431
|
+
}
|
|
432
|
+
const cfSourcesResult = this.configureCustomFieldSources(q, table, entity, db, opts, qualify);
|
|
433
|
+
q = cfSourcesResult.builder;
|
|
434
|
+
const cfSources = cfSourcesResult.sources;
|
|
435
|
+
const entityIdToSource = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const source of cfSources) {
|
|
437
|
+
entityIdToSource.set(String(source.entityId), source);
|
|
438
|
+
}
|
|
439
|
+
const requestedCustomFieldKeys = !isSortKeysProjection && Array.isArray(opts.includeCustomFields) ? opts.includeCustomFields.map((key) => String(key)) : [];
|
|
440
|
+
const cfKeys = /* @__PURE__ */ new Set();
|
|
441
|
+
const keySource = /* @__PURE__ */ new Map();
|
|
442
|
+
let resolvedCustomFieldDefinitions2;
|
|
443
|
+
if (!isSortKeysProjection) {
|
|
444
|
+
for (const f of opts.fields || []) {
|
|
445
|
+
if (typeof f === "string" && f.startsWith("cf:")) cfKeys.add(f.slice(3));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const f of cfFilters) {
|
|
449
|
+
if (typeof f.field === "string" && f.field.startsWith("cf:")) cfKeys.add(f.field.slice(3));
|
|
450
|
+
}
|
|
451
|
+
if (!isSortKeysProjection && opts.includeCustomFields === true) {
|
|
452
|
+
if (entityIdToSource.size > 0) {
|
|
453
|
+
const entityIdList = Array.from(entityIdToSource.keys());
|
|
454
|
+
const entityOrder = /* @__PURE__ */ new Map();
|
|
455
|
+
entityIdList.forEach((id, idx) => entityOrder.set(id, idx));
|
|
456
|
+
const rows = await db.selectFrom("custom_field_defs").select([
|
|
457
|
+
"key",
|
|
458
|
+
"entity_id",
|
|
459
|
+
"config_json",
|
|
460
|
+
"kind",
|
|
461
|
+
"organization_id",
|
|
462
|
+
"tenant_id",
|
|
463
|
+
"updated_at",
|
|
464
|
+
"deleted_at"
|
|
465
|
+
]).where("entity_id", "in", entityIdList).where("is_active", "=", true).where((eb) => eb.or([
|
|
466
|
+
eb("tenant_id", "=", tenantId),
|
|
467
|
+
eb("tenant_id", "is", null)
|
|
468
|
+
])).execute();
|
|
469
|
+
const orgCandidates = resolveCfDefIndexOrgCandidates(opts.organizationIds, opts.organizationId ?? null);
|
|
470
|
+
const definitionRows = rows.map((row) => ({
|
|
488
471
|
key: String(row.key),
|
|
489
472
|
entityId: String(row.entity_id),
|
|
490
|
-
kind: String(row.kind
|
|
491
|
-
|
|
473
|
+
kind: row.kind == null ? null : String(row.kind),
|
|
474
|
+
configJson: row.config_json,
|
|
475
|
+
organizationId: row.organization_id == null ? null : String(row.organization_id),
|
|
476
|
+
tenantId: row.tenant_id == null ? null : String(row.tenant_id),
|
|
477
|
+
deletedAt: row.deleted_at ?? null,
|
|
478
|
+
updatedAt: row.updated_at ?? null
|
|
479
|
+
}));
|
|
480
|
+
resolvedCustomFieldDefinitions2 = {
|
|
481
|
+
index: buildCustomFieldDefinitionIndexFromRows(definitionRows, { organizationIds: orgCandidates }),
|
|
482
|
+
entityIds: entityIdList,
|
|
483
|
+
tenantId: tenantId ?? null,
|
|
484
|
+
organizationIds: orgCandidates
|
|
492
485
|
};
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
486
|
+
const sorted = rows.map((row) => {
|
|
487
|
+
const raw = row.config_json;
|
|
488
|
+
let cfg = {};
|
|
489
|
+
if (raw && typeof raw === "string") {
|
|
490
|
+
try {
|
|
491
|
+
cfg = JSON.parse(raw);
|
|
492
|
+
} catch {
|
|
493
|
+
cfg = {};
|
|
494
|
+
}
|
|
495
|
+
} else if (raw && typeof raw === "object") {
|
|
496
|
+
cfg = raw;
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
key: String(row.key),
|
|
500
|
+
entityId: String(row.entity_id),
|
|
501
|
+
kind: String(row.kind || ""),
|
|
502
|
+
config: cfg
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
sorted.sort((a, b) => {
|
|
506
|
+
const ai = entityOrder.get(a.entityId) ?? Number.MAX_SAFE_INTEGER;
|
|
507
|
+
const bi = entityOrder.get(b.entityId) ?? Number.MAX_SAFE_INTEGER;
|
|
508
|
+
if (ai !== bi) return ai - bi;
|
|
509
|
+
return a.key.localeCompare(b.key);
|
|
510
|
+
});
|
|
511
|
+
const selectedSources = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const row of sorted) {
|
|
513
|
+
const source = entityIdToSource.get(row.entityId);
|
|
514
|
+
if (!source) continue;
|
|
515
|
+
const cfg = row.config || {};
|
|
516
|
+
const entityIndex = entityOrder.get(row.entityId) ?? Number.MAX_SAFE_INTEGER;
|
|
517
|
+
const scores = computeCustomFieldScore(cfg, row.kind, entityIndex);
|
|
518
|
+
const existing = selectedSources.get(row.key);
|
|
519
|
+
if (!existing || scores.base > existing.score || scores.base === existing.score && (scores.penalty < existing.penalty || scores.penalty === existing.penalty && scores.entityIndex < existing.entityIndex)) {
|
|
520
|
+
selectedSources.set(row.key, { source, score: scores.base, penalty: scores.penalty, entityIndex: scores.entityIndex });
|
|
521
|
+
}
|
|
522
|
+
cfKeys.add(row.key);
|
|
523
|
+
}
|
|
524
|
+
for (const [key, entry] of selectedSources.entries()) {
|
|
525
|
+
keySource.set(key, entry.source);
|
|
510
526
|
}
|
|
511
|
-
cfKeys.add(row.key);
|
|
512
527
|
}
|
|
513
|
-
|
|
514
|
-
|
|
528
|
+
} else if (!isSortKeysProjection && requestedCustomFieldKeys.length > 0) {
|
|
529
|
+
for (const key of requestedCustomFieldKeys) cfKeys.add(key);
|
|
530
|
+
}
|
|
531
|
+
const unresolvedKeys = Array.from(cfKeys).filter((key) => !keySource.has(key));
|
|
532
|
+
if (unresolvedKeys.length > 0 && entityIdToSource.size > 0) {
|
|
533
|
+
const rows = await db.selectFrom("custom_field_defs").select(["key", "entity_id"]).where("entity_id", "in", Array.from(entityIdToSource.keys())).where("key", "in", unresolvedKeys).where("is_active", "=", true).where((eb) => eb.or([
|
|
534
|
+
eb("tenant_id", "=", tenantId),
|
|
535
|
+
eb("tenant_id", "is", null)
|
|
536
|
+
])).execute();
|
|
537
|
+
for (const row of rows) {
|
|
538
|
+
const source = entityIdToSource.get(String(row.entity_id));
|
|
539
|
+
if (!source) continue;
|
|
540
|
+
if (!keySource.has(row.key)) keySource.set(row.key, source);
|
|
515
541
|
}
|
|
516
542
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
eb("tenant_id", "=", tenantId),
|
|
524
|
-
eb("tenant_id", "is", null)
|
|
525
|
-
])).execute();
|
|
526
|
-
for (const row of rows) {
|
|
527
|
-
const source = entityIdToSource.get(String(row.entity_id));
|
|
543
|
+
const cfValueExprByKey = {};
|
|
544
|
+
const cfSelectedAliases = [];
|
|
545
|
+
const cfJsonAliases2 = /* @__PURE__ */ new Set();
|
|
546
|
+
const cfMultiAliasByAlias2 = /* @__PURE__ */ new Map();
|
|
547
|
+
for (const key of cfKeys) {
|
|
548
|
+
const source = keySource.get(key);
|
|
528
549
|
if (!source) continue;
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
eb(`${valAlias}.tenant_id`, "=", tenantId),
|
|
556
|
-
eb(`${valAlias}.tenant_id`, "is", null)
|
|
557
|
-
]))
|
|
558
|
-
);
|
|
559
|
-
const caseExpr = sql`CASE ${sql.ref(`${defAlias}.kind`)}
|
|
560
|
-
WHEN 'integer' THEN (${sql.ref(`${valAlias}.value_int`)})::text
|
|
561
|
-
WHEN 'float' THEN (${sql.ref(`${valAlias}.value_float`)})::text
|
|
562
|
-
WHEN 'boolean' THEN (${sql.ref(`${valAlias}.value_bool`)})::text
|
|
563
|
-
WHEN 'multiline' THEN (${sql.ref(`${valAlias}.value_multiline`)})::text
|
|
564
|
-
ELSE (${sql.ref(`${valAlias}.value_text`)})::text
|
|
565
|
-
END`;
|
|
566
|
-
cfValueExprByKey[key] = caseExpr;
|
|
567
|
-
const alias = sanitize(`cf:${key}`);
|
|
568
|
-
if ((opts.fields || []).includes(`cf:${key}`) || opts.includeCustomFields === true || requestedCustomFieldKeys.length > 0 && requestedCustomFieldKeys.includes(key)) {
|
|
569
|
-
const multiAlias = `${alias}__is_multi`;
|
|
570
|
-
const isMultiExpr = sql`bool_or(coalesce((${sql.ref(`${defAlias}.config_json`)}->>'multi')::boolean, false))`;
|
|
571
|
-
const aggregatedArray = sql`array_remove(array_agg(DISTINCT ${caseExpr}), NULL)`;
|
|
572
|
-
const projExpr = sql`CASE WHEN ${isMultiExpr}
|
|
573
|
-
THEN to_jsonb(${aggregatedArray})
|
|
574
|
-
ELSE to_jsonb(max(${caseExpr}))
|
|
550
|
+
const entityIdForKey = source.entityId;
|
|
551
|
+
const recordIdExpr = source.recordIdExpr;
|
|
552
|
+
const sourceAliasSafe = sanitize(source.alias || "src");
|
|
553
|
+
const keyAliasSafe = sanitize(key);
|
|
554
|
+
const defAlias = `cfd_${sourceAliasSafe}_${keyAliasSafe}`;
|
|
555
|
+
const valAlias = `cfv_${sourceAliasSafe}_${keyAliasSafe}`;
|
|
556
|
+
q = q.leftJoin(
|
|
557
|
+
`custom_field_defs as ${defAlias}`,
|
|
558
|
+
(jb) => jb.on(`${defAlias}.entity_id`, "=", String(entityIdForKey)).on(`${defAlias}.key`, "=", key).on(`${defAlias}.is_active`, "=", true).on((eb) => eb.or([
|
|
559
|
+
eb(`${defAlias}.tenant_id`, "=", tenantId),
|
|
560
|
+
eb(`${defAlias}.tenant_id`, "is", null)
|
|
561
|
+
]))
|
|
562
|
+
);
|
|
563
|
+
q = q.leftJoin(
|
|
564
|
+
`custom_field_values as ${valAlias}`,
|
|
565
|
+
(jb) => jb.on(`${valAlias}.entity_id`, "=", String(entityIdForKey)).on(`${valAlias}.field_key`, "=", key).onRef(`${valAlias}.record_id`, "=", recordIdExpr).on((eb) => eb.or([
|
|
566
|
+
eb(`${valAlias}.tenant_id`, "=", tenantId),
|
|
567
|
+
eb(`${valAlias}.tenant_id`, "is", null)
|
|
568
|
+
]))
|
|
569
|
+
);
|
|
570
|
+
const caseExpr = sql`CASE ${sql.ref(`${defAlias}.kind`)}
|
|
571
|
+
WHEN 'integer' THEN (${sql.ref(`${valAlias}.value_int`)})::text
|
|
572
|
+
WHEN 'float' THEN (${sql.ref(`${valAlias}.value_float`)})::text
|
|
573
|
+
WHEN 'boolean' THEN (${sql.ref(`${valAlias}.value_bool`)})::text
|
|
574
|
+
WHEN 'multiline' THEN (${sql.ref(`${valAlias}.value_multiline`)})::text
|
|
575
|
+
ELSE (${sql.ref(`${valAlias}.value_text`)})::text
|
|
575
576
|
END`;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
577
|
+
cfValueExprByKey[key] = caseExpr;
|
|
578
|
+
const alias = sanitize(`cf:${key}`);
|
|
579
|
+
if (!isSortKeysProjection && ((opts.fields || []).includes(`cf:${key}`) || opts.includeCustomFields === true || requestedCustomFieldKeys.length > 0 && requestedCustomFieldKeys.includes(key))) {
|
|
580
|
+
const multiAlias = `${alias}__is_multi`;
|
|
581
|
+
const isMultiExpr = sql`bool_or(coalesce((${sql.ref(`${defAlias}.config_json`)}->>'multi')::boolean, false))`;
|
|
582
|
+
const aggregatedArray = sql`array_remove(array_agg(DISTINCT ${caseExpr}), NULL)`;
|
|
583
|
+
const projExpr = sql`CASE WHEN ${isMultiExpr}
|
|
584
|
+
THEN to_jsonb(${aggregatedArray})
|
|
585
|
+
ELSE to_jsonb(max(${caseExpr}))
|
|
586
|
+
END`;
|
|
587
|
+
q = q.select(projExpr.as(alias));
|
|
588
|
+
q = q.select(isMultiExpr.as(multiAlias));
|
|
589
|
+
cfSelectedAliases.push(alias);
|
|
590
|
+
cfJsonAliases2.add(alias);
|
|
591
|
+
cfMultiAliasByAlias2.set(alias, multiAlias);
|
|
592
|
+
}
|
|
581
593
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
594
|
+
for (const f of cfFilters) {
|
|
595
|
+
if (!f.field.startsWith("cf:")) continue;
|
|
596
|
+
const key = f.field.slice(3);
|
|
597
|
+
const expr = cfValueExprByKey[key];
|
|
598
|
+
if (!expr) continue;
|
|
599
|
+
if ((f.op === "like" || f.op === "ilike") && searchActive && typeof f.value === "string") {
|
|
600
|
+
const tokens = tokenizeText(String(f.value), searchConfig);
|
|
601
|
+
const hashes = tokens.hashes;
|
|
602
|
+
if (hashes.length) {
|
|
603
|
+
const result = this.applySearchTokens(q, {
|
|
604
|
+
entity: String(entity),
|
|
605
|
+
field: f.field,
|
|
606
|
+
hashes,
|
|
607
|
+
recordIdColumn,
|
|
608
|
+
tenantId: opts.tenantId ?? null,
|
|
609
|
+
organizationScope: orgScope,
|
|
610
|
+
tokens: tokens.tokens
|
|
611
|
+
});
|
|
612
|
+
this.logSearchDebug("search:cf-filter", {
|
|
613
|
+
entity: String(entity),
|
|
614
|
+
field: f.field,
|
|
615
|
+
tokens: tokens.tokens,
|
|
616
|
+
hashes,
|
|
617
|
+
applied: result.applied,
|
|
618
|
+
tenantId: opts.tenantId ?? null,
|
|
619
|
+
organizationScope: orgScope
|
|
620
|
+
});
|
|
621
|
+
if (result.applied) {
|
|
622
|
+
q = result.builder;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
this.logSearchDebug("search:cf-skip-empty-hashes", {
|
|
627
|
+
entity: String(entity),
|
|
628
|
+
field: f.field,
|
|
629
|
+
value: f.value
|
|
630
|
+
});
|
|
613
631
|
}
|
|
614
|
-
} else {
|
|
615
|
-
this.logSearchDebug("search:cf-skip-empty-hashes", {
|
|
616
|
-
entity: String(entity),
|
|
617
|
-
field: f.field,
|
|
618
|
-
value: f.value
|
|
619
|
-
});
|
|
620
632
|
}
|
|
633
|
+
q = this.applyColumnOp(q, expr, f.op, f.value);
|
|
621
634
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
);
|
|
635
|
+
if (opts.includeExtensions) {
|
|
636
|
+
const { getModules } = await import("@open-mercato/shared/lib/i18n/server");
|
|
637
|
+
const allMods = getModules();
|
|
638
|
+
const allExts = allMods.flatMap((m) => m.entityExtensions || []);
|
|
639
|
+
const exts = allExts.filter((e) => e.base === entity);
|
|
640
|
+
const chosen = Array.isArray(opts.includeExtensions) ? exts.filter((e) => opts.includeExtensions.includes(e.extension)) : exts;
|
|
641
|
+
for (const e of chosen) {
|
|
642
|
+
const [, extName] = e.extension.split(":");
|
|
643
|
+
const extTable = extName.endsWith("s") ? extName : `${extName}s`;
|
|
644
|
+
const alias = `ext_${sanitize(extName)}`;
|
|
645
|
+
q = q.leftJoin(
|
|
646
|
+
`${extTable} as ${alias}`,
|
|
647
|
+
(jb) => jb.onRef(`${alias}.${e.join.extensionKey}`, "=", `${table}.${e.join.baseKey}`)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
638
650
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
651
|
+
for (const s of resolvedSorts) {
|
|
652
|
+
if (s.field.startsWith("cf:")) {
|
|
653
|
+
const key = s.field.slice(3);
|
|
654
|
+
const alias = sanitize(`cf:${key}`);
|
|
655
|
+
if (!cfSelectedAliases.includes(alias)) {
|
|
656
|
+
const expr = cfValueExprByKey[key];
|
|
657
|
+
if (expr) {
|
|
658
|
+
q = q.select(sql`max(${expr})`.as(alias));
|
|
659
|
+
cfSelectedAliases.push(alias);
|
|
660
|
+
}
|
|
649
661
|
}
|
|
662
|
+
if (!requiresPlaintextSort) q = q.orderBy(alias, s.dir ?? "asc");
|
|
663
|
+
} else {
|
|
664
|
+
if (!requiresPlaintextSort) q = q.orderBy(qualify(s.field), s.dir ?? "asc");
|
|
650
665
|
}
|
|
651
|
-
if (!requiresPlaintextSort) q = q.orderBy(alias, s.dir ?? "asc");
|
|
652
|
-
} else {
|
|
653
|
-
if (!requiresPlaintextSort) q = q.orderBy(qualify(s.field), s.dir ?? "asc");
|
|
654
666
|
}
|
|
655
|
-
|
|
667
|
+
const hasJoinedAggregates2 = opts.includeExtensions && (Array.isArray(opts.includeExtensions) ? opts.includeExtensions.length > 0 : true) || Object.keys(cfValueExprByKey).length > 0;
|
|
668
|
+
if (hasJoinedAggregates2) {
|
|
669
|
+
q = q.groupBy(`${table}.id`);
|
|
670
|
+
}
|
|
671
|
+
return { builder: q, hasJoinedAggregates: hasJoinedAggregates2, cfJsonAliases: cfJsonAliases2, cfMultiAliasByAlias: cfMultiAliasByAlias2, resolvedCustomFieldDefinitions: resolvedCustomFieldDefinitions2 };
|
|
672
|
+
};
|
|
656
673
|
const page = opts.page?.page ?? 1;
|
|
657
674
|
const pageSize = opts.page?.pageSize ?? 20;
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
675
|
+
const {
|
|
676
|
+
builder: qFull,
|
|
677
|
+
hasJoinedAggregates,
|
|
678
|
+
cfJsonAliases,
|
|
679
|
+
cfMultiAliasByAlias,
|
|
680
|
+
resolvedCustomFieldDefinitions
|
|
681
|
+
} = await buildQuery("full");
|
|
662
682
|
const mayMultiplyBaseRows = hasJoinedAggregates || Array.isArray(opts.joins) && opts.joins.length > 0 || Array.isArray(opts.customFieldSources) && opts.customFieldSources.length > 0;
|
|
663
683
|
const countExpr = mayMultiplyBaseRows ? sql`count(distinct ${sql.ref(`${table}.id`)})` : sql`count(*)`;
|
|
664
|
-
const countBuilder = hasJoinedAggregates ?
|
|
684
|
+
const countBuilder = hasJoinedAggregates ? qFull.clearSelect().clearOrderBy().clearGroupBy().select(countExpr.as("count")) : qFull.clearSelect().clearOrderBy().select(countExpr.as("count"));
|
|
665
685
|
const countRow = await countBuilder.executeTakeFirst();
|
|
666
686
|
const total = Number(countRow?.count ?? 0);
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
687
|
+
const svc = encryptionService;
|
|
688
|
+
const decryptPayload = svc?.decryptEntityPayload?.bind(svc);
|
|
689
|
+
const decryptRow = async (item) => {
|
|
690
|
+
if (!decryptPayload) return item;
|
|
691
|
+
try {
|
|
692
|
+
const decrypted = await decryptPayload(
|
|
693
|
+
entity,
|
|
694
|
+
item,
|
|
695
|
+
item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null,
|
|
696
|
+
item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null
|
|
697
|
+
);
|
|
698
|
+
return { ...item, ...decrypted };
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.error("QueryEngine: error decrypting entity payload", err);
|
|
701
|
+
return item;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
const normalizeCfJsonAliases = (rows) => {
|
|
705
|
+
if (cfJsonAliases.size === 0) return;
|
|
706
|
+
for (const row of rows) {
|
|
671
707
|
for (const alias of cfJsonAliases) {
|
|
672
708
|
const multiAlias = cfMultiAliasByAlias.get(alias);
|
|
673
709
|
const isMulti = multiAlias ? Boolean(row[multiAlias]) : false;
|
|
@@ -689,30 +725,47 @@ class BasicQueryEngine {
|
|
|
689
725
|
if (multiAlias) delete row[multiAlias];
|
|
690
726
|
}
|
|
691
727
|
}
|
|
728
|
+
};
|
|
729
|
+
let pagedItems;
|
|
730
|
+
let encryptedSortRowCapWarning;
|
|
731
|
+
if (requiresPlaintextSort) {
|
|
732
|
+
const cap = resolveEncryptedSortMaxRows();
|
|
733
|
+
let qSort = (await buildQuery("sortKeys")).builder;
|
|
734
|
+
if (cap !== null) {
|
|
735
|
+
qSort = qSort.limit(cap).orderBy(qualify("id"), "asc");
|
|
736
|
+
}
|
|
737
|
+
const candidateRows = await qSort.execute();
|
|
738
|
+
const decryptedCandidates = decryptPayload ? await mapWithConcurrency(candidateRows, DECRYPT_CONCURRENCY, decryptRow) : candidateRows;
|
|
739
|
+
const orderedCandidates = sortRowsInMemory(decryptedCandidates, resolvedSorts);
|
|
740
|
+
const pageIds = orderedCandidates.slice((page - 1) * pageSize, page * pageSize).map((row) => row.id);
|
|
741
|
+
if (cap !== null && total > cap) {
|
|
742
|
+
encryptedSortRowCapWarning = {
|
|
743
|
+
entity,
|
|
744
|
+
sortFields: resolvedSorts.map((s) => s.field),
|
|
745
|
+
maxRows: cap,
|
|
746
|
+
totalMatched: total
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
if (pageIds.length === 0) {
|
|
750
|
+
pagedItems = [];
|
|
751
|
+
} else {
|
|
752
|
+
const pageRows = await qFull.where(qualify("id"), "in", pageIds).execute();
|
|
753
|
+
normalizeCfJsonAliases(pageRows);
|
|
754
|
+
const decryptedPageRows = decryptPayload ? await mapWithConcurrency(pageRows, DECRYPT_CONCURRENCY, decryptRow) : pageRows;
|
|
755
|
+
const byId = new Map(decryptedPageRows.map((row) => [String(row.id), row]));
|
|
756
|
+
pagedItems = pageIds.map((id) => byId.get(String(id))).filter((row) => row != null);
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
const dataQuery = qFull.limit(pageSize).offset((page - 1) * pageSize);
|
|
760
|
+
const items = await dataQuery.execute();
|
|
761
|
+
normalizeCfJsonAliases(items);
|
|
762
|
+
pagedItems = decryptPayload ? await mapWithConcurrency(items, DECRYPT_CONCURRENCY, decryptRow) : items;
|
|
692
763
|
}
|
|
693
|
-
const svc = encryptionService;
|
|
694
|
-
const decryptPayload = svc?.decryptEntityPayload?.bind(svc);
|
|
695
|
-
let decryptedItems = items;
|
|
696
|
-
if (decryptPayload) {
|
|
697
|
-
decryptedItems = await Promise.all(
|
|
698
|
-
items.map(async (item) => {
|
|
699
|
-
try {
|
|
700
|
-
const decrypted = await decryptPayload(
|
|
701
|
-
entity,
|
|
702
|
-
item,
|
|
703
|
-
item?.tenant_id ?? item?.tenantId ?? opts.tenantId ?? null,
|
|
704
|
-
item?.organization_id ?? item?.organizationId ?? fallbackOrgId ?? null
|
|
705
|
-
);
|
|
706
|
-
return { ...item, ...decrypted };
|
|
707
|
-
} catch (err) {
|
|
708
|
-
console.error("QueryEngine: error decrypting entity payload", err);
|
|
709
|
-
return item;
|
|
710
|
-
}
|
|
711
|
-
})
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
const pagedItems = requiresPlaintextSort ? sortRowsInMemory(decryptedItems, resolvedSorts).slice((page - 1) * pageSize, page * pageSize) : decryptedItems;
|
|
715
764
|
let queryResult = { items: pagedItems, page, pageSize, total };
|
|
765
|
+
if (encryptedSortRowCapWarning) {
|
|
766
|
+
const meta = { encryptedSortRowCapWarning };
|
|
767
|
+
queryResult.meta = meta;
|
|
768
|
+
}
|
|
716
769
|
if (ext && extensionCtx) {
|
|
717
770
|
const diCtx = ext.resolve ? { resolve: ext.resolve } : noop;
|
|
718
771
|
queryResult = await runAfterQueryPipeline(
|