@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.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
- if (query.filters && query.filters.length > 0) {
161
- for (const filter of query.filters) {
162
- const colExpr = this.resolveFieldSql(cube, filter.member);
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
- resolveDimensionSql(cube, member) {
201
- const fieldName = member.includes(".") ? member.split(".")[1] : member;
202
- const dim = cube.dimensions[fieldName];
203
- return dim ? dim.sql : fieldName;
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
- resolveMeasureSql(cube, member) {
206
- const fieldName = member.includes(".") ? member.split(".")[1] : member;
207
- const measure = cube.measures[fieldName];
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 fieldName = dim.includes(".") ? dim.split(".")[1] : dim;
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
- if (query.filters && query.filters.length > 0) {
315
- for (const f of query.filters) {
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.dimensions[fieldName];
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.measures[fieldName];
558
+ const measure = this.lookupMember(cube, member, "measure");
380
559
  if (measure) return measure.sql.replace(/^\$/, "");
381
560
  }
382
- return fieldName;
561
+ return member.includes(".") ? member.split(".")[1] : member;
383
562
  }
384
563
  resolveMeasureAggregation(cube, measureName) {
385
- const fieldName = measureName.includes(".") ? measureName.split(".")[1] : measureName;
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 values[0];
596
+ return v0;
416
597
  case "notEquals":
417
- return { $ne: values[0] };
598
+ return { $ne: v0 };
418
599
  case "gt":
419
- return { $gt: values[0] };
600
+ return { $gt: v0 };
420
601
  case "gte":
421
- return { $gte: values[0] };
602
+ return { $gte: v0 };
422
603
  case "lt":
423
- return { $lt: values[0] };
604
+ return { $lt: v0 };
424
605
  case "lte":
425
- return { $lte: values[0] };
606
+ return { $lte: v0 };
426
607
  case "contains":
427
608
  return { $regex: values[0] };
428
609
  case "in":
429
- return { $in: values };
610
+ return { $in: all };
430
611
  case "notIn":
431
- return { $nin: values };
612
+ return { $nin: all };
432
613
  default:
433
- return values[0];
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 fieldName = dim.includes(".") ? dim.split(".")[1] : dim;
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
- const normalized = this.normalizeQuery(query);
498
- this.ensureCube(normalized);
499
- const strategy = this.resolveStrategy(normalized);
500
- this.logger.debug(`[Analytics] Query on cube "${normalized.cube}" \u2192 ${strategy.name}`);
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
- const normalized = this.normalizeQuery(query);
531
- this.ensureCube(normalized);
532
- const strategy = this.resolveStrategy(normalized);
533
- this.logger.debug(`[Analytics] generateSql on cube "${normalized.cube}" \u2192 ${strategy.name}`);
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
- for (const f of query.filters || []) {
633
- const key = stripPrefix(f.member);
634
- if (dimensions[key] || measures[key]) continue;
635
- dimensions[key] = { name: key, label: key, type: "string", sql: key };
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: !!this.options.executeRawSql,
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: this.options.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);