@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 +51 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +51 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -131,8 +131,13 @@ interface AnalyticsServiceConfig {
|
|
|
131
131
|
* object (exactly what `RLSCompiler` emits). The service binds the active
|
|
132
132
|
* context per query and the strategy compiles the filter into alias-qualified
|
|
133
133
|
* SQL injected into every base and joined table.
|
|
134
|
+
*
|
|
135
|
+
* MAY be async: the production bridge resolves RLS from the `security`
|
|
136
|
+
* service's `getReadFilter`, which can hit the database. The service
|
|
137
|
+
* pre-resolves the scope for every base + joined object of a query (before
|
|
138
|
+
* the synchronous SQL builder runs), so a sync return still works unchanged.
|
|
134
139
|
*/
|
|
135
|
-
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined
|
|
140
|
+
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
|
|
136
141
|
/**
|
|
137
142
|
* ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
|
|
138
143
|
* Joins outside this set are rejected by the strategy. Compiled datasets
|
|
@@ -186,6 +191,20 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
186
191
|
* `getReadScope(objectName)` that already knows the active tenant.
|
|
187
192
|
*/
|
|
188
193
|
private callCtx;
|
|
194
|
+
/**
|
|
195
|
+
* Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
|
|
196
|
+
* AND every joined object of the query's cube, keyed by object name. This is
|
|
197
|
+
* the async pre-pass that lets the synchronous strategy enforce scoping even
|
|
198
|
+
* when the provider (security `getReadFilter`) resolves asynchronously.
|
|
199
|
+
*
|
|
200
|
+
* The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a
|
|
201
|
+
* SUPERSET of what the strategy actually scans (the strategy only joins along
|
|
202
|
+
* declared relationships), so no scanned object is ever left unscoped.
|
|
203
|
+
*
|
|
204
|
+
* Fail-closed: if the provider throws for an object, the whole query is
|
|
205
|
+
* rejected rather than emitting SQL with that object unscoped.
|
|
206
|
+
*/
|
|
207
|
+
private resolveReadScopes;
|
|
189
208
|
/**
|
|
190
209
|
* Execute an analytical query by delegating to the first capable strategy.
|
|
191
210
|
*/
|
|
@@ -270,7 +289,7 @@ interface AnalyticsServicePluginOptions {
|
|
|
270
289
|
* emits). When omitted, the plugin auto-bridges to a registered `'security'`
|
|
271
290
|
* service exposing `getReadFilter(object, context)` if one is present.
|
|
272
291
|
*/
|
|
273
|
-
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined
|
|
292
|
+
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
|
|
274
293
|
/**
|
|
275
294
|
* ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
|
|
276
295
|
* Typically wired from the dataset registry's compiled `allowedRelationships`.
|
package/dist/index.d.ts
CHANGED
|
@@ -131,8 +131,13 @@ interface AnalyticsServiceConfig {
|
|
|
131
131
|
* object (exactly what `RLSCompiler` emits). The service binds the active
|
|
132
132
|
* context per query and the strategy compiles the filter into alias-qualified
|
|
133
133
|
* SQL injected into every base and joined table.
|
|
134
|
+
*
|
|
135
|
+
* MAY be async: the production bridge resolves RLS from the `security`
|
|
136
|
+
* service's `getReadFilter`, which can hit the database. The service
|
|
137
|
+
* pre-resolves the scope for every base + joined object of a query (before
|
|
138
|
+
* the synchronous SQL builder runs), so a sync return still works unchanged.
|
|
134
139
|
*/
|
|
135
|
-
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined
|
|
140
|
+
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
|
|
136
141
|
/**
|
|
137
142
|
* ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
|
|
138
143
|
* Joins outside this set are rejected by the strategy. Compiled datasets
|
|
@@ -186,6 +191,20 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
186
191
|
* `getReadScope(objectName)` that already knows the active tenant.
|
|
187
192
|
*/
|
|
188
193
|
private callCtx;
|
|
194
|
+
/**
|
|
195
|
+
* Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
|
|
196
|
+
* AND every joined object of the query's cube, keyed by object name. This is
|
|
197
|
+
* the async pre-pass that lets the synchronous strategy enforce scoping even
|
|
198
|
+
* when the provider (security `getReadFilter`) resolves asynchronously.
|
|
199
|
+
*
|
|
200
|
+
* The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a
|
|
201
|
+
* SUPERSET of what the strategy actually scans (the strategy only joins along
|
|
202
|
+
* declared relationships), so no scanned object is ever left unscoped.
|
|
203
|
+
*
|
|
204
|
+
* Fail-closed: if the provider throws for an object, the whole query is
|
|
205
|
+
* rejected rather than emitting SQL with that object unscoped.
|
|
206
|
+
*/
|
|
207
|
+
private resolveReadScopes;
|
|
189
208
|
/**
|
|
190
209
|
* Execute an analytical query by delegating to the first capable strategy.
|
|
191
210
|
*/
|
|
@@ -270,7 +289,7 @@ interface AnalyticsServicePluginOptions {
|
|
|
270
289
|
* emits). When omitted, the plugin auto-bridges to a registered `'security'`
|
|
271
290
|
* service exposing `getReadFilter(object, context)` if one is present.
|
|
272
291
|
*/
|
|
273
|
-
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined
|
|
292
|
+
getReadScope?: (objectName: string, context?: ExecutionContext) => FilterCondition | null | undefined | Promise<FilterCondition | null | undefined>;
|
|
274
293
|
/**
|
|
275
294
|
* ADR-0021 D-C — join allowlist per cube (the dataset's declared `include`).
|
|
276
295
|
* Typically wired from the dataset registry's compiled `allowedRelationships`.
|
package/dist/index.js
CHANGED
|
@@ -1139,13 +1139,60 @@ var AnalyticsService = class {
|
|
|
1139
1139
|
* current request's ExecutionContext (ADR-0021 D-C). The strategy then sees a
|
|
1140
1140
|
* `getReadScope(objectName)` that already knows the active tenant.
|
|
1141
1141
|
*/
|
|
1142
|
-
callCtx(context) {
|
|
1142
|
+
async callCtx(query, context) {
|
|
1143
1143
|
if (!this.readScopeProvider) return this.baseCtx;
|
|
1144
|
+
const scopes = await this.resolveReadScopes(query, context);
|
|
1144
1145
|
return {
|
|
1145
1146
|
...this.baseCtx,
|
|
1146
|
-
getReadScope: (objectName) =>
|
|
1147
|
+
getReadScope: (objectName) => scopes.get(objectName) ?? null
|
|
1147
1148
|
};
|
|
1148
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Resolve the read scope (tenant + RLS `FilterCondition`) for the base object
|
|
1152
|
+
* AND every joined object of the query's cube, keyed by object name. This is
|
|
1153
|
+
* the async pre-pass that lets the synchronous strategy enforce scoping even
|
|
1154
|
+
* when the provider (security `getReadFilter`) resolves asynchronously.
|
|
1155
|
+
*
|
|
1156
|
+
* The object set is `cube.sql` (base) plus every `cube.joins[*].name` — a
|
|
1157
|
+
* SUPERSET of what the strategy actually scans (the strategy only joins along
|
|
1158
|
+
* declared relationships), so no scanned object is ever left unscoped.
|
|
1159
|
+
*
|
|
1160
|
+
* Fail-closed: if the provider throws for an object, the whole query is
|
|
1161
|
+
* rejected rather than emitting SQL with that object unscoped.
|
|
1162
|
+
*/
|
|
1163
|
+
async resolveReadScopes(query, context) {
|
|
1164
|
+
const map = /* @__PURE__ */ new Map();
|
|
1165
|
+
const provider = this.readScopeProvider;
|
|
1166
|
+
if (!provider || !query.cube) return map;
|
|
1167
|
+
const cube = this.cubeRegistry.get(query.cube);
|
|
1168
|
+
if (!cube) return map;
|
|
1169
|
+
const objects = /* @__PURE__ */ new Set();
|
|
1170
|
+
if (typeof cube.sql === "string" && cube.sql.trim()) {
|
|
1171
|
+
objects.add(cube.sql.trim());
|
|
1172
|
+
}
|
|
1173
|
+
const joins = cube.joins;
|
|
1174
|
+
if (joins) {
|
|
1175
|
+
for (const [alias, j] of Object.entries(joins)) {
|
|
1176
|
+
objects.add(j?.name ?? alias);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
for (const object of objects) {
|
|
1180
|
+
let filter;
|
|
1181
|
+
try {
|
|
1182
|
+
filter = await provider(object, context);
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
this.logger.error?.(
|
|
1185
|
+
`[Analytics] read-scope resolution failed for object "${object}" \u2014 rejecting query (fail-closed, ADR-0021 D-C)`,
|
|
1186
|
+
e instanceof Error ? e : new Error(String(e))
|
|
1187
|
+
);
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`[Analytics] read-scope resolution failed for "${object}"; query denied (fail-closed).`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (filter != null) map.set(object, filter);
|
|
1193
|
+
}
|
|
1194
|
+
return map;
|
|
1195
|
+
}
|
|
1149
1196
|
/**
|
|
1150
1197
|
* Execute an analytical query by delegating to the first capable strategy.
|
|
1151
1198
|
*/
|
|
@@ -1154,7 +1201,7 @@ var AnalyticsService = class {
|
|
|
1154
1201
|
throw new Error("Cube name is required in analytics query");
|
|
1155
1202
|
}
|
|
1156
1203
|
this.ensureCube(query);
|
|
1157
|
-
const ctx = this.callCtx(context);
|
|
1204
|
+
const ctx = await this.callCtx(query, context);
|
|
1158
1205
|
const strategy = this.resolveStrategy(query, ctx);
|
|
1159
1206
|
this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
|
|
1160
1207
|
return strategy.execute(query, ctx);
|
|
@@ -1209,7 +1256,7 @@ var AnalyticsService = class {
|
|
|
1209
1256
|
throw new Error("Cube name is required for SQL generation");
|
|
1210
1257
|
}
|
|
1211
1258
|
this.ensureCube(query);
|
|
1212
|
-
const ctx = this.callCtx(context);
|
|
1259
|
+
const ctx = await this.callCtx(query, context);
|
|
1213
1260
|
const strategy = this.resolveStrategy(query, ctx);
|
|
1214
1261
|
this.logger.debug(`[Analytics] generateSql on cube "${query.cube}" \u2192 ${strategy.name}`);
|
|
1215
1262
|
return strategy.generateSql(query, ctx);
|