@objectstack/service-analytics 4.0.5 → 4.1.0
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/dist/index.cjs +271 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -11
- package/dist/index.d.ts +46 -11
- package/dist/index.js +271 -122
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -146,6 +146,92 @@ var CubeRegistry = class {
|
|
|
146
146
|
}
|
|
147
147
|
};
|
|
148
148
|
|
|
149
|
+
// src/strategies/filter-normalizer.ts
|
|
150
|
+
var MONGO_TO_CUBE_OP = {
|
|
151
|
+
$eq: "equals",
|
|
152
|
+
$ne: "notEquals",
|
|
153
|
+
$gt: "gt",
|
|
154
|
+
$gte: "gte",
|
|
155
|
+
$lt: "lt",
|
|
156
|
+
$lte: "lte",
|
|
157
|
+
$in: "in",
|
|
158
|
+
$nin: "notIn",
|
|
159
|
+
$contains: "contains",
|
|
160
|
+
$notContains: "notContains",
|
|
161
|
+
$exists: "set"
|
|
162
|
+
};
|
|
163
|
+
function stringifyForCube(v) {
|
|
164
|
+
if (v == null) return "";
|
|
165
|
+
if (typeof v === "boolean") return v ? "1" : "0";
|
|
166
|
+
if (v instanceof Date) return v.toISOString();
|
|
167
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
168
|
+
return String(v);
|
|
169
|
+
}
|
|
170
|
+
function flattenCondition(cond, out) {
|
|
171
|
+
for (const [key, raw] of Object.entries(cond)) {
|
|
172
|
+
if (raw === void 0) continue;
|
|
173
|
+
if (key === "$and" && Array.isArray(raw)) {
|
|
174
|
+
for (const sub of raw) {
|
|
175
|
+
if (sub && typeof sub === "object") {
|
|
176
|
+
flattenCondition(sub, out);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (key === "$or" || key === "$not") continue;
|
|
182
|
+
if (raw === null) {
|
|
183
|
+
out.push({ member: key, operator: "notSet", values: [] });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
187
|
+
const wrapper = raw;
|
|
188
|
+
const opKeys = Object.keys(wrapper).filter((k) => k.startsWith("$"));
|
|
189
|
+
if (opKeys.length > 0) {
|
|
190
|
+
for (const opKey of opKeys) {
|
|
191
|
+
const cubeOp = MONGO_TO_CUBE_OP[opKey];
|
|
192
|
+
if (!cubeOp) continue;
|
|
193
|
+
const v = wrapper[opKey];
|
|
194
|
+
const values = Array.isArray(v) ? v.map(stringifyForCube) : [stringifyForCube(v)];
|
|
195
|
+
out.push({ member: key, operator: cubeOp, values });
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {
|
|
200
|
+
flattenCondition({ [`${key}.${nestedKey}`]: nestedVal }, out);
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(raw)) {
|
|
205
|
+
out.push({ member: key, operator: "in", values: raw.map(stringifyForCube) });
|
|
206
|
+
} else {
|
|
207
|
+
out.push({ member: key, operator: "equals", values: [stringifyForCube(raw)] });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function normalizeAnalyticsFilters(query) {
|
|
212
|
+
if (!query || typeof query !== "object") return [];
|
|
213
|
+
const out = [];
|
|
214
|
+
const where = query.where;
|
|
215
|
+
if (where && typeof where === "object" && !Array.isArray(where)) {
|
|
216
|
+
flattenCondition(where, out);
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
function coerceFilterValueForSql(s) {
|
|
221
|
+
if (s === "true") return 1;
|
|
222
|
+
if (s === "false") return 0;
|
|
223
|
+
if (s === "null") return null;
|
|
224
|
+
if (/^-?\d+$/.test(s)) {
|
|
225
|
+
const n = Number(s);
|
|
226
|
+
if (Number.isFinite(n)) return n;
|
|
227
|
+
}
|
|
228
|
+
if (/^-?\d+\.\d+$/.test(s)) {
|
|
229
|
+
const n = Number(s);
|
|
230
|
+
if (Number.isFinite(n)) return n;
|
|
231
|
+
}
|
|
232
|
+
return s;
|
|
233
|
+
}
|
|
234
|
+
|
|
149
235
|
// src/strategies/native-sql-strategy.ts
|
|
150
236
|
var NativeSQLStrategy = class {
|
|
151
237
|
constructor() {
|
|
@@ -173,30 +259,33 @@ var NativeSQLStrategy = class {
|
|
|
173
259
|
const params = [];
|
|
174
260
|
const selectClauses = [];
|
|
175
261
|
const groupByClauses = [];
|
|
262
|
+
const tableName = this.extractObjectName(cube);
|
|
263
|
+
const joins = /* @__PURE__ */ new Map();
|
|
176
264
|
if (query.dimensions && query.dimensions.length > 0) {
|
|
177
265
|
for (const dim of query.dimensions) {
|
|
178
|
-
const colExpr = this.resolveDimensionSql(cube, dim);
|
|
266
|
+
const colExpr = this.resolveDimensionSql(cube, dim, tableName, joins);
|
|
179
267
|
selectClauses.push(`${colExpr} AS "${dim}"`);
|
|
180
268
|
groupByClauses.push(colExpr);
|
|
181
269
|
}
|
|
182
270
|
}
|
|
183
271
|
if (query.measures && query.measures.length > 0) {
|
|
184
272
|
for (const measure of query.measures) {
|
|
185
|
-
const aggExpr = this.resolveMeasureSql(cube, measure);
|
|
273
|
+
const aggExpr = this.resolveMeasureSql(cube, measure, tableName, joins);
|
|
186
274
|
selectClauses.push(`${aggExpr} AS "${measure}"`);
|
|
187
275
|
}
|
|
188
276
|
}
|
|
189
277
|
const whereClauses = [];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
278
|
+
const normalizedFilters = normalizeAnalyticsFilters(query);
|
|
279
|
+
if (normalizedFilters.length > 0) {
|
|
280
|
+
for (const filter of normalizedFilters) {
|
|
281
|
+
const colExpr = this.resolveFieldSql(cube, filter.member, tableName, joins);
|
|
193
282
|
const clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params);
|
|
194
283
|
if (clause) whereClauses.push(clause);
|
|
195
284
|
}
|
|
196
285
|
}
|
|
197
286
|
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
198
287
|
for (const td of query.timeDimensions) {
|
|
199
|
-
const colExpr = this.resolveFieldSql(cube, td.dimension);
|
|
288
|
+
const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);
|
|
200
289
|
if (td.dateRange) {
|
|
201
290
|
const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
202
291
|
if (range.length === 2) {
|
|
@@ -206,8 +295,10 @@ var NativeSQLStrategy = class {
|
|
|
206
295
|
}
|
|
207
296
|
}
|
|
208
297
|
}
|
|
209
|
-
const tableName = this.extractObjectName(cube);
|
|
210
298
|
let sql = `SELECT ${selectClauses.join(", ")} FROM "${tableName}"`;
|
|
299
|
+
if (joins.size > 0) {
|
|
300
|
+
sql += " " + Array.from(joins.values()).join(" ");
|
|
301
|
+
}
|
|
211
302
|
if (whereClauses.length > 0) {
|
|
212
303
|
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
213
304
|
}
|
|
@@ -227,16 +318,77 @@ var NativeSQLStrategy = class {
|
|
|
227
318
|
return { sql, params };
|
|
228
319
|
}
|
|
229
320
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
321
|
+
/**
|
|
322
|
+
* Resolve a dimension/measure/filter SQL expression that may reference a
|
|
323
|
+
* related table via dot notation (e.g. `account.industry`).
|
|
324
|
+
*
|
|
325
|
+
* When the resolved `sql` contains a dot, treat the prefix as a lookup
|
|
326
|
+
* field on the cube's table and synthesise a `LEFT JOIN` against the
|
|
327
|
+
* related table. The convention (matching the auto-cube generator and
|
|
328
|
+
* ObjectStack object schemas) is:
|
|
329
|
+
*
|
|
330
|
+
* <parentTable>.<lookupField> = <lookupField>.id
|
|
331
|
+
*
|
|
332
|
+
* i.e. the lookup field name on the parent table equals the related
|
|
333
|
+
* table name. This holds for all `Field.lookup({ object: '...' })`
|
|
334
|
+
* declarations where the field is named after its target object.
|
|
335
|
+
*
|
|
336
|
+
* Returns the qualified SQL reference (e.g. `"account"."industry"`).
|
|
337
|
+
* Pure column references (no dot) are returned as-is.
|
|
338
|
+
*/
|
|
339
|
+
qualifyAndRegisterJoin(rawSql, parentTable, joins) {
|
|
340
|
+
if (!rawSql.includes(".")) return rawSql;
|
|
341
|
+
const [alias, ...rest] = rawSql.split(".");
|
|
342
|
+
if (!alias || rest.length === 0) return rawSql;
|
|
343
|
+
const column = rest.join(".");
|
|
344
|
+
if (!joins.has(alias)) {
|
|
345
|
+
joins.set(
|
|
346
|
+
alias,
|
|
347
|
+
`LEFT JOIN "${alias}" ON "${parentTable}"."${alias}" = "${alias}"."id"`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return `"${alias}"."${column}"`;
|
|
234
351
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Resolve a member reference (dimension, measure, or filter field) to its
|
|
354
|
+
* cube definition.
|
|
355
|
+
*
|
|
356
|
+
* Accepts three naming conventions:
|
|
357
|
+
* 1. `<cube>.<field>` — the canonical analytics qualifier (stripped to `<field>`).
|
|
358
|
+
* 2. `<lookup>.<field>` — a relation traversal (e.g. `account.industry`).
|
|
359
|
+
* First tried as the literal key, then as the underscore-flattened
|
|
360
|
+
* key (`account_industry`), and finally returned as a synthetic
|
|
361
|
+
* definition whose `sql` is the dotted reference so the JOIN
|
|
362
|
+
* machinery can pick it up.
|
|
363
|
+
* 3. `<field>` — a bare field name on the cube's table.
|
|
364
|
+
*/
|
|
365
|
+
lookupMember(cube, member, kind) {
|
|
366
|
+
const bag = kind === "dimension" ? cube.dimensions : cube.measures;
|
|
367
|
+
if (bag[member]) return bag[member];
|
|
368
|
+
if (member.includes(".")) {
|
|
369
|
+
const [first, ...rest] = member.split(".");
|
|
370
|
+
const tail = rest.join(".");
|
|
371
|
+
if (first === cube.name && bag[tail]) return bag[tail];
|
|
372
|
+
if (bag[tail]) return bag[tail];
|
|
373
|
+
const flat = member.replace(/\./g, "_");
|
|
374
|
+
if (bag[flat]) return bag[flat];
|
|
375
|
+
if (kind === "dimension") {
|
|
376
|
+
return { sql: member, type: "string" };
|
|
377
|
+
}
|
|
378
|
+
} else if (bag[member]) {
|
|
379
|
+
return bag[member];
|
|
380
|
+
}
|
|
381
|
+
return void 0;
|
|
382
|
+
}
|
|
383
|
+
resolveDimensionSql(cube, member, parentTable, joins) {
|
|
384
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
385
|
+
const raw = dim ? dim.sql : member.includes(".") ? member.split(".")[1] : member;
|
|
386
|
+
return this.qualifyAndRegisterJoin(raw, parentTable, joins);
|
|
387
|
+
}
|
|
388
|
+
resolveMeasureSql(cube, member, parentTable, joins) {
|
|
389
|
+
const measure = this.lookupMember(cube, member, "measure");
|
|
238
390
|
if (!measure) return `COUNT(*)`;
|
|
239
|
-
const col = measure.sql;
|
|
391
|
+
const col = measure.sql === "*" ? "*" : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins);
|
|
240
392
|
switch (measure.type) {
|
|
241
393
|
case "count":
|
|
242
394
|
return "COUNT(*)";
|
|
@@ -254,12 +406,12 @@ var NativeSQLStrategy = class {
|
|
|
254
406
|
return `COUNT(*)`;
|
|
255
407
|
}
|
|
256
408
|
}
|
|
257
|
-
resolveFieldSql(cube, member) {
|
|
409
|
+
resolveFieldSql(cube, member, parentTable, joins) {
|
|
410
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
411
|
+
if (dim) return this.qualifyAndRegisterJoin(dim.sql, parentTable, joins);
|
|
412
|
+
const measure = this.lookupMember(cube, member, "measure");
|
|
413
|
+
if (measure) return this.qualifyAndRegisterJoin(measure.sql, parentTable, joins);
|
|
258
414
|
const fieldName = member.includes(".") ? member.split(".")[1] : member;
|
|
259
|
-
const dim = cube.dimensions[fieldName];
|
|
260
|
-
if (dim) return dim.sql;
|
|
261
|
-
const measure = cube.measures[fieldName];
|
|
262
|
-
if (measure) return measure.sql;
|
|
263
415
|
return fieldName;
|
|
264
416
|
}
|
|
265
417
|
buildFilterClause(col, operator, values, params) {
|
|
@@ -278,7 +430,7 @@ var NativeSQLStrategy = class {
|
|
|
278
430
|
if (operator === "in" || operator === "notIn") {
|
|
279
431
|
if (!values || values.length === 0) return null;
|
|
280
432
|
const placeholders = values.map((v) => {
|
|
281
|
-
params.push(v);
|
|
433
|
+
params.push(coerceFilterValueForSql(v));
|
|
282
434
|
return `$${params.length}`;
|
|
283
435
|
}).join(", ");
|
|
284
436
|
return `${col} ${operator === "in" ? "IN" : "NOT IN"} (${placeholders})`;
|
|
@@ -288,7 +440,7 @@ var NativeSQLStrategy = class {
|
|
|
288
440
|
if (operator === "contains" || operator === "notContains") {
|
|
289
441
|
params.push(`%${values[0]}%`);
|
|
290
442
|
} else {
|
|
291
|
-
params.push(values[0]);
|
|
443
|
+
params.push(coerceFilterValueForSql(values[0]));
|
|
292
444
|
}
|
|
293
445
|
return `${col} ${sqlOp} $${params.length}`;
|
|
294
446
|
}
|
|
@@ -299,8 +451,7 @@ var NativeSQLStrategy = class {
|
|
|
299
451
|
const fields = [];
|
|
300
452
|
if (query.dimensions) {
|
|
301
453
|
for (const dim of query.dimensions) {
|
|
302
|
-
const
|
|
303
|
-
const d = cube.dimensions[fieldName];
|
|
454
|
+
const d = this.lookupMember(cube, dim, "dimension");
|
|
304
455
|
fields.push({ name: dim, type: d?.type || "string" });
|
|
305
456
|
}
|
|
306
457
|
}
|
|
@@ -341,8 +492,9 @@ var ObjectQLStrategy = class {
|
|
|
341
492
|
}
|
|
342
493
|
}
|
|
343
494
|
const filter = {};
|
|
344
|
-
|
|
345
|
-
|
|
495
|
+
const normalizedFilters = normalizeAnalyticsFilters(query);
|
|
496
|
+
if (normalizedFilters.length > 0) {
|
|
497
|
+
for (const f of normalizedFilters) {
|
|
346
498
|
const fieldName = this.resolveFieldName(cube, f.member, "any");
|
|
347
499
|
filter[fieldName] = this.convertFilter(f.operator, f.values);
|
|
348
500
|
}
|
|
@@ -399,27 +551,54 @@ var ObjectQLStrategy = class {
|
|
|
399
551
|
return { sql, params: [] };
|
|
400
552
|
}
|
|
401
553
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
554
|
+
/**
|
|
555
|
+
* Resolve a member ref to a `{ sql, type? }` definition.
|
|
556
|
+
*
|
|
557
|
+
* Mirrors `NativeSQLStrategy.lookupMember` so the two strategies
|
|
558
|
+
* accept the same naming conventions:
|
|
559
|
+
* 1. `<cube>.<field>` — canonical analytics qualifier.
|
|
560
|
+
* 2. `<lookup>.<field>` — relation traversal (e.g. `account.industry`).
|
|
561
|
+
* Tries literal key, then underscore-flattened key, then falls
|
|
562
|
+
* back to a synthetic dim whose `sql` is the dotted path so the
|
|
563
|
+
* ObjectQL aggregate engine can traverse it via the lookup field.
|
|
564
|
+
* 3. `<field>` — bare column on the cube's table.
|
|
565
|
+
*/
|
|
566
|
+
lookupMember(cube, member, kind) {
|
|
567
|
+
const bag = kind === "dimension" ? cube.dimensions : cube.measures;
|
|
568
|
+
if (bag[member]) return bag[member];
|
|
569
|
+
if (member.includes(".")) {
|
|
570
|
+
const [first, ...rest] = member.split(".");
|
|
571
|
+
const tail = rest.join(".");
|
|
572
|
+
if (first === cube.name && bag[tail]) return bag[tail];
|
|
573
|
+
if (bag[tail]) return bag[tail];
|
|
574
|
+
const flat = member.replace(/\./g, "_");
|
|
575
|
+
if (bag[flat]) return bag[flat];
|
|
576
|
+
if (kind === "dimension") return { sql: member, type: "string" };
|
|
577
|
+
} else if (bag[member]) {
|
|
578
|
+
return bag[member];
|
|
579
|
+
}
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
402
582
|
resolveFieldName(cube, member, kind) {
|
|
403
|
-
const fieldName = member.includes(".") ? member.split(".")[1] : member;
|
|
404
583
|
if (kind === "dimension" || kind === "any") {
|
|
405
|
-
const dim = cube
|
|
584
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
406
585
|
if (dim) return dim.sql.replace(/^\$/, "");
|
|
407
586
|
}
|
|
408
587
|
if (kind === "measure" || kind === "any") {
|
|
409
|
-
const measure = cube
|
|
588
|
+
const measure = this.lookupMember(cube, member, "measure");
|
|
410
589
|
if (measure) return measure.sql.replace(/^\$/, "");
|
|
411
590
|
}
|
|
412
|
-
return
|
|
591
|
+
return member.includes(".") ? member.split(".")[1] : member;
|
|
413
592
|
}
|
|
414
593
|
resolveMeasureAggregation(cube, measureName) {
|
|
415
|
-
const
|
|
416
|
-
const direct = cube.measures[fieldName];
|
|
594
|
+
const direct = this.lookupMember(cube, measureName, "measure");
|
|
417
595
|
if (direct) {
|
|
418
596
|
return {
|
|
419
597
|
field: direct.sql.replace(/^\$/, ""),
|
|
420
598
|
method: direct.type === "count_distinct" ? "count_distinct" : direct.type
|
|
421
599
|
};
|
|
422
600
|
}
|
|
601
|
+
const fieldName = measureName.includes(".") ? measureName.split(".")[1] : measureName;
|
|
423
602
|
const aggTypes = ["count", "sum", "avg", "min", "max", "count_distinct"];
|
|
424
603
|
for (const type of aggTypes) {
|
|
425
604
|
const suffix = `_${type}`;
|
|
@@ -440,27 +619,29 @@ var ObjectQLStrategy = class {
|
|
|
440
619
|
if (operator === "set") return { $ne: null };
|
|
441
620
|
if (operator === "notSet") return null;
|
|
442
621
|
if (!values || values.length === 0) return void 0;
|
|
622
|
+
const v0 = coerceFilterValueForSql(values[0]);
|
|
623
|
+
const all = values.map(coerceFilterValueForSql);
|
|
443
624
|
switch (operator) {
|
|
444
625
|
case "equals":
|
|
445
|
-
return
|
|
626
|
+
return v0;
|
|
446
627
|
case "notEquals":
|
|
447
|
-
return { $ne:
|
|
628
|
+
return { $ne: v0 };
|
|
448
629
|
case "gt":
|
|
449
|
-
return { $gt:
|
|
630
|
+
return { $gt: v0 };
|
|
450
631
|
case "gte":
|
|
451
|
-
return { $gte:
|
|
632
|
+
return { $gte: v0 };
|
|
452
633
|
case "lt":
|
|
453
|
-
return { $lt:
|
|
634
|
+
return { $lt: v0 };
|
|
454
635
|
case "lte":
|
|
455
|
-
return { $lte:
|
|
636
|
+
return { $lte: v0 };
|
|
456
637
|
case "contains":
|
|
457
638
|
return { $regex: values[0] };
|
|
458
639
|
case "in":
|
|
459
|
-
return { $in:
|
|
640
|
+
return { $in: all };
|
|
460
641
|
case "notIn":
|
|
461
|
-
return { $nin:
|
|
642
|
+
return { $nin: all };
|
|
462
643
|
default:
|
|
463
|
-
return
|
|
644
|
+
return v0;
|
|
464
645
|
}
|
|
465
646
|
}
|
|
466
647
|
extractObjectName(cube) {
|
|
@@ -470,8 +651,7 @@ var ObjectQLStrategy = class {
|
|
|
470
651
|
const fields = [];
|
|
471
652
|
if (query.dimensions) {
|
|
472
653
|
for (const dim of query.dimensions) {
|
|
473
|
-
const
|
|
474
|
-
const d = cube.dimensions[fieldName];
|
|
654
|
+
const d = this.lookupMember(cube, dim, "dimension");
|
|
475
655
|
fields.push({ name: dim, type: d?.type || "string" });
|
|
476
656
|
}
|
|
477
657
|
}
|
|
@@ -524,11 +704,10 @@ var AnalyticsService = class {
|
|
|
524
704
|
if (!query.cube) {
|
|
525
705
|
throw new Error("Cube name is required in analytics query");
|
|
526
706
|
}
|
|
527
|
-
|
|
528
|
-
this.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
return strategy.execute(normalized, this.strategyCtx);
|
|
707
|
+
this.ensureCube(query);
|
|
708
|
+
const strategy = this.resolveStrategy(query);
|
|
709
|
+
this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
|
|
710
|
+
return strategy.execute(query, this.strategyCtx);
|
|
532
711
|
}
|
|
533
712
|
/**
|
|
534
713
|
* Get cube metadata for discovery.
|
|
@@ -557,49 +736,12 @@ var AnalyticsService = class {
|
|
|
557
736
|
if (!query.cube) {
|
|
558
737
|
throw new Error("Cube name is required for SQL generation");
|
|
559
738
|
}
|
|
560
|
-
|
|
561
|
-
this.
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return strategy.generateSql(normalized, this.strategyCtx);
|
|
739
|
+
this.ensureCube(query);
|
|
740
|
+
const strategy = this.resolveStrategy(query);
|
|
741
|
+
this.logger.debug(`[Analytics] generateSql on cube "${query.cube}" \u2192 ${strategy.name}`);
|
|
742
|
+
return strategy.generateSql(query, this.strategyCtx);
|
|
565
743
|
}
|
|
566
744
|
// ── Internal ─────────────────────────────────────────────────────
|
|
567
|
-
/**
|
|
568
|
-
* Normalise a query into a canonical shape that strategies can rely on:
|
|
569
|
-
*
|
|
570
|
-
* 1. **Filters as object** — Some clients (e.g. dashboard widget translators)
|
|
571
|
-
* send `filters` as a Mongo-style object `{ stage: { $nin: [...] } }`
|
|
572
|
-
* instead of the schema's `Array<{ member, operator, values }>`. We
|
|
573
|
-
* translate the object form into the array form so existing strategies
|
|
574
|
-
* work unchanged.
|
|
575
|
-
* 2. Other shapes are returned as-is.
|
|
576
|
-
*/
|
|
577
|
-
normalizeQuery(query) {
|
|
578
|
-
const filters = query.filters;
|
|
579
|
-
if (!filters || Array.isArray(filters)) return query;
|
|
580
|
-
if (typeof filters !== "object") return query;
|
|
581
|
-
const arr = [];
|
|
582
|
-
for (const [member, raw] of Object.entries(filters)) {
|
|
583
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
584
|
-
for (const [op, val] of Object.entries(raw)) {
|
|
585
|
-
const mapped = mongoOperatorToFilter(op, val);
|
|
586
|
-
if (!mapped) continue;
|
|
587
|
-
if (mapped.multi) {
|
|
588
|
-
arr.push({ member, operator: mapped.operator, values: mapped.values });
|
|
589
|
-
} else if (mapped.values.length === 0) {
|
|
590
|
-
arr.push({ member, operator: mapped.operator, values: [] });
|
|
591
|
-
} else {
|
|
592
|
-
for (const v of mapped.values) arr.push({ member, operator: mapped.operator, values: [v] });
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
} else if (Array.isArray(raw)) {
|
|
596
|
-
arr.push({ member, operator: "in", values: raw.map(String) });
|
|
597
|
-
} else {
|
|
598
|
-
arr.push({ member, operator: "equals", values: [String(raw)] });
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return { ...query, filters: arr };
|
|
602
|
-
}
|
|
603
745
|
/**
|
|
604
746
|
* Ensure a cube exists for the given query and that it knows about every
|
|
605
747
|
* measure referenced by the query.
|
|
@@ -659,10 +801,13 @@ var AnalyticsService = class {
|
|
|
659
801
|
if (dimensions[key]) continue;
|
|
660
802
|
dimensions[key] = { name: key, label: key, type: "string", sql: key };
|
|
661
803
|
}
|
|
662
|
-
|
|
663
|
-
const key
|
|
664
|
-
|
|
665
|
-
|
|
804
|
+
if (query.where && typeof query.where === "object" && !Array.isArray(query.where)) {
|
|
805
|
+
for (const key of Object.keys(query.where)) {
|
|
806
|
+
if (key.startsWith("$")) continue;
|
|
807
|
+
const stripped = stripPrefix(key);
|
|
808
|
+
if (dimensions[stripped] || measures[stripped]) continue;
|
|
809
|
+
dimensions[stripped] = { name: stripped, label: stripped, type: "string", sql: stripped };
|
|
810
|
+
}
|
|
666
811
|
}
|
|
667
812
|
for (const td of query.timeDimensions || []) {
|
|
668
813
|
const key = stripPrefix(td.dimension);
|
|
@@ -718,33 +863,6 @@ function inferMeasure(key) {
|
|
|
718
863
|
}
|
|
719
864
|
return { name: key, label: key, type: "sum", sql: key };
|
|
720
865
|
}
|
|
721
|
-
function mongoOperatorToFilter(op, val) {
|
|
722
|
-
const toStrArr = (v) => v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)];
|
|
723
|
-
switch (op) {
|
|
724
|
-
case "$eq":
|
|
725
|
-
return { operator: "equals", values: toStrArr(val) };
|
|
726
|
-
case "$ne":
|
|
727
|
-
return { operator: "notEquals", values: toStrArr(val) };
|
|
728
|
-
case "$gt":
|
|
729
|
-
return { operator: "gt", values: toStrArr(val) };
|
|
730
|
-
case "$gte":
|
|
731
|
-
return { operator: "gte", values: toStrArr(val) };
|
|
732
|
-
case "$lt":
|
|
733
|
-
return { operator: "lt", values: toStrArr(val) };
|
|
734
|
-
case "$lte":
|
|
735
|
-
return { operator: "lte", values: toStrArr(val) };
|
|
736
|
-
case "$in":
|
|
737
|
-
return { operator: "in", values: toStrArr(val), multi: true };
|
|
738
|
-
case "$nin":
|
|
739
|
-
return { operator: "notIn", values: toStrArr(val), multi: true };
|
|
740
|
-
case "$regex":
|
|
741
|
-
return { operator: "contains", values: toStrArr(val) };
|
|
742
|
-
case "$exists":
|
|
743
|
-
return val ? { operator: "set", values: [] } : { operator: "notSet", values: [] };
|
|
744
|
-
default:
|
|
745
|
-
return null;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
866
|
var FallbackDelegateStrategy = class {
|
|
749
867
|
constructor() {
|
|
750
868
|
this.name = "FallbackDelegateStrategy";
|
|
@@ -823,8 +941,36 @@ var AnalyticsServicePlugin = class {
|
|
|
823
941
|
};
|
|
824
942
|
autoBridged = true;
|
|
825
943
|
}
|
|
944
|
+
let executeRawSql = this.options.executeRawSql;
|
|
945
|
+
let autoBridgedRawSql = false;
|
|
946
|
+
if (!executeRawSql) {
|
|
947
|
+
const tryGetExecutor = () => {
|
|
948
|
+
try {
|
|
949
|
+
const svc = ctx.getService("data");
|
|
950
|
+
return svc && typeof svc.execute === "function" ? svc : void 0;
|
|
951
|
+
} catch {
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
executeRawSql = async (_objectName, sql, params) => {
|
|
956
|
+
const engine = tryGetExecutor();
|
|
957
|
+
if (!engine || !engine.execute) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
'[Analytics] Cannot execute raw SQL: no IDataEngine ("data") service with execute() is registered.'
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const knexSql = sql.replace(/\$(\d+)/g, "?");
|
|
963
|
+
const result = await engine.execute(knexSql, { args: params });
|
|
964
|
+
if (Array.isArray(result)) return result;
|
|
965
|
+
if (result && typeof result === "object" && "rows" in result) {
|
|
966
|
+
return result.rows;
|
|
967
|
+
}
|
|
968
|
+
return [];
|
|
969
|
+
};
|
|
970
|
+
autoBridgedRawSql = true;
|
|
971
|
+
}
|
|
826
972
|
const queryCapabilities = this.options.queryCapabilities ?? (() => ({
|
|
827
|
-
nativeSql: !!
|
|
973
|
+
nativeSql: !!executeRawSql,
|
|
828
974
|
objectqlAggregate: !!executeAggregate,
|
|
829
975
|
inMemory: false
|
|
830
976
|
}));
|
|
@@ -832,13 +978,16 @@ var AnalyticsServicePlugin = class {
|
|
|
832
978
|
cubes: this.options.cubes,
|
|
833
979
|
logger: ctx.logger,
|
|
834
980
|
queryCapabilities,
|
|
835
|
-
executeRawSql
|
|
981
|
+
executeRawSql,
|
|
836
982
|
executeAggregate,
|
|
837
983
|
fallbackService
|
|
838
984
|
};
|
|
839
985
|
if (autoBridged) {
|
|
840
986
|
ctx.logger.info('[Analytics] Auto-bridged executeAggregate \u2192 "data" service (IDataEngine)');
|
|
841
987
|
}
|
|
988
|
+
if (autoBridgedRawSql) {
|
|
989
|
+
ctx.logger.info('[Analytics] Auto-bridged executeRawSql \u2192 "data" service (IDataEngine.execute)');
|
|
990
|
+
}
|
|
842
991
|
this.service = new AnalyticsService(config);
|
|
843
992
|
if (fallbackService) {
|
|
844
993
|
ctx.replaceService("analytics", this.service);
|