@objectstack/service-analytics 4.0.5 → 4.1.1
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.js
CHANGED
|
@@ -116,6 +116,92 @@ var CubeRegistry = class {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
|
|
119
|
+
// src/strategies/filter-normalizer.ts
|
|
120
|
+
var MONGO_TO_CUBE_OP = {
|
|
121
|
+
$eq: "equals",
|
|
122
|
+
$ne: "notEquals",
|
|
123
|
+
$gt: "gt",
|
|
124
|
+
$gte: "gte",
|
|
125
|
+
$lt: "lt",
|
|
126
|
+
$lte: "lte",
|
|
127
|
+
$in: "in",
|
|
128
|
+
$nin: "notIn",
|
|
129
|
+
$contains: "contains",
|
|
130
|
+
$notContains: "notContains",
|
|
131
|
+
$exists: "set"
|
|
132
|
+
};
|
|
133
|
+
function stringifyForCube(v) {
|
|
134
|
+
if (v == null) return "";
|
|
135
|
+
if (typeof v === "boolean") return v ? "1" : "0";
|
|
136
|
+
if (v instanceof Date) return v.toISOString();
|
|
137
|
+
if (typeof v === "object") return JSON.stringify(v);
|
|
138
|
+
return String(v);
|
|
139
|
+
}
|
|
140
|
+
function flattenCondition(cond, out) {
|
|
141
|
+
for (const [key, raw] of Object.entries(cond)) {
|
|
142
|
+
if (raw === void 0) continue;
|
|
143
|
+
if (key === "$and" && Array.isArray(raw)) {
|
|
144
|
+
for (const sub of raw) {
|
|
145
|
+
if (sub && typeof sub === "object") {
|
|
146
|
+
flattenCondition(sub, out);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (key === "$or" || key === "$not") continue;
|
|
152
|
+
if (raw === null) {
|
|
153
|
+
out.push({ member: key, operator: "notSet", values: [] });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
157
|
+
const wrapper = raw;
|
|
158
|
+
const opKeys = Object.keys(wrapper).filter((k) => k.startsWith("$"));
|
|
159
|
+
if (opKeys.length > 0) {
|
|
160
|
+
for (const opKey of opKeys) {
|
|
161
|
+
const cubeOp = MONGO_TO_CUBE_OP[opKey];
|
|
162
|
+
if (!cubeOp) continue;
|
|
163
|
+
const v = wrapper[opKey];
|
|
164
|
+
const values = Array.isArray(v) ? v.map(stringifyForCube) : [stringifyForCube(v)];
|
|
165
|
+
out.push({ member: key, operator: cubeOp, values });
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {
|
|
170
|
+
flattenCondition({ [`${key}.${nestedKey}`]: nestedVal }, out);
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(raw)) {
|
|
175
|
+
out.push({ member: key, operator: "in", values: raw.map(stringifyForCube) });
|
|
176
|
+
} else {
|
|
177
|
+
out.push({ member: key, operator: "equals", values: [stringifyForCube(raw)] });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function normalizeAnalyticsFilters(query) {
|
|
182
|
+
if (!query || typeof query !== "object") return [];
|
|
183
|
+
const out = [];
|
|
184
|
+
const where = query.where;
|
|
185
|
+
if (where && typeof where === "object" && !Array.isArray(where)) {
|
|
186
|
+
flattenCondition(where, out);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function coerceFilterValueForSql(s) {
|
|
191
|
+
if (s === "true") return 1;
|
|
192
|
+
if (s === "false") return 0;
|
|
193
|
+
if (s === "null") return null;
|
|
194
|
+
if (/^-?\d+$/.test(s)) {
|
|
195
|
+
const n = Number(s);
|
|
196
|
+
if (Number.isFinite(n)) return n;
|
|
197
|
+
}
|
|
198
|
+
if (/^-?\d+\.\d+$/.test(s)) {
|
|
199
|
+
const n = Number(s);
|
|
200
|
+
if (Number.isFinite(n)) return n;
|
|
201
|
+
}
|
|
202
|
+
return s;
|
|
203
|
+
}
|
|
204
|
+
|
|
119
205
|
// src/strategies/native-sql-strategy.ts
|
|
120
206
|
var NativeSQLStrategy = class {
|
|
121
207
|
constructor() {
|
|
@@ -143,30 +229,33 @@ var NativeSQLStrategy = class {
|
|
|
143
229
|
const params = [];
|
|
144
230
|
const selectClauses = [];
|
|
145
231
|
const groupByClauses = [];
|
|
232
|
+
const tableName = this.extractObjectName(cube);
|
|
233
|
+
const joins = /* @__PURE__ */ new Map();
|
|
146
234
|
if (query.dimensions && query.dimensions.length > 0) {
|
|
147
235
|
for (const dim of query.dimensions) {
|
|
148
|
-
const colExpr = this.resolveDimensionSql(cube, dim);
|
|
236
|
+
const colExpr = this.resolveDimensionSql(cube, dim, tableName, joins);
|
|
149
237
|
selectClauses.push(`${colExpr} AS "${dim}"`);
|
|
150
238
|
groupByClauses.push(colExpr);
|
|
151
239
|
}
|
|
152
240
|
}
|
|
153
241
|
if (query.measures && query.measures.length > 0) {
|
|
154
242
|
for (const measure of query.measures) {
|
|
155
|
-
const aggExpr = this.resolveMeasureSql(cube, measure);
|
|
243
|
+
const aggExpr = this.resolveMeasureSql(cube, measure, tableName, joins);
|
|
156
244
|
selectClauses.push(`${aggExpr} AS "${measure}"`);
|
|
157
245
|
}
|
|
158
246
|
}
|
|
159
247
|
const whereClauses = [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
248
|
+
const normalizedFilters = normalizeAnalyticsFilters(query);
|
|
249
|
+
if (normalizedFilters.length > 0) {
|
|
250
|
+
for (const filter of normalizedFilters) {
|
|
251
|
+
const colExpr = this.resolveFieldSql(cube, filter.member, tableName, joins);
|
|
163
252
|
const clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params);
|
|
164
253
|
if (clause) whereClauses.push(clause);
|
|
165
254
|
}
|
|
166
255
|
}
|
|
167
256
|
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
168
257
|
for (const td of query.timeDimensions) {
|
|
169
|
-
const colExpr = this.resolveFieldSql(cube, td.dimension);
|
|
258
|
+
const colExpr = this.resolveFieldSql(cube, td.dimension, tableName, joins);
|
|
170
259
|
if (td.dateRange) {
|
|
171
260
|
const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
172
261
|
if (range.length === 2) {
|
|
@@ -176,8 +265,10 @@ var NativeSQLStrategy = class {
|
|
|
176
265
|
}
|
|
177
266
|
}
|
|
178
267
|
}
|
|
179
|
-
const tableName = this.extractObjectName(cube);
|
|
180
268
|
let sql = `SELECT ${selectClauses.join(", ")} FROM "${tableName}"`;
|
|
269
|
+
if (joins.size > 0) {
|
|
270
|
+
sql += " " + Array.from(joins.values()).join(" ");
|
|
271
|
+
}
|
|
181
272
|
if (whereClauses.length > 0) {
|
|
182
273
|
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
183
274
|
}
|
|
@@ -197,16 +288,77 @@ var NativeSQLStrategy = class {
|
|
|
197
288
|
return { sql, params };
|
|
198
289
|
}
|
|
199
290
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
291
|
+
/**
|
|
292
|
+
* Resolve a dimension/measure/filter SQL expression that may reference a
|
|
293
|
+
* related table via dot notation (e.g. `account.industry`).
|
|
294
|
+
*
|
|
295
|
+
* When the resolved `sql` contains a dot, treat the prefix as a lookup
|
|
296
|
+
* field on the cube's table and synthesise a `LEFT JOIN` against the
|
|
297
|
+
* related table. The convention (matching the auto-cube generator and
|
|
298
|
+
* ObjectStack object schemas) is:
|
|
299
|
+
*
|
|
300
|
+
* <parentTable>.<lookupField> = <lookupField>.id
|
|
301
|
+
*
|
|
302
|
+
* i.e. the lookup field name on the parent table equals the related
|
|
303
|
+
* table name. This holds for all `Field.lookup({ object: '...' })`
|
|
304
|
+
* declarations where the field is named after its target object.
|
|
305
|
+
*
|
|
306
|
+
* Returns the qualified SQL reference (e.g. `"account"."industry"`).
|
|
307
|
+
* Pure column references (no dot) are returned as-is.
|
|
308
|
+
*/
|
|
309
|
+
qualifyAndRegisterJoin(rawSql, parentTable, joins) {
|
|
310
|
+
if (!rawSql.includes(".")) return rawSql;
|
|
311
|
+
const [alias, ...rest] = rawSql.split(".");
|
|
312
|
+
if (!alias || rest.length === 0) return rawSql;
|
|
313
|
+
const column = rest.join(".");
|
|
314
|
+
if (!joins.has(alias)) {
|
|
315
|
+
joins.set(
|
|
316
|
+
alias,
|
|
317
|
+
`LEFT JOIN "${alias}" ON "${parentTable}"."${alias}" = "${alias}"."id"`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return `"${alias}"."${column}"`;
|
|
204
321
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Resolve a member reference (dimension, measure, or filter field) to its
|
|
324
|
+
* cube definition.
|
|
325
|
+
*
|
|
326
|
+
* Accepts three naming conventions:
|
|
327
|
+
* 1. `<cube>.<field>` — the canonical analytics qualifier (stripped to `<field>`).
|
|
328
|
+
* 2. `<lookup>.<field>` — a relation traversal (e.g. `account.industry`).
|
|
329
|
+
* First tried as the literal key, then as the underscore-flattened
|
|
330
|
+
* key (`account_industry`), and finally returned as a synthetic
|
|
331
|
+
* definition whose `sql` is the dotted reference so the JOIN
|
|
332
|
+
* machinery can pick it up.
|
|
333
|
+
* 3. `<field>` — a bare field name on the cube's table.
|
|
334
|
+
*/
|
|
335
|
+
lookupMember(cube, member, kind) {
|
|
336
|
+
const bag = kind === "dimension" ? cube.dimensions : cube.measures;
|
|
337
|
+
if (bag[member]) return bag[member];
|
|
338
|
+
if (member.includes(".")) {
|
|
339
|
+
const [first, ...rest] = member.split(".");
|
|
340
|
+
const tail = rest.join(".");
|
|
341
|
+
if (first === cube.name && bag[tail]) return bag[tail];
|
|
342
|
+
if (bag[tail]) return bag[tail];
|
|
343
|
+
const flat = member.replace(/\./g, "_");
|
|
344
|
+
if (bag[flat]) return bag[flat];
|
|
345
|
+
if (kind === "dimension") {
|
|
346
|
+
return { sql: member, type: "string" };
|
|
347
|
+
}
|
|
348
|
+
} else if (bag[member]) {
|
|
349
|
+
return bag[member];
|
|
350
|
+
}
|
|
351
|
+
return void 0;
|
|
352
|
+
}
|
|
353
|
+
resolveDimensionSql(cube, member, parentTable, joins) {
|
|
354
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
355
|
+
const raw = dim ? dim.sql : member.includes(".") ? member.split(".")[1] : member;
|
|
356
|
+
return this.qualifyAndRegisterJoin(raw, parentTable, joins);
|
|
357
|
+
}
|
|
358
|
+
resolveMeasureSql(cube, member, parentTable, joins) {
|
|
359
|
+
const measure = this.lookupMember(cube, member, "measure");
|
|
208
360
|
if (!measure) return `COUNT(*)`;
|
|
209
|
-
const col = measure.sql;
|
|
361
|
+
const col = measure.sql === "*" ? "*" : this.qualifyAndRegisterJoin(measure.sql, parentTable, joins);
|
|
210
362
|
switch (measure.type) {
|
|
211
363
|
case "count":
|
|
212
364
|
return "COUNT(*)";
|
|
@@ -224,12 +376,12 @@ var NativeSQLStrategy = class {
|
|
|
224
376
|
return `COUNT(*)`;
|
|
225
377
|
}
|
|
226
378
|
}
|
|
227
|
-
resolveFieldSql(cube, member) {
|
|
379
|
+
resolveFieldSql(cube, member, parentTable, joins) {
|
|
380
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
381
|
+
if (dim) return this.qualifyAndRegisterJoin(dim.sql, parentTable, joins);
|
|
382
|
+
const measure = this.lookupMember(cube, member, "measure");
|
|
383
|
+
if (measure) return this.qualifyAndRegisterJoin(measure.sql, parentTable, joins);
|
|
228
384
|
const fieldName = member.includes(".") ? member.split(".")[1] : member;
|
|
229
|
-
const dim = cube.dimensions[fieldName];
|
|
230
|
-
if (dim) return dim.sql;
|
|
231
|
-
const measure = cube.measures[fieldName];
|
|
232
|
-
if (measure) return measure.sql;
|
|
233
385
|
return fieldName;
|
|
234
386
|
}
|
|
235
387
|
buildFilterClause(col, operator, values, params) {
|
|
@@ -248,7 +400,7 @@ var NativeSQLStrategy = class {
|
|
|
248
400
|
if (operator === "in" || operator === "notIn") {
|
|
249
401
|
if (!values || values.length === 0) return null;
|
|
250
402
|
const placeholders = values.map((v) => {
|
|
251
|
-
params.push(v);
|
|
403
|
+
params.push(coerceFilterValueForSql(v));
|
|
252
404
|
return `$${params.length}`;
|
|
253
405
|
}).join(", ");
|
|
254
406
|
return `${col} ${operator === "in" ? "IN" : "NOT IN"} (${placeholders})`;
|
|
@@ -258,7 +410,7 @@ var NativeSQLStrategy = class {
|
|
|
258
410
|
if (operator === "contains" || operator === "notContains") {
|
|
259
411
|
params.push(`%${values[0]}%`);
|
|
260
412
|
} else {
|
|
261
|
-
params.push(values[0]);
|
|
413
|
+
params.push(coerceFilterValueForSql(values[0]));
|
|
262
414
|
}
|
|
263
415
|
return `${col} ${sqlOp} $${params.length}`;
|
|
264
416
|
}
|
|
@@ -269,8 +421,7 @@ var NativeSQLStrategy = class {
|
|
|
269
421
|
const fields = [];
|
|
270
422
|
if (query.dimensions) {
|
|
271
423
|
for (const dim of query.dimensions) {
|
|
272
|
-
const
|
|
273
|
-
const d = cube.dimensions[fieldName];
|
|
424
|
+
const d = this.lookupMember(cube, dim, "dimension");
|
|
274
425
|
fields.push({ name: dim, type: d?.type || "string" });
|
|
275
426
|
}
|
|
276
427
|
}
|
|
@@ -311,8 +462,9 @@ var ObjectQLStrategy = class {
|
|
|
311
462
|
}
|
|
312
463
|
}
|
|
313
464
|
const filter = {};
|
|
314
|
-
|
|
315
|
-
|
|
465
|
+
const normalizedFilters = normalizeAnalyticsFilters(query);
|
|
466
|
+
if (normalizedFilters.length > 0) {
|
|
467
|
+
for (const f of normalizedFilters) {
|
|
316
468
|
const fieldName = this.resolveFieldName(cube, f.member, "any");
|
|
317
469
|
filter[fieldName] = this.convertFilter(f.operator, f.values);
|
|
318
470
|
}
|
|
@@ -369,27 +521,54 @@ var ObjectQLStrategy = class {
|
|
|
369
521
|
return { sql, params: [] };
|
|
370
522
|
}
|
|
371
523
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
524
|
+
/**
|
|
525
|
+
* Resolve a member ref to a `{ sql, type? }` definition.
|
|
526
|
+
*
|
|
527
|
+
* Mirrors `NativeSQLStrategy.lookupMember` so the two strategies
|
|
528
|
+
* accept the same naming conventions:
|
|
529
|
+
* 1. `<cube>.<field>` — canonical analytics qualifier.
|
|
530
|
+
* 2. `<lookup>.<field>` — relation traversal (e.g. `account.industry`).
|
|
531
|
+
* Tries literal key, then underscore-flattened key, then falls
|
|
532
|
+
* back to a synthetic dim whose `sql` is the dotted path so the
|
|
533
|
+
* ObjectQL aggregate engine can traverse it via the lookup field.
|
|
534
|
+
* 3. `<field>` — bare column on the cube's table.
|
|
535
|
+
*/
|
|
536
|
+
lookupMember(cube, member, kind) {
|
|
537
|
+
const bag = kind === "dimension" ? cube.dimensions : cube.measures;
|
|
538
|
+
if (bag[member]) return bag[member];
|
|
539
|
+
if (member.includes(".")) {
|
|
540
|
+
const [first, ...rest] = member.split(".");
|
|
541
|
+
const tail = rest.join(".");
|
|
542
|
+
if (first === cube.name && bag[tail]) return bag[tail];
|
|
543
|
+
if (bag[tail]) return bag[tail];
|
|
544
|
+
const flat = member.replace(/\./g, "_");
|
|
545
|
+
if (bag[flat]) return bag[flat];
|
|
546
|
+
if (kind === "dimension") return { sql: member, type: "string" };
|
|
547
|
+
} else if (bag[member]) {
|
|
548
|
+
return bag[member];
|
|
549
|
+
}
|
|
550
|
+
return void 0;
|
|
551
|
+
}
|
|
372
552
|
resolveFieldName(cube, member, kind) {
|
|
373
|
-
const fieldName = member.includes(".") ? member.split(".")[1] : member;
|
|
374
553
|
if (kind === "dimension" || kind === "any") {
|
|
375
|
-
const dim = cube
|
|
554
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
376
555
|
if (dim) return dim.sql.replace(/^\$/, "");
|
|
377
556
|
}
|
|
378
557
|
if (kind === "measure" || kind === "any") {
|
|
379
|
-
const measure = cube
|
|
558
|
+
const measure = this.lookupMember(cube, member, "measure");
|
|
380
559
|
if (measure) return measure.sql.replace(/^\$/, "");
|
|
381
560
|
}
|
|
382
|
-
return
|
|
561
|
+
return member.includes(".") ? member.split(".")[1] : member;
|
|
383
562
|
}
|
|
384
563
|
resolveMeasureAggregation(cube, measureName) {
|
|
385
|
-
const
|
|
386
|
-
const direct = cube.measures[fieldName];
|
|
564
|
+
const direct = this.lookupMember(cube, measureName, "measure");
|
|
387
565
|
if (direct) {
|
|
388
566
|
return {
|
|
389
567
|
field: direct.sql.replace(/^\$/, ""),
|
|
390
568
|
method: direct.type === "count_distinct" ? "count_distinct" : direct.type
|
|
391
569
|
};
|
|
392
570
|
}
|
|
571
|
+
const fieldName = measureName.includes(".") ? measureName.split(".")[1] : measureName;
|
|
393
572
|
const aggTypes = ["count", "sum", "avg", "min", "max", "count_distinct"];
|
|
394
573
|
for (const type of aggTypes) {
|
|
395
574
|
const suffix = `_${type}`;
|
|
@@ -410,27 +589,29 @@ var ObjectQLStrategy = class {
|
|
|
410
589
|
if (operator === "set") return { $ne: null };
|
|
411
590
|
if (operator === "notSet") return null;
|
|
412
591
|
if (!values || values.length === 0) return void 0;
|
|
592
|
+
const v0 = coerceFilterValueForSql(values[0]);
|
|
593
|
+
const all = values.map(coerceFilterValueForSql);
|
|
413
594
|
switch (operator) {
|
|
414
595
|
case "equals":
|
|
415
|
-
return
|
|
596
|
+
return v0;
|
|
416
597
|
case "notEquals":
|
|
417
|
-
return { $ne:
|
|
598
|
+
return { $ne: v0 };
|
|
418
599
|
case "gt":
|
|
419
|
-
return { $gt:
|
|
600
|
+
return { $gt: v0 };
|
|
420
601
|
case "gte":
|
|
421
|
-
return { $gte:
|
|
602
|
+
return { $gte: v0 };
|
|
422
603
|
case "lt":
|
|
423
|
-
return { $lt:
|
|
604
|
+
return { $lt: v0 };
|
|
424
605
|
case "lte":
|
|
425
|
-
return { $lte:
|
|
606
|
+
return { $lte: v0 };
|
|
426
607
|
case "contains":
|
|
427
608
|
return { $regex: values[0] };
|
|
428
609
|
case "in":
|
|
429
|
-
return { $in:
|
|
610
|
+
return { $in: all };
|
|
430
611
|
case "notIn":
|
|
431
|
-
return { $nin:
|
|
612
|
+
return { $nin: all };
|
|
432
613
|
default:
|
|
433
|
-
return
|
|
614
|
+
return v0;
|
|
434
615
|
}
|
|
435
616
|
}
|
|
436
617
|
extractObjectName(cube) {
|
|
@@ -440,8 +621,7 @@ var ObjectQLStrategy = class {
|
|
|
440
621
|
const fields = [];
|
|
441
622
|
if (query.dimensions) {
|
|
442
623
|
for (const dim of query.dimensions) {
|
|
443
|
-
const
|
|
444
|
-
const d = cube.dimensions[fieldName];
|
|
624
|
+
const d = this.lookupMember(cube, dim, "dimension");
|
|
445
625
|
fields.push({ name: dim, type: d?.type || "string" });
|
|
446
626
|
}
|
|
447
627
|
}
|
|
@@ -494,11 +674,10 @@ var AnalyticsService = class {
|
|
|
494
674
|
if (!query.cube) {
|
|
495
675
|
throw new Error("Cube name is required in analytics query");
|
|
496
676
|
}
|
|
497
|
-
|
|
498
|
-
this.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
return strategy.execute(normalized, this.strategyCtx);
|
|
677
|
+
this.ensureCube(query);
|
|
678
|
+
const strategy = this.resolveStrategy(query);
|
|
679
|
+
this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
|
|
680
|
+
return strategy.execute(query, this.strategyCtx);
|
|
502
681
|
}
|
|
503
682
|
/**
|
|
504
683
|
* Get cube metadata for discovery.
|
|
@@ -527,49 +706,12 @@ var AnalyticsService = class {
|
|
|
527
706
|
if (!query.cube) {
|
|
528
707
|
throw new Error("Cube name is required for SQL generation");
|
|
529
708
|
}
|
|
530
|
-
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return strategy.generateSql(normalized, this.strategyCtx);
|
|
709
|
+
this.ensureCube(query);
|
|
710
|
+
const strategy = this.resolveStrategy(query);
|
|
711
|
+
this.logger.debug(`[Analytics] generateSql on cube "${query.cube}" \u2192 ${strategy.name}`);
|
|
712
|
+
return strategy.generateSql(query, this.strategyCtx);
|
|
535
713
|
}
|
|
536
714
|
// ── Internal ─────────────────────────────────────────────────────
|
|
537
|
-
/**
|
|
538
|
-
* Normalise a query into a canonical shape that strategies can rely on:
|
|
539
|
-
*
|
|
540
|
-
* 1. **Filters as object** — Some clients (e.g. dashboard widget translators)
|
|
541
|
-
* send `filters` as a Mongo-style object `{ stage: { $nin: [...] } }`
|
|
542
|
-
* instead of the schema's `Array<{ member, operator, values }>`. We
|
|
543
|
-
* translate the object form into the array form so existing strategies
|
|
544
|
-
* work unchanged.
|
|
545
|
-
* 2. Other shapes are returned as-is.
|
|
546
|
-
*/
|
|
547
|
-
normalizeQuery(query) {
|
|
548
|
-
const filters = query.filters;
|
|
549
|
-
if (!filters || Array.isArray(filters)) return query;
|
|
550
|
-
if (typeof filters !== "object") return query;
|
|
551
|
-
const arr = [];
|
|
552
|
-
for (const [member, raw] of Object.entries(filters)) {
|
|
553
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
|
|
554
|
-
for (const [op, val] of Object.entries(raw)) {
|
|
555
|
-
const mapped = mongoOperatorToFilter(op, val);
|
|
556
|
-
if (!mapped) continue;
|
|
557
|
-
if (mapped.multi) {
|
|
558
|
-
arr.push({ member, operator: mapped.operator, values: mapped.values });
|
|
559
|
-
} else if (mapped.values.length === 0) {
|
|
560
|
-
arr.push({ member, operator: mapped.operator, values: [] });
|
|
561
|
-
} else {
|
|
562
|
-
for (const v of mapped.values) arr.push({ member, operator: mapped.operator, values: [v] });
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
} else if (Array.isArray(raw)) {
|
|
566
|
-
arr.push({ member, operator: "in", values: raw.map(String) });
|
|
567
|
-
} else {
|
|
568
|
-
arr.push({ member, operator: "equals", values: [String(raw)] });
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return { ...query, filters: arr };
|
|
572
|
-
}
|
|
573
715
|
/**
|
|
574
716
|
* Ensure a cube exists for the given query and that it knows about every
|
|
575
717
|
* measure referenced by the query.
|
|
@@ -629,10 +771,13 @@ var AnalyticsService = class {
|
|
|
629
771
|
if (dimensions[key]) continue;
|
|
630
772
|
dimensions[key] = { name: key, label: key, type: "string", sql: key };
|
|
631
773
|
}
|
|
632
|
-
|
|
633
|
-
const key
|
|
634
|
-
|
|
635
|
-
|
|
774
|
+
if (query.where && typeof query.where === "object" && !Array.isArray(query.where)) {
|
|
775
|
+
for (const key of Object.keys(query.where)) {
|
|
776
|
+
if (key.startsWith("$")) continue;
|
|
777
|
+
const stripped = stripPrefix(key);
|
|
778
|
+
if (dimensions[stripped] || measures[stripped]) continue;
|
|
779
|
+
dimensions[stripped] = { name: stripped, label: stripped, type: "string", sql: stripped };
|
|
780
|
+
}
|
|
636
781
|
}
|
|
637
782
|
for (const td of query.timeDimensions || []) {
|
|
638
783
|
const key = stripPrefix(td.dimension);
|
|
@@ -688,33 +833,6 @@ function inferMeasure(key) {
|
|
|
688
833
|
}
|
|
689
834
|
return { name: key, label: key, type: "sum", sql: key };
|
|
690
835
|
}
|
|
691
|
-
function mongoOperatorToFilter(op, val) {
|
|
692
|
-
const toStrArr = (v) => v == null ? [] : Array.isArray(v) ? v.map(String) : [String(v)];
|
|
693
|
-
switch (op) {
|
|
694
|
-
case "$eq":
|
|
695
|
-
return { operator: "equals", values: toStrArr(val) };
|
|
696
|
-
case "$ne":
|
|
697
|
-
return { operator: "notEquals", values: toStrArr(val) };
|
|
698
|
-
case "$gt":
|
|
699
|
-
return { operator: "gt", values: toStrArr(val) };
|
|
700
|
-
case "$gte":
|
|
701
|
-
return { operator: "gte", values: toStrArr(val) };
|
|
702
|
-
case "$lt":
|
|
703
|
-
return { operator: "lt", values: toStrArr(val) };
|
|
704
|
-
case "$lte":
|
|
705
|
-
return { operator: "lte", values: toStrArr(val) };
|
|
706
|
-
case "$in":
|
|
707
|
-
return { operator: "in", values: toStrArr(val), multi: true };
|
|
708
|
-
case "$nin":
|
|
709
|
-
return { operator: "notIn", values: toStrArr(val), multi: true };
|
|
710
|
-
case "$regex":
|
|
711
|
-
return { operator: "contains", values: toStrArr(val) };
|
|
712
|
-
case "$exists":
|
|
713
|
-
return val ? { operator: "set", values: [] } : { operator: "notSet", values: [] };
|
|
714
|
-
default:
|
|
715
|
-
return null;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
836
|
var FallbackDelegateStrategy = class {
|
|
719
837
|
constructor() {
|
|
720
838
|
this.name = "FallbackDelegateStrategy";
|
|
@@ -793,8 +911,36 @@ var AnalyticsServicePlugin = class {
|
|
|
793
911
|
};
|
|
794
912
|
autoBridged = true;
|
|
795
913
|
}
|
|
914
|
+
let executeRawSql = this.options.executeRawSql;
|
|
915
|
+
let autoBridgedRawSql = false;
|
|
916
|
+
if (!executeRawSql) {
|
|
917
|
+
const tryGetExecutor = () => {
|
|
918
|
+
try {
|
|
919
|
+
const svc = ctx.getService("data");
|
|
920
|
+
return svc && typeof svc.execute === "function" ? svc : void 0;
|
|
921
|
+
} catch {
|
|
922
|
+
return void 0;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
executeRawSql = async (_objectName, sql, params) => {
|
|
926
|
+
const engine = tryGetExecutor();
|
|
927
|
+
if (!engine || !engine.execute) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
'[Analytics] Cannot execute raw SQL: no IDataEngine ("data") service with execute() is registered.'
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
const knexSql = sql.replace(/\$(\d+)/g, "?");
|
|
933
|
+
const result = await engine.execute(knexSql, { args: params });
|
|
934
|
+
if (Array.isArray(result)) return result;
|
|
935
|
+
if (result && typeof result === "object" && "rows" in result) {
|
|
936
|
+
return result.rows;
|
|
937
|
+
}
|
|
938
|
+
return [];
|
|
939
|
+
};
|
|
940
|
+
autoBridgedRawSql = true;
|
|
941
|
+
}
|
|
796
942
|
const queryCapabilities = this.options.queryCapabilities ?? (() => ({
|
|
797
|
-
nativeSql: !!
|
|
943
|
+
nativeSql: !!executeRawSql,
|
|
798
944
|
objectqlAggregate: !!executeAggregate,
|
|
799
945
|
inMemory: false
|
|
800
946
|
}));
|
|
@@ -802,13 +948,16 @@ var AnalyticsServicePlugin = class {
|
|
|
802
948
|
cubes: this.options.cubes,
|
|
803
949
|
logger: ctx.logger,
|
|
804
950
|
queryCapabilities,
|
|
805
|
-
executeRawSql
|
|
951
|
+
executeRawSql,
|
|
806
952
|
executeAggregate,
|
|
807
953
|
fallbackService
|
|
808
954
|
};
|
|
809
955
|
if (autoBridged) {
|
|
810
956
|
ctx.logger.info('[Analytics] Auto-bridged executeAggregate \u2192 "data" service (IDataEngine)');
|
|
811
957
|
}
|
|
958
|
+
if (autoBridgedRawSql) {
|
|
959
|
+
ctx.logger.info('[Analytics] Auto-bridged executeRawSql \u2192 "data" service (IDataEngine.execute)');
|
|
960
|
+
}
|
|
812
961
|
this.service = new AnalyticsService(config);
|
|
813
962
|
if (fallbackService) {
|
|
814
963
|
ctx.replaceService("analytics", this.service);
|