@open-mercato/shared 0.5.1-develop.2683.4878a05b8e → 0.5.1-develop.2694.732417c5ec
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 +2 -2
- package/build.mjs +13 -102
- package/dist/lib/api/crud.js +1 -1
- package/dist/lib/api/crud.js.map +2 -2
- package/dist/lib/auth/server.js +1 -1
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/data/engine.js +68 -27
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/mikro.js +18 -22
- package/dist/lib/db/mikro.js.map +2 -2
- package/dist/lib/indexers/error-log.js +10 -12
- package/dist/lib/indexers/error-log.js.map +2 -2
- package/dist/lib/indexers/status-log.js +14 -16
- package/dist/lib/indexers/status-log.js.map +2 -2
- package/dist/lib/query/engine.js +220 -228
- package/dist/lib/query/engine.js.map +3 -3
- package/dist/lib/query/join-utils.js +28 -23
- package/dist/lib/query/join-utils.js.map +2 -2
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/jest.config.cjs +4 -2
- package/package.json +1 -1
- package/src/lib/api/__tests__/crud.test.ts +5 -3
- package/src/lib/api/crud.ts +1 -1
- package/src/lib/auth/__tests__/server.apiKeyCache.test.ts +10 -4
- package/src/lib/auth/server.ts +1 -1
- package/src/lib/bootstrap/types.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.test.ts +27 -17
- package/src/lib/data/engine.ts +95 -47
- package/src/lib/db/mikro.ts +26 -25
- package/src/lib/indexers/error-log.ts +23 -23
- package/src/lib/indexers/status-log.ts +36 -33
- package/src/lib/query/__tests__/engine.scope-and-or.test.ts +253 -114
- package/src/lib/query/__tests__/engine.test.ts +206 -139
- package/src/lib/query/engine.ts +306 -263
- package/src/lib/query/join-utils.ts +38 -30
package/dist/lib/query/engine.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
1
2
|
import {
|
|
2
3
|
applyJoinFilters,
|
|
3
4
|
normalizeFilters,
|
|
@@ -84,7 +85,7 @@ function buildFilterableCustomFieldJoins(sources) {
|
|
|
84
85
|
}];
|
|
85
86
|
});
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
+
function computeCustomFieldScore(cfg, kind, entityIndex) {
|
|
88
89
|
const listVisibleScore = cfg.listVisible === false ? 0 : 1;
|
|
89
90
|
const formEditableScore = cfg.formEditable === false ? 0 : 1;
|
|
90
91
|
const filterableScore = cfg.filterable ? 1 : 0;
|
|
@@ -111,11 +112,11 @@ const computeScore = (cfg, kind, entityIndex) => {
|
|
|
111
112
|
const base = listVisibleScore * 16 + formEditableScore * 8 + filterableScore * 4 + kindScore + optionsBonus + dictionaryBonus;
|
|
112
113
|
const penalty = typeof cfg.priority === "number" ? cfg.priority : 0;
|
|
113
114
|
return { base, penalty, entityIndex };
|
|
114
|
-
}
|
|
115
|
+
}
|
|
115
116
|
class BasicQueryEngine {
|
|
116
|
-
constructor(em,
|
|
117
|
+
constructor(em, getDbFn, resolveEncryptionService) {
|
|
117
118
|
this.em = em;
|
|
118
|
-
this.
|
|
119
|
+
this.getDbFn = getDbFn;
|
|
119
120
|
this.resolveEncryptionService = resolveEncryptionService;
|
|
120
121
|
this.columnCache = /* @__PURE__ */ new Map();
|
|
121
122
|
this.tableCache = /* @__PURE__ */ new Map();
|
|
@@ -128,6 +129,12 @@ class BasicQueryEngine {
|
|
|
128
129
|
return null;
|
|
129
130
|
}
|
|
130
131
|
}
|
|
132
|
+
getDb() {
|
|
133
|
+
if (this.getDbFn) return this.getDbFn();
|
|
134
|
+
const emAny = this.em;
|
|
135
|
+
if (typeof emAny?.getKysely === "function") return emAny.getKysely();
|
|
136
|
+
throw new Error("BasicQueryEngine requires an EntityManager exposing getKysely() (MikroORM v7)");
|
|
137
|
+
}
|
|
131
138
|
async query(entity, opts = {}) {
|
|
132
139
|
const ext = opts.extensions;
|
|
133
140
|
let effectiveOpts = opts;
|
|
@@ -156,8 +163,8 @@ class BasicQueryEngine {
|
|
|
156
163
|
const { extensions: _ext, ...coreOpts } = effectiveOpts;
|
|
157
164
|
opts = coreOpts;
|
|
158
165
|
const table = resolveEntityTableName(this.em, entity);
|
|
159
|
-
const
|
|
160
|
-
let q =
|
|
166
|
+
const db = this.getDb();
|
|
167
|
+
let q = db.selectFrom(table);
|
|
161
168
|
const qualify = (col) => `${table}.${col}`;
|
|
162
169
|
const orgScope = this.resolveOrganizationScope(opts);
|
|
163
170
|
this.searchAliasSeq = 0;
|
|
@@ -171,10 +178,10 @@ class BasicQueryEngine {
|
|
|
171
178
|
q = this.applyOrganizationScope(q, qualify("organization_id"), orgScope);
|
|
172
179
|
}
|
|
173
180
|
if (!skipAutoScope && await this.columnExists(table, "tenant_id")) {
|
|
174
|
-
q = q.where(qualify("tenant_id"), opts.tenantId);
|
|
181
|
+
q = q.where(qualify("tenant_id"), "=", opts.tenantId);
|
|
175
182
|
}
|
|
176
183
|
if (!opts.withDeleted && await this.columnExists(table, "deleted_at")) {
|
|
177
|
-
q = q.
|
|
184
|
+
q = q.where(qualify("deleted_at"), "is", null);
|
|
178
185
|
}
|
|
179
186
|
const normalizedFilters = normalizeFilters(opts.filters);
|
|
180
187
|
const resolvedJoins = resolveJoins(
|
|
@@ -230,11 +237,11 @@ class BasicQueryEngine {
|
|
|
230
237
|
}
|
|
231
238
|
const recordIdColumn = qualify("id");
|
|
232
239
|
const applyFilterOp = (builder, column, op, value, fieldName) => {
|
|
233
|
-
if ((op === "like" || op === "ilike") && searchActive && typeof value === "string" && fieldName) {
|
|
240
|
+
if ((op === "like" || op === "ilike") && searchActive && typeof value === "string" && fieldName && typeof column === "string") {
|
|
234
241
|
const tokens = tokenizeText(String(value), searchConfig);
|
|
235
242
|
const hashes = tokens.hashes;
|
|
236
243
|
if (hashes.length) {
|
|
237
|
-
const
|
|
244
|
+
const result = this.applySearchTokens(builder, {
|
|
238
245
|
entity: String(entity),
|
|
239
246
|
field: fieldName,
|
|
240
247
|
hashes,
|
|
@@ -248,11 +255,11 @@ class BasicQueryEngine {
|
|
|
248
255
|
field: fieldName,
|
|
249
256
|
tokens: tokens.tokens,
|
|
250
257
|
hashes,
|
|
251
|
-
applied,
|
|
258
|
+
applied: result.applied,
|
|
252
259
|
tenantId: opts.tenantId ?? null,
|
|
253
260
|
organizationScope: orgScope
|
|
254
261
|
});
|
|
255
|
-
if (applied) return builder;
|
|
262
|
+
if (result.applied) return result.builder;
|
|
256
263
|
} else {
|
|
257
264
|
this.logSearchDebug("search:skip-empty-hashes", {
|
|
258
265
|
entity: String(entity),
|
|
@@ -261,60 +268,21 @@ class BasicQueryEngine {
|
|
|
261
268
|
});
|
|
262
269
|
}
|
|
263
270
|
}
|
|
264
|
-
|
|
265
|
-
case "eq":
|
|
266
|
-
if (value === null) builder.whereNull(column);
|
|
267
|
-
else builder.where(column, value);
|
|
268
|
-
break;
|
|
269
|
-
case "ne":
|
|
270
|
-
if (value === null) builder.whereNotNull(column);
|
|
271
|
-
else builder.whereNot(column, value);
|
|
272
|
-
break;
|
|
273
|
-
case "gt":
|
|
274
|
-
builder.where(column, ">", value);
|
|
275
|
-
break;
|
|
276
|
-
case "gte":
|
|
277
|
-
builder.where(column, ">=", value);
|
|
278
|
-
break;
|
|
279
|
-
case "lt":
|
|
280
|
-
builder.where(column, "<", value);
|
|
281
|
-
break;
|
|
282
|
-
case "lte":
|
|
283
|
-
builder.where(column, "<=", value);
|
|
284
|
-
break;
|
|
285
|
-
case "in":
|
|
286
|
-
builder.whereIn(column, Array.isArray(value) ? value : [value]);
|
|
287
|
-
break;
|
|
288
|
-
case "nin":
|
|
289
|
-
builder.whereNotIn(column, Array.isArray(value) ? value : [value]);
|
|
290
|
-
break;
|
|
291
|
-
case "like":
|
|
292
|
-
builder.where(column, "like", value);
|
|
293
|
-
break;
|
|
294
|
-
case "ilike":
|
|
295
|
-
builder.where(column, "ilike", value);
|
|
296
|
-
break;
|
|
297
|
-
case "exists":
|
|
298
|
-
value ? builder.whereNotNull(column) : builder.whereNull(column);
|
|
299
|
-
break;
|
|
300
|
-
default:
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
return builder;
|
|
271
|
+
return this.applyColumnOp(builder, column, op, value);
|
|
304
272
|
};
|
|
305
273
|
const applyJoinFilterOp = async (builder, filter, _qualified, join) => {
|
|
306
|
-
if (!searchEnabled || !join.entityId) return false;
|
|
307
|
-
if (!["
|
|
308
|
-
if (typeof filter.value !== "string" || filter.value.trim().length === 0) return false;
|
|
274
|
+
if (!searchEnabled || !join.entityId) return { applied: false, builder };
|
|
275
|
+
if (!["like", "ilike"].includes(filter.op)) return { applied: false, builder };
|
|
276
|
+
if (typeof filter.value !== "string" || filter.value.trim().length === 0) return { applied: false, builder };
|
|
309
277
|
let searchAvailable = joinSearchAvailability.get(join.entityId);
|
|
310
278
|
if (searchAvailable === void 0) {
|
|
311
279
|
searchAvailable = await this.hasSearchTokens(join.entityId, opts.tenantId ?? null, orgScope);
|
|
312
280
|
joinSearchAvailability.set(join.entityId, searchAvailable);
|
|
313
281
|
}
|
|
314
|
-
if (!searchAvailable) return false;
|
|
282
|
+
if (!searchAvailable) return { applied: false, builder };
|
|
315
283
|
const tokens = tokenizeText(String(filter.value), searchConfig);
|
|
316
|
-
if (!tokens.hashes.length) return false;
|
|
317
|
-
|
|
284
|
+
if (!tokens.hashes.length) return { applied: false, builder };
|
|
285
|
+
const result = this.applySearchTokens(builder, {
|
|
318
286
|
entity: join.entityId,
|
|
319
287
|
field: filter.column,
|
|
320
288
|
hashes: tokens.hashes,
|
|
@@ -323,6 +291,7 @@ class BasicQueryEngine {
|
|
|
323
291
|
organizationScope: orgScope,
|
|
324
292
|
tokens: tokens.tokens
|
|
325
293
|
});
|
|
294
|
+
return { applied: result.applied, builder: result.builder };
|
|
326
295
|
};
|
|
327
296
|
const regularBaseFilters = baseFilters.filter((f) => !f.orGroup);
|
|
328
297
|
const orGroupFilters = baseFilters.filter((f) => f.orGroup);
|
|
@@ -348,7 +317,7 @@ class BasicQueryEngine {
|
|
|
348
317
|
}
|
|
349
318
|
qualified = qualify(column);
|
|
350
319
|
}
|
|
351
|
-
applyFilterOp(q, qualified, filter.op, filter.value, fieldName);
|
|
320
|
+
q = applyFilterOp(q, qualified, filter.op, filter.value, fieldName);
|
|
352
321
|
}
|
|
353
322
|
if (orGroupFilters.length > 0) {
|
|
354
323
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -374,33 +343,27 @@ class BasicQueryEngine {
|
|
|
374
343
|
if (resolved.length > 0) resolvedGroupFilters.push(resolved);
|
|
375
344
|
}
|
|
376
345
|
if (resolvedGroupFilters.length > 0) {
|
|
377
|
-
q = q.where(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
};
|
|
383
|
-
applyConjunctiveGroup(this, resolvedGroupFilters[0]);
|
|
384
|
-
for (let gi = 1; gi < resolvedGroupFilters.length; gi++) {
|
|
385
|
-
this.orWhere(function(nested) {
|
|
386
|
-
applyConjunctiveGroup(nested, resolvedGroupFilters[gi]);
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
});
|
|
346
|
+
q = q.where((eb) => eb.or(
|
|
347
|
+
resolvedGroupFilters.map((group) => eb.and(
|
|
348
|
+
group.map((rf) => this.buildColumnOpExpression(eb, rf.qualified, rf.op, rf.value))
|
|
349
|
+
))
|
|
350
|
+
));
|
|
390
351
|
}
|
|
391
352
|
}
|
|
392
353
|
const applyAliasScopes = async (builder, aliasName) => {
|
|
393
354
|
const targetTable = aliasTables.get(aliasName);
|
|
394
|
-
if (!targetTable) return;
|
|
355
|
+
if (!targetTable) return builder;
|
|
356
|
+
let next = builder;
|
|
395
357
|
if (!skipAutoScope && orgScope && await this.columnExists(targetTable, "organization_id")) {
|
|
396
|
-
this.applyOrganizationScope(
|
|
358
|
+
next = this.applyOrganizationScope(next, `${aliasName}.organization_id`, orgScope);
|
|
397
359
|
}
|
|
398
360
|
if (!skipAutoScope && opts.tenantId && await this.columnExists(targetTable, "tenant_id")) {
|
|
399
|
-
|
|
361
|
+
next = next.where(`${aliasName}.tenant_id`, "=", opts.tenantId);
|
|
400
362
|
}
|
|
363
|
+
return next;
|
|
401
364
|
};
|
|
402
|
-
await applyJoinFilters({
|
|
403
|
-
|
|
365
|
+
q = await applyJoinFilters({
|
|
366
|
+
db,
|
|
404
367
|
baseTable: table,
|
|
405
368
|
builder: q,
|
|
406
369
|
joinMap,
|
|
@@ -408,22 +371,23 @@ class BasicQueryEngine {
|
|
|
408
371
|
aliasTables,
|
|
409
372
|
qualifyBase: (column) => qualify(column),
|
|
410
373
|
applyAliasScope: (builder, alias) => applyAliasScopes(builder, alias),
|
|
411
|
-
applyFilterOp,
|
|
374
|
+
applyFilterOp: (builder, column, op, value) => applyFilterOp(builder, column, op, value),
|
|
412
375
|
applyJoinFilterOp,
|
|
413
376
|
columnExists: (tbl, column) => this.columnExists(tbl, column)
|
|
414
377
|
});
|
|
415
378
|
if (opts.fields && opts.fields.length) {
|
|
416
379
|
const cols = opts.fields.filter((f) => !f.startsWith("cf:"));
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
q = q.select(baseSelects);
|
|
380
|
+
for (const c of cols) {
|
|
381
|
+
q = q.select(sql.ref(qualify(c)).as(c));
|
|
420
382
|
}
|
|
421
383
|
} else {
|
|
422
|
-
q = q.select(
|
|
384
|
+
q = q.select(sql`${sql.ref(table)}.*`.as("__all"));
|
|
423
385
|
}
|
|
424
386
|
const tenantId = opts.tenantId;
|
|
425
387
|
const sanitize = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
426
|
-
const
|
|
388
|
+
const cfSourcesResult = this.configureCustomFieldSources(q, table, entity, db, opts, qualify);
|
|
389
|
+
q = cfSourcesResult.builder;
|
|
390
|
+
const cfSources = cfSourcesResult.sources;
|
|
427
391
|
const entityIdToSource = /* @__PURE__ */ new Map();
|
|
428
392
|
for (const source of cfSources) {
|
|
429
393
|
entityIdToSource.set(String(source.entityId), source);
|
|
@@ -442,11 +406,10 @@ class BasicQueryEngine {
|
|
|
442
406
|
const entityIdList = Array.from(entityIdToSource.keys());
|
|
443
407
|
const entityOrder = /* @__PURE__ */ new Map();
|
|
444
408
|
entityIdList.forEach((id, idx) => entityOrder.set(id, idx));
|
|
445
|
-
const rows = await
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
});
|
|
409
|
+
const rows = await db.selectFrom("custom_field_defs").select(["key", "entity_id", "config_json", "kind"]).where("entity_id", "in", entityIdList).where("is_active", "=", true).where((eb) => eb.or([
|
|
410
|
+
eb("tenant_id", "=", tenantId),
|
|
411
|
+
eb("tenant_id", "is", null)
|
|
412
|
+
])).execute();
|
|
450
413
|
const sorted = rows.map((row) => {
|
|
451
414
|
const raw = row.config_json;
|
|
452
415
|
let cfg = {};
|
|
@@ -478,7 +441,7 @@ class BasicQueryEngine {
|
|
|
478
441
|
if (!source) continue;
|
|
479
442
|
const cfg = row.config || {};
|
|
480
443
|
const entityIndex = entityOrder.get(row.entityId) ?? Number.MAX_SAFE_INTEGER;
|
|
481
|
-
const scores =
|
|
444
|
+
const scores = computeCustomFieldScore(cfg, row.kind, entityIndex);
|
|
482
445
|
const existing = selectedSources.get(row.key);
|
|
483
446
|
if (!existing || scores.base > existing.score || scores.base === existing.score && (scores.penalty < existing.penalty || scores.penalty === existing.penalty && scores.entityIndex < existing.entityIndex)) {
|
|
484
447
|
selectedSources.set(row.key, { source, score: scores.base, penalty: scores.penalty, entityIndex: scores.entityIndex });
|
|
@@ -494,11 +457,10 @@ class BasicQueryEngine {
|
|
|
494
457
|
}
|
|
495
458
|
const unresolvedKeys = Array.from(cfKeys).filter((key) => !keySource.has(key));
|
|
496
459
|
if (unresolvedKeys.length > 0 && entityIdToSource.size > 0) {
|
|
497
|
-
const rows = await
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
});
|
|
460
|
+
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([
|
|
461
|
+
eb("tenant_id", "=", tenantId),
|
|
462
|
+
eb("tenant_id", "is", null)
|
|
463
|
+
])).execute();
|
|
502
464
|
for (const row of rows) {
|
|
503
465
|
const source = entityIdToSource.get(String(row.entity_id));
|
|
504
466
|
if (!source) continue;
|
|
@@ -518,33 +480,39 @@ class BasicQueryEngine {
|
|
|
518
480
|
const keyAliasSafe = sanitize(key);
|
|
519
481
|
const defAlias = `cfd_${sourceAliasSafe}_${keyAliasSafe}`;
|
|
520
482
|
const valAlias = `cfv_${sourceAliasSafe}_${keyAliasSafe}`;
|
|
521
|
-
q = q.leftJoin(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
END`
|
|
483
|
+
q = q.leftJoin(
|
|
484
|
+
`custom_field_defs as ${defAlias}`,
|
|
485
|
+
(jb) => jb.on(`${defAlias}.entity_id`, "=", String(entityIdForKey)).on(`${defAlias}.key`, "=", key).on(`${defAlias}.is_active`, "=", true).on((eb) => eb.or([
|
|
486
|
+
eb(`${defAlias}.tenant_id`, "=", tenantId),
|
|
487
|
+
eb(`${defAlias}.tenant_id`, "is", null)
|
|
488
|
+
]))
|
|
489
|
+
);
|
|
490
|
+
q = q.leftJoin(
|
|
491
|
+
`custom_field_values as ${valAlias}`,
|
|
492
|
+
(jb) => jb.on(`${valAlias}.entity_id`, "=", String(entityIdForKey)).on(`${valAlias}.field_key`, "=", key).onRef(`${valAlias}.record_id`, "=", recordIdExpr).on((eb) => eb.or([
|
|
493
|
+
eb(`${valAlias}.tenant_id`, "=", tenantId),
|
|
494
|
+
eb(`${valAlias}.tenant_id`, "is", null)
|
|
495
|
+
]))
|
|
535
496
|
);
|
|
497
|
+
const caseExpr = sql`CASE ${sql.ref(`${defAlias}.kind`)}
|
|
498
|
+
WHEN 'integer' THEN (${sql.ref(`${valAlias}.value_int`)})::text
|
|
499
|
+
WHEN 'float' THEN (${sql.ref(`${valAlias}.value_float`)})::text
|
|
500
|
+
WHEN 'boolean' THEN (${sql.ref(`${valAlias}.value_bool`)})::text
|
|
501
|
+
WHEN 'multiline' THEN (${sql.ref(`${valAlias}.value_multiline`)})::text
|
|
502
|
+
ELSE (${sql.ref(`${valAlias}.value_text`)})::text
|
|
503
|
+
END`;
|
|
536
504
|
cfValueExprByKey[key] = caseExpr;
|
|
537
505
|
const alias = sanitize(`cf:${key}`);
|
|
538
506
|
if ((opts.fields || []).includes(`cf:${key}`) || opts.includeCustomFields === true || requestedCustomFieldKeys.length > 0 && requestedCustomFieldKeys.includes(key)) {
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
const
|
|
507
|
+
const multiAlias = `${alias}__is_multi`;
|
|
508
|
+
const isMultiExpr = sql`bool_or(coalesce((${sql.ref(`${defAlias}.config_json`)}->>'multi')::boolean, false))`;
|
|
509
|
+
const aggregatedArray = sql`array_remove(array_agg(DISTINCT ${caseExpr}), NULL)`;
|
|
510
|
+
const projExpr = sql`CASE WHEN ${isMultiExpr}
|
|
542
511
|
THEN to_jsonb(${aggregatedArray})
|
|
543
|
-
ELSE to_jsonb(max(${caseExpr
|
|
512
|
+
ELSE to_jsonb(max(${caseExpr}))
|
|
544
513
|
END`;
|
|
545
|
-
|
|
546
|
-
q = q.select(
|
|
547
|
-
q = q.select(knex.raw(`${isMulti.toString()} as ??`, [multiAlias]));
|
|
514
|
+
q = q.select(projExpr.as(alias));
|
|
515
|
+
q = q.select(isMultiExpr.as(multiAlias));
|
|
548
516
|
cfSelectedAliases.push(alias);
|
|
549
517
|
cfJsonAliases.add(alias);
|
|
550
518
|
cfMultiAliasByAlias.set(alias, multiAlias);
|
|
@@ -559,7 +527,7 @@ class BasicQueryEngine {
|
|
|
559
527
|
const tokens = tokenizeText(String(f.value), searchConfig);
|
|
560
528
|
const hashes = tokens.hashes;
|
|
561
529
|
if (hashes.length) {
|
|
562
|
-
const
|
|
530
|
+
const result = this.applySearchTokens(q, {
|
|
563
531
|
entity: String(entity),
|
|
564
532
|
field: f.field,
|
|
565
533
|
hashes,
|
|
@@ -573,11 +541,14 @@ class BasicQueryEngine {
|
|
|
573
541
|
field: f.field,
|
|
574
542
|
tokens: tokens.tokens,
|
|
575
543
|
hashes,
|
|
576
|
-
applied,
|
|
544
|
+
applied: result.applied,
|
|
577
545
|
tenantId: opts.tenantId ?? null,
|
|
578
546
|
organizationScope: orgScope
|
|
579
547
|
});
|
|
580
|
-
if (applied)
|
|
548
|
+
if (result.applied) {
|
|
549
|
+
q = result.builder;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
581
552
|
} else {
|
|
582
553
|
this.logSearchDebug("search:cf-skip-empty-hashes", {
|
|
583
554
|
entity: String(entity),
|
|
@@ -586,41 +557,7 @@ class BasicQueryEngine {
|
|
|
586
557
|
});
|
|
587
558
|
}
|
|
588
559
|
}
|
|
589
|
-
|
|
590
|
-
case "eq":
|
|
591
|
-
q = q.where(expr, "=", f.value);
|
|
592
|
-
break;
|
|
593
|
-
case "ne":
|
|
594
|
-
q = q.where(expr, "!=", f.value);
|
|
595
|
-
break;
|
|
596
|
-
case "gt":
|
|
597
|
-
q = q.where(expr, ">", f.value);
|
|
598
|
-
break;
|
|
599
|
-
case "gte":
|
|
600
|
-
q = q.where(expr, ">=", f.value);
|
|
601
|
-
break;
|
|
602
|
-
case "lt":
|
|
603
|
-
q = q.where(expr, "<", f.value);
|
|
604
|
-
break;
|
|
605
|
-
case "lte":
|
|
606
|
-
q = q.where(expr, "<=", f.value);
|
|
607
|
-
break;
|
|
608
|
-
case "in":
|
|
609
|
-
q = q.whereIn(expr, f.value ?? []);
|
|
610
|
-
break;
|
|
611
|
-
case "nin":
|
|
612
|
-
q = q.whereNotIn(expr, f.value ?? []);
|
|
613
|
-
break;
|
|
614
|
-
case "like":
|
|
615
|
-
q = q.where(expr, "like", f.value);
|
|
616
|
-
break;
|
|
617
|
-
case "ilike":
|
|
618
|
-
q = q.where(expr, "ilike", f.value);
|
|
619
|
-
break;
|
|
620
|
-
case "exists":
|
|
621
|
-
f.value ? q = q.whereNotNull(expr) : q = q.whereNull(expr);
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
560
|
+
q = this.applyColumnOp(q, expr, f.op, f.value);
|
|
624
561
|
}
|
|
625
562
|
if (opts.includeExtensions) {
|
|
626
563
|
const { getModules } = await import("@open-mercato/shared/lib/i18n/server");
|
|
@@ -632,9 +569,10 @@ class BasicQueryEngine {
|
|
|
632
569
|
const [, extName] = e.extension.split(":");
|
|
633
570
|
const extTable = extName.endsWith("s") ? extName : `${extName}s`;
|
|
634
571
|
const alias = `ext_${sanitize(extName)}`;
|
|
635
|
-
q = q.leftJoin(
|
|
636
|
-
|
|
637
|
-
|
|
572
|
+
q = q.leftJoin(
|
|
573
|
+
`${extTable} as ${alias}`,
|
|
574
|
+
(jb) => jb.onRef(`${alias}.${e.join.extensionKey}`, "=", `${table}.${e.join.baseKey}`)
|
|
575
|
+
);
|
|
638
576
|
}
|
|
639
577
|
}
|
|
640
578
|
for (const s of opts.sort || []) {
|
|
@@ -644,7 +582,7 @@ class BasicQueryEngine {
|
|
|
644
582
|
if (!cfSelectedAliases.includes(alias)) {
|
|
645
583
|
const expr = cfValueExprByKey[key];
|
|
646
584
|
if (expr) {
|
|
647
|
-
q = q.select(
|
|
585
|
+
q = q.select(sql`max(${expr})`.as(alias));
|
|
648
586
|
cfSelectedAliases.push(alias);
|
|
649
587
|
}
|
|
650
588
|
}
|
|
@@ -657,16 +595,14 @@ class BasicQueryEngine {
|
|
|
657
595
|
}
|
|
658
596
|
const page = opts.page?.page ?? 1;
|
|
659
597
|
const pageSize = opts.page?.pageSize ?? 20;
|
|
660
|
-
|
|
598
|
+
const hasJoinedAggregates = opts.includeExtensions && (Array.isArray(opts.includeExtensions) ? opts.includeExtensions.length > 0 : true) || Object.keys(cfValueExprByKey).length > 0;
|
|
599
|
+
if (hasJoinedAggregates) {
|
|
661
600
|
q = q.groupBy(`${table}.id`);
|
|
662
601
|
}
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
if (typeof countClone.clearOrder === "function") countClone.clearOrder();
|
|
666
|
-
if (typeof countClone.clearGroup === "function") countClone.clearGroup();
|
|
667
|
-
const countRow = await countClone.countDistinct(`${table}.id as count`).first();
|
|
602
|
+
const countBuilder = hasJoinedAggregates ? q.clearSelect().clearOrderBy().clearGroupBy().select(sql`count(distinct ${sql.ref(`${table}.id`)})`.as("count")) : q.clearSelect().clearOrderBy().select(sql`count(distinct ${sql.ref(`${table}.id`)})`.as("count"));
|
|
603
|
+
const countRow = await countBuilder.executeTakeFirst();
|
|
668
604
|
const total = Number(countRow?.count ?? 0);
|
|
669
|
-
const items = await q.limit(pageSize).offset((page - 1) * pageSize);
|
|
605
|
+
const items = await q.limit(pageSize).offset((page - 1) * pageSize).execute();
|
|
670
606
|
if (cfJsonAliases.size > 0) {
|
|
671
607
|
for (const row of items) {
|
|
672
608
|
for (const alias of cfJsonAliases) {
|
|
@@ -725,6 +661,62 @@ class BasicQueryEngine {
|
|
|
725
661
|
}
|
|
726
662
|
return queryResult;
|
|
727
663
|
}
|
|
664
|
+
applyColumnOp(builder, column, op, value) {
|
|
665
|
+
switch (op) {
|
|
666
|
+
case "eq":
|
|
667
|
+
return value === null ? builder.where(column, "is", null) : builder.where(column, "=", value);
|
|
668
|
+
case "ne":
|
|
669
|
+
return value === null ? builder.where(column, "is not", null) : builder.where(column, "!=", value);
|
|
670
|
+
case "gt":
|
|
671
|
+
return builder.where(column, ">", value);
|
|
672
|
+
case "gte":
|
|
673
|
+
return builder.where(column, ">=", value);
|
|
674
|
+
case "lt":
|
|
675
|
+
return builder.where(column, "<", value);
|
|
676
|
+
case "lte":
|
|
677
|
+
return builder.where(column, "<=", value);
|
|
678
|
+
case "in":
|
|
679
|
+
return builder.where(column, "in", Array.isArray(value) ? value : [value]);
|
|
680
|
+
case "nin":
|
|
681
|
+
return builder.where(column, "not in", Array.isArray(value) ? value : [value]);
|
|
682
|
+
case "like":
|
|
683
|
+
return builder.where(column, "like", value);
|
|
684
|
+
case "ilike":
|
|
685
|
+
return builder.where(column, "ilike", value);
|
|
686
|
+
case "exists":
|
|
687
|
+
return value ? builder.where(column, "is not", null) : builder.where(column, "is", null);
|
|
688
|
+
default:
|
|
689
|
+
return builder;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
buildColumnOpExpression(eb, column, op, value) {
|
|
693
|
+
switch (op) {
|
|
694
|
+
case "eq":
|
|
695
|
+
return value === null ? eb(column, "is", null) : eb(column, "=", value);
|
|
696
|
+
case "ne":
|
|
697
|
+
return value === null ? eb(column, "is not", null) : eb(column, "!=", value);
|
|
698
|
+
case "gt":
|
|
699
|
+
return eb(column, ">", value);
|
|
700
|
+
case "gte":
|
|
701
|
+
return eb(column, ">=", value);
|
|
702
|
+
case "lt":
|
|
703
|
+
return eb(column, "<", value);
|
|
704
|
+
case "lte":
|
|
705
|
+
return eb(column, "<=", value);
|
|
706
|
+
case "in":
|
|
707
|
+
return eb(column, "in", Array.isArray(value) ? value : [value]);
|
|
708
|
+
case "nin":
|
|
709
|
+
return eb(column, "not in", Array.isArray(value) ? value : [value]);
|
|
710
|
+
case "like":
|
|
711
|
+
return eb(column, "like", value);
|
|
712
|
+
case "ilike":
|
|
713
|
+
return eb(column, "ilike", value);
|
|
714
|
+
case "exists":
|
|
715
|
+
return value ? eb(column, "is not", null) : eb(column, "is", null);
|
|
716
|
+
default:
|
|
717
|
+
return eb.val(true);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
728
720
|
async resolveBaseColumn(table, field) {
|
|
729
721
|
if (await this.columnExists(table, field)) return field;
|
|
730
722
|
if (field === "organization_id" && await this.columnExists(table, "id")) return "id";
|
|
@@ -737,8 +729,8 @@ class BasicQueryEngine {
|
|
|
737
729
|
if (cached === true) return true;
|
|
738
730
|
this.columnCache.delete(key);
|
|
739
731
|
}
|
|
740
|
-
const
|
|
741
|
-
const exists = await
|
|
732
|
+
const db = this.getDb();
|
|
733
|
+
const exists = await db.selectFrom("information_schema.columns").select(sql`1`.as("one")).where("table_name", "=", table).where("column_name", "=", column).limit(1).executeTakeFirst();
|
|
742
734
|
const present = !!exists;
|
|
743
735
|
if (present) this.columnCache.set(key, true);
|
|
744
736
|
else this.columnCache.delete(key);
|
|
@@ -746,23 +738,23 @@ class BasicQueryEngine {
|
|
|
746
738
|
}
|
|
747
739
|
async tableExists(table) {
|
|
748
740
|
if (this.tableCache.has(table)) return this.tableCache.get(table) ?? false;
|
|
749
|
-
const
|
|
750
|
-
const exists = await
|
|
741
|
+
const db = this.getDb();
|
|
742
|
+
const exists = await db.selectFrom("information_schema.tables").select(sql`1`.as("one")).where("table_name", "=", table).limit(1).executeTakeFirst();
|
|
751
743
|
const present = !!exists;
|
|
752
744
|
this.tableCache.set(table, present);
|
|
753
745
|
return present;
|
|
754
746
|
}
|
|
755
747
|
async hasSearchTokens(entity, tenantId, orgScope) {
|
|
756
748
|
try {
|
|
757
|
-
const
|
|
758
|
-
|
|
749
|
+
const db = this.getDb();
|
|
750
|
+
let query = db.selectFrom("search_tokens").select(sql`1`.as("one")).where("entity_type", "=", entity).limit(1);
|
|
759
751
|
if (tenantId !== void 0) {
|
|
760
|
-
query.
|
|
752
|
+
query = query.where(sql`tenant_id is not distinct from ${tenantId}`);
|
|
761
753
|
}
|
|
762
754
|
if (orgScope) {
|
|
763
|
-
this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
|
|
755
|
+
query = this.applyOrganizationScope(query, "search_tokens.organization_id", orgScope);
|
|
764
756
|
}
|
|
765
|
-
const row = await query.
|
|
757
|
+
const row = await query.executeTakeFirst();
|
|
766
758
|
return !!row;
|
|
767
759
|
} catch (err) {
|
|
768
760
|
this.logSearchDebug("search:has-tokens-error", {
|
|
@@ -782,10 +774,9 @@ class BasicQueryEngine {
|
|
|
782
774
|
tenantId: opts.tenantId ?? null,
|
|
783
775
|
organizationScope: opts.organizationScope
|
|
784
776
|
});
|
|
785
|
-
return false;
|
|
777
|
+
return { applied: false, builder: q };
|
|
786
778
|
}
|
|
787
779
|
const alias = `st_${this.searchAliasSeq++}`;
|
|
788
|
-
const combineWith = opts.combineWith === "or" ? "orWhereExists" : "whereExists";
|
|
789
780
|
const engine = this;
|
|
790
781
|
this.logSearchDebug("search:apply-search-tokens", {
|
|
791
782
|
entity: opts.entity,
|
|
@@ -797,23 +788,30 @@ class BasicQueryEngine {
|
|
|
797
788
|
organizationScope: opts.organizationScope,
|
|
798
789
|
combineWith: opts.combineWith ?? "and"
|
|
799
790
|
});
|
|
800
|
-
|
|
801
|
-
|
|
791
|
+
const buildSub = (eb) => {
|
|
792
|
+
let sub = eb.selectFrom(`search_tokens as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(`${alias}.field`, "=", opts.field).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`).where(`${alias}.token_hash`, "in", opts.hashes).groupBy([`${alias}.entity_id`, `${alias}.field`]).having(sql`count(distinct ${sql.ref(`${alias}.token_hash`)}) >= ${opts.hashes.length}`);
|
|
802
793
|
if (opts.tenantId !== void 0) {
|
|
803
|
-
|
|
794
|
+
sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
|
|
804
795
|
}
|
|
805
796
|
if (opts.organizationScope) {
|
|
806
|
-
engine.applyOrganizationScope(
|
|
797
|
+
sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
|
|
807
798
|
}
|
|
808
|
-
|
|
809
|
-
|
|
799
|
+
return sub;
|
|
800
|
+
};
|
|
801
|
+
const combiner = opts.combineWith === "or" ? "or" : "and";
|
|
802
|
+
if (combiner === "or") {
|
|
803
|
+
const next2 = q.where((eb) => eb.or([eb.exists(buildSub(eb))]));
|
|
804
|
+
return { applied: true, builder: next2 };
|
|
805
|
+
}
|
|
806
|
+
const next = q.where((eb) => eb.exists(buildSub(eb)));
|
|
807
|
+
return { applied: true, builder: next };
|
|
810
808
|
}
|
|
811
809
|
applyIndexDocFilter(q, opts) {
|
|
812
810
|
if ((opts.op === "like" || opts.op === "ilike") && opts.searchActive && typeof opts.value === "string") {
|
|
813
811
|
const tokens = tokenizeText(String(opts.value), opts.searchConfig);
|
|
814
812
|
const hashes = tokens.hashes;
|
|
815
813
|
if (hashes.length) {
|
|
816
|
-
const
|
|
814
|
+
const result = this.applySearchTokens(q, {
|
|
817
815
|
entity: opts.entity,
|
|
818
816
|
field: opts.field,
|
|
819
817
|
hashes,
|
|
@@ -827,11 +825,11 @@ class BasicQueryEngine {
|
|
|
827
825
|
field: opts.field,
|
|
828
826
|
tokens: tokens.tokens,
|
|
829
827
|
hashes,
|
|
830
|
-
applied,
|
|
828
|
+
applied: result.applied,
|
|
831
829
|
tenantId: opts.tenantId ?? null,
|
|
832
830
|
organizationScope: opts.organizationScope
|
|
833
831
|
});
|
|
834
|
-
if (applied) return
|
|
832
|
+
if (result.applied) return result.builder;
|
|
835
833
|
} else {
|
|
836
834
|
this.logSearchDebug("search:index-doc-skip-empty-hashes", {
|
|
837
835
|
entity: opts.entity,
|
|
@@ -841,66 +839,71 @@ class BasicQueryEngine {
|
|
|
841
839
|
}
|
|
842
840
|
return q;
|
|
843
841
|
}
|
|
844
|
-
const knex = this.getKnexFn ? this.getKnexFn() : this.em.getConnection().getKnex();
|
|
845
842
|
const alias = `ei_${this.searchAliasSeq++}`;
|
|
846
843
|
const engine = this;
|
|
847
|
-
return q.
|
|
848
|
-
|
|
844
|
+
return q.where((eb) => eb.exists((() => {
|
|
845
|
+
let sub = eb.selectFrom(`entity_indexes as ${alias}`).select(sql`1`.as("one")).where(`${alias}.entity_type`, "=", opts.entity).where(sql`${sql.ref(`${alias}.entity_id`)} = ${sql.ref(opts.recordIdColumn)}::text`);
|
|
849
846
|
if (opts.tenantId !== void 0) {
|
|
850
|
-
|
|
847
|
+
sub = sub.where(sql`${sql.ref(`${alias}.tenant_id`)} is not distinct from ${opts.tenantId ?? null}`);
|
|
851
848
|
}
|
|
852
849
|
if (opts.organizationScope) {
|
|
853
|
-
engine.applyOrganizationScope(
|
|
850
|
+
sub = engine.applyOrganizationScope(sub, `${alias}.organization_id`, opts.organizationScope);
|
|
854
851
|
}
|
|
855
852
|
if (!opts.withDeleted) {
|
|
856
|
-
|
|
853
|
+
sub = sub.where(`${alias}.deleted_at`, "is", null);
|
|
857
854
|
}
|
|
858
|
-
const
|
|
855
|
+
const textExpr = sql`(${sql.ref(`${alias}.doc`)} ->> ${opts.field})`;
|
|
859
856
|
switch (opts.op) {
|
|
860
857
|
case "eq":
|
|
861
|
-
|
|
858
|
+
sub = sub.where(sql`${textExpr} = ${opts.value}`);
|
|
862
859
|
break;
|
|
863
860
|
case "ne":
|
|
864
|
-
|
|
861
|
+
sub = sub.where(sql`${textExpr} <> ${opts.value}`);
|
|
865
862
|
break;
|
|
866
863
|
case "gt":
|
|
867
864
|
case "gte":
|
|
868
865
|
case "lt":
|
|
869
866
|
case "lte": {
|
|
870
|
-
const operator = opts.op === "gt" ? ">" : opts.op === "gte" ? ">=" : opts.op === "lt" ? "<" : "<=";
|
|
871
|
-
|
|
867
|
+
const operator = sql.raw(opts.op === "gt" ? ">" : opts.op === "gte" ? ">=" : opts.op === "lt" ? "<" : "<=");
|
|
868
|
+
sub = sub.where(sql`${textExpr} ${operator} ${opts.value}`);
|
|
872
869
|
break;
|
|
873
870
|
}
|
|
874
|
-
case "in":
|
|
875
|
-
|
|
871
|
+
case "in": {
|
|
872
|
+
const vals = Array.isArray(opts.value) ? opts.value : [opts.value];
|
|
873
|
+
sub = sub.where(sql`${textExpr} in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
|
|
876
874
|
break;
|
|
877
|
-
|
|
878
|
-
|
|
875
|
+
}
|
|
876
|
+
case "nin": {
|
|
877
|
+
const vals = Array.isArray(opts.value) ? opts.value : [opts.value];
|
|
878
|
+
sub = sub.where(sql`${textExpr} not in (${sql.join(vals.map((v) => sql`${v}`), sql`, `)})`);
|
|
879
879
|
break;
|
|
880
|
+
}
|
|
880
881
|
case "like":
|
|
881
|
-
|
|
882
|
+
sub = sub.where(sql`${textExpr} like ${opts.value}`);
|
|
882
883
|
break;
|
|
883
884
|
case "ilike":
|
|
884
|
-
|
|
885
|
+
sub = sub.where(sql`${textExpr} ilike ${opts.value}`);
|
|
885
886
|
break;
|
|
886
887
|
case "exists":
|
|
887
|
-
opts.value ?
|
|
888
|
+
sub = opts.value ? sub.where(sql`${textExpr} is not null`) : sub.where(sql`${textExpr} is null`);
|
|
888
889
|
break;
|
|
889
890
|
default:
|
|
890
891
|
break;
|
|
891
892
|
}
|
|
892
|
-
|
|
893
|
+
return sub;
|
|
894
|
+
})()));
|
|
893
895
|
}
|
|
894
|
-
configureCustomFieldSources(q, baseTable, baseEntity,
|
|
896
|
+
configureCustomFieldSources(q, baseTable, baseEntity, db, opts, qualify) {
|
|
895
897
|
const sources = [
|
|
896
898
|
{
|
|
897
899
|
entityId: baseEntity,
|
|
898
900
|
alias: "base",
|
|
899
901
|
table: baseTable,
|
|
900
|
-
recordIdExpr:
|
|
902
|
+
recordIdExpr: sql`${sql.ref(`${baseTable}.id`)}::text`
|
|
901
903
|
}
|
|
902
904
|
];
|
|
903
905
|
const extras = opts.customFieldSources ?? [];
|
|
906
|
+
let next = q;
|
|
904
907
|
extras.forEach((srcOpt, index) => {
|
|
905
908
|
const joinTable = srcOpt.table ?? resolveEntityTableName(this.em, srcOpt.entityId);
|
|
906
909
|
const alias = srcOpt.alias ?? `cfs_${index}`;
|
|
@@ -908,22 +911,17 @@ class BasicQueryEngine {
|
|
|
908
911
|
if (!join) {
|
|
909
912
|
throw new Error(`QueryEngine: customFieldSources entry for ${String(srcOpt.entityId)} requires a join configuration`);
|
|
910
913
|
}
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
this.on(`${alias}.${join.toField}`, "=", qualify(join.fromField));
|
|
914
|
-
};
|
|
915
|
-
const joinType = join.type ?? "left";
|
|
916
|
-
if (joinType === "inner") q.join(joinArgs, joinCallback);
|
|
917
|
-
else q.leftJoin(joinArgs, joinCallback);
|
|
914
|
+
const joinFn = (join.type ?? "left") === "inner" ? "innerJoin" : "leftJoin";
|
|
915
|
+
next = next[joinFn](`${joinTable} as ${alias}`, (jb) => jb.onRef(`${alias}.${join.toField}`, "=", qualify(join.fromField)));
|
|
918
916
|
const recordColumn = srcOpt.recordIdColumn ?? "id";
|
|
919
917
|
sources.push({
|
|
920
918
|
entityId: srcOpt.entityId,
|
|
921
919
|
alias,
|
|
922
920
|
table: joinTable,
|
|
923
|
-
recordIdExpr:
|
|
921
|
+
recordIdExpr: sql`${sql.ref(`${alias}.${recordColumn}`)}::text`
|
|
924
922
|
});
|
|
925
923
|
});
|
|
926
|
-
return sources;
|
|
924
|
+
return { builder: next, sources };
|
|
927
925
|
}
|
|
928
926
|
logSearchDebug(event, payload) {
|
|
929
927
|
try {
|
|
@@ -947,20 +945,14 @@ class BasicQueryEngine {
|
|
|
947
945
|
applyOrganizationScope(q, column, scope) {
|
|
948
946
|
if (!scope) return q;
|
|
949
947
|
if (scope.ids.length === 0 && !scope.includeNull) {
|
|
950
|
-
return q.
|
|
951
|
-
}
|
|
952
|
-
return q.where((
|
|
953
|
-
|
|
954
|
-
if (scope.ids.length > 0)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
if (scope.includeNull) {
|
|
959
|
-
if (applied) builder.orWhereNull(column);
|
|
960
|
-
else builder.whereNull(column);
|
|
961
|
-
applied = true;
|
|
962
|
-
}
|
|
963
|
-
if (!applied) builder.whereRaw("1 = 0");
|
|
948
|
+
return q.where(sql`1 = 0`);
|
|
949
|
+
}
|
|
950
|
+
return q.where((eb) => {
|
|
951
|
+
const parts = [];
|
|
952
|
+
if (scope.ids.length > 0) parts.push(eb(column, "in", scope.ids));
|
|
953
|
+
if (scope.includeNull) parts.push(eb(column, "is", null));
|
|
954
|
+
if (parts.length === 1) return parts[0];
|
|
955
|
+
return eb.or(parts);
|
|
964
956
|
});
|
|
965
957
|
}
|
|
966
958
|
}
|