@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 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
- if (query.filters && query.filters.length > 0) {
191
- for (const filter of query.filters) {
192
- const colExpr = this.resolveFieldSql(cube, filter.member);
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
- resolveDimensionSql(cube, member) {
231
- const fieldName = member.includes(".") ? member.split(".")[1] : member;
232
- const dim = cube.dimensions[fieldName];
233
- return dim ? dim.sql : fieldName;
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
- resolveMeasureSql(cube, member) {
236
- const fieldName = member.includes(".") ? member.split(".")[1] : member;
237
- const measure = cube.measures[fieldName];
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 fieldName = dim.includes(".") ? dim.split(".")[1] : dim;
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
- if (query.filters && query.filters.length > 0) {
345
- for (const f of query.filters) {
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.dimensions[fieldName];
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.measures[fieldName];
588
+ const measure = this.lookupMember(cube, member, "measure");
410
589
  if (measure) return measure.sql.replace(/^\$/, "");
411
590
  }
412
- return fieldName;
591
+ return member.includes(".") ? member.split(".")[1] : member;
413
592
  }
414
593
  resolveMeasureAggregation(cube, measureName) {
415
- const fieldName = measureName.includes(".") ? measureName.split(".")[1] : measureName;
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 values[0];
626
+ return v0;
446
627
  case "notEquals":
447
- return { $ne: values[0] };
628
+ return { $ne: v0 };
448
629
  case "gt":
449
- return { $gt: values[0] };
630
+ return { $gt: v0 };
450
631
  case "gte":
451
- return { $gte: values[0] };
632
+ return { $gte: v0 };
452
633
  case "lt":
453
- return { $lt: values[0] };
634
+ return { $lt: v0 };
454
635
  case "lte":
455
- return { $lte: values[0] };
636
+ return { $lte: v0 };
456
637
  case "contains":
457
638
  return { $regex: values[0] };
458
639
  case "in":
459
- return { $in: values };
640
+ return { $in: all };
460
641
  case "notIn":
461
- return { $nin: values };
642
+ return { $nin: all };
462
643
  default:
463
- return values[0];
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 fieldName = dim.includes(".") ? dim.split(".")[1] : dim;
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
- const normalized = this.normalizeQuery(query);
528
- this.ensureCube(normalized);
529
- const strategy = this.resolveStrategy(normalized);
530
- this.logger.debug(`[Analytics] Query on cube "${normalized.cube}" \u2192 ${strategy.name}`);
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
- const normalized = this.normalizeQuery(query);
561
- this.ensureCube(normalized);
562
- const strategy = this.resolveStrategy(normalized);
563
- this.logger.debug(`[Analytics] generateSql on cube "${normalized.cube}" \u2192 ${strategy.name}`);
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
- for (const f of query.filters || []) {
663
- const key = stripPrefix(f.member);
664
- if (dimensions[key] || measures[key]) continue;
665
- dimensions[key] = { name: key, label: key, type: "string", sql: key };
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: !!this.options.executeRawSql,
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: this.options.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);