@objectstack/service-analytics 17.1.0 → 17.3.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/CHANGELOG.md +796 -0
- package/README.md +5 -3
- package/dist/index.cjs +465 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +203 -20
- package/dist/index.d.ts +203 -20
- package/dist/index.js +473 -37
- package/dist/index.js.map +1 -1
- package/package.json +16 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,801 @@
|
|
|
1
1
|
# Changelog — @objectstack/service-analytics
|
|
2
2
|
|
|
3
|
+
## 17.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 74cee59: Resolve `{current_user_id}` (and every other filter placeholder) on the direct analytics query path, at parity with the list path and the dashboard dataset path.
|
|
8
|
+
|
|
9
|
+
What changes for an app author: a widget or report whose filter says `owner: '{current_user_id}'` used to render `0` for every viewer whenever the query reached the SQL strategy — the literal text was bound into the `WHERE` and matched no row, silently. Now the same filter expression means the same thing on every surface: `AnalyticsService.query` and `generateSql` expand `where`, `timeDimensions[].dateRange`, and a registered dataset's own filter / measure filters against the requesting user before any strategy compiles, so each viewer gets their own rows. A placeholder that cannot be resolved — an unknown spelling, or `{current_user_id}` on an unauthenticated request — now refuses loudly with `FILTER_TOKEN_UNKNOWN` / `FILTER_TOKEN_UNRESOLVED` (HTTP 400) instead of charting a plausible zero.
|
|
10
|
+
|
|
11
|
+
This also closes a gap on the dashboard dataset door: the dataset-scope channel used to hand strategies the registry's unresolved filter copy, which was ANDed in beside the resolved one (`owner = $viewer AND owner = '{current_user_id}'`) and selected nothing.
|
|
12
|
+
- 399ecad: `ObjectQLStrategy` now refuses a cross-object leaf in a compiled measure's own `filter`, on both of its doors, instead of sending it to an engine that cannot join (#11461). This is the third producer of a predicate on that path — after the caller's `where` and the dataset's definition-level `filter` (#10861) — and the one `filterMemberView` did not fold in: #10413 phase 2 lowers `measureFilters[m]` onto that measure's `aggregations[].filter` entry (#10576), and the envelope check enumerated only two origins while its `query.measures` arm read each measure's resolved *field* and never its filter.
|
|
13
|
+
|
|
14
|
+
Measured on one fixture before the change, both doors in one run: a measure declaring `filter: { 'account.region': 'West' }` on a cube with `include: ['account']` was ACCEPTED, `engine.aggregate` received `{field:"*",method:"count",alias:"west_count",filter:{"account.region":"West"}}`, and an honest evaluator answered `west_count: 0` where the truthful answer was `2` — beside a correct `total_count: 3`, so the wrong number came back wearing the same response shape as the right one. The `/analytics/sql` echo rendered `COUNT(CASE WHEN account.region = $1 THEN 1 END)` over a `FROM` carrying no join at all. Both doors now answer `INVALID_FIELD`/400 before the engine is reached, naming the offending field, the dataset, and — the locator neither sibling refusal has — the measure whose declaration holds the leaf.
|
|
15
|
+
|
|
16
|
+
Ordinary per-measure filters are unaffected and still reach the engine carrying their own `aggregations[].filter`, and a cross-object filter declared on a measure a query does not ask for changes nothing: only the measures in `query.measures` are judged, which is exactly the set both doors lower. The same definition remains valid on a native-SQL driver, which the refusal says.
|
|
17
|
+
- d5b330d: feat(spec,analytics): `AnalyticsResult.fields[].builtinAggregate` — a closed discriminator for a measure column whose display name is the server's built-in default (#14492)
|
|
18
|
+
|
|
19
|
+
**What a consumer sees.** `queryDataset()` (and `POST /api/v1/analytics/dataset/query`,
|
|
20
|
+
which relays the result verbatim) now carries an optional
|
|
21
|
+
`fields[].builtinAggregate?: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'`
|
|
22
|
+
on a measure column. It is present exactly when the dataset measure behind the
|
|
23
|
+
column declares an `aggregate` and **no** `label` — the producer then has nothing
|
|
24
|
+
but the aggregate to name the column by, so it says which aggregate that is. It is
|
|
25
|
+
absent whenever the author declared a label (a plain string or an inline locale
|
|
26
|
+
map, even one with no entry for the request locale: an author's text is never
|
|
27
|
+
re-labelled by a consumer), and absent on dimension columns and derived measures.
|
|
28
|
+
The vocabulary is `AggregationFunction` (`data/query.zod.ts`), the one closed
|
|
29
|
+
aggregate enum — no second spelling. `AnalyticsResultResponseSchema`
|
|
30
|
+
(`api/analytics.zod.ts`) mirrors the member, refusing a spelling outside the enum.
|
|
31
|
+
|
|
32
|
+
**Why.** An AI-built dashboard's "count of customers by status" chart showed the
|
|
33
|
+
English axis title "Count" on a Chinese UI. The renderer (objectui
|
|
34
|
+
`buildChartSeries()` / `labelOf()`) treats `fields[].label` as resolved author
|
|
35
|
+
content and passes it through verbatim — correctly, since a real custom label
|
|
36
|
+
("Tasks") must survive. What it could not tell apart was an author's text from
|
|
37
|
+
the server's built-in default for a bare `count`. Guessing from the label text
|
|
38
|
+
was refused (it would catch an author who really named a field `Count`, and break
|
|
39
|
+
the moment the default is spelled in another language); translating on the
|
|
40
|
+
server was not taken (it copies the front end's language decision into the
|
|
41
|
+
producer and leaves nothing for a per-widget override). The ruling (2026-09-02,
|
|
42
|
+
option B) is a structured discriminator on the contract: the consumer prefers a
|
|
43
|
+
locale lookup keyed by `builtinAggregate` — mirroring its existing
|
|
44
|
+
`report.aggregate.*` keys — and falls back to `label`, then `name`.
|
|
45
|
+
|
|
46
|
+
**Producer-side changes.**
|
|
47
|
+
|
|
48
|
+
- `@objectstack/service-analytics` — `queryDataset`'s measure enrichment sets
|
|
49
|
+
`builtinAggregate` from the dataset measure's own `aggregate` when the measure
|
|
50
|
+
has no authored `label`. Judged on the authored key, never on the resolved
|
|
51
|
+
string.
|
|
52
|
+
- `@objectstack/spec` — the `dataset` create seed (`metadata-create-seeds.ts`)
|
|
53
|
+
drops its hardcoded `label: 'Count'` from the seeded `count` measure, so a
|
|
54
|
+
dataset created from Studio is a built-in default (wire: `builtinAggregate:
|
|
55
|
+
'count'`) instead of an authored English literal. `getMeta()` for such a
|
|
56
|
+
dataset now titles the metric by its name (`count`) rather than `Count`;
|
|
57
|
+
`CubeMeta.measures[].type` already carried the aggregate there.
|
|
58
|
+
|
|
59
|
+
Purely additive: no key is removed or renamed, no authorable schema changes shape,
|
|
60
|
+
and a consumer that ignores the member sees exactly the response it saw before.
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- c8be110: refactor(service-analytics): derive the analytics auto-bridge's engine view from the declared contracts (#11833)
|
|
65
|
+
|
|
66
|
+
`plugin.ts` named the data engine through a consumer-local structural
|
|
67
|
+
`DataEngineLike` — the second of the two sites #11833 records, after the
|
|
68
|
+
datasource half that landed as PR #12011. It is now derived from the declared
|
|
69
|
+
contracts: `IDataEngine.aggregate` / `execute?` /
|
|
70
|
+
`resolveEffectiveDatasource?` / `getDriverForObject?` and
|
|
71
|
+
`IObjectQLEngine.getObject`. Optionality is preserved exactly — `aggregate`
|
|
72
|
+
required, everything else `Partial<>` — because these probes are the plugin's
|
|
73
|
+
graceful-degradation seam.
|
|
74
|
+
|
|
75
|
+
**Why this is `patch` and not a type-only no-op.** Four of the five members
|
|
76
|
+
substitute with no behaviour change. The fifth does not: the deleted structural
|
|
77
|
+
type declared `aggregations[].function` as `string`, while the contract
|
|
78
|
+
declares the six-value `AggregationFunction`. The bridge therefore forwarded
|
|
79
|
+
whatever method string reached it. That forward is now parsed with the spec's
|
|
80
|
+
own enum, so a method the engine contract does not declare is refused at the
|
|
81
|
+
bridge — loudly, naming the aggregation and the legal vocabulary — instead of
|
|
82
|
+
reaching the engine, where `driver-sql` blamed a `function` key the author
|
|
83
|
+
never wrote and the in-memory evaluator answered `null` for every bucket under
|
|
84
|
+
the author's own measure name.
|
|
85
|
+
|
|
86
|
+
No authored analytics can trigger the new refusal: the one reachable producer
|
|
87
|
+
of a non-aggregate method — a custom-SQL measure (`AggregationMetricType`
|
|
88
|
+
`number` / `string` / `boolean`) — is already refused earlier, caller-facing,
|
|
89
|
+
by `ObjectQLStrategy.resolveMeasureAggregation` (#12209). What is left is host
|
|
90
|
+
drift (a cube object registered without meeting `CubeSchema`), which is why
|
|
91
|
+
the new refusal is a bare `Error` in the undeclared-500 tier rather than an
|
|
92
|
+
ADR-0112 400 that would blame the caller for something they did not write.
|
|
93
|
+
- aa16721: fix(service-analytics): the consumer-local `executeAggregate` config mirrors narrow `aggregations[].method` to `AggregationFunction` (#12940)
|
|
94
|
+
|
|
95
|
+
#12776 narrowed the contract — `StrategyContext.executeAggregate`'s
|
|
96
|
+
`aggregations[].method` went from `string` to the six-value
|
|
97
|
+
`AggregationFunction` — but this package's two CONSUMER-LOCAL config mirrors
|
|
98
|
+
of that same slot kept declaring `string`, so the compile-time vocabulary the
|
|
99
|
+
narrowing bought for strategy authors stopped at the package boundary and
|
|
100
|
+
never reached the people who write a custom bridge.
|
|
101
|
+
|
|
102
|
+
FROM → TO, at all three sites the tree carries (the card enumerated two):
|
|
103
|
+
|
|
104
|
+
- `AnalyticsServicePluginOptions.executeAggregate` (`plugin.ts`) —
|
|
105
|
+
`aggregations[].method: string` → `AggregationFunction`. This is the
|
|
106
|
+
declaration an app author's own `executeAggregate` bridge is typed against.
|
|
107
|
+
- `AnalyticsServiceConfig.executeAggregate` (`analytics-service.ts`) — the
|
|
108
|
+
same narrowing on the config twin whose own comment says it is kept in
|
|
109
|
+
lockstep with `StrategyContext.executeAggregate`; that claim is true again,
|
|
110
|
+
and now names the member so the next drift is visible.
|
|
111
|
+
- `parseEngineAggregateFunction`'s `method` parameter (`plugin.ts`), the
|
|
112
|
+
auto-bridge's runtime parse — narrowed for the same reason: it was the
|
|
113
|
+
third place a reader was told this vocabulary is open.
|
|
114
|
+
|
|
115
|
+
Who breaks at compile time on upgrade: CALLERS that fill `method` with a
|
|
116
|
+
value typed `string` (or a literal outside the six) when invoking one of
|
|
117
|
+
these bridges — the values the bridge already refused at runtime (#11833).
|
|
118
|
+
IMPLEMENTORS are source-compatible: a handler that accepts `method: string`
|
|
119
|
+
accepts a superset and stays assignable to the narrowed member (parameter
|
|
120
|
+
contravariance), which is why the ~nine test doubles in this package that
|
|
121
|
+
declare their own `{ field, method: string, alias }` mirrors still compile
|
|
122
|
+
untouched.
|
|
123
|
+
|
|
124
|
+
No runtime change. The auto-bridge's runtime parse-and-refuse (#11833) stays
|
|
125
|
+
exactly where it was — with both ends of the `method` → `function` rename now
|
|
126
|
+
declaring the same enum, it is defence in depth behind a compile-time check
|
|
127
|
+
rather than the only check, and the two comments that explained it by
|
|
128
|
+
pointing at the old `method: string` declaration say so instead.
|
|
129
|
+
- e7191ce: fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
|
|
130
|
+
|
|
131
|
+
**Published-surface change, zero runtime change.** No emitted byte moves; what
|
|
132
|
+
moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
|
|
133
|
+
(decision batch #3, verbatim 「同意」) chose declaring the files over deleting
|
|
134
|
+
them.
|
|
135
|
+
|
|
136
|
+
## What was wrong
|
|
137
|
+
|
|
138
|
+
These 28 packages are `"type": "module"` and dual-built, and each spelled one
|
|
139
|
+
`types` condition as a **sibling** of `import`/`require`:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
"exports": { ".": {
|
|
143
|
+
"types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
|
|
144
|
+
} }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A sibling `types` answers for **both** conditions, so a CommonJS consumer was
|
|
148
|
+
handed `dist/index.d.ts` — an ES-module declaration, because the package is
|
|
149
|
+
`"type": "module"` — for an entry point it reaches with `require`. Measured with
|
|
150
|
+
`tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
|
|
151
|
+
node16`:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
error TS1479: The current file is a CommonJS module whose imports will produce
|
|
155
|
+
'require' calls; however, the referenced file is an ECMAScript module and cannot
|
|
156
|
+
be imported with 'require'.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
|
|
160
|
+
has asserted that for months). It is the **types** that told the consumer the
|
|
161
|
+
supported `require` entry point could not be required. The `dist/index.d.cts`
|
|
162
|
+
twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
|
|
163
|
+
no condition at all and shipped in every tarball unreachable.
|
|
164
|
+
|
|
165
|
+
## What changed
|
|
166
|
+
|
|
167
|
+
Each condition now names its own declaration, the shape TypeScript documents:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
"exports": { ".": {
|
|
171
|
+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
|
|
172
|
+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
|
|
173
|
+
} }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
33 entry points across 27 packages, subpaths included. The root `types` field is
|
|
177
|
+
untouched, so `node10` resolvers are unaffected; the `import` condition resolves
|
|
178
|
+
exactly what it resolved before, measured as an unchanged control in the same
|
|
179
|
+
run.
|
|
180
|
+
|
|
181
|
+
## `@objectstack/core` is deliberately NOT changed
|
|
182
|
+
|
|
183
|
+
Splitting a declaration in two makes TypeScript compare it nominally, and
|
|
184
|
+
`ObjectKernel` carries a `private plugins` member that reaches every plugin
|
|
185
|
+
through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
|
|
186
|
+
fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
|
|
187
|
+
declarations of a private property 'plugins'"); with core held back and the
|
|
188
|
+
other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
|
|
189
|
+
its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
|
|
190
|
+
`check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
|
|
191
|
+
types, not about an exports map.
|
|
192
|
+
|
|
193
|
+
## For consumers
|
|
194
|
+
|
|
195
|
+
- **ESM consumers: nothing changes.** Same declaration file, byte for byte.
|
|
196
|
+
- **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
|
|
197
|
+
declarations they get are the ones built for CommonJS.
|
|
198
|
+
- **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
|
|
199
|
+
read `exports`.
|
|
200
|
+
- Nothing is removed: every path that resolved before still resolves.
|
|
201
|
+
|
|
202
|
+
Packages that are CJS-first (`require` → `./dist/index.js`, no `"type": "module"`)
|
|
203
|
+
were already correct and are untouched — their `dist/index.d.ts` really is the
|
|
204
|
+
CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
|
|
205
|
+
`import` condition) is a separate, larger population and is filed separately per
|
|
206
|
+
the ruling, not fixed here.
|
|
207
|
+
|
|
208
|
+
`check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
|
|
209
|
+
shape, so the drift cannot return silently.
|
|
210
|
+
- 017130a: The ObjectQL analytics strategy now refuses a custom-SQL measure (`AggregationMetricType` `number` / `string` / `boolean`) with a loud `400 INVALID_FIELD` naming the measure and its metric type, instead of forwarding the raw SQL expression into `engine.aggregate` — where `driver-sql` rejected it blaming a `function` key the author never wrote, and the in-memory evaluator silently answered `null` for every bucket under the measure's own name.
|
|
211
|
+
|
|
212
|
+
What stops being served, and for whom: on deployments whose driver has no native SQL capability (the ObjectQL aggregate path — e.g. Mongo or in-memory), a query or dataset widget selecting a custom-SQL measure now answers a 400 that says to use an aggregate measure (count/sum/avg/min/max/count_distinct) or run the cube on a native-SQL driver. Those queries previously "succeeded" with a per-bucket `null` (or a mis-attributed driver error), never with a correct number. Native-SQL driver behaviour is unchanged: custom-SQL measures still run there, emitted verbatim.
|
|
213
|
+
- 466b389: **Fix:** `/api/v1/analytics/query` on the ObjectQL door (MongoDB, the memory driver, or any deployment whose driver reports `objectqlAggregate` but not `nativeSql`) now honours a measure's own scoped `filter` — `won_count` and `won_amount`-style conditional measures answer the same numbers the dashboard door and the native-SQL door already did (#10413 phase 2).
|
|
214
|
+
|
|
215
|
+
`ObjectQLStrategy.execute` lowers each measure's `filter` into the ONE aggregation it belongs to, via the per-aggregation `filter` field #10576 added to `engine.aggregate`'s contract (SQL `FILTER (WHERE …)` semantics) — not into the whole-call filter, which would have narrowed every measure (a fix shaped that way would make a conditional measure right while making every unconditional sibling measure in the same query wrong). An aggregation with no measure filter is unchanged and keeps the native-pushdown-eligible shape.
|
|
216
|
+
|
|
217
|
+
`ObjectQLStrategy.generateSql` (the `/analytics/sql` echo) renders the same conditional aggregate — `COUNT(CASE WHEN … THEN … END)`-style — so the preview stays an honest description of what `execute()` now actually runs, matching the native-SQL strategy's existing echo for the same class of measure.
|
|
218
|
+
|
|
219
|
+
Phase 1 (PR #10758) already ANDed a dataset's definition-level `filter` into the whole-call filter on this door; this closes the remaining half of the two-door disagreement #10413 reported. `NativeSQLStrategy` (#10298 / PR #10411) is unaffected by this change.
|
|
220
|
+
- b0d7d54: `ObjectQLStrategy` now refuses a read scope that does not bind, before handing it to the engine (`READ_SCOPE_COMPILE_FAILED` / 500). That strategy merges `StrategyContext.getReadScope` output straight into the `FilterCondition` it gives `engine.aggregate` and never reaches `compileScopedFilterToSql`, so the empty-`$nin` refusal that compiler gained guarded the NativeSQL path and the `/analytics/sql` echo only. Measured against a real engine, a non-RLS scope provider handing `{ f: { $nin: [] } }`, `{ $not: { f: { $in: [] } } }`, `{ $not: { f: [] } }` or `{ $not: { f: { $in: [], $ne: 'x' } } }` received the WHOLE TABLE on any query this strategy served; all four are now refused, at both engine-bound merges (the aggregate filter and the FK→attribute resolution). Deliberately unchanged: `$in: []` keeps its ruled constant-FALSE fold, so the RLS compiler's live composite — an emptied membership `$or`-ed beside an own-rows grant — still admits exactly the own rows; and the NativeSQL path and the SQL echo keep the disposition they already had.
|
|
221
|
+
- 967402a: The analytics read-scope compiler (`read-scope-sql.ts`) now refuses an empty `$nin` (`READ_SCOPE_COMPILE_FAILED` / 500) instead of folding it to constant TRUE. An emptied exclusion ("NOT IN () excludes nothing") vacated the whole read scope — every row admitted — on the ADR-0021 lowering, where a widening is scope over-reach; no in-repo producer can emit the shape (the CEL lowering never emits `$nin`, and the RLS guard drops even-polarity empty-`$nin` policies upstream), so the refusal costs no live traffic. Deliberately asymmetric: `$in: []` keeps its ruled constant-FALSE fold (#5322/#5243), which the RLS compiler's inert positive composite — an emptied membership `$or`-ed beside an own-rows grant — depends on.
|
|
222
|
+
- 5c7cbe3: Refuse a non-binding (vacating) read scope at the two remaining `getReadScope` merge sites: the `/analytics/sql` echo (`ObjectQLStrategy.generateSql`) and `NativeSQLStrategy.applyReadScope`. The `$not`-over-`$in: []` family compiled to a constant-TRUE predicate on those routes, so the echo rendered — and the native strategy actually executed — a whole-table `WHERE` for a scope the ObjectQL execution path already refused (#13640). All three faces now answer one verdict, in the same `READ_SCOPE_COMPILE_FAILED` / 500 envelope; the ruled `$in: []` zero-rows reduction, the live RLS empty-membership composite, and `compileScopedFilterToSql` itself (the ruled #13571 residue included) are unchanged.
|
|
223
|
+
- a3c4215: fix(service-analytics): wire the `typecheck` script so turbo stops silently no-opping the gate, and clear the 10 type errors it was hiding (#12939)
|
|
224
|
+
|
|
225
|
+
`packages/services/service-analytics/package.json` declared only `build` and
|
|
226
|
+
`test`. Root `typecheck` is `turbo run typecheck`, which **no-ops a package
|
|
227
|
+
that has no such script and reports success** — so no tsc read this package's
|
|
228
|
+
`src/` from the typecheck lane at all. `build` is tsup (esbuild; the DTS pass
|
|
229
|
+
processes declarations only) and `test` is vitest (esbuild transform), and
|
|
230
|
+
neither type-checks. The package was reached only by the `check:type-check-debt`
|
|
231
|
+
ratchet, which asserts the error count does not *grow* — never that it is zero.
|
|
232
|
+
|
|
233
|
+
Adding the one-line script (mirroring its sibling `service-settings`, repaired
|
|
234
|
+
the same way in #7925) makes the task real. The tests are already inside the
|
|
235
|
+
program — the package `tsconfig.json` includes `src` and the tests live in
|
|
236
|
+
`src/__tests__/**` — so `tsc --noEmit --listFiles` lists **83 of the 83**
|
|
237
|
+
`*.test.ts` files on disk. The new gate reads the tests, not just the source.
|
|
238
|
+
|
|
239
|
+
All 10 errors were stale tests, not source defects; no non-test source file
|
|
240
|
+
changed. Nothing was silenced: no `any` added, no `@ts-expect-error`, no
|
|
241
|
+
`@ts-nocheck`, `strict` untouched, and the tsconfig `include`/`exclude` are
|
|
242
|
+
byte-identical — excluding the tests would have converted a missing gate into
|
|
243
|
+
a lying one.
|
|
244
|
+
|
|
245
|
+
- `__tests__/measure-source-field-gate.test.ts` (7 x TS2339). `promise.catch(fn)`
|
|
246
|
+
does not drop the resolved branch from the type, so
|
|
247
|
+
`service.query(...).catch((e) => e as Error)` was `AnalyticsResult | Error`
|
|
248
|
+
and every `err.message` / `err.field` / `err.member` / `err.param` read was a
|
|
249
|
+
property access on `AnalyticsResult`. A local `refusalOf()` helper narrows it
|
|
250
|
+
once via `then<never, Refusal>`; as a bonus the resolved branch now fails by
|
|
251
|
+
name instead of surfacing later as `expect(undefined).toMatch(...)`.
|
|
252
|
+
- `__tests__/objectql-timedimension-projection.test.ts` (2 x TS7053). The
|
|
253
|
+
`TABLE` fixture was inferred as `{ id: number; due_date: string; priority:
|
|
254
|
+
string }[]` and the aggregate stand-in indexes it by a computed `string` key.
|
|
255
|
+
Annotated as the `Row` (`Record<string, unknown>`) the file already declares.
|
|
256
|
+
- `__tests__/analytics-service.test.ts` (1 x TS6133). An unused
|
|
257
|
+
`AnalyticsDriverCapabilities` type import. The capability literals in this
|
|
258
|
+
file are inline `ctx` objects checked contextually at each `canHandle` call
|
|
259
|
+
site, so the import added no coverage and is removed.
|
|
260
|
+
|
|
261
|
+
`service-analytics` graduates out of the `check:type-check-coverage` DEBT
|
|
262
|
+
ledger: 65/78 -> 66/78 workspace packages type-checked, 382 -> 372 frozen raw
|
|
263
|
+
errors, 13 -> 12 ledger entries.
|
|
264
|
+
- d028b37: fix(spec): `StrategyContext.executeAggregate` `aggregations[].method` narrows from `string` to `AggregationFunction` (#12776)
|
|
265
|
+
|
|
266
|
+
<!-- adr-0087: registered strategy-context-aggregation-method-narrowed -->
|
|
267
|
+
|
|
268
|
+
**BREAKING** accept-set narrowing on a published contract, landing after the
|
|
269
|
+
v17.0.0 cut (the lockstep launch-window convention ships it as `minor`).
|
|
270
|
+
|
|
271
|
+
Two spec-declared surfaces described the same slot and disagreed about its
|
|
272
|
+
type: `IDataEngine.aggregate`'s `aggregations[].function` is the closed
|
|
273
|
+
six-value `AggregationFunction` enum, while the analytics strategy contract's
|
|
274
|
+
`StrategyContext.executeAggregate` declared the same value as
|
|
275
|
+
`aggregations[].method: string`. The analytics bridge renames one to the
|
|
276
|
+
other, so nothing on the analytics side of that seam was compile-checked
|
|
277
|
+
against the engine's vocabulary — a strategy author (very often an AI) got
|
|
278
|
+
no compile-time help and hit the bridge's runtime refusal instead.
|
|
279
|
+
|
|
280
|
+
FROM → TO:
|
|
281
|
+
|
|
282
|
+
- `aggregations[].method: string` →
|
|
283
|
+
`aggregations[].method: AggregationFunction`
|
|
284
|
+
(`'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'`, the spec's
|
|
285
|
+
own enum from `@objectstack/spec/data`). One slot, one declaration.
|
|
286
|
+
|
|
287
|
+
Who breaks at compile time on upgrade:
|
|
288
|
+
|
|
289
|
+
- external CALLERS of `StrategyContext.executeAggregate` that fill `method`
|
|
290
|
+
with a value typed `string` (or a literal outside the six) — the values the
|
|
291
|
+
bridge already refused at runtime (#11833) now fail `tsc`.
|
|
292
|
+
- external IMPLEMENTORS of `StrategyContext` stay source-compatible: a
|
|
293
|
+
handler that accepts `method: string` accepts a superset and remains
|
|
294
|
+
assignable to the narrowed member.
|
|
295
|
+
|
|
296
|
+
The bridge's runtime parse-and-refuse (#11833) stays as defence in depth.
|
|
297
|
+
In-repo, `ObjectQLStrategy`'s aggregation locals now carry the enum
|
|
298
|
+
end-to-end (`@objectstack/service-analytics`, runtime behaviour unchanged —
|
|
299
|
+
the census measured every reachable producer already emitting enum-legal
|
|
300
|
+
values only).
|
|
301
|
+
- a40c0f9: Guard the analytics record-label lookup with `assertReadScopeCannotVacate` — the fourth read-scope door
|
|
302
|
+
|
|
303
|
+
`AnalyticsServicePlugin`'s `fetchRecordLabels` hook `$and`s the **referenced** object's read scope with an `id $in [...]` filter and hands the result straight to `executeAggregate`. Unlike the three faces unified previously (the ObjectQL engine merge, the `/analytics/sql` echo merge, and `NativeSQLStrategy.applyReadScope`), it met neither `compileScopedFilterToSql` nor the vacancy guard, so a read scope that lowers to a boolean constant — the `$not`-over-`$in: []` family reachable from any out-of-repo `StrategyContext.getReadScope` producer — let that per-record read run effectively unscoped for the ids in hand, surfacing the display names the referenced object's RLS exists to hide.
|
|
304
|
+
|
|
305
|
+
The hook now calls the already-exported `assertReadScopeCannotVacate` on the referenced object's scope before composing the filter, refusing in the same envelope as its siblings (`READ_SCOPE_COMPILE_FAILED` / 500). No behaviour changes for scopes that bind: an ordinary referenced-object scope still narrows the label lookup, and the `$in: []` zero-rows reduction (including the live RLS composite that pairs it with an own-rows grant) still passes through untouched. The read-scope SQL compiler is unchanged.
|
|
306
|
+
- Updated dependencies [809d417]
|
|
307
|
+
- Updated dependencies [387e231]
|
|
308
|
+
- Updated dependencies [f794e4e]
|
|
309
|
+
- Updated dependencies [cae2169]
|
|
310
|
+
- Updated dependencies [b812a54]
|
|
311
|
+
- Updated dependencies [2d4fa75]
|
|
312
|
+
- Updated dependencies [0e4e51b]
|
|
313
|
+
- Updated dependencies [e84bbf6]
|
|
314
|
+
- Updated dependencies [effae80]
|
|
315
|
+
- Updated dependencies [efb3513]
|
|
316
|
+
- Updated dependencies [d62f990]
|
|
317
|
+
- Updated dependencies [c45d8e6]
|
|
318
|
+
- Updated dependencies [2e3e8c7]
|
|
319
|
+
- Updated dependencies [e621291]
|
|
320
|
+
- Updated dependencies [655b106]
|
|
321
|
+
- Updated dependencies [40a93b5]
|
|
322
|
+
- Updated dependencies [101ad2c]
|
|
323
|
+
- Updated dependencies [d5b330d]
|
|
324
|
+
- Updated dependencies [dda969c]
|
|
325
|
+
- Updated dependencies [1f45690]
|
|
326
|
+
- Updated dependencies [277948f]
|
|
327
|
+
- Updated dependencies [8bdd955]
|
|
328
|
+
- Updated dependencies [f3bbbef]
|
|
329
|
+
- Updated dependencies [4f24e9d]
|
|
330
|
+
- Updated dependencies [e27583e]
|
|
331
|
+
- Updated dependencies [4bd6faa]
|
|
332
|
+
- Updated dependencies [86cbe37]
|
|
333
|
+
- Updated dependencies [6a180e4]
|
|
334
|
+
- Updated dependencies [474242f]
|
|
335
|
+
- Updated dependencies [63cd487]
|
|
336
|
+
- Updated dependencies [bd4aa4e]
|
|
337
|
+
- Updated dependencies [803eaab]
|
|
338
|
+
- Updated dependencies [f8e8f03]
|
|
339
|
+
- Updated dependencies [983edf1]
|
|
340
|
+
- Updated dependencies [eae824e]
|
|
341
|
+
- Updated dependencies [f6fa22c]
|
|
342
|
+
- Updated dependencies [8a483b3]
|
|
343
|
+
- Updated dependencies [97bcd99]
|
|
344
|
+
- Updated dependencies [df59de0]
|
|
345
|
+
- Updated dependencies [96e25a8]
|
|
346
|
+
- Updated dependencies [f75a38a]
|
|
347
|
+
- Updated dependencies [7a25e7d]
|
|
348
|
+
- Updated dependencies [1fa05a6]
|
|
349
|
+
- Updated dependencies [c85a265]
|
|
350
|
+
- Updated dependencies [dcb10a5]
|
|
351
|
+
- Updated dependencies [773a999]
|
|
352
|
+
- Updated dependencies [35dffea]
|
|
353
|
+
- Updated dependencies [d8024f0]
|
|
354
|
+
- Updated dependencies [8120808]
|
|
355
|
+
- Updated dependencies [776a098]
|
|
356
|
+
- Updated dependencies [5060877]
|
|
357
|
+
- Updated dependencies [4f6325d]
|
|
358
|
+
- Updated dependencies [52954c0]
|
|
359
|
+
- Updated dependencies [2aa8456]
|
|
360
|
+
- Updated dependencies [93809a3]
|
|
361
|
+
- Updated dependencies [7c0d0c3]
|
|
362
|
+
- Updated dependencies [daae7aa]
|
|
363
|
+
- Updated dependencies [8dc22d6]
|
|
364
|
+
- Updated dependencies [279431e]
|
|
365
|
+
- Updated dependencies [948dd6b]
|
|
366
|
+
- Updated dependencies [3b4c56c]
|
|
367
|
+
- Updated dependencies [ae8edd2]
|
|
368
|
+
- Updated dependencies [e25403c]
|
|
369
|
+
- Updated dependencies [a81aa9d]
|
|
370
|
+
- Updated dependencies [64baa68]
|
|
371
|
+
- Updated dependencies [9fa70d7]
|
|
372
|
+
- Updated dependencies [09db64a]
|
|
373
|
+
- Updated dependencies [92916e7]
|
|
374
|
+
- Updated dependencies [a84f3ea]
|
|
375
|
+
- Updated dependencies [f2eaae8]
|
|
376
|
+
- Updated dependencies [56c093c]
|
|
377
|
+
- Updated dependencies [c09451b]
|
|
378
|
+
- Updated dependencies [ba64877]
|
|
379
|
+
- Updated dependencies [7345308]
|
|
380
|
+
- Updated dependencies [79b6a22]
|
|
381
|
+
- Updated dependencies [30d96ab]
|
|
382
|
+
- Updated dependencies [f658793]
|
|
383
|
+
- Updated dependencies [c95ad19]
|
|
384
|
+
- Updated dependencies [e58ea8b]
|
|
385
|
+
- Updated dependencies [4a17645]
|
|
386
|
+
- Updated dependencies [3795c5f]
|
|
387
|
+
- Updated dependencies [8ab926b]
|
|
388
|
+
- Updated dependencies [7317cf2]
|
|
389
|
+
- Updated dependencies [e25e839]
|
|
390
|
+
- Updated dependencies [5997207]
|
|
391
|
+
- Updated dependencies [8b13cc8]
|
|
392
|
+
- Updated dependencies [4a4a35d]
|
|
393
|
+
- Updated dependencies [86e765a]
|
|
394
|
+
- Updated dependencies [1d7e76a]
|
|
395
|
+
- Updated dependencies [53dc739]
|
|
396
|
+
- Updated dependencies [fd289be]
|
|
397
|
+
- Updated dependencies [03bf7b1]
|
|
398
|
+
- Updated dependencies [f90e820]
|
|
399
|
+
- Updated dependencies [18d816a]
|
|
400
|
+
- Updated dependencies [e8bd715]
|
|
401
|
+
- Updated dependencies [b91c351]
|
|
402
|
+
- Updated dependencies [a28a3c0]
|
|
403
|
+
- Updated dependencies [daeaaf9]
|
|
404
|
+
- Updated dependencies [c459da6]
|
|
405
|
+
- Updated dependencies [e914733]
|
|
406
|
+
- Updated dependencies [f887e52]
|
|
407
|
+
- Updated dependencies [881f8d8]
|
|
408
|
+
- Updated dependencies [3bfa1e6]
|
|
409
|
+
- Updated dependencies [0a8ebf3]
|
|
410
|
+
- Updated dependencies [901355c]
|
|
411
|
+
- Updated dependencies [34ce8e7]
|
|
412
|
+
- Updated dependencies [33681ea]
|
|
413
|
+
- Updated dependencies [bfe13c8]
|
|
414
|
+
- Updated dependencies [0fb3044]
|
|
415
|
+
- Updated dependencies [4635f3e]
|
|
416
|
+
- Updated dependencies [fd289be]
|
|
417
|
+
- Updated dependencies [ee3595c]
|
|
418
|
+
- Updated dependencies [b2eab95]
|
|
419
|
+
- Updated dependencies [93940d4]
|
|
420
|
+
- Updated dependencies [3a04b01]
|
|
421
|
+
- Updated dependencies [45b9051]
|
|
422
|
+
- Updated dependencies [b9e9227]
|
|
423
|
+
- Updated dependencies [d395692]
|
|
424
|
+
- Updated dependencies [5894d30]
|
|
425
|
+
- Updated dependencies [a3765f6]
|
|
426
|
+
- Updated dependencies [2d5cee3]
|
|
427
|
+
- Updated dependencies [e22158f]
|
|
428
|
+
- Updated dependencies [7404925]
|
|
429
|
+
- Updated dependencies [0c2334f]
|
|
430
|
+
- Updated dependencies [778c59f]
|
|
431
|
+
- Updated dependencies [d2619fd]
|
|
432
|
+
- Updated dependencies [af56546]
|
|
433
|
+
- Updated dependencies [6acb11a]
|
|
434
|
+
- Updated dependencies [33c5fd3]
|
|
435
|
+
- Updated dependencies [20b0fdb]
|
|
436
|
+
- Updated dependencies [905019b]
|
|
437
|
+
- Updated dependencies [a286411]
|
|
438
|
+
- Updated dependencies [98c0d33]
|
|
439
|
+
- Updated dependencies [368a82e]
|
|
440
|
+
- Updated dependencies [a3d5724]
|
|
441
|
+
- Updated dependencies [93ea19b]
|
|
442
|
+
- Updated dependencies [9ee2dcf]
|
|
443
|
+
- Updated dependencies [8cb96ec]
|
|
444
|
+
- Updated dependencies [8f10a79]
|
|
445
|
+
- Updated dependencies [6269a55]
|
|
446
|
+
- Updated dependencies [a17da05]
|
|
447
|
+
- Updated dependencies [a8c00e2]
|
|
448
|
+
- Updated dependencies [22e5236]
|
|
449
|
+
- Updated dependencies [0fb8760]
|
|
450
|
+
- Updated dependencies [e5ce2ed]
|
|
451
|
+
- Updated dependencies [be21955]
|
|
452
|
+
- Updated dependencies [bc56e18]
|
|
453
|
+
- Updated dependencies [be21955]
|
|
454
|
+
- Updated dependencies [a9ee989]
|
|
455
|
+
- Updated dependencies [4d0d944]
|
|
456
|
+
- Updated dependencies [15d58db]
|
|
457
|
+
- Updated dependencies [d63b014]
|
|
458
|
+
- Updated dependencies [9abe4e4]
|
|
459
|
+
- Updated dependencies [2cc7122]
|
|
460
|
+
- Updated dependencies [50d6c92]
|
|
461
|
+
- Updated dependencies [9e0ba21]
|
|
462
|
+
- Updated dependencies [311433f]
|
|
463
|
+
- Updated dependencies [3e5ad08]
|
|
464
|
+
- Updated dependencies [9abe4e4]
|
|
465
|
+
- Updated dependencies [b7131f3]
|
|
466
|
+
- Updated dependencies [e5812fa]
|
|
467
|
+
- Updated dependencies [7085f90]
|
|
468
|
+
- Updated dependencies [dee4dd4]
|
|
469
|
+
- Updated dependencies [ce7e497]
|
|
470
|
+
- Updated dependencies [51ecb2f]
|
|
471
|
+
- Updated dependencies [9086761]
|
|
472
|
+
- Updated dependencies [42a117b]
|
|
473
|
+
- Updated dependencies [1401ae7]
|
|
474
|
+
- Updated dependencies [4297fe7]
|
|
475
|
+
- Updated dependencies [e398863]
|
|
476
|
+
- Updated dependencies [d16df74]
|
|
477
|
+
- Updated dependencies [f11fc61]
|
|
478
|
+
- Updated dependencies [e808890]
|
|
479
|
+
- Updated dependencies [8f79379]
|
|
480
|
+
- Updated dependencies [e6ca40e]
|
|
481
|
+
- Updated dependencies [0c77ea4]
|
|
482
|
+
- Updated dependencies [52954c0]
|
|
483
|
+
- Updated dependencies [89eb997]
|
|
484
|
+
- Updated dependencies [7131f12]
|
|
485
|
+
- Updated dependencies [aa5994e]
|
|
486
|
+
- Updated dependencies [be93457]
|
|
487
|
+
- Updated dependencies [a65db76]
|
|
488
|
+
- Updated dependencies [2cf5a96]
|
|
489
|
+
- Updated dependencies [15eb2c9]
|
|
490
|
+
- Updated dependencies [5691b07]
|
|
491
|
+
- Updated dependencies [2a6122b]
|
|
492
|
+
- Updated dependencies [225e769]
|
|
493
|
+
- Updated dependencies [8af88dd]
|
|
494
|
+
- Updated dependencies [fb5fbb8]
|
|
495
|
+
- Updated dependencies [d7b3963]
|
|
496
|
+
- Updated dependencies [33184fd]
|
|
497
|
+
- Updated dependencies [7c41693]
|
|
498
|
+
- Updated dependencies [b72db01]
|
|
499
|
+
- Updated dependencies [dce5cd4]
|
|
500
|
+
- Updated dependencies [9688f58]
|
|
501
|
+
- Updated dependencies [556ebc1]
|
|
502
|
+
- Updated dependencies [177ebdc]
|
|
503
|
+
- Updated dependencies [8d237b4]
|
|
504
|
+
- Updated dependencies [2d2e6f0]
|
|
505
|
+
- Updated dependencies [2d8dd8d]
|
|
506
|
+
- Updated dependencies [22d573e]
|
|
507
|
+
- Updated dependencies [b5a2398]
|
|
508
|
+
- Updated dependencies [348860c]
|
|
509
|
+
- Updated dependencies [5383fa6]
|
|
510
|
+
- Updated dependencies [5b3ff63]
|
|
511
|
+
- Updated dependencies [1a6a19c]
|
|
512
|
+
- Updated dependencies [527e050]
|
|
513
|
+
- Updated dependencies [dd33bf9]
|
|
514
|
+
- Updated dependencies [4cb2a90]
|
|
515
|
+
- Updated dependencies [74a7804]
|
|
516
|
+
- Updated dependencies [53d3689]
|
|
517
|
+
- Updated dependencies [b3a63d3]
|
|
518
|
+
- Updated dependencies [49f0dcf]
|
|
519
|
+
- Updated dependencies [033a34c]
|
|
520
|
+
- Updated dependencies [4d25d22]
|
|
521
|
+
- Updated dependencies [1ffee51]
|
|
522
|
+
- Updated dependencies [5ae4303]
|
|
523
|
+
- Updated dependencies [ece4dad]
|
|
524
|
+
- Updated dependencies [e9b377e]
|
|
525
|
+
- Updated dependencies [146f448]
|
|
526
|
+
- Updated dependencies [735f5c7]
|
|
527
|
+
- Updated dependencies [a7e18de]
|
|
528
|
+
- Updated dependencies [366f895]
|
|
529
|
+
- Updated dependencies [dc75ba8]
|
|
530
|
+
- Updated dependencies [cce0aa9]
|
|
531
|
+
- Updated dependencies [e764507]
|
|
532
|
+
- Updated dependencies [cff17af]
|
|
533
|
+
- Updated dependencies [39404f3]
|
|
534
|
+
- Updated dependencies [ca1965f]
|
|
535
|
+
- Updated dependencies [8619f95]
|
|
536
|
+
- Updated dependencies [b706af9]
|
|
537
|
+
- Updated dependencies [db8c288]
|
|
538
|
+
- Updated dependencies [0e5fe7f]
|
|
539
|
+
- Updated dependencies [add4360]
|
|
540
|
+
- Updated dependencies [fc9ba76]
|
|
541
|
+
- Updated dependencies [0f94cc7]
|
|
542
|
+
- Updated dependencies [a11c1a5]
|
|
543
|
+
- Updated dependencies [71f9cd1]
|
|
544
|
+
- Updated dependencies [ee17d86]
|
|
545
|
+
- Updated dependencies [cdbd920]
|
|
546
|
+
- Updated dependencies [18c432e]
|
|
547
|
+
- Updated dependencies [3c418c4]
|
|
548
|
+
- Updated dependencies [fa8715a]
|
|
549
|
+
- Updated dependencies [a933ed7]
|
|
550
|
+
- Updated dependencies [b3ca463]
|
|
551
|
+
- Updated dependencies [a933ed7]
|
|
552
|
+
- Updated dependencies [0d4a6a8]
|
|
553
|
+
- Updated dependencies [518d5e5]
|
|
554
|
+
- Updated dependencies [6643ba1]
|
|
555
|
+
- Updated dependencies [eeba2ef]
|
|
556
|
+
- Updated dependencies [ec4c4d2]
|
|
557
|
+
- Updated dependencies [424f73c]
|
|
558
|
+
- Updated dependencies [cccbe51]
|
|
559
|
+
- Updated dependencies [a8d6b1d]
|
|
560
|
+
- Updated dependencies [e4a7695]
|
|
561
|
+
- Updated dependencies [87075b1]
|
|
562
|
+
- Updated dependencies [fc58a99]
|
|
563
|
+
- Updated dependencies [14cfc00]
|
|
564
|
+
- Updated dependencies [1c6f7b4]
|
|
565
|
+
- Updated dependencies [e854a53]
|
|
566
|
+
- Updated dependencies [dfebfc8]
|
|
567
|
+
- Updated dependencies [d028b37]
|
|
568
|
+
- Updated dependencies [f7b25c5]
|
|
569
|
+
- Updated dependencies [122ef38]
|
|
570
|
+
- Updated dependencies [4a37870]
|
|
571
|
+
- Updated dependencies [428f9b2]
|
|
572
|
+
- Updated dependencies [aa7ff56]
|
|
573
|
+
- Updated dependencies [c41b42e]
|
|
574
|
+
- Updated dependencies [c4db311]
|
|
575
|
+
- Updated dependencies [750fff5]
|
|
576
|
+
- Updated dependencies [c19035e]
|
|
577
|
+
- Updated dependencies [ececf7a]
|
|
578
|
+
- Updated dependencies [d173125]
|
|
579
|
+
- Updated dependencies [8eeca27]
|
|
580
|
+
- Updated dependencies [8425c17]
|
|
581
|
+
- Updated dependencies [a5ef1d8]
|
|
582
|
+
- Updated dependencies [87ad30c]
|
|
583
|
+
- Updated dependencies [772d5de]
|
|
584
|
+
- Updated dependencies [ce80ec2]
|
|
585
|
+
- Updated dependencies [b372318]
|
|
586
|
+
- Updated dependencies [97a2263]
|
|
587
|
+
- Updated dependencies [29d0676]
|
|
588
|
+
- Updated dependencies [0169d49]
|
|
589
|
+
- Updated dependencies [6bd3231]
|
|
590
|
+
- Updated dependencies [d2b5ba8]
|
|
591
|
+
- Updated dependencies [b799ac5]
|
|
592
|
+
- Updated dependencies [8f74307]
|
|
593
|
+
- Updated dependencies [d23dc08]
|
|
594
|
+
- Updated dependencies [644ad50]
|
|
595
|
+
- Updated dependencies [9735662]
|
|
596
|
+
- Updated dependencies [4d5b4f8]
|
|
597
|
+
- Updated dependencies [0da7cd2]
|
|
598
|
+
- Updated dependencies [28a5c3e]
|
|
599
|
+
- Updated dependencies [4bc18e5]
|
|
600
|
+
- @objectstack/spec@17.3.0
|
|
601
|
+
- @objectstack/core@17.3.0
|
|
602
|
+
- @objectstack/types@17.3.0
|
|
603
|
+
|
|
604
|
+
## 17.2.0
|
|
605
|
+
|
|
606
|
+
### Minor Changes
|
|
607
|
+
|
|
608
|
+
- 57e4571: **BREAKING**: `/analytics/query` now refuses a cross-object filter nested inside a
|
|
609
|
+
combinator on the ObjectQL path, instead of silently answering the wrong number
|
|
610
|
+
(#10759).
|
|
611
|
+
|
|
612
|
+
`ObjectQLStrategy` runs one cross-object envelope check, from two call sites.
|
|
613
|
+
`generateSql()` (the `/analytics/sql` preview) asked it about every member the
|
|
614
|
+
`where` touches, flattened out of the filter tree. `execute()` asked it about the
|
|
615
|
+
built engine filter — where an AND-ed leaf sits at the top level and is seen, but
|
|
616
|
+
anything structural (an `$or`, a `$not`, a nested `$and` that cannot merge) has
|
|
617
|
+
been folded into `filter.$and`, so the only key readable for it was the literal
|
|
618
|
+
`$and`, which is never a field name.
|
|
619
|
+
|
|
620
|
+
One query therefore got two answers, measured over one fixture in one run:
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
where: { $or: [{ 'account.region': 'West' }, { stage: 'won' }] }
|
|
624
|
+
|
|
625
|
+
before /analytics/sql 400 INVALID_FIELD cross-object filter "account.region"
|
|
626
|
+
/analytics/query 200, rows
|
|
627
|
+
after both 400 INVALID_FIELD cross-object filter "account.region"
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
`engine.aggregate` cannot join. The half that returned rows was not answering the
|
|
631
|
+
cross-object query: the disjunct naming a column the base object does not have
|
|
632
|
+
can never match, so the query silently collapsed to its remaining branches and
|
|
633
|
+
reported a narrower figure as if it were the answer. Both call sites now derive
|
|
634
|
+
the member list from one shared view, so the invariant the strategy already
|
|
635
|
+
stated for itself — the preview accepts and rejects the same set the execution
|
|
636
|
+
door does — holds by construction rather than by two call sites agreeing.
|
|
637
|
+
|
|
638
|
+
Who is affected: a deployment whose driver reports `objectqlAggregate` but not
|
|
639
|
+
`nativeSql` (Mongo, the memory driver), running an analytics query that puts a
|
|
640
|
+
related object's field inside `$or` or `$not`. Such a query now returns
|
|
641
|
+
`400 INVALID_FIELD` naming the member. The refusal already existed and already
|
|
642
|
+
had these words; what changed is that the execution door reaches it too. Nothing
|
|
643
|
+
an author writes in metadata changes, no stored shape is affected, and queries
|
|
644
|
+
whose combinators name only base-object fields are untouched — that set is pinned
|
|
645
|
+
in `crossobject-conjunct-refusal.test.ts` alongside the new refusal, because a
|
|
646
|
+
fix that refused every combinator would have looked identical from the refusal
|
|
647
|
+
side alone.
|
|
648
|
+
|
|
649
|
+
The remedy for an affected query is the one the error message has always carried:
|
|
650
|
+
run it on a native-SQL driver, which can join, or drop the cross-object member
|
|
651
|
+
from the filter.
|
|
652
|
+
|
|
653
|
+
<!-- adr-0087: not-required (no-migration-prescription) A runtime query-shape refusal on /analytics/query, not a metadata surface: no authorable key, export or config field is removed or renamed, so `objectstack migrate meta` has nothing to rewrite and an upgrader has no stored shape to convert. The affected input is an ad-hoc request body, and the error itself names the member and the two ways out. -->
|
|
654
|
+
- 13a3dca: **BREAKING**: on the ObjectQL path, a compiled dataset whose definition-level
|
|
655
|
+
`filter` is itself cross-object is now refused by both analytics doors instead
|
|
656
|
+
of reaching `engine.aggregate` with a predicate it cannot join (#10861).
|
|
657
|
+
|
|
658
|
+
PR #10758 gave the dataset's own definition-level `filter` a route onto this
|
|
659
|
+
door for the first time. That route was outside the member view the cross-object
|
|
660
|
+
envelope check judges, so nothing ever saw it:
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
dataset: object 'opportunity', include: ['account'],
|
|
664
|
+
filter: { 'account.region': 'West' }
|
|
665
|
+
|
|
666
|
+
before /analytics/query 200, rows -> engine.aggregate received
|
|
667
|
+
{"$and":[{"account.region":"West"}]}
|
|
668
|
+
/analytics/sql 200, SQL
|
|
669
|
+
after both 400 INVALID_FIELD, member "account.region",
|
|
670
|
+
cube "<dataset>"; the engine is never reached
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
`engine.aggregate` cannot join. `account.region` is not a column of
|
|
674
|
+
`opportunity`, so on any driver that evaluates the predicate honestly it matches
|
|
675
|
+
nothing, and the widget answered a number that was neither the scoped number nor
|
|
676
|
+
the unscoped one — with no error anywhere. That is the silent mis-bucket #3654's
|
|
677
|
+
loud refusal exists to prevent, arriving through a producer #3654 predates.
|
|
678
|
+
|
|
679
|
+
**Breaking, and argued rather than assumed.** A query that returns `200` with
|
|
680
|
+
rows today starts answering `400`, on a *saved* dataset rather than on anything
|
|
681
|
+
in the request — a dashboard that renders today can start showing an error. That
|
|
682
|
+
is the strongest reading of "breaking" and it is why this is called out here
|
|
683
|
+
rather than filed as a quiet fix. What is *not* lost is any correct answer: the
|
|
684
|
+
rows that stop being served were already wrong, and wrong in the way that hides
|
|
685
|
+
itself. The refusal names the member, names the dataset, and says the same
|
|
686
|
+
definition is valid on a native-SQL deployment, so the operator has somewhere to
|
|
687
|
+
go; the previous behaviour gave them a plausible number and nothing to notice.
|
|
688
|
+
Rejecting the dataset at compile time in `dataset-compiler.ts` was considered and
|
|
689
|
+
not taken (maintainer ruling, 2026-08-22): the compiler cannot see which driver
|
|
690
|
+
will serve the dataset, and the same definition is legal on a native-SQL one.
|
|
691
|
+
|
|
692
|
+
Who is affected: a deployment whose driver reports `objectqlAggregate` but not
|
|
693
|
+
`nativeSql` (Mongo, the memory driver), serving a dataset whose definition-level
|
|
694
|
+
`filter` names a field on a related object. Nothing an author writes changes
|
|
695
|
+
shape, no stored document is rewritten, and an **ordinary** dataset scope
|
|
696
|
+
(`filter: { is_deleted: false }`) still passes both doors and still reaches the
|
|
697
|
+
engine carrying its predicate — that direction is pinned one character away from
|
|
698
|
+
the new refusal in `crossobject-conjunct-refusal.test.ts`, because an
|
|
699
|
+
implementation that refused *every* dataset scope would look identical from the
|
|
700
|
+
refusal side alone and would break every scoped dataset shipping today.
|
|
701
|
+
|
|
702
|
+
<!-- adr-0087: not-required (no-migration-prescription) No authorable surface is
|
|
703
|
+
retired, renamed or re-shaped: `DatasetSchema`'s `filter` key stays exactly as it
|
|
704
|
+
is, every stored dataset document stays valid as written, and the very same
|
|
705
|
+
document remains correct on a native-SQL deployment. There is therefore nothing
|
|
706
|
+
`objectstack migrate meta` could rewrite — a mechanical rewrite would have to
|
|
707
|
+
know which driver will serve the dataset, which is precisely the capability the
|
|
708
|
+
2026-08-22 ruling records as invisible to the compile-time placement. This is a
|
|
709
|
+
query-time refusal on one driver family, not a surface retirement, so the ledger
|
|
710
|
+
has no entry to carry and the upgrade guide has no prescription to print. -->
|
|
711
|
+
|
|
712
|
+
### Patch Changes
|
|
713
|
+
|
|
714
|
+
- 7bf3fb7: Point every documentation link in these packages' published READMEs — and in
|
|
715
|
+
the project `create-objectstack` scaffolds — at the canonical docs origin
|
|
716
|
+
`https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling.
|
|
717
|
+
|
|
718
|
+
Both spellings reach the same pages (the alias redirects to the apex,
|
|
719
|
+
path-preserving), so no link was broken. The reason it needs a release rather
|
|
720
|
+
than an in-repo fix alone: a README ships inside the npm tarball, so the
|
|
721
|
+
version already on npm keeps showing the old host to every reader of the
|
|
722
|
+
package page until a new one is published.
|
|
723
|
+
- 112a8c6: Apply a dataset's definition-level `filter` on the ObjectQL analytics path
|
|
724
|
+
(#10413, phase 1). `/api/v1/analytics/query` served by a driver that reports
|
|
725
|
+
`objectqlAggregate` but not `nativeSql` (MongoDB, the memory driver) reached
|
|
726
|
+
`engine.aggregate` with no `filter` key at all: the dataset's own scope — a
|
|
727
|
+
`filter: { is_deleted: false }` on the dataset definition — was dropped, so
|
|
728
|
+
every measure aggregated the whole table while the dashboard door, on the same
|
|
729
|
+
cube and the same measure names, answered the scoped numbers. The scope is now
|
|
730
|
+
ANDed into the strategy's whole-call filter (never merged key-by-key, so a
|
|
731
|
+
caller's own `where` and the time windows cannot be overwritten by it), and the
|
|
732
|
+
representative SQL echo renders it too.
|
|
733
|
+
|
|
734
|
+
Per-MEASURE `filter`s on this path are still not applied: an
|
|
735
|
+
`engine.aggregate` aggregation is `{ field, method, alias }` and cannot carry a
|
|
736
|
+
predicate of its own. Widening that contract is #10576; lowering the measure
|
|
737
|
+
filters into it is phase 2 of #10413. The native-SQL path already applies both
|
|
738
|
+
(#10298).
|
|
739
|
+
- 6439f8b: Analytics measures are now compiled from everything they declare — `aggregate`, `field` **and** `filter` — on both the dashboard path and `POST /api/v1/analytics/query`.
|
|
740
|
+
|
|
741
|
+
**Reported figures change, and the new ones are the declared ones.** Two corrections, both of which move numbers a dashboard or an API consumer is already reading:
|
|
742
|
+
|
|
743
|
+
- A measure written `{ aggregate: 'count', field: 'some_column' }` used to compile to `COUNT(*)` and count **rows**. It now compiles to `COUNT("some_column")` and counts **non-null values**. Any such measure will report the same number as before or a **smaller** one, and a rate built on top of it (a numerator over a total) will drop accordingly — a "100%" tile whose column was mostly empty was reading its own denominator.
|
|
744
|
+
- `POST /api/v1/analytics/query` used to drop every per-measure `filter`, and the dataset's definition-level `filter` with it, returning unfiltered aggregates under the author's measure names. It now applies both, so the endpoint answers what the dashboard already answered for the same cube. Figures pulled through the API — agent tools, exports, downstream reports — will move to the filtered values; a measure declaring `filter: { stage: 'closed_won' }` stops counting every row.
|
|
745
|
+
|
|
746
|
+
Measures that declare no `field` still compile to `COUNT(*)`, and a cube that is not a compiled dataset (an inferred or manifest cube) emits byte-for-byte the statement it did before. Measure filters lower to portable `CASE WHEN` conditional aggregates rather than `FILTER (WHERE …)`, which MySQL does not have.
|
|
747
|
+
|
|
748
|
+
If a saved figure or a screenshot disagrees with what the platform now reports, the new number is the one the metadata declares.
|
|
749
|
+
- Updated dependencies [6936d07]
|
|
750
|
+
- Updated dependencies [59eb04d]
|
|
751
|
+
- Updated dependencies [9f05b7d]
|
|
752
|
+
- Updated dependencies [3b2af5e]
|
|
753
|
+
- Updated dependencies [7d2d112]
|
|
754
|
+
- Updated dependencies [5fa0d72]
|
|
755
|
+
- Updated dependencies [02b3b07]
|
|
756
|
+
- Updated dependencies [46d34ab]
|
|
757
|
+
- Updated dependencies [914c413]
|
|
758
|
+
- Updated dependencies [55809a0]
|
|
759
|
+
- Updated dependencies [ee2ff45]
|
|
760
|
+
- Updated dependencies [47cd3ec]
|
|
761
|
+
- Updated dependencies [52db1d1]
|
|
762
|
+
- Updated dependencies [5649efb]
|
|
763
|
+
- Updated dependencies [9d7d2de]
|
|
764
|
+
- Updated dependencies [c815c50]
|
|
765
|
+
- Updated dependencies [795ea05]
|
|
766
|
+
- Updated dependencies [2306a76]
|
|
767
|
+
- Updated dependencies [e5ea701]
|
|
768
|
+
- Updated dependencies [a40dcc1]
|
|
769
|
+
- Updated dependencies [def0d3e]
|
|
770
|
+
- Updated dependencies [8d0bb79]
|
|
771
|
+
- Updated dependencies [5acb58d]
|
|
772
|
+
- Updated dependencies [2e3cf95]
|
|
773
|
+
- Updated dependencies [4c93387]
|
|
774
|
+
- Updated dependencies [504c8d5]
|
|
775
|
+
- Updated dependencies [a037f7c]
|
|
776
|
+
- Updated dependencies [3ee8ddf]
|
|
777
|
+
- Updated dependencies [16cef97]
|
|
778
|
+
- Updated dependencies [a79bd35]
|
|
779
|
+
- Updated dependencies [6ceaa4b]
|
|
780
|
+
- Updated dependencies [15ea214]
|
|
781
|
+
- Updated dependencies [de19489]
|
|
782
|
+
- Updated dependencies [c684d00]
|
|
783
|
+
- Updated dependencies [923c424]
|
|
784
|
+
- Updated dependencies [1ec36b7]
|
|
785
|
+
- Updated dependencies [5f2e54c]
|
|
786
|
+
- Updated dependencies [189373b]
|
|
787
|
+
- Updated dependencies [35ad101]
|
|
788
|
+
- Updated dependencies [ceb33a9]
|
|
789
|
+
- Updated dependencies [73d9795]
|
|
790
|
+
- Updated dependencies [8012960]
|
|
791
|
+
- Updated dependencies [f34f56b]
|
|
792
|
+
- Updated dependencies [f399618]
|
|
793
|
+
- Updated dependencies [75e9301]
|
|
794
|
+
- Updated dependencies [2810695]
|
|
795
|
+
- @objectstack/spec@17.2.0
|
|
796
|
+
- @objectstack/core@17.2.0
|
|
797
|
+
- @objectstack/types@17.2.0
|
|
798
|
+
|
|
3
799
|
## 17.1.0
|
|
4
800
|
|
|
5
801
|
### Patch Changes
|