|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/service-analytics": minor |
| 4 | +"@objectstack/verify": minor |
| 5 | +--- |
| 6 | + |
| 7 | +feat(analytics): the executeAggregate bridge carries ExecutionContext — ADR-0021 D-C second belt |
| 8 | + |
| 9 | +The analytics→engine bridge now forwards the request's `ExecutionContext` to |
| 10 | +`engine.aggregate`, so the engine's own middleware chain scopes analytics reads |
| 11 | +independently of the analytics layer's `getReadScope`. |
| 12 | + |
| 13 | +**Why.** `BaseEngineOptions.context` has always been `.optional()`, so nothing |
| 14 | +forced the bridge to pass it — and it did not. An authenticated aggregate |
| 15 | +reached the engine with no principal, plugin-security's principal-less fall-open |
| 16 | +skipped its RLS injection, and the only thing left scoping the query was the |
| 17 | +strategy remembering to call `getReadScope`. #3597 was a strategy that did not, |
| 18 | +and both belts were off at once. |
| 19 | + |
| 20 | +`getReadScope` stays: the two resolve scope through different paths (engine |
| 21 | +middleware vs `security.getReadFilter`), and a deployment without |
| 22 | +plugin-security has only the analytics layer. This is depth, not a replacement. |
| 23 | + |
| 24 | +- `StrategyContext` gains `context?: ExecutionContext`, bound per call by |
| 25 | + `AnalyticsService` from `query()` / `generateSql()` / `queryDataset()`. |
| 26 | +- `StrategyContext.executeAggregate` and the `AnalyticsServicePlugin` / |
| 27 | + `AnalyticsService` `executeAggregate` config options gain `context?: |
| 28 | + ExecutionContext`. **Custom bridges should forward it** to their engine; the |
| 29 | + built-in auto-bridge does. Purely additive — an existing bridge that ignores |
| 30 | + it keeps working exactly as before. |
| 31 | +- `DimensionLabelDeps.fetchRecordLabels` and `resolveDimensionLabels` each gain |
| 32 | + an optional trailing `context`, beside the `scope` / `resolveScope` that |
| 33 | + #3639 added — the same two-belt split as the aggregate path. |
| 34 | +- `BootOptions.analytics` (`@objectstack/verify`) overrides the |
| 35 | + AnalyticsServicePlugin instance, so a gate can boot with the analytics belt |
| 36 | + off and assert the engine-side belt alone still scopes. |
| 37 | + |
| 38 | +**Also fixed on the same seam:** |
| 39 | + |
| 40 | +- `fetchRecordLabels` — the dimension display-label lookup — is row-granular |
| 41 | + (one row per record, real display names). #3639 gave it the analytics-layer |
| 42 | + belt (the referenced object's own read scope); it now also carries the |
| 43 | + context, so the engine scopes the same read independently. |
| 44 | +- `ObjectQLStrategy.generateSql` emitted no `WHERE` at all, so the |
| 45 | + `/analytics/sql` preview read as an unscoped table scan while the real |
| 46 | + aggregate was scoped. It now renders the caller's filters and the read scope. |
| 47 | + The preview never executed, so this was misleading output rather than a leak. |
0 commit comments