@objectstack/service-analytics 8.0.0 → 8.0.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 CHANGED
@@ -1176,13 +1176,60 @@ var AnalyticsService = class {
1176
1176
  * current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a
1177
1177
  * `getReadScope(objectName)` that already knows the active tenant.
1178
1178
  */
1179
- callCtx(context) {
1179
+ async callCtx(query, context) {
1180
1180
  if (!this.readScopeProvider) return this.baseCtx;
1181
+ const scopes = await this.resolveReadScopes(query, context);
1181
1182
  return {
1182
1183
  ...this.baseCtx,
1183
- getReadScope: (objectName) => this.readScopeProvider(objectName, context)
1184
+ getReadScope: (objectName) => scopes.get(objectName) ?? null
1184
1185
  };
1185
1186
  }
1187
+ /**
1188
+ * Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
1189
+ * AND every joined object of the query's cube, keyed by object name. This is
1190
+ * the async pre-pass that lets the synchronous strategy enforce scoping even
1191
+ * when the provider (security `getReadFilter`) resolves asynchronously.
1192
+ *
1193
+ * The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a
1194
+ * SUPERSET of what the strategy actually scans (the strategy only joins along
1195
+ * declared relationships), so no scanned object is ever left unscoped.
1196
+ *
1197
+ * Fail-closed: if the provider throws for an object, the whole query is
1198
+ * rejected rather than emitting SQL with that object unscoped.
1199
+ */
1200
+ async resolveReadScopes(query, context) {
1201
+ const map = /* @__PURE__ */ new Map();
1202
+ const provider = this.readScopeProvider;
1203
+ if (!provider || !query.cube) return map;
1204
+ const cube = this.cubeRegistry.get(query.cube);
1205
+ if (!cube) return map;
1206
+ const objects = /* @__PURE__ */ new Set();
1207
+ if (typeof cube.sql === "string" && cube.sql.trim()) {
1208
+ objects.add(cube.sql.trim());
1209
+ }
1210
+ const joins = cube.joins;
1211
+ if (joins) {
1212
+ for (const [alias, j] of Object.entries(joins)) {
1213
+ objects.add(j?.name ?? alias);
1214
+ }
1215
+ }
1216
+ for (const object of objects) {
1217
+ let filter;
1218
+ try {
1219
+ filter = await provider(object, context);
1220
+ } catch (e) {
1221
+ this.logger.error?.(
1222
+ `[Analytics] read-scope resolution failed for object "${object}" \u2014 rejecting query (fail-closed, ADR-0021 D-C)`,
1223
+ e instanceof Error ? e : new Error(String(e))
1224
+ );
1225
+ throw new Error(
1226
+ `[Analytics] read-scope resolution failed for "${object}"; query denied (fail-closed).`
1227
+ );
1228
+ }
1229
+ if (filter != null) map.set(object, filter);
1230
+ }
1231
+ return map;
1232
+ }
1186
1233
  /**
1187
1234
  * Execute an analytical query by delegating to the first capable strategy.
1188
1235
  */
@@ -1191,7 +1238,7 @@ var AnalyticsService = class {
1191
1238
  throw new Error("Cube name is required in analytics query");
1192
1239
  }
1193
1240
  this.ensureCube(query);
1194
- const ctx = this.callCtx(context);
1241
+ const ctx = await this.callCtx(query, context);
1195
1242
  const strategy = this.resolveStrategy(query, ctx);
1196
1243
  this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
1197
1244
  return strategy.execute(query, ctx);
@@ -1246,7 +1293,7 @@ var AnalyticsService = class {
1246
1293
  throw new Error("Cube name is required for SQL generation");
1247
1294
  }
1248
1295
  this.ensureCube(query);
1249
- const ctx = this.callCtx(context);
1296
+ const ctx = await this.callCtx(query, context);
1250
1297
  const strategy = this.resolveStrategy(query, ctx);
1251
1298
  this.logger.debug(`[Analytics] generateSql on cube "${query.cube}" \u2192 ${strategy.name}`);
1252
1299
  return strategy.generateSql(query, ctx);