@objectstack/service-analytics 17.0.0-rc.0 → 17.0.0-rc.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/CHANGELOG.md +2656 -0
- package/dist/index.cjs +414 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +406 -124
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,2656 @@
|
|
|
1
|
+
# Changelog — @objectstack/service-analytics
|
|
2
|
+
|
|
3
|
+
## 17.0.0-rc.1
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 99ffc04: fix(analytics)!: a measure emits what it declares, instead of `COUNT(*)` (#4157)
|
|
8
|
+
|
|
9
|
+
`NativeSQLStrategy.resolveMeasureSql` answered `COUNT(*)` to three different
|
|
10
|
+
questions it could not otherwise answer — each time aliased under the name the
|
|
11
|
+
caller asked for, so the result looked like an answer:
|
|
12
|
+
|
|
13
|
+
1. **A measure the cube does not declare.** `lookupMember`'s synthetic
|
|
14
|
+
relation fallback is dimension-only, so any undeclared or mistyped measure
|
|
15
|
+
name landed here. `measures: ['revenue']` against a cube without it returned
|
|
16
|
+
`COUNT(*) AS "revenue"` — a row count presented as revenue.
|
|
17
|
+
2. **A `number`/`string`/`boolean` metric.** `AggregationMetricType` documents
|
|
18
|
+
these as _"Custom SQL expression returning a number / string / boolean"_: the
|
|
19
|
+
measure's `sql` **is** the computation — a ratio, a `CASE`, a window
|
|
20
|
+
function. The expression was discarded and replaced by a row count.
|
|
21
|
+
3. **An unrecognised `type`.** Same silent substitution.
|
|
22
|
+
|
|
23
|
+
Now: an undeclared measure and an unrecognised type **throw**, naming the
|
|
24
|
+
declared measures and both accepted vocabularies respectively; a custom-
|
|
25
|
+
expression type emits its expression unwrapped. The six aggregates are
|
|
26
|
+
unchanged.
|
|
27
|
+
|
|
28
|
+
**A dot no longer implies a relationship hop.** `qualifyAndRegisterJoin` split
|
|
29
|
+
any dotted string into a join chain, so the expression `SUM(account.amount)`
|
|
30
|
+
became `"SUM(account"."amount)"` _plus_ a `LEFT JOIN "SUM(account"` — invalid
|
|
31
|
+
SQL naming a table that does not exist. Harmless only while the result was
|
|
32
|
+
being thrown away for `COUNT(*)`; emitting the expression makes it matter. A
|
|
33
|
+
dotted string is now treated as a path only when every segment is a bare
|
|
34
|
+
identifier, so `account.amount` still lowers to a qualified column and a join,
|
|
35
|
+
and an expression is emitted as written. That also fixes the same mangling for
|
|
36
|
+
an _aggregate_ measure whose `sql` is an expression — `type: 'sum'` with
|
|
37
|
+
`sql: 'SUM(account.amount)'` was producing the same garbage.
|
|
38
|
+
|
|
39
|
+
**Breaking, narrowly.** Two inputs that used to produce SQL now raise: a query
|
|
40
|
+
naming an undeclared measure, and a cube measure with a type outside
|
|
41
|
+
`AggregationMetricType`. Both were returning a wrong number rather than data,
|
|
42
|
+
so nothing correct can depend on them — but a caller that was silently getting
|
|
43
|
+
row counts will now see an error, which is the point. This is the trade #3948
|
|
44
|
+
settled for the drivers.
|
|
45
|
+
|
|
46
|
+
Datasets are unaffected: `aggregateToMetricType` only ever emits an
|
|
47
|
+
`AggregationFunction` member, so a compiled dataset never had a
|
|
48
|
+
custom-expression measure or an unknown type. The reachable path is a
|
|
49
|
+
hand-authored Cube.
|
|
50
|
+
|
|
51
|
+
`metric-type-coverage.test.ts` asserts the aggregate and expression sets
|
|
52
|
+
_partition_ `AggregationMetricType`, so a tenth metric type fails a test rather
|
|
53
|
+
than reaching the throw. Both sets are named, not derived as each other's
|
|
54
|
+
complement — deriving would classify a new _aggregate_ as an expression and emit
|
|
55
|
+
a bare column, a different silent wrong answer.
|
|
56
|
+
|
|
57
|
+
Verified: **460 tests across 35 files** green, including the four suites that
|
|
58
|
+
assert `COUNT(*)` — all of them use a _declared_ `type: 'count'` metric, so none
|
|
59
|
+
relied on a fallback. The 14 new tests were confirmed to fail against the old
|
|
60
|
+
behaviour (6 of 10 in the behaviour suite) before the fix.
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- b4be309: fix(analytics): a new spec aggregate can no longer silently return a row count
|
|
65
|
+
|
|
66
|
+
Track C item 4 of objectstack-ai/objectui#2945 — _"`AggregationFunction`: three
|
|
67
|
+
places in lockstep"_. They agreed only by coincidence, and the failure mode when
|
|
68
|
+
they stopped agreeing was silent wrong numbers.
|
|
69
|
+
|
|
70
|
+
The three:
|
|
71
|
+
|
|
72
|
+
1. `AggregationFunction` (`@objectstack/spec/data`) — eight members, what an
|
|
73
|
+
author may declare as a dataset measure's `aggregate`.
|
|
74
|
+
2. `UNSUPPORTED_AGGREGATES` (`dataset-compiler.ts`) — `array_agg`/`string_agg`,
|
|
75
|
+
rejected at compile time with a clear error.
|
|
76
|
+
3. The aggregate `switch` in `native-sql-strategy.ts` — six cases, then
|
|
77
|
+
`default: return 'COUNT(*)'`.
|
|
78
|
+
|
|
79
|
+
8 − 2 = 6 = the six cases, today. Add a ninth member to the spec — `median`,
|
|
80
|
+
`percentile`, anything — and it would:
|
|
81
|
+
|
|
82
|
+
- pass the compiler's gate, since it is not in `UNSUPPORTED_AGGREGATES`;
|
|
83
|
+
- be **advertised as supported** by that gate's error message, which listed
|
|
84
|
+
`count, sum, avg, min, max, count_distinct` as hand-written prose — a third
|
|
85
|
+
copy of the vocabulary;
|
|
86
|
+
- reach the strategy's `switch`, match no case, and fall to
|
|
87
|
+
`default: COUNT(*)`.
|
|
88
|
+
|
|
89
|
+
The author asks for a median and gets a row count. No error, no log, wrong
|
|
90
|
+
figures on a dashboard — the same silent-wrong-answer shape as the filter
|
|
91
|
+
operators in #3948, in the analytics SQL builder.
|
|
92
|
+
|
|
93
|
+
**The fix is derivation plus a guard, with no behaviour change.** The `switch`
|
|
94
|
+
becomes `AGGREGATE_SQL`, a table whose coverage is assertable; the error
|
|
95
|
+
message's prose list becomes `SUPPORTED_AGGREGATES`, derived as
|
|
96
|
+
`AggregationFunction.options` minus `UNSUPPORTED_AGGREGATES`; and
|
|
97
|
+
`aggregation-lockstep.test.ts` asserts the arithmetic — the lowered set equals
|
|
98
|
+
the admitted set, every spec member is either lowered or explicitly rejected,
|
|
99
|
+
nothing is both, and the rejection list names only aggregates the spec has.
|
|
100
|
+
|
|
101
|
+
Verified by adding a hypothetical `median` to the spec, which now fails three
|
|
102
|
+
assertions naming it, including _"these would fall through to the COUNT(_)
|
|
103
|
+
fallback and return a row count"\*. Before this change the same edit was green.
|
|
104
|
+
|
|
105
|
+
Nothing is narrowed and no SQL changes: the same six aggregates lower to the
|
|
106
|
+
same six expressions, and the `COUNT(*)` fallback still catches everything else.
|
|
107
|
+
|
|
108
|
+
**Reported, not fixed:** that fallback is also reached by a measure whose `type`
|
|
109
|
+
is `number`/`string`/`boolean` — a custom SQL _expression_, per
|
|
110
|
+
`AggregationMetricType` — whose expression is then replaced by a row count.
|
|
111
|
+
Datasets cannot produce one (`aggregateToMetricType` only ever returns an
|
|
112
|
+
`AggregationFunction` member), so it is reachable only from a hand-authored
|
|
113
|
+
Cube. Emitting `col` instead is a behavioural change in an analytics SQL path
|
|
114
|
+
and deserves its own change with its own tests; the strategy's doc comment now
|
|
115
|
+
records it.
|
|
116
|
+
|
|
117
|
+
- 7a55913: fix(service-analytics): a `$between` analytics filter no longer vanishes from the query (ADR-0053 D-A3.1)
|
|
118
|
+
|
|
119
|
+
A dashboard widget or dataset whose filter used `$between` was querying **every
|
|
120
|
+
row**. `normalizeAnalyticsFilters` maps Mongo-style operators onto the internal
|
|
121
|
+
pipeline form, `$between` was missing from that map, and an unmapped operator is
|
|
122
|
+
skipped — so the predicate was silently dropped from the compiled WHERE clause.
|
|
123
|
+
Both strategies read that normalizer, so both the raw-SQL and the ObjectQL
|
|
124
|
+
aggregate paths were affected. The symptom is #3650's: a chart that draws the
|
|
125
|
+
whole dataset instead of the requested window, with nothing in the SQL to
|
|
126
|
+
suggest a filter was ever asked for.
|
|
127
|
+
|
|
128
|
+
`$between [min, max]` now lowers to its two bounds (`gte` + `lte`) instead of
|
|
129
|
+
gaining an operator of its own, so a range's max inherits the calendar-day
|
|
130
|
+
whole-day rule (#3777) from each strategy's existing upper-bound handling —
|
|
131
|
+
`NativeSQLStrategy` compiles a bare-day upper bound half-open itself, and the
|
|
132
|
+
ObjectQL path gets the same rule from the driver — rather than needing a second
|
|
133
|
+
implementation to keep in step. A malformed `$between` (not a two-element
|
|
134
|
+
array) now throws instead of being dropped, matching the stance driver-memory
|
|
135
|
+
took for the same shape in #3948: an unbounded read is exactly the failure this
|
|
136
|
+
prevents, and it is indistinguishable from a legitimately wide query.
|
|
137
|
+
|
|
138
|
+
Found by giving the temporal conformance matrix its missing sixth consumer
|
|
139
|
+
(`native-sql-temporal-conformance.test.ts`), which executes the shared cases
|
|
140
|
+
against a real SQLite engine and asserts row ids — a dropped predicate is
|
|
141
|
+
invisible to the SQL-string assertions the strategy's other suites use.
|
|
142
|
+
|
|
143
|
+
- 7a55913: fix(service-analytics): every authorable filter operator now reaches the query (#4128)
|
|
144
|
+
|
|
145
|
+
Closes the cause behind the `$between` defect rather than just that instance.
|
|
146
|
+
`normalizeAnalyticsFilters` skipped any operator missing from its map, and a
|
|
147
|
+
skipped predicate does not narrow a query — it **widens** it: the compiled SQL
|
|
148
|
+
stays valid and returns rows the author excluded. Four operators from the
|
|
149
|
+
spec's authorable vocabulary sat in that state, plus one that was mapped
|
|
150
|
+
incorrectly.
|
|
151
|
+
|
|
152
|
+
- **`$startsWith` / `$endsWith`** were dropped entirely. Both strategies now
|
|
153
|
+
compile them — anchored `LIKE 'x%'` / `LIKE '%x'` on the raw-SQL path, and
|
|
154
|
+
the canonical `$startsWith` / `$endsWith` operators (which every driver
|
|
155
|
+
implements directly) on the ObjectQL path, so an anchored match does not
|
|
156
|
+
depend on regex dialect.
|
|
157
|
+
- **`$null`** was dropped. It is the shape the console emits for an "is empty"
|
|
158
|
+
/ "is not empty" filter, so such a widget was showing every row. Now compiles
|
|
159
|
+
to `IS NULL` / `IS NOT NULL` per its boolean.
|
|
160
|
+
- **`$exists`** was mapped value-_independently_ to `set`, so `{$exists: false}`
|
|
161
|
+
compiled to `IS NOT NULL` — the exact inverse of what it asks for. It and
|
|
162
|
+
`$null` are now resolved explicitly, because a key→name map cannot express an
|
|
163
|
+
operator whose meaning flips with its value.
|
|
164
|
+
- **`$notContains`** reached the ObjectQL strategy, which had no arm for it and
|
|
165
|
+
fell through to a `default` returning a bare value — compiling "does not
|
|
166
|
+
contain x" as "**equals** x".
|
|
167
|
+
- **Unknown operators now throw** on both surfaces instead of being silently
|
|
168
|
+
dropped (normalizer) or reinterpreted as an equality (ObjectQL strategy). An
|
|
169
|
+
operator outside the vocabulary is a caller error, and a loud one beats a
|
|
170
|
+
silently widened read — the call driver-memory made for the same shape in
|
|
171
|
+
#3948.
|
|
172
|
+
|
|
173
|
+
Still declared as a gap, but no longer a silent one: `$or` / `$not` are skipped,
|
|
174
|
+
since expressing them needs a recursive WHERE builder rather than the flat
|
|
175
|
+
array the strategies consume.
|
|
176
|
+
|
|
177
|
+
Cover is `filter-operator-coverage.test.ts`, which runs the whole vocabulary
|
|
178
|
+
against a real SQLite engine and asserts **row ids** — six of its cases fail
|
|
179
|
+
without this change. A dropped predicate is invisible to the SQL-string
|
|
180
|
+
assertions the strategies' other suites use, which is how these survived.
|
|
181
|
+
|
|
182
|
+
- f5ab1c7: fix(service-analytics): a `$or` / `$not` filter no longer vanishes from an analytics query (#4128 follow-up)
|
|
183
|
+
|
|
184
|
+
The last of the silently-dropped filter family. `normalizeAnalyticsFilters`
|
|
185
|
+
produced a flat **array**, which cannot carry a disjunction, so both strategies
|
|
186
|
+
skipped `$or` and `$not` outright — a widget or dataset whose filter used
|
|
187
|
+
either compiled a WHERE clause that simply did not contain it, and drew every
|
|
188
|
+
row. That is #3650's symptom, and unlike a rejected query it looks like a
|
|
189
|
+
working chart.
|
|
190
|
+
|
|
191
|
+
The normalizer now produces a **tree** (`normalizeAnalyticsFilterTree`), and
|
|
192
|
+
each strategy compiles it the way its own backend expresses a disjunction:
|
|
193
|
+
|
|
194
|
+
- **`NativeSQLStrategy`** builds the WHERE recursively, routing every leaf
|
|
195
|
+
through its existing clause emitter — so the storage-form coercion and the
|
|
196
|
+
calendar-day upper-bound rule (#3777) apply at every depth, including inside
|
|
197
|
+
an `$or`. Parentheses are explicit rather than relying on SQL precedence.
|
|
198
|
+
- **`ObjectQLStrategy`** hands `$or` / `$not` to the engine, which speaks them
|
|
199
|
+
natively. AND-ed leaves still merge per field exactly as before, so a query
|
|
200
|
+
without combinators produces byte-identical engine input.
|
|
201
|
+
- **`/analytics/sql`** renders the same tree, so the echoed statement keeps
|
|
202
|
+
reproducing what executes rather than showing a conjunction where the engine
|
|
203
|
+
runs a disjunction.
|
|
204
|
+
- The **cross-object envelope check** now sees members nested inside an `$or`.
|
|
205
|
+
It rejects cross-object filters, so a member it could not see was a filter it
|
|
206
|
+
could not reject.
|
|
207
|
+
|
|
208
|
+
Empty `$and` / `$or` arrays now throw instead of being ignored, matching the
|
|
209
|
+
fail-closed stance of `read-scope-sql.ts` — the compiler in this same package
|
|
210
|
+
that has always handled the full tree, and whose semantics the tree walker now
|
|
211
|
+
mirrors deliberately.
|
|
212
|
+
|
|
213
|
+
Cover is `native-sql-filter-logic-conformance.test.ts`, which runs the shared
|
|
214
|
+
combinator table (`FILTER_LOGIC_CASES`, #3774) against a real SQLite engine and
|
|
215
|
+
asserts row ids. The analytics raw-SQL path now stands beside `driver-sql`,
|
|
216
|
+
`driver-memory`, `formula` and `read-scope-sql` under that one standard; 14 of
|
|
217
|
+
its 17 cases fail without this change.
|
|
218
|
+
|
|
219
|
+
- 3abd233: fix(analytics): project a `timeDimensions` bucket into the result rows and fields (#4033)
|
|
220
|
+
|
|
221
|
+
An analytics query that buckets by `timeDimensions` alone grouped correctly —
|
|
222
|
+
the echoed SQL read `date_trunc('month', due_date) AS "due_date"` — but the row
|
|
223
|
+
mapper and `buildFieldMeta` both enumerated `query.dimensions` only, so the
|
|
224
|
+
bucket never reached the caller: rows carried just the measures and `fields`
|
|
225
|
+
never mentioned the dimension. A trend chart got N values and no x-axis. The
|
|
226
|
+
same query written with `dimensions: ['due_date']` was unaffected, which is why
|
|
227
|
+
it went unnoticed.
|
|
228
|
+
|
|
229
|
+
Grouping, row mapping and field metadata now derive the projected set from one
|
|
230
|
+
`projectedDimensions()` helper — `dimensions` plus every _granular_
|
|
231
|
+
`timeDimensions` entry not already among them. A `timeDimensions` entry without
|
|
232
|
+
a granularity contributes only its `dateRange` predicate and stays out of the
|
|
233
|
+
projection, so no phantom column is declared.
|
|
234
|
+
|
|
235
|
+
- 0af50a3: fix(driver-sql,service-analytics): a bare-day upper bound covers the whole day on `Field.datetime` (#3777)
|
|
236
|
+
|
|
237
|
+
A bare `YYYY-MM-DD` comparand anchors to midnight UTC. That is right for a
|
|
238
|
+
lower bound and was silently wrong for an upper one: the dashboard date-range
|
|
239
|
+
filter compiles `{ $gte: from, $lte: to }` with bare-day bounds, so on a
|
|
240
|
+
`datetime` column every row created after 00:00 of the `to` day vanished from
|
|
241
|
+
the result — no error, the chart renders, the numbers are just smaller. The
|
|
242
|
+
default configuration hit it: the filter's default field is `created_at`
|
|
243
|
+
(a system-injected `Field.datetime`) and 7 of the 13 presets end "today".
|
|
244
|
+
|
|
245
|
+
The translation is operator-sensitive and half-open, applied at every
|
|
246
|
+
comparison emitter:
|
|
247
|
+
|
|
248
|
+
- `SqlDriver` (and `SqliteWasmDriver` by inheritance): `$lte`/`<=` with a
|
|
249
|
+
bare-day comparand on a `datetime` column compiles to `< next-day-midnight`
|
|
250
|
+
in the column's storage form; `$between [min, max]` with a bare-day max
|
|
251
|
+
decomposes to `>= min AND < next-day(max)`. Both the plain and the
|
|
252
|
+
legacy-repair (mixed-storage) column paths, both `where` spellings.
|
|
253
|
+
- `NativeSQLStrategy`: `dateRange` windows and `lte` filters bind `< next-day`
|
|
254
|
+
instead of an inclusive `BETWEEN`/`<=` when the bound is a bare day.
|
|
255
|
+
- The `/analytics/sql` rendering and the dataset preview evaluator apply the
|
|
256
|
+
same rule, so the echoed SQL and drafted numbers reproduce execution.
|
|
257
|
+
|
|
258
|
+
`@objectstack/core` gains the shared primitive `nextUtcCalendarDay(value)`:
|
|
259
|
+
the next calendar day of a valid bare `YYYY-MM-DD` (else `null` — instants,
|
|
260
|
+
`Date`s and impossible days are never widened).
|
|
261
|
+
|
|
262
|
+
Unchanged on purpose, per the semantics table on #3777: `date`/`time` columns
|
|
263
|
+
(`<= day` is already whole-day-correct there), full-ISO/`Date` comparands
|
|
264
|
+
(instant semantics), and `$gte`/`$gt`/`$lt` (midnight anchoring is correct for
|
|
265
|
+
those). No authored metadata changes: a dashboard's existing
|
|
266
|
+
`{ $gte, $lte }` window now simply includes its final day.
|
|
267
|
+
|
|
268
|
+
- 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
|
|
269
|
+
|
|
270
|
+
The AGENTS.md post-task checklist requires breaking changesets to carry their
|
|
271
|
+
FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
|
|
272
|
+
inside the npm package and is what an upgrading agent greps after the tombstone
|
|
273
|
+
error." That delivery path was severed for 68 of the 69 publishable packages:
|
|
274
|
+
npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
|
|
275
|
+
older npm versions — not `CHANGELOG.md`, and the canonical
|
|
276
|
+
`"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
|
|
277
|
+
10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
|
|
278
|
+
70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
|
|
279
|
+
explicitly.
|
|
280
|
+
|
|
281
|
+
The tombstone-error scenario is precisely the one where the repo is out of
|
|
282
|
+
reach — the upgrading agent has `node_modules` and nothing else — so the
|
|
283
|
+
migration text has to ride in the tarball. Every publishable package now
|
|
284
|
+
declares `CHANGELOG.md` in `files`, and the canonical whitelist is
|
|
285
|
+
`["dist", "README.md", "CHANGELOG.md"]`.
|
|
286
|
+
|
|
287
|
+
The other half is the gate: `check:published-files` gains a fifth invariant,
|
|
288
|
+
COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
|
|
289
|
+
always-required lint job, so the next package cannot silently sever the path
|
|
290
|
+
again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
|
|
291
|
+
into the canonical set.
|
|
292
|
+
|
|
293
|
+
Consumer-visible change: one more file per install (the package's changelog,
|
|
294
|
+
e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
|
|
295
|
+
node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
|
|
296
|
+
promised.
|
|
297
|
+
|
|
298
|
+
- c8124e5: fix(driver-sql): give `Field.datetime` one UTC storage form per dialect (#3912, #3942)
|
|
299
|
+
|
|
300
|
+
Any window filter on a `Field.datetime` column returned an empty set on SQLite —
|
|
301
|
+
a dashboard `dateRange: last_30_days` on `created_date` read 0 while 29 matching
|
|
302
|
+
rows existed.
|
|
303
|
+
|
|
304
|
+
There was never a storage _convention_, only a description of what better-sqlite3
|
|
305
|
+
happened to do with a bound JS `Date`. Nothing enforced it — `formatInput`
|
|
306
|
+
deliberately left `datetime` untouched — so the form was decided by whichever
|
|
307
|
+
writer got there first: a JS `Date` landed as INTEGER epoch ms, while a REST/JSON
|
|
308
|
+
write (JSON has no `Date` type), a `defaultValue: 'NOW()'` slot, and the
|
|
309
|
+
platform's own `created_at` / `updated_at` all landed as ISO **TEXT**. One column
|
|
310
|
+
held both forms while the read path coerced comparands to epoch ms purely from
|
|
311
|
+
the _declared_ type. On SQLite's type ordering (`INTEGER < TEXT`) a two-sided
|
|
312
|
+
window collapsed to zero rows, and a one-sided `>=` matched every TEXT row
|
|
313
|
+
regardless of the bound.
|
|
314
|
+
|
|
315
|
+
`Field.datetime` now has one canonical instant per dialect, produced by one
|
|
316
|
+
function applied on write **and** to every filter comparand, so the two sides of
|
|
317
|
+
a comparison cannot disagree about shape:
|
|
318
|
+
|
|
319
|
+
- **SQLite** — `YYYY-MM-DDTHH:MM:SS.sssZ` text. Lexicographic order _is_
|
|
320
|
+
chronological order, so range filters and `ORDER BY` read the column directly
|
|
321
|
+
and can use an index; `strftime` parses it, so the date-bucket expression needs
|
|
322
|
+
no CASE.
|
|
323
|
+
- **Postgres** — `timestamptz`, unchanged. The fix here is on the write and
|
|
324
|
+
comparand side: a zone-naive write was previously resolved against the
|
|
325
|
+
_server's_ timezone (measured 8 hours off on `Asia/Shanghai`), and an
|
|
326
|
+
un-anchored `YYYY-MM-DD` comparand meant the server's local midnight, so the
|
|
327
|
+
identical query over the identical instant landed a row on a different calendar
|
|
328
|
+
day than SQLite did.
|
|
329
|
+
- **MySQL** — `DATETIME(3)` instead of `TIMESTAMP`, a connection pinned to UTC on
|
|
330
|
+
both the mysql2 and the server layer, and a MySQL-spelled bind carrying the
|
|
331
|
+
same UTC wall clock. MySQL accepts neither the `T` separator nor the `Z` suffix
|
|
332
|
+
in a datetime literal, so datetime writes over REST had always failed outright;
|
|
333
|
+
`TIMESTAMP` additionally truncated milliseconds and could not store an instant
|
|
334
|
+
outside 1970..2038.
|
|
335
|
+
|
|
336
|
+
Existing rows converge at schema sync. Both migrations are allowed to fail: they
|
|
337
|
+
log, mark nothing, and the read paths keep a repair expression, so an un-migrated
|
|
338
|
+
column still compares and buckets **correctly** — just unindexed. Neither can
|
|
339
|
+
repair instants the old timezone-ambiguous write path recorded wrongly; they
|
|
340
|
+
preserve what is on disk.
|
|
341
|
+
|
|
342
|
+
Also closes #3928 (datetime `ORDER BY` mis-sorted on mixed storage) by
|
|
343
|
+
construction. Rationale is recorded as ADR-0053 addendum D-B1..D-B4.
|
|
344
|
+
|
|
345
|
+
The analytics change is additive: a `coerceTemporalFilterColumn` companion to the
|
|
346
|
+
existing `coerceTemporalFilterValue` hook, so a raw-SQL strategy can normalise the
|
|
347
|
+
column side too. Absent hook → byte-identical SQL.
|
|
348
|
+
|
|
349
|
+
- be7360c: chore(plugins,services): declare `providesServices` on the 20 remaining init-time service providers (ADR-0116 follow-up, #4131)
|
|
350
|
+
|
|
351
|
+
ADR-0116 gave the kernel a declared ordering contract, but only
|
|
352
|
+
`ObjectQLPlugin` and `MetadataPlugin` had declared what their `init()`
|
|
353
|
+
registers. The pre-Phase-1 ordering check can only _name a provider_ for
|
|
354
|
+
services someone declared, so its coverage was two plugins wide.
|
|
355
|
+
|
|
356
|
+
An audit of every plugin's `init()` body (brace-matched, comments stripped,
|
|
357
|
+
each call classified by whether it sits inside a `try`/`if`) found 20 plugins
|
|
358
|
+
that register a service on every path without declaring it. All 20 now
|
|
359
|
+
declare `providesServices`. Purely additive: no ordering changes, no new
|
|
360
|
+
failure modes — a `providesServices` entry only lets the kernel say _who_
|
|
361
|
+
provides a service when it reports a misordering, and enriches the Phase-1
|
|
362
|
+
`getService` miss diagnostic.
|
|
363
|
+
|
|
364
|
+
Three needed a closer read before declaring, because they register the same
|
|
365
|
+
service from several branches (`cache`, `queue`, `job`): each early-return
|
|
366
|
+
branch plus the fallback registers it, so every path does — the declaration
|
|
367
|
+
is honest. ADR-0116's rule that a _conditionally_ registered service must
|
|
368
|
+
never be declared is unchanged and was applied throughout.
|
|
369
|
+
|
|
370
|
+
The same audit found 12 plugins that hard-resolve a service during `init()`
|
|
371
|
+
(11 of them `manifest`) without declaring `requiresServices`. None is a live
|
|
372
|
+
exposure — every one already declares a hard `dependencies` entry on the
|
|
373
|
+
provider, so the kernel orders them correctly today. Those are tracked
|
|
374
|
+
separately: with a hard dependency in place, `requiresServices` mostly
|
|
375
|
+
restates what the kernel already enforces, and its real value is on
|
|
376
|
+
_soft_-dependency consumers, of which `AppPlugin` is currently the only one.
|
|
377
|
+
|
|
378
|
+
- f752ee3: feat(analytics): order the time axis by default, and give reports a sort declaration (#3916)
|
|
379
|
+
|
|
380
|
+
A matrix report with a date dimension across rendered its columns in arbitrary
|
|
381
|
+
order — `2026-07-01, 2026-07-05, …, 2026-07-02`. Declaring `dateGranularity` on
|
|
382
|
+
the dataset dimension made the bucket keys _sortable_ (`2026-07`, `2026-Q3`)
|
|
383
|
+
without making anything _sort_ them, and the report author had no way to ask:
|
|
384
|
+
`DatasetSelection.order` existed on the wire, but `ReportSchema` had no ordering
|
|
385
|
+
field at all (dashboard widgets had their own `options.sortBy` channel; reports
|
|
386
|
+
did not). Nothing in the chain supplied an order either — `resolveOrdering`
|
|
387
|
+
returned `undefined` unless the selection carried one explicitly, the ObjectQL
|
|
388
|
+
aggregate path has no ordering grammar so its buckets came back in Map-insertion
|
|
389
|
+
order, and the pivot builds its column headers in row-arrival order.
|
|
390
|
+
|
|
391
|
+
- **A selected time dimension is now chronological by default.** When a
|
|
392
|
+
selection states no `order` (and no `limit`, whose own fallback already
|
|
393
|
+
ordered by every dimension), each selected dimension the cube types as `time`
|
|
394
|
+
defaults to ASCENDING, in selection order. Bucket keys are minted sort-stable
|
|
395
|
+
precisely so this works — `2026-07` sorts after `2026-06`, `2026-Q3` after
|
|
396
|
+
`2026-Q1`. This lands on both strategy paths: a real `ORDER BY` where native
|
|
397
|
+
SQL serves the query, and the executor's post-pass where a date-bucketed query
|
|
398
|
+
is handed to the ObjectQL path. Null / empty buckets stay last, as everywhere
|
|
399
|
+
else. Deliberately narrow: only time dimensions get a default, so grids with
|
|
400
|
+
nothing wrong with them are not reordered.
|
|
401
|
+
- **Reports can declare an ordering.** `ReportSchema.order` (and
|
|
402
|
+
`blocks[].order` for a `joined` report) is a list of `{ by, direction }` sort
|
|
403
|
+
keys, most significant first — an array, not a `Record`, because key order is
|
|
404
|
+
the contract and JSON object key order should not have to be. `by` must name a
|
|
405
|
+
dimension the report groups by (`rows` / `columns`) or a measure it displays
|
|
406
|
+
(`values`); anything else fails at authoring time rather than becoming an
|
|
407
|
+
ordering that silently does nothing. Duplicate keys are rejected. A `joined`
|
|
408
|
+
report orders per block — declaring `order` on the container is an error.
|
|
409
|
+
`reportSelectionOrder()` lowers the list into the `DatasetSelection.order` a
|
|
410
|
+
renderer posts, and returns `undefined` for an empty list so the runtime's own
|
|
411
|
+
defaults still apply.
|
|
412
|
+
|
|
413
|
+
An explicit `order` still wins outright — the chronological default is a
|
|
414
|
+
default, not a policy, so "newest month first" is one declaration away.
|
|
415
|
+
|
|
416
|
+
`report.order` ships as `planned` + `authorWarn` in the liveness ledger: the
|
|
417
|
+
framework half is complete and live (schema, lowering helper, executor), but
|
|
418
|
+
objectui's `DatasetReportRenderer` does not yet carry `report.order` into the
|
|
419
|
+
selection it posts. The default time-axis ordering needs no renderer change and
|
|
420
|
+
is live now.
|
|
421
|
+
|
|
422
|
+
- b3a3d83: feat(spec): a shared temporal conformance matrix, and the `$between` gap it found (ADR-0053 D-A3, #4081)
|
|
423
|
+
|
|
424
|
+
`@objectstack/spec/data` gains `TEMPORAL_ROWS` and `TEMPORAL_CASES` — the
|
|
425
|
+
single set of temporal filter cases every backend is checked against, the twin
|
|
426
|
+
of the existing `FILTER_LOGIC_CASES`. Five backends consume it and assert **row
|
|
427
|
+
results**: `driver-sql` (and, through the live-dialect CI job, real Postgres and
|
|
428
|
+
MySQL), `driver-memory`, `driver-mongodb` (real MongoDB), the analytics preview
|
|
429
|
+
evaluator, and `formula`'s RLS write-side `check`.
|
|
430
|
+
|
|
431
|
+
This is the regression backstop ADR-0053 D-A3 has asked for since 2026-06 and
|
|
432
|
+
the last of its decisions to be actioned. Four separate incidents — #3650,
|
|
433
|
+
#3773, #3777, #4047 — were each found by a human by accident, and each left a
|
|
434
|
+
suite proving only its own issue against its own fixture. Nothing held the
|
|
435
|
+
backends to one standard, so the fifth divergence had nowhere to fail.
|
|
436
|
+
|
|
437
|
+
**`service-analytics` — a real fix the matrix found on its first run.** The
|
|
438
|
+
draft-preview evaluator had no `$between` case, so it fell through to its
|
|
439
|
+
permissive `default` and matched **every** row: a drafted dashboard carrying a
|
|
440
|
+
range filter charted the entire dataset, then changed its numbers at publish —
|
|
441
|
+
the exact continuity the preview exists to provide. It now evaluates
|
|
442
|
+
`$between`, sharing the upper-bound helper with `$lte` so the whole-day
|
|
443
|
+
calendar-day rule (#3777) applies to a range's max as well.
|
|
444
|
+
|
|
445
|
+
Also recorded (ADR-0053 D-A3.1): `$gt` with a bare-day comparand on a
|
|
446
|
+
`datetime` column cannot agree between typed and type-blind backends, and the
|
|
447
|
+
gap is irreducible without field types. It is asserted in the shared matrix on
|
|
448
|
+
`date` only, with the `datetime` cell left to the typed drivers' own suites,
|
|
449
|
+
rather than papered over.
|
|
450
|
+
|
|
451
|
+
- 35accbf: feat(spec): promote the temporal storage hooks onto the IDataDriver contract (ADR-0053 D-A2)
|
|
452
|
+
|
|
453
|
+
`temporalFilterValue` and `temporalFilterColumnSql` — the pair that closed
|
|
454
|
+
#3912's storage-form drift — were duck-typed: analytics probed
|
|
455
|
+
`typeof driver.x === 'function'` against a locally-invented interface, and
|
|
456
|
+
nothing at the type level said a driver must implement both or neither. The
|
|
457
|
+
lesson of #3912 is precisely that coercing the comparand without normalising
|
|
458
|
+
the column reintroduces half the bug, so a driver implementing one hook alone
|
|
459
|
+
would silently regress.
|
|
460
|
+
|
|
461
|
+
Both are now optional members of `IDataDriver`
|
|
462
|
+
(`@objectstack/spec/contracts`), documented as a pair with "absent = identity"
|
|
463
|
+
semantics for drivers whose storage form is the wire form (memory, mongo).
|
|
464
|
+
`SqlDriver implements IDataDriver`, so its signatures are compile-checked from
|
|
465
|
+
here on; analytics derives its driver seam by `Pick`-ing the contract instead
|
|
466
|
+
of a local duck type. Runtime `typeof` guards remain — that is the correct way
|
|
467
|
+
to consume an optional contract member — but the shape they guard now has one
|
|
468
|
+
authoritative definition.
|
|
469
|
+
|
|
470
|
+
No runtime behaviour change. ADR-0053 D-A2 is recorded as resolved.
|
|
471
|
+
|
|
472
|
+
- e4c2dc8: Order temporal operands correctly when one side is a JS `Date` on the two
|
|
473
|
+
type-blind filter backends (ADR-0053 D-A3 / #4191).
|
|
474
|
+
|
|
475
|
+
`utcInstantMs` joins `nextUtcCalendarDay` in `@objectstack/spec/data`
|
|
476
|
+
(re-exported from `@objectstack/core`): it reads the UTC instant a temporal
|
|
477
|
+
operand denotes, accepting only unambiguous spellings — a `Date`, epoch ms, a
|
|
478
|
+
bare `YYYY-MM-DD`, and an ISO timestamp with or without an explicit zone (a
|
|
479
|
+
zone-naive one being UTC, per D-B2) — and returning `null` for everything
|
|
480
|
+
else, notably a bare wall clock, which denotes no instant.
|
|
481
|
+
|
|
482
|
+
Both type-blind evaluators now use it to compare a `Date` against wire text,
|
|
483
|
+
which JS relational operators cannot do: `<` and friends coerce with hint
|
|
484
|
+
`number`, so the `Date` becomes its epoch and the string becomes `NaN`.
|
|
485
|
+
|
|
486
|
+
- `formula`'s `matchesFilterCondition` (the RLS write-side `check`) dropped
|
|
487
|
+
every `Date`-valued row in 10 of the 16 shared conformance cases. The
|
|
488
|
+
post-image is the caller's raw write payload, so an SDK write of
|
|
489
|
+
`new Date()` hit this directly, and fail-closed turned it into a **denied
|
|
490
|
+
write**.
|
|
491
|
+
- `service-analytics`' preview evaluator diverged on the same 10 cases in
|
|
492
|
+
BOTH directions, because `String(new Date())` sorts after every `'2026-…'`
|
|
493
|
+
comparand — a drafted chart both lost rows and gained ones, then changed
|
|
494
|
+
its numbers at publish. Rows from a mongo-backed dataset arrive as BSON
|
|
495
|
+
`Date`s, so this was reachable in normal use.
|
|
496
|
+
|
|
497
|
+
Comparisons that did not involve a `Date` are unchanged.
|
|
498
|
+
|
|
499
|
+
- Updated dependencies [6a67d7a]
|
|
500
|
+
- Updated dependencies [0ecc656]
|
|
501
|
+
- Updated dependencies [06772eb]
|
|
502
|
+
- Updated dependencies [270650f]
|
|
503
|
+
- Updated dependencies [3aef718]
|
|
504
|
+
- Updated dependencies [1ea6bce]
|
|
505
|
+
- Updated dependencies [c1dcacd]
|
|
506
|
+
- Updated dependencies [ad303ed]
|
|
507
|
+
- Updated dependencies [32ccb23]
|
|
508
|
+
- Updated dependencies [f5a4ef0]
|
|
509
|
+
- Updated dependencies [2d3e255]
|
|
510
|
+
- Updated dependencies [7d7521f]
|
|
511
|
+
- Updated dependencies [5dc4d02]
|
|
512
|
+
- Updated dependencies [05154a1]
|
|
513
|
+
- Updated dependencies [9b6fe7c]
|
|
514
|
+
- Updated dependencies [8c711fb]
|
|
515
|
+
- Updated dependencies [09e4547]
|
|
516
|
+
- Updated dependencies [91f4c78]
|
|
517
|
+
- Updated dependencies [820eff9]
|
|
518
|
+
- Updated dependencies [8d895ff]
|
|
519
|
+
- Updated dependencies [f6472d7]
|
|
520
|
+
- Updated dependencies [78caf51]
|
|
521
|
+
- Updated dependencies [62a789b]
|
|
522
|
+
- Updated dependencies [789ad63]
|
|
523
|
+
- Updated dependencies [2af1988]
|
|
524
|
+
- Updated dependencies [0af50a3]
|
|
525
|
+
- Updated dependencies [2e836de]
|
|
526
|
+
- Updated dependencies [12a19a8]
|
|
527
|
+
- Updated dependencies [41dcda3]
|
|
528
|
+
- Updated dependencies [c8124e5]
|
|
529
|
+
- Updated dependencies [a1a4140]
|
|
530
|
+
- Updated dependencies [217e2e6]
|
|
531
|
+
- Updated dependencies [86a71d1]
|
|
532
|
+
- Updated dependencies [d5c75e2]
|
|
533
|
+
- Updated dependencies [03d26f7]
|
|
534
|
+
- Updated dependencies [4384921]
|
|
535
|
+
- Updated dependencies [3c628ce]
|
|
536
|
+
- Updated dependencies [7cb922e]
|
|
537
|
+
- Updated dependencies [1d22114]
|
|
538
|
+
- Updated dependencies [b5f9397]
|
|
539
|
+
- Updated dependencies [ed77493]
|
|
540
|
+
- Updated dependencies [58a03d2]
|
|
541
|
+
- Updated dependencies [dc530b4]
|
|
542
|
+
- Updated dependencies [e59786e]
|
|
543
|
+
- Updated dependencies [bcf1112]
|
|
544
|
+
- Updated dependencies [9774b78]
|
|
545
|
+
- Updated dependencies [b07d829]
|
|
546
|
+
- Updated dependencies [a648e96]
|
|
547
|
+
- Updated dependencies [a47ac06]
|
|
548
|
+
- Updated dependencies [e4c61a7]
|
|
549
|
+
- Updated dependencies [cc60165]
|
|
550
|
+
- Updated dependencies [081aa6f]
|
|
551
|
+
- Updated dependencies [91f4c78]
|
|
552
|
+
- Updated dependencies [e8d0c21]
|
|
553
|
+
- Updated dependencies [45dc446]
|
|
554
|
+
- Updated dependencies [c1d44f7]
|
|
555
|
+
- Updated dependencies [ab9fb5c]
|
|
556
|
+
- Updated dependencies [f985b3f]
|
|
557
|
+
- Updated dependencies [9a4932a]
|
|
558
|
+
- Updated dependencies [f9fc874]
|
|
559
|
+
- Updated dependencies [011b386]
|
|
560
|
+
- Updated dependencies [7777e8f]
|
|
561
|
+
- Updated dependencies [507b92a]
|
|
562
|
+
- Updated dependencies [7309c81]
|
|
563
|
+
- Updated dependencies [20bc1ec]
|
|
564
|
+
- Updated dependencies [90c2b15]
|
|
565
|
+
- Updated dependencies [42eeb7d]
|
|
566
|
+
- Updated dependencies [01e124d]
|
|
567
|
+
- Updated dependencies [7ce02eb]
|
|
568
|
+
- Updated dependencies [a13827e]
|
|
569
|
+
- Updated dependencies [7733604]
|
|
570
|
+
- Updated dependencies [40e420f]
|
|
571
|
+
- Updated dependencies [d13004a]
|
|
572
|
+
- Updated dependencies [be7360c]
|
|
573
|
+
- Updated dependencies [5b47ab5]
|
|
574
|
+
- Updated dependencies [b09d8d9]
|
|
575
|
+
- Updated dependencies [b09d8d9]
|
|
576
|
+
- Updated dependencies [8675db6]
|
|
577
|
+
- Updated dependencies [b09d8d9]
|
|
578
|
+
- Updated dependencies [3eb1b2b]
|
|
579
|
+
- Updated dependencies [59b85c0]
|
|
580
|
+
- Updated dependencies [6e357ed]
|
|
581
|
+
- Updated dependencies [d6938bf]
|
|
582
|
+
- Updated dependencies [31e0be9]
|
|
583
|
+
- Updated dependencies [4bfd455]
|
|
584
|
+
- Updated dependencies [ffd2ce2]
|
|
585
|
+
- Updated dependencies [62f8017]
|
|
586
|
+
- Updated dependencies [a831df1]
|
|
587
|
+
- Updated dependencies [f752ee3]
|
|
588
|
+
- Updated dependencies [a1b61e0]
|
|
589
|
+
- Updated dependencies [cd6b9f2]
|
|
590
|
+
- Updated dependencies [2cb6d3c]
|
|
591
|
+
- Updated dependencies [af2a095]
|
|
592
|
+
- Updated dependencies [ec796d5]
|
|
593
|
+
- Updated dependencies [e87fea1]
|
|
594
|
+
- Updated dependencies [c65e529]
|
|
595
|
+
- Updated dependencies [3ca34c1]
|
|
596
|
+
- Updated dependencies [239c3a3]
|
|
597
|
+
- Updated dependencies [94a0bbc]
|
|
598
|
+
- Updated dependencies [d6bfb3d]
|
|
599
|
+
- Updated dependencies [a2266a6]
|
|
600
|
+
- Updated dependencies [d25a0ec]
|
|
601
|
+
- Updated dependencies [667b83e]
|
|
602
|
+
- Updated dependencies [627b188]
|
|
603
|
+
- Updated dependencies [8d4eae7]
|
|
604
|
+
- Updated dependencies [857a6cf]
|
|
605
|
+
- Updated dependencies [65a3a84]
|
|
606
|
+
- Updated dependencies [ccd9397]
|
|
607
|
+
- Updated dependencies [bca935b]
|
|
608
|
+
- Updated dependencies [d92c72d]
|
|
609
|
+
- Updated dependencies [c54c822]
|
|
610
|
+
- Updated dependencies [8dcc0f5]
|
|
611
|
+
- Updated dependencies [75b9e51]
|
|
612
|
+
- Updated dependencies [0a2f233]
|
|
613
|
+
- Updated dependencies [8621cdd]
|
|
614
|
+
- Updated dependencies [6f23667]
|
|
615
|
+
- Updated dependencies [5d21a48]
|
|
616
|
+
- Updated dependencies [19365b7]
|
|
617
|
+
- Updated dependencies [b7ed26d]
|
|
618
|
+
- Updated dependencies [b3a3d83]
|
|
619
|
+
- Updated dependencies [7a55913]
|
|
620
|
+
- Updated dependencies [35accbf]
|
|
621
|
+
- Updated dependencies [6038de7]
|
|
622
|
+
- Updated dependencies [eb95d97]
|
|
623
|
+
- Updated dependencies [e4c2dc8]
|
|
624
|
+
- Updated dependencies [1bd2795]
|
|
625
|
+
- Updated dependencies [8186a70]
|
|
626
|
+
- Updated dependencies [a329cca]
|
|
627
|
+
- Updated dependencies [6eec18c]
|
|
628
|
+
- Updated dependencies [4d7bebf]
|
|
629
|
+
- Updated dependencies [821ac7a]
|
|
630
|
+
- Updated dependencies [8f81731]
|
|
631
|
+
- Updated dependencies [8b50cb3]
|
|
632
|
+
- Updated dependencies [8c2db68]
|
|
633
|
+
- Updated dependencies [22b5e54]
|
|
634
|
+
- Updated dependencies [0166bd5]
|
|
635
|
+
- Updated dependencies [9b702dc]
|
|
636
|
+
- Updated dependencies [ab16331]
|
|
637
|
+
- @objectstack/spec@17.0.0-rc.1
|
|
638
|
+
- @objectstack/core@17.0.0-rc.1
|
|
639
|
+
|
|
640
|
+
## 17.0.0-rc.0
|
|
641
|
+
|
|
642
|
+
### Minor Changes
|
|
643
|
+
|
|
644
|
+
- 840ee4b: fix(analytics,runtime,types): gate cube auto-inference on object existence; stop the dispatcher boundary returning raw SQL (#3867)
|
|
645
|
+
|
|
646
|
+
Two independent defects on the `/analytics` surface, found while verifying #3770
|
|
647
|
+
against a real server. On an authenticated CRM dev server, before this change:
|
|
648
|
+
|
|
649
|
+
```
|
|
650
|
+
POST /api/v1/analytics/query {"cube":"sqlite_master","measures":["count"],"dimensions":["type"]}
|
|
651
|
+
→ 200 {"rows":[{"type":"index","count":262},{"type":"table","count":71},{"type":"view","count":1}],
|
|
652
|
+
"sql":"SELECT type AS \"type\", COUNT(*) AS \"count\" FROM \"sqlite_master\" GROUP BY type"}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
That is SQLite's internal schema table — never a registered object — read
|
|
656
|
+
successfully through the analytics endpoint. Not merely "the name reaches the
|
|
657
|
+
driver and errors": **any table the connection can see was readable.**
|
|
658
|
+
|
|
659
|
+
**① The cube name reached the driver as a table name.** `AnalyticsService.ensureCube`
|
|
660
|
+
auto-infers a minimal Cube when none is registered, with `cube.sql = <the queried
|
|
661
|
+
name>`. That is the intended "metric over an object" path — an `object-metric` KPI
|
|
662
|
+
widget queries `crm_account` with no authored Cube — but it accepted _any_ string,
|
|
663
|
+
so the endpoint could aggregate over an arbitrary physical table. The
|
|
664
|
+
analytics-side twin of the data-path gap #3770 closed, and it was not covered by
|
|
665
|
+
that fix: #3770 gated the protocol's `analyticsQuery`, which is the _degraded
|
|
666
|
+
fallback_; a deployment with `@objectstack/service-analytics` installed runs the
|
|
667
|
+
real engine instead (`ctx.replaceService`).
|
|
668
|
+
|
|
669
|
+
Inference is now gated on the same schema registry the data path consults, via a
|
|
670
|
+
new optional `AnalyticsServiceConfig.isRegisteredObject` that `plugin.ts` wires
|
|
671
|
+
from the `data` engine's `getObject`. Three-way rule: a registered Cube runs
|
|
672
|
+
untouched (its `sql` is whatever it declares); an unregistered name that IS an
|
|
673
|
+
object still auto-infers exactly as before; neither → `CUBE_NOT_FOUND` / 404
|
|
674
|
+
raised before any SQL exists, naming both ways to make the request valid. With no
|
|
675
|
+
probe configured the gate stands down and warns once — the same tiering #3770
|
|
676
|
+
took for a missing registry. `generateSql` (`/analytics/sql`) is gated too.
|
|
677
|
+
|
|
678
|
+
**② The dispatcher boundary returned `err.message` verbatim.** `errorResponseBase`
|
|
679
|
+
is the single error exit for _every_ route the dispatcher plugin mounts —
|
|
680
|
+
`/analytics`, `/packages`, `/i18n`, `/storage`, `/automation`, `/auth`,
|
|
681
|
+
`/notifications`, `/mcp`. `@objectstack/rest` has guarded its data routes against
|
|
682
|
+
driver dumps forever (`mapDataError`); this boundary guarded nothing, so any
|
|
683
|
+
driver error on any of those routes shipped its SQL to the client. Unlike ①, this
|
|
684
|
+
half is unconditional — it does not depend on the cube being invalid.
|
|
685
|
+
|
|
686
|
+
The leak heuristic moved out of `rest-server.ts` into `@objectstack/types` as
|
|
687
|
+
`looksLikeInternalErrorLeak` (both packages already depend on it) and is now
|
|
688
|
+
applied at both boundaries — one predicate, one place to widen when a new
|
|
689
|
+
dialect's phrasing shows up. `mapDataError`'s behaviour is unchanged. At the
|
|
690
|
+
dispatcher it applies **only to 5xx**: a 4xx message is a deliberate
|
|
691
|
+
business/validation answer and must reach the caller intact. Sanitising costs no
|
|
692
|
+
diagnostics — the untouched error still reaches `errorReporter` through the
|
|
693
|
+
existing `__obsRecordedError` side-channel.
|
|
694
|
+
|
|
695
|
+
**Also fixed in the same function:** `errorResponseBase` read only
|
|
696
|
+
`err.statusCode`, while domain errors across this codebase carry `status` (and
|
|
697
|
+
`HttpDispatcher.errorFromThrown` already reads `status` first). Every deliberate
|
|
698
|
+
4xx thrown through a dispatcher route — including #3770's `OBJECT_NOT_FOUND` on
|
|
699
|
+
the analytics fallback path — was rendered as a **500**. It now reads `status`
|
|
700
|
+
then `statusCode`.
|
|
701
|
+
|
|
702
|
+
**Behaviour change.** `/analytics/query` and `/analytics/sql` return 404
|
|
703
|
+
`CUBE_NOT_FOUND` for a cube that is neither registered nor a registered object;
|
|
704
|
+
previously the name was passed to the driver. Dashboards and KPI widgets pointed
|
|
705
|
+
at real objects or authored cubes are unaffected. A 5xx on a dispatcher route
|
|
706
|
+
whose message looks like a driver dump now reads `Internal server error` — check
|
|
707
|
+
server logs or your error reporter for the original.
|
|
708
|
+
|
|
709
|
+
- 587fc91: feat(analytics): the executeAggregate bridge carries ExecutionContext — ADR-0021 D-C second belt
|
|
710
|
+
|
|
711
|
+
The analytics→engine bridge now forwards the request's `ExecutionContext` to
|
|
712
|
+
`engine.aggregate`, so the engine's own middleware chain scopes analytics reads
|
|
713
|
+
independently of the analytics layer's `getReadScope`.
|
|
714
|
+
|
|
715
|
+
**Why.** `BaseEngineOptions.context` has always been `.optional()`, so nothing
|
|
716
|
+
forced the bridge to pass it — and it did not. An authenticated aggregate
|
|
717
|
+
reached the engine with no principal, plugin-security's principal-less fall-open
|
|
718
|
+
skipped its RLS injection, and the only thing left scoping the query was the
|
|
719
|
+
strategy remembering to call `getReadScope`. #3597 was a strategy that did not,
|
|
720
|
+
and both belts were off at once.
|
|
721
|
+
|
|
722
|
+
`getReadScope` stays: the two resolve scope through different paths (engine
|
|
723
|
+
middleware vs `security.getReadFilter`), and a deployment without
|
|
724
|
+
plugin-security has only the analytics layer. This is depth, not a replacement.
|
|
725
|
+
|
|
726
|
+
- `StrategyContext` gains `context?: ExecutionContext`, bound per call by
|
|
727
|
+
`AnalyticsService` from `query()` / `generateSql()` / `queryDataset()`.
|
|
728
|
+
- `StrategyContext.executeAggregate` and the `AnalyticsServicePlugin` /
|
|
729
|
+
`AnalyticsService` `executeAggregate` config options gain `context?:
|
|
730
|
+
ExecutionContext`. **Custom bridges should forward it** to their engine; the
|
|
731
|
+
built-in auto-bridge does. Purely additive — an existing bridge that ignores
|
|
732
|
+
it keeps working exactly as before.
|
|
733
|
+
- `DimensionLabelDeps.fetchRecordLabels` and `resolveDimensionLabels` each gain
|
|
734
|
+
an optional trailing `context`, beside the `scope` / `resolveScope` that
|
|
735
|
+
#3639 added — the same two-belt split as the aggregate path.
|
|
736
|
+
- `BootOptions.analytics` (`@objectstack/verify`) overrides the
|
|
737
|
+
AnalyticsServicePlugin instance, so a gate can boot with the analytics belt
|
|
738
|
+
off and assert the engine-side belt alone still scopes.
|
|
739
|
+
|
|
740
|
+
**Also fixed on the same seam:**
|
|
741
|
+
|
|
742
|
+
- `fetchRecordLabels` — the dimension display-label lookup — is row-granular
|
|
743
|
+
(one row per record, real display names). #3639 gave it the analytics-layer
|
|
744
|
+
belt (the referenced object's own read scope); it now also carries the
|
|
745
|
+
context, so the engine scopes the same read independently.
|
|
746
|
+
- `ObjectQLStrategy.generateSql` emitted no `WHERE` at all, so the
|
|
747
|
+
`/analytics/sql` preview read as an unscoped table scan while the real
|
|
748
|
+
aggregate was scoped. It now renders the caller's filters and the read scope.
|
|
749
|
+
The preview never executed, so this was misleading output rather than a leak.
|
|
750
|
+
|
|
751
|
+
- 763931e: feat(filters): evaluate `{filter-token}` placeholders server-side (#3582)
|
|
752
|
+
|
|
753
|
+
Filter values travel as JSON, so a time- or user-scoped slice writes a
|
|
754
|
+
placeholder instead of code:
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
filter: { close_date: { $gte: '{current_year_start}' }, owner: '{current_user_id}' }
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
The vocabulary has been in `@objectstack/spec` for a while (`date-macros.zod.ts`,
|
|
761
|
+
`context-tokens.zod.ts`) and `objectstack build` rejects tokens outside it
|
|
762
|
+
(#3574). What was missing is the half that _substitutes a value_: **nothing on
|
|
763
|
+
the server ever did**. A placeholder reached the driver as the literal string
|
|
764
|
+
`'{current_year_start}'`, compared as text, and matched nothing.
|
|
765
|
+
|
|
766
|
+
That failure is invisible — an empty widget looks exactly like a metric that is
|
|
767
|
+
legitimately zero — so apps worked around it by computing dates at module load,
|
|
768
|
+
which freezes "this year" into the built artifact and quietly goes stale.
|
|
769
|
+
|
|
770
|
+
**New: `resolveFilterTokens()` in `@objectstack/core`**, wired into the two
|
|
771
|
+
server-side seams every filter passes through:
|
|
772
|
+
|
|
773
|
+
- **ObjectQL read path** — `find` / `findOne` / `count` / `aggregate`, so REST
|
|
774
|
+
queries, related lists, saved-view filters and flow `find_records` all resolve.
|
|
775
|
+
It runs before the middleware chain, so only author-supplied filters are
|
|
776
|
+
inspected; RLS/sharing filters are injected downstream from concrete values.
|
|
777
|
+
- **Analytics dataset executor** — a dataset's intrinsic `filter`, a widget's
|
|
778
|
+
`runtimeFilter`, measure-scoped filters, and time-dimension `dateRange`s.
|
|
779
|
+
This path needs its own call: `NativeSQLStrategy` compiles raw SQL and binds
|
|
780
|
+
comparands directly, so a dashboard widget never passes through `engine.find()`.
|
|
781
|
+
|
|
782
|
+
Behavioural notes:
|
|
783
|
+
|
|
784
|
+
- Date tokens resolve to ISO strings (`YYYY-MM-DD`, or a full timestamp for
|
|
785
|
+
`{now}` / `{N_hours_ago}` / `{N_minutes_ago}`). Turning that into a column's
|
|
786
|
+
on-disk form stays the driver's job (`SqlDriver.temporalFilterValue`), so
|
|
787
|
+
there is still exactly one source of truth for the storage convention.
|
|
788
|
+
- Calendar boundaries follow `ExecutionContext.timezone`; one instant is pinned
|
|
789
|
+
per filter tree, so a `>= {current_month_start}` / `< {next_month_start}` pair
|
|
790
|
+
can never straddle a boundary.
|
|
791
|
+
- `{current_org_id}` reads `ExecutionContext.tenantId`; `{current_user_id}` reads
|
|
792
|
+
`userId`. A request carrying neither now **throws** instead of resolving to
|
|
793
|
+
`null` — a null comparand degrades to `IS NULL` on most drivers and would hand
|
|
794
|
+
back the rows the filter was written to exclude.
|
|
795
|
+
- An unrecognised placeholder **throws**, carrying the near-miss fix
|
|
796
|
+
(`{current_user}` → `{current_user_id}`, `{this_quarter_start}` →
|
|
797
|
+
`{current_quarter_start}`). This matches what `objectstack build` already
|
|
798
|
+
enforces. Consequence, previously implicit and now load-bearing: a filter value
|
|
799
|
+
that is _entirely_ `{...}` is always read as a placeholder, so a literal value
|
|
800
|
+
of that shape is not expressible — rename the value.
|
|
801
|
+
|
|
802
|
+
Also in this change: `notify` no longer sends the six-character string
|
|
803
|
+
`"undefined"` as an audience member. `to: ['{record.owner.manager}']` walks
|
|
804
|
+
`.manager` on a scalar foreign-key id, resolves to nothing, and `String(undefined)`
|
|
805
|
+
turned that into a phantom recipient — the emit "succeeded", addressed nobody,
|
|
806
|
+
and said nothing. Unresolved recipients are now dropped, and a node with no
|
|
807
|
+
recipient left fails naming the offending template and pointing at the start
|
|
808
|
+
node's `config.expand` (#3475), which does hydrate the relation.
|
|
809
|
+
|
|
810
|
+
- fc5f126: feat(analytics): serve in-envelope cross-object grouping on the ObjectQL path by FK-expand (#3654)
|
|
811
|
+
|
|
812
|
+
`engine.aggregate()` cannot join, so the ObjectQL fallback path (date-granularity
|
|
813
|
+
bucketing, in-memory driver, federated objects) previously REJECTED any
|
|
814
|
+
cross-object grouping like `revenue by account.region` (#3664 stopgap — a loud
|
|
815
|
+
error instead of the earlier silent `(null)` mis-bucket). It now SERVES the
|
|
816
|
+
common case directly.
|
|
817
|
+
|
|
818
|
+
For a single-hop cross-object DIMENSION with recombinable measures, the strategy:
|
|
819
|
+
|
|
820
|
+
1. groups the base aggregate on the lookup FK column (`account`) — which the
|
|
821
|
+
engine can do — scoped to the base object;
|
|
822
|
+
2. resolves each FK id to the related attribute (`region`) with a read of the
|
|
823
|
+
referenced object **scoped to that object's own RLS**; then
|
|
824
|
+
3. re-buckets by the resolved attribute in memory, recombining the measures
|
|
825
|
+
(sum/count add; min/max take the extremum).
|
|
826
|
+
|
|
827
|
+
A base row whose referenced record the caller cannot read buckets under an
|
|
828
|
+
explicit `(restricted)` group: its measure still counts (grand totals are
|
|
829
|
+
preserved) but the hidden record's attribute never appears — no leak (ADR-0021
|
|
830
|
+
D-C, the #3602 class). `/analytics/sql` renders the equivalent `LEFT JOIN`.
|
|
831
|
+
|
|
832
|
+
Deliberately bounded — still REJECTED (loud, never silently wrong): cross-object
|
|
833
|
+
references in a MEASURE or FILTER (need a real join to evaluate), multi-hop
|
|
834
|
+
dimensions (`a.b.c`), and non-recombinable measures (`avg`, `count_distinct`)
|
|
835
|
+
with a cross-object dimension. Cross-object queries on `NativeSQLStrategy` (the
|
|
836
|
+
normal SQL path) are unchanged — it hand-compiles the joins.
|
|
837
|
+
|
|
838
|
+
### Patch Changes
|
|
839
|
+
|
|
840
|
+
- c7f4417: fix(driver-sql,analytics): stop `aggregate()` / `distinct()` leaking SQLite's raw epoch storage (#3797)
|
|
841
|
+
|
|
842
|
+
Both returned `await builder` directly, without the `formatOutput` pass every
|
|
843
|
+
`find()` row gets. On SQLite — the one dialect where a `Field.datetime` is
|
|
844
|
+
stored as INTEGER epoch milliseconds rather than a native timestamp — that raw
|
|
845
|
+
storage form went straight to the caller:
|
|
846
|
+
|
|
847
|
+
| call | before | after |
|
|
848
|
+
| -------------------------------------- | ---------------------------- | -------------------------------- |
|
|
849
|
+
| `find()` | `"2026-01-10T09:00:00.000Z"` | unchanged |
|
|
850
|
+
| `distinct('closed_at')` | `[1768035600000]` | `["2026-01-10T09:00:00.000Z"]` |
|
|
851
|
+
| `aggregate()` `max(closed_at)` | `1768035600000` | `"2026-01-10T09:00:00.000Z"` |
|
|
852
|
+
| `aggregate()` `groupBy: ['closed_at']` | key `1768035600000` | key `"2026-01-10T09:00:00.000Z"` |
|
|
853
|
+
|
|
854
|
+
Same root cause as #3773, different exit. `Field.date` was never affected — it
|
|
855
|
+
is ISO TEXT on every dialect, so its storage form already equals its
|
|
856
|
+
presentation.
|
|
857
|
+
|
|
858
|
+
The visible surfaces were a `_max`/`_min` measure over a datetime (a "last
|
|
859
|
+
closed" KPI tile rendered `1768035600000`) and a `groupBy` on a raw datetime
|
|
860
|
+
dimension, which also disagreed with the in-memory `applyInMemoryAggregation`
|
|
861
|
+
fallback — that one consumes already-formatted `find()` rows, so the same
|
|
862
|
+
dataset changed key type depending on which path served it.
|
|
863
|
+
|
|
864
|
+
Which columns hold an instant is now recorded while the statement is built,
|
|
865
|
+
because that is the only point where a column name and its meaning are both
|
|
866
|
+
known: a `min()` lands under its alias and never under the field name, while a
|
|
867
|
+
date-BUCKETED column lands under the field name but holds a label (`'2026-01'`)
|
|
868
|
+
rather than an instant. Matching on names afterwards gets both backwards.
|
|
869
|
+
|
|
870
|
+
`distinct()` additionally re-deduplicates after presenting: SQL `DISTINCT`
|
|
871
|
+
compares STORED values, and one SQLite datetime column holds both INTEGER and
|
|
872
|
+
TEXT forms, so two rows recording the same instant survived as two and then
|
|
873
|
+
presented identically. It has no in-repo callers today; this keeps it honest
|
|
874
|
+
rather than leaving a second convention in the driver.
|
|
875
|
+
|
|
876
|
+
**`cross-object-rebucket` was fixed alongside it, because presenting min/max
|
|
877
|
+
correctly is what exposed it.** `recombine()` coerced every operand with
|
|
878
|
+
`Number()`, which silently depended on receiving an epoch: handed the ISO string
|
|
879
|
+
the driver now returns it produced `NaN`, and on Postgres/MySQL (where knex
|
|
880
|
+
returns a `Date`) it had always flattened the value back to an epoch integer one
|
|
881
|
+
layer above the driver. `min`/`max` now order by the instant and return the
|
|
882
|
+
winning value in the shape it arrived in; `sum`/`count` stay numeric.
|
|
883
|
+
|
|
884
|
+
- 7101ca2: fix(analytics): apply the EFFECTIVE date granularity to bucket labels and drill ranges (#3588 follow-up)
|
|
885
|
+
|
|
886
|
+
`selection.dateGranularity` (shipped in #3652) reached the `GROUP BY` but not the
|
|
887
|
+
post-processing: the bucket-label formatter and the drill-range inverter both
|
|
888
|
+
kept reading the DATASET dimension's default. A query was grouped one way and
|
|
889
|
+
described another. Found by driving a real dashboard query in a browser against
|
|
890
|
+
a dataset whose dimension declares `dateGranularity: 'month'`:
|
|
891
|
+
|
|
892
|
+
- selection `year` → the row came back labelled **`1970-01`** — a year bucket
|
|
893
|
+
re-formatted with the dataset's month granularity, its `"2026"` key re-read as
|
|
894
|
+
2026 _milliseconds_ past the epoch;
|
|
895
|
+
- selection `day` → day buckets were re-labelled as months, so ten distinct days
|
|
896
|
+
collapsed into two duplicated keys;
|
|
897
|
+
- selection `quarter` / `year` / `day` / `week` → `drillRanges` came back empty,
|
|
898
|
+
silently removing drill-through from every bucketed chart.
|
|
899
|
+
|
|
900
|
+
Granularity precedence now lives in one exported function,
|
|
901
|
+
`resolveDimensionGranularity`, called from all three sites that must agree — the
|
|
902
|
+
query's `GROUP BY`, the label formatter, and the range inverter. The drift was
|
|
903
|
+
possible only because each site resolved it independently.
|
|
904
|
+
|
|
905
|
+
Two consequences beyond the override case:
|
|
906
|
+
|
|
907
|
+
- A dataset dimension that declares **no** granularity but is bucketed by the
|
|
908
|
+
widget now gets drill ranges too. Previously the range sidecar keyed off the
|
|
909
|
+
dataset's own `dateGranularity`, so this case — the one #3588 is actually
|
|
910
|
+
about — could never drill.
|
|
911
|
+
- `formatDateBucket` no longer mistakes a bare year key for an epoch timestamp.
|
|
912
|
+
A year bucket's canonical key IS `"2026"`, which is the only bucket key that
|
|
913
|
+
collides with the pure-digit epoch heuristic (`"2026-Q2"`, `"2026-07"` and
|
|
914
|
+
`"2026-07-15"` all fail it). Being idempotent over already-formatted keys is
|
|
915
|
+
that function's stated contract; the year case just never held.
|
|
916
|
+
|
|
917
|
+
- 415254c: fix(analytics): scope the dimension-label lookup to the referenced object's RLS (#3602)
|
|
918
|
+
|
|
919
|
+
When a dataset groups by a `lookup`/`master_detail` dimension, analytics resolves
|
|
920
|
+
the grouped FK ids to the related record's display name via a per-record read
|
|
921
|
+
(`group by id`) dressed as an aggregate. That read carried **no read scope**, so
|
|
922
|
+
it revealed related-record display names whenever the referenced object's RLS is
|
|
923
|
+
stricter than the base object whose rows carry the id — a user could see a name
|
|
924
|
+
the referenced object's own RLS would hide. (Same-object and looser-referenced
|
|
925
|
+
cases were already safe because the ids come from the post-#3597 scoped
|
|
926
|
+
aggregate; this closes the stricter-referenced case.)
|
|
927
|
+
|
|
928
|
+
The label lookup now applies the **referenced object's own** read scope — bound
|
|
929
|
+
to the request via the same `getReadScope` provider the aggregate path uses,
|
|
930
|
+
composed with `$and` (never key-merge) so it can't be displaced by the id
|
|
931
|
+
predicate. Fail-closed: if that object's scope can't be resolved, the dimension's
|
|
932
|
+
labels are skipped (the raw id renders) rather than fetched unscoped. No behaviour
|
|
933
|
+
change when no read-scope provider is configured.
|
|
934
|
+
|
|
935
|
+
Internal `DimensionLabelDeps.fetchRecordLabels` gains an optional `scope` argument
|
|
936
|
+
and `resolveDimensionLabels` an optional `resolveScope` resolver; both are
|
|
937
|
+
service-analytics-internal (no spec/contract change).
|
|
938
|
+
|
|
939
|
+
- 1f8390b: fix(analytics): ObjectQLStrategy now enforces the read scope (RLS + tenant) (#3597)
|
|
940
|
+
|
|
941
|
+
`ObjectQLStrategy` never consumed `getReadScope`, so any analytics query served by
|
|
942
|
+
that path ran with **no RLS or tenant predicate** — an authenticated caller
|
|
943
|
+
received aggregates computed over every tenant's rows.
|
|
944
|
+
|
|
945
|
+
Both belts were off at once. The strategy dropped the pre-resolved read scope, and
|
|
946
|
+
the engine could not compensate: the `executeAggregate` bridge passes no
|
|
947
|
+
`ExecutionContext`, so plugin-security's principal-less fall-open skipped its own
|
|
948
|
+
RLS injection. Only `NativeSQLStrategy` was ever wired for ADR-0021 D-C.
|
|
949
|
+
|
|
950
|
+
The exposure was **not** limited to exotic drivers. `NativeSQLStrategy` declines —
|
|
951
|
+
handing the query to this path — on any date-bucketed query
|
|
952
|
+
(`timeDimensions[].granularity`, the most common dashboard shape, on Postgres and
|
|
953
|
+
SQLite too), on `RAW_SQL_UNSUPPORTED` (in-memory driver), and on federated objects.
|
|
954
|
+
|
|
955
|
+
The scope is composed with `$and`, never by key merge, so a caller filter naming
|
|
956
|
+
the same field (e.g. `organization_id`) cannot displace the security predicate.
|
|
957
|
+
|
|
958
|
+
**Behaviour change to be aware of:** a query that references a **joined** object
|
|
959
|
+
carrying its own read scope is now REJECTED on this path rather than run
|
|
960
|
+
partially-scoped. `engine.aggregate`'s `where` addresses the base object, so a
|
|
961
|
+
per-join predicate cannot be expressed there; failing closed matches the posture
|
|
962
|
+
already taken by `resolveReadScopes` and `compileScopedFilterToSql`. Such a query
|
|
963
|
+
previously returned results that omitted the joined object's tenant predicate.
|
|
964
|
+
Run it on a native-SQL driver (`NativeSQLStrategy` scopes each join), or drop the
|
|
965
|
+
cross-object dimension/measure.
|
|
966
|
+
|
|
967
|
+
Deployments with no read-scope provider configured are unaffected — that path
|
|
968
|
+
stays unscoped by documented contract.
|
|
969
|
+
|
|
970
|
+
- 3167e29: fix(analytics): sort dataset selections by the display label for select/lookup dimensions (#3680)
|
|
971
|
+
|
|
972
|
+
`DatasetSelection.order` (what a widget's `options.sortBy` lowers to) sorted a
|
|
973
|
+
`select` or `lookup`/`master_detail` dimension by its STORED value — the option
|
|
974
|
+
value or the foreign-key id — while the response rows carry the resolved display
|
|
975
|
+
label. A "sort by Account" therefore ordered by opaque ids and read as arbitrary;
|
|
976
|
+
a localized select sorted by its ASCII value while showing a non-ASCII label.
|
|
977
|
+
|
|
978
|
+
Order keys naming a label-bearing dimension now sort by the display label the
|
|
979
|
+
user reads. The executor receives an injected sort-key hook (`OrderLabelResolver`,
|
|
980
|
+
built by `queryDataset` over the same label-resolution capabilities and #3602
|
|
981
|
+
read scoping as the display pass); only the COMPARISON substitutes the label —
|
|
982
|
+
rows keep their raw values until the display pass, so drill metadata still
|
|
983
|
+
snapshots stored values, and ordering + windowing stay one adjacent step (a
|
|
984
|
+
"top 10 by account name" truncates the right ten).
|
|
985
|
+
|
|
986
|
+
Cost model: sorting by a measure or a plain/date dimension is unchanged (SQL
|
|
987
|
+
pushdown included). A label-ordered `select` resolves from field metadata (no
|
|
988
|
+
query). A label-ordered `lookup` costs one batched id→name read over the
|
|
989
|
+
pre-window grouped ids (chunked, and reused by the display pass via a
|
|
990
|
+
per-request cache), and its window can no longer be pushed into SQL — the
|
|
991
|
+
inherent price of ordering by a value the database doesn't store.
|
|
992
|
+
|
|
993
|
+
- 0a6fb1e: fix(analytics): the read-scope auto-bridge no longer depends on plugin order (#3618)
|
|
994
|
+
|
|
995
|
+
`getReadScope` was only wired when the `security` service already existed at this
|
|
996
|
+
plugin's `init()`. The closure itself resolved lazily, but the ASSIGNMENT was
|
|
997
|
+
gated on an init-time probe — so a kernel that registers `AnalyticsServicePlugin`
|
|
998
|
+
before the security plugin got **no read-scope provider at all**, and every
|
|
999
|
+
analytics strategy ran unscoped with only a WARN to show for it.
|
|
1000
|
+
|
|
1001
|
+
Both sibling bridges (`executeAggregate`, `executeRawSql`) are wired
|
|
1002
|
+
unconditionally and resolve at call time, and this one's own comment claimed the
|
|
1003
|
+
same. Now it actually does: the probe only decides the log wording.
|
|
1004
|
+
|
|
1005
|
+
The CLI (`os serve`) registers security before analytics, so that path was
|
|
1006
|
+
already correct. The exposure was for embedders composing their own kernel — and
|
|
1007
|
+
for this repo's own `bootStack` harness, which registers analytics first, meaning
|
|
1008
|
+
the entire dogfood/verify suite had analytics RLS silently disabled and any RLS
|
|
1009
|
+
assertion written there passed vacuously.
|
|
1010
|
+
|
|
1011
|
+
Also corrects the WARN text: with no provider, scoping is absent on ALL paths and
|
|
1012
|
+
ALL objects, not just "the raw-SQL path" and "joined objects" as it claimed.
|
|
1013
|
+
|
|
1014
|
+
Adds `analytics-rls.dogfood.test.ts`: an owner-scoped RLS fixture driven over real
|
|
1015
|
+
HTTP as a real non-admin, asserting the rows a member's aggregate actually
|
|
1016
|
+
returns. Reverting either this fix or the #3597 strategy fix turns it red.
|
|
1017
|
+
|
|
1018
|
+
- 1986594: feat(analytics): honour widget `dateGranularity`, `sortBy`/`sortOrder`, and `limit` in the dataset query (#3588)
|
|
1019
|
+
|
|
1020
|
+
Three presentation options were accepted by the metadata layer and then dropped
|
|
1021
|
+
by the analytics query builder. They reached no SQL, produced no error, and the
|
|
1022
|
+
only way to notice was to read the `sql` a dataset response echoes — so a
|
|
1023
|
+
dashboard could declare `dateGranularity: 'month'` and quietly render one bar
|
|
1024
|
+
per record.
|
|
1025
|
+
|
|
1026
|
+
- **`dateGranularity` now buckets.** `DatasetSelection` gained an optional
|
|
1027
|
+
`dateGranularity`, applied to every selected `date` dimension. Precedence per
|
|
1028
|
+
dimension: an explicit `timeDimensions` granularity, then the selection's,
|
|
1029
|
+
then the dataset dimension's own default. A widget can bucket a trend by month
|
|
1030
|
+
without the dataset committing every other consumer to that granularity.
|
|
1031
|
+
- **`order` / `limit` / `offset` now apply on every path.** They are applied to
|
|
1032
|
+
the ASSEMBLED grid — after measure-scoped sub-queries merge, after `compareTo`
|
|
1033
|
+
columns attach, and after derived measures are computed — so a derived measure
|
|
1034
|
+
is a valid sort key and the ObjectQL aggregate path (which has no ordering
|
|
1035
|
+
grammar, and which native SQL hands every date-bucketed query to) orders
|
|
1036
|
+
identically to native SQL. A single-query selection still pushes the window
|
|
1037
|
+
down into the statement. An `order` key that names nothing the selection
|
|
1038
|
+
projects is now rejected (400) rather than silently ignored.
|
|
1039
|
+
- **`limit` is deterministic.** Without an `order`, a limit orders by the
|
|
1040
|
+
selected dimensions first, so it truncates a reproducible window instead of an
|
|
1041
|
+
arbitrary subset.
|
|
1042
|
+
- **Widget `options` is a contract again.** The four query-affecting keys
|
|
1043
|
+
(`dateGranularity`, `sortBy`, `sortOrder`, `limit`) plus `stageOrder` are
|
|
1044
|
+
declared on `DashboardWidgetOptionsSchema`, so a typo like `sortDirection` is
|
|
1045
|
+
an author-time error. The bag stays open — renderer extras (`icon`, `columns`,
|
|
1046
|
+
`striped`, …) pass through untouched.
|
|
1047
|
+
|
|
1048
|
+
Two latent bugs surfaced while fixing the above and are fixed here too:
|
|
1049
|
+
|
|
1050
|
+
- `order`/`limit` were forwarded to EVERY sub-query. A measure-scoped
|
|
1051
|
+
supplementary query selects one measure, so an inherited `ORDER BY` named a
|
|
1052
|
+
column it never selected, and an inherited `LIMIT` truncated it before the
|
|
1053
|
+
merge — dropping rows from the assembled grid. Nothing hit this only because
|
|
1054
|
+
nothing passed `order`.
|
|
1055
|
+
- The `compareTo` pass built its query by hand and skipped granularity
|
|
1056
|
+
resolution, so a month-bucketed primary grid was merged against raw-timestamp
|
|
1057
|
+
comparison rows. No dimension key matched and every `<measure>__compare`
|
|
1058
|
+
column came back empty.
|
|
1059
|
+
|
|
1060
|
+
`ObjectQLStrategy` now also echoes a representative `sql` (with `date_trunc`,
|
|
1061
|
+
`WHERE`, `ORDER BY`, and `LIMIT`; filter values parameterized, never inlined).
|
|
1062
|
+
Previously the `sql` field simply vanished from the response whenever a query
|
|
1063
|
+
was date-bucketed, leaving an author unable to tell "not implemented" from "this
|
|
1064
|
+
strategy doesn't report".
|
|
1065
|
+
|
|
1066
|
+
- a227ed7: fix(objectql)!: one key for the empty group bucket — real `null`, on both aggregation paths (#3839)
|
|
1067
|
+
|
|
1068
|
+
A grouped row whose dimension value is empty now carries `null` for that
|
|
1069
|
+
dimension no matter which way the aggregate ran. Downstream code can test the
|
|
1070
|
+
empty bucket with a plain `value == null` again: charts render their own empty
|
|
1071
|
+
label, drill-through on that bucket builds `field = null` and returns the rows
|
|
1072
|
+
it should, and a dashboard no longer changes shape when the driver, the
|
|
1073
|
+
granularity or the reference timezone changes.
|
|
1074
|
+
|
|
1075
|
+
### What was wrong
|
|
1076
|
+
|
|
1077
|
+
`engine.aggregate` has two implementations of one feature. It pushes the
|
|
1078
|
+
aggregate down as SQL when the driver advertises every requested granularity and
|
|
1079
|
+
the reference timezone is UTC; otherwise it fetches rows and buckets them in JS.
|
|
1080
|
+
The two disagreed about how to spell "empty":
|
|
1081
|
+
|
|
1082
|
+
```
|
|
1083
|
+
--- same dataset, same query, one row with a NULL value ---
|
|
1084
|
+
pushed-down SQL : [{ "key": null, "type": "null", "total": 2 }, …]
|
|
1085
|
+
in-memory : [{ "key": "(null)", "type": "string", "total": 2 }, …]
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
The measures were always right — only the key's type and literal differed —
|
|
1089
|
+
which is why this went unnoticed for so long: every total reconciled. But the
|
|
1090
|
+
engine picks a path per query, so the same data produced a different bucket key
|
|
1091
|
+
on SQLite-plus-UTC-plus-`month` than on `week` (which SQLite does not advertise),
|
|
1092
|
+
a non-UTC timezone, or `driver-rest` / `driver-memory` / a remote Turso, all of
|
|
1093
|
+
which bucket in memory unconditionally.
|
|
1094
|
+
|
|
1095
|
+
It was never date-specific either. A plain `groupBy: ['stage']` over a NULL
|
|
1096
|
+
column diverged the same way.
|
|
1097
|
+
|
|
1098
|
+
Consumers are written against `null` — they check `== null` and supply their own
|
|
1099
|
+
empty label ('—', '(empty)', a localized "Uncategorized"). The sentinel defeated
|
|
1100
|
+
every one of them: it rendered a raw English debug string in the UI, and a drill
|
|
1101
|
+
on the empty bucket compiled to `field = '(null)'` and matched nothing.
|
|
1102
|
+
|
|
1103
|
+
The in-memory path's comment justified the string as staying "consistent with
|
|
1104
|
+
the client `useReportData` hook". That hook was removed with ADR-0021, and the
|
|
1105
|
+
literal never appeared in it.
|
|
1106
|
+
|
|
1107
|
+
### What changed
|
|
1108
|
+
|
|
1109
|
+
- `applyInMemoryAggregation` and `bucketDateValue` (`@objectstack/objectql`) key
|
|
1110
|
+
the empty bucket as `null`. `bucketDateValue` now returns `string | null`. A
|
|
1111
|
+
null instant and an unparseable one still share one bucket, because SQL cannot
|
|
1112
|
+
tell them apart either (`strftime('%Y-%m', 'not-a-date')` is NULL).
|
|
1113
|
+
- The internal composite bucket id is JSON-encoded, so the empty bucket stays
|
|
1114
|
+
distinct from a row whose value is the literal string `"null"`.
|
|
1115
|
+
- `bucketKeyToCalendarRange` (`@objectstack/core`) accepts `string | null`. The
|
|
1116
|
+
empty bucket has no calendar span, so a drill on it opens the unscoped
|
|
1117
|
+
superset instead of an invented bound — unchanged behavior, honest signature.
|
|
1118
|
+
- The driver output contract in `@objectstack/spec` now states the rule: a row
|
|
1119
|
+
with no value keys as `null`, never a sentinel. Propagating NULL through the
|
|
1120
|
+
bucket expression is the whole of it; a driver only breaks it by adding a
|
|
1121
|
+
`COALESCE`.
|
|
1122
|
+
|
|
1123
|
+
### Gates
|
|
1124
|
+
|
|
1125
|
+
`checkDateBucketParity` (`@objectstack/verify`) deliberately carried no null
|
|
1126
|
+
instant, because the divergence would have failed it for a reason it was not
|
|
1127
|
+
about. Its fixture now has one, so the convergence is held in place — including
|
|
1128
|
+
for out-of-tree drivers that run the check against themselves.
|
|
1129
|
+
|
|
1130
|
+
Two fixes were needed to make that fixture meaningful:
|
|
1131
|
+
|
|
1132
|
+
- The check folded bucket labels through `String(value)`, which turns SQL NULL
|
|
1133
|
+
into `'null'` — a label a TEXT column can genuinely hold. A driver spelling
|
|
1134
|
+
"empty" as a string could compare equal to one returning real NULL. The empty
|
|
1135
|
+
bucket is now keyed out of band.
|
|
1136
|
+
- Label sets were compared with `JSON.stringify`, which is sensitive to key
|
|
1137
|
+
insertion order. Row order is not part of this contract and the two paths
|
|
1138
|
+
naturally differ (SQL sorts its groups; the in-memory path emits first-seen
|
|
1139
|
+
order), so a driver with entirely correct buckets could be reported as
|
|
1140
|
+
disagreeing — with an empty diff message, since nothing actually differed.
|
|
1141
|
+
The comparison is now order-insensitive.
|
|
1142
|
+
|
|
1143
|
+
A new dogfood check covers the non-date half against real drivers: same dataset,
|
|
1144
|
+
plain and date-bucketed `groupBy`, both paths, one key.
|
|
1145
|
+
|
|
1146
|
+
- adabaa8: fix(analytics): fail closed on cross-object aggregation the ObjectQL path cannot join (#3654)
|
|
1147
|
+
|
|
1148
|
+
`engine.aggregate()` has no join — it never expands a lookup and the SQL driver's
|
|
1149
|
+
aggregate emits no `JOIN`. So a dotted dimension/measure like `account.region`
|
|
1150
|
+
reaching `ObjectQLStrategy` (the fallback NativeSQL declines: date-granularity
|
|
1151
|
+
bucketing, in-memory driver, federated objects) failed SILENTLY: the in-memory
|
|
1152
|
+
path bucketed every row under one `(null)` group and summed the whole table into
|
|
1153
|
+
it (a plausible number that is actually a mislabelled full-table total), and the
|
|
1154
|
+
native path errored on the unresolved column.
|
|
1155
|
+
|
|
1156
|
+
`ObjectQLStrategy` now rejects any cross-object reference outright, with a clear
|
|
1157
|
+
message, before the query reaches the engine. This generalizes the #3597 guard
|
|
1158
|
+
(which only rejected when the joined object carried a read scope, and skipped the
|
|
1159
|
+
check entirely when no read-scope provider was configured — so the silent
|
|
1160
|
+
`(null)` bucket still shipped on unsecured/in-memory setups) into an
|
|
1161
|
+
unconditional one, and subsumes it: a rejected query never loads the joined
|
|
1162
|
+
object, so there is nothing left unscoped.
|
|
1163
|
+
|
|
1164
|
+
Cross-object datasets are unaffected on `NativeSQLStrategy`, which hand-compiles
|
|
1165
|
+
the LEFT JOINs (and scopes each). This only changes the fallback path, turning a
|
|
1166
|
+
silent wrong answer into a loud, actionable error. Full lookup-traversal support
|
|
1167
|
+
in the aggregate path is left as follow-up (see #3654).
|
|
1168
|
+
|
|
1169
|
+
- 605c23f: fix(analytics): ObjectQLStrategy applies `timeDimensions[].dateRange` — the predicate every date-bucketed chart was missing (#3650)
|
|
1170
|
+
|
|
1171
|
+
`ObjectQLStrategy.execute()` built its engine filter purely from
|
|
1172
|
+
`normalizeAnalyticsFilters(query)`, which reads only `query.where`. But
|
|
1173
|
+
`dateRange` is a **sibling** of `where`, never folded into it — so the window
|
|
1174
|
+
was dropped on the floor. No error, no warning: the chart rendered, and the
|
|
1175
|
+
numbers were for all of history.
|
|
1176
|
+
|
|
1177
|
+
This was not a "some drivers only" corner. `NativeSQLStrategy.canHandle`
|
|
1178
|
+
declines any query carrying a `granularity`, so a **date-bucketed trend lands on
|
|
1179
|
+
the ObjectQL path on every driver**, Postgres and SQLite included — and a
|
|
1180
|
+
bucketed trend is precisely the shape that also carries a range ("last 12
|
|
1181
|
+
months", "this quarter"). The other two paths always applied it
|
|
1182
|
+
(`NativeSQLStrategy` as `BETWEEN`, `preview-evaluator` row-wise); only this one
|
|
1183
|
+
did not.
|
|
1184
|
+
|
|
1185
|
+
**Two visible symptoms:**
|
|
1186
|
+
|
|
1187
|
+
- A trend chart with a time filter plotted **every row ever recorded** instead
|
|
1188
|
+
of the selected window.
|
|
1189
|
+
- `compareTo` (period-over-period) was **structurally dead**. `runCompare`
|
|
1190
|
+
builds the comparison pass by shifting `dateRange` and changing nothing else,
|
|
1191
|
+
so with the window ignored both passes issued a byte-identical aggregate:
|
|
1192
|
+
every `<measure>__compare` column equalled its primary and the delta was a
|
|
1193
|
+
flat 0%. And since `compareTo` requires a time dimension, it always took this
|
|
1194
|
+
path.
|
|
1195
|
+
|
|
1196
|
+
The window now lowers to an inclusive `{$gte, $lte}` on the resolved field — the
|
|
1197
|
+
same shape `NativeSQLStrategy` binds as `BETWEEN` and the memory driver builds
|
|
1198
|
+
as a `$match` — so one dashboard reads the same on every driver. No storage
|
|
1199
|
+
coercion is applied here on purpose: unlike the raw-SQL path (which had to learn
|
|
1200
|
+
about SQLite's INTEGER epoch in #2034), this path goes through
|
|
1201
|
+
`engine.aggregate()`, where the driver's own CRUD filter coercion already
|
|
1202
|
+
handles a `where` bound on that same column.
|
|
1203
|
+
|
|
1204
|
+
**Same-field composition was fixed alongside it**, because the window makes it
|
|
1205
|
+
routine. Operands merged into one field entry by spreading, which silently kept
|
|
1206
|
+
whichever came last: a `where` bound and a window bound on `close_date` would
|
|
1207
|
+
have had one erase the other, and a `where` that names one field twice through
|
|
1208
|
+
`$and` (`{$and: [{stage: 'won'}, {stage: {$ne: 'lost'}}]}`) already lost its
|
|
1209
|
+
first operand today. Operands that name **different** operators still share one
|
|
1210
|
+
entry; colliding ones become their own `$and` conjunct, so the engine
|
|
1211
|
+
intersects them instead of the strategy picking a winner.
|
|
1212
|
+
|
|
1213
|
+
`generateSql()` renders the window as a parameterised `BETWEEN` to match — its
|
|
1214
|
+
comment previously explained why a `BETWEEN` was deliberately absent, which was
|
|
1215
|
+
correct only while `execute()` dropped the window. Bounds bind as `$n`
|
|
1216
|
+
placeholders, never inlined: the echoed statement travels to the browser.
|
|
1217
|
+
|
|
1218
|
+
A window on a **cross-object** time dimension is still rejected, and is now
|
|
1219
|
+
reported as the bucketing error it is rather than as the "cross-object filter"
|
|
1220
|
+
its lowered predicate would otherwise resemble. `execute()` and
|
|
1221
|
+
`/analytics/sql` continue to accept and reject the same set.
|
|
1222
|
+
|
|
1223
|
+
Relative-phrase ranges ("Last 7 days") are still not resolved on this path, and
|
|
1224
|
+
a bare-string `dateRange` degenerates to a single point — both matching
|
|
1225
|
+
`NativeSQLStrategy` exactly, rather than inventing a second interpretation for
|
|
1226
|
+
the driver-independent path.
|
|
1227
|
+
|
|
1228
|
+
- Updated dependencies [50616d9]
|
|
1229
|
+
- Updated dependencies [08b5a3d]
|
|
1230
|
+
- Updated dependencies [d99aeb3]
|
|
1231
|
+
- Updated dependencies [4727eb8]
|
|
1232
|
+
- Updated dependencies [f63cd09]
|
|
1233
|
+
- Updated dependencies [fa3d0cf]
|
|
1234
|
+
- Updated dependencies [af5a224]
|
|
1235
|
+
- Updated dependencies [71f76e1]
|
|
1236
|
+
- Updated dependencies [37b1346]
|
|
1237
|
+
- Updated dependencies [99736a0]
|
|
1238
|
+
- Updated dependencies [fe67e34]
|
|
1239
|
+
- Updated dependencies [fdb4f50]
|
|
1240
|
+
- Updated dependencies [1bd5652]
|
|
1241
|
+
- Updated dependencies [14252d3]
|
|
1242
|
+
- Updated dependencies [7fb436c]
|
|
1243
|
+
- Updated dependencies [879ea13]
|
|
1244
|
+
- Updated dependencies [201b31f]
|
|
1245
|
+
- Updated dependencies [e2616e0]
|
|
1246
|
+
- Updated dependencies [6fdc5c6]
|
|
1247
|
+
- Updated dependencies [8b9d71e]
|
|
1248
|
+
- Updated dependencies [33f5e23]
|
|
1249
|
+
- Updated dependencies [259af21]
|
|
1250
|
+
- Updated dependencies [587fc91]
|
|
1251
|
+
- Updated dependencies [1986594]
|
|
1252
|
+
- Updated dependencies [ad4af62]
|
|
1253
|
+
- Updated dependencies [d44dbfa]
|
|
1254
|
+
- Updated dependencies [474fe39]
|
|
1255
|
+
- Updated dependencies [0bc685a]
|
|
1256
|
+
- Updated dependencies [b949059]
|
|
1257
|
+
- Updated dependencies [be1c52c]
|
|
1258
|
+
- Updated dependencies [c5ff96d]
|
|
1259
|
+
- Updated dependencies [84e7be9]
|
|
1260
|
+
- Updated dependencies [a6c3f38]
|
|
1261
|
+
- Updated dependencies [debc23a]
|
|
1262
|
+
- Updated dependencies [0f8ad09]
|
|
1263
|
+
- Updated dependencies [8f9689f]
|
|
1264
|
+
- Updated dependencies [57a3bb3]
|
|
1265
|
+
- Updated dependencies [5f9a987]
|
|
1266
|
+
- Updated dependencies [db02d47]
|
|
1267
|
+
- Updated dependencies [0bfdf46]
|
|
1268
|
+
- Updated dependencies [376a061]
|
|
1269
|
+
- Updated dependencies [7c7e246]
|
|
1270
|
+
- Updated dependencies [f35cdc5]
|
|
1271
|
+
- Updated dependencies [9ea2bc5]
|
|
1272
|
+
- Updated dependencies [c2d9098]
|
|
1273
|
+
- Updated dependencies [a227ed7]
|
|
1274
|
+
- Updated dependencies [9613396]
|
|
1275
|
+
- Updated dependencies [e47b342]
|
|
1276
|
+
- Updated dependencies [4ed7ed4]
|
|
1277
|
+
- Updated dependencies [2fa4ca1]
|
|
1278
|
+
- Updated dependencies [f5a2320]
|
|
1279
|
+
- Updated dependencies [deb538f]
|
|
1280
|
+
- Updated dependencies [5b89711]
|
|
1281
|
+
- Updated dependencies [0c8a22f]
|
|
1282
|
+
- Updated dependencies [763931e]
|
|
1283
|
+
- Updated dependencies [de9af8a]
|
|
1284
|
+
- Updated dependencies [c4df271]
|
|
1285
|
+
- Updated dependencies [a41ba5c]
|
|
1286
|
+
- Updated dependencies [189854c]
|
|
1287
|
+
- Updated dependencies [0e3a226]
|
|
1288
|
+
- Updated dependencies [1d4756e]
|
|
1289
|
+
- Updated dependencies [720c5ad]
|
|
1290
|
+
- Updated dependencies [a8d1e24]
|
|
1291
|
+
- Updated dependencies [41642b0]
|
|
1292
|
+
- Updated dependencies [4cca74c]
|
|
1293
|
+
- Updated dependencies [88ef03e]
|
|
1294
|
+
- Updated dependencies [9e2caf3]
|
|
1295
|
+
- Updated dependencies [81ce41a]
|
|
1296
|
+
- Updated dependencies [85e1e4e]
|
|
1297
|
+
- Updated dependencies [dac6a08]
|
|
1298
|
+
- Updated dependencies [394b7a1]
|
|
1299
|
+
- Updated dependencies [677b591]
|
|
1300
|
+
- Updated dependencies [d77d1b7]
|
|
1301
|
+
- Updated dependencies [5b79a34]
|
|
1302
|
+
- Updated dependencies [c757854]
|
|
1303
|
+
- Updated dependencies [0045682]
|
|
1304
|
+
- Updated dependencies [2a5f04a]
|
|
1305
|
+
- Updated dependencies [4f740b0]
|
|
1306
|
+
- Updated dependencies [67452d1]
|
|
1307
|
+
- Updated dependencies [0fc6219]
|
|
1308
|
+
- Updated dependencies [605e190]
|
|
1309
|
+
- Updated dependencies [c6c59f1]
|
|
1310
|
+
- Updated dependencies [b0e78a8]
|
|
1311
|
+
- Updated dependencies [f31cc8d]
|
|
1312
|
+
- Updated dependencies [f343dc4]
|
|
1313
|
+
- Updated dependencies [8269e32]
|
|
1314
|
+
- Updated dependencies [74f7339]
|
|
1315
|
+
- Updated dependencies [a6c35a2]
|
|
1316
|
+
- Updated dependencies [c2f1002]
|
|
1317
|
+
- Updated dependencies [f163028]
|
|
1318
|
+
- Updated dependencies [f07808c]
|
|
1319
|
+
- Updated dependencies [7ffc3d3]
|
|
1320
|
+
- Updated dependencies [88346ba]
|
|
1321
|
+
- Updated dependencies [4631592]
|
|
1322
|
+
- Updated dependencies [32ff033]
|
|
1323
|
+
- Updated dependencies [5ac93d4]
|
|
1324
|
+
- Updated dependencies [93f267f]
|
|
1325
|
+
- Updated dependencies [0024abf]
|
|
1326
|
+
- Updated dependencies [acbf364]
|
|
1327
|
+
- Updated dependencies [7687f7b]
|
|
1328
|
+
- Updated dependencies [1659072]
|
|
1329
|
+
- Updated dependencies [abceb0d]
|
|
1330
|
+
- Updated dependencies [0c302a7]
|
|
1331
|
+
- Updated dependencies [6633337]
|
|
1332
|
+
- Updated dependencies [f00d8d4]
|
|
1333
|
+
- Updated dependencies [503be86]
|
|
1334
|
+
- Updated dependencies [cde1975]
|
|
1335
|
+
- Updated dependencies [0bc685a]
|
|
1336
|
+
- Updated dependencies [11949fc]
|
|
1337
|
+
- Updated dependencies [b098b0e]
|
|
1338
|
+
- Updated dependencies [4d00b13]
|
|
1339
|
+
- Updated dependencies [57bab76]
|
|
1340
|
+
- Updated dependencies [b90086a]
|
|
1341
|
+
- Updated dependencies [b95577a]
|
|
1342
|
+
- Updated dependencies [83c161f]
|
|
1343
|
+
- Updated dependencies [d8c4957]
|
|
1344
|
+
- Updated dependencies [f24cb83]
|
|
1345
|
+
- Updated dependencies [5dbbb92]
|
|
1346
|
+
- Updated dependencies [69f1dfd]
|
|
1347
|
+
- @objectstack/spec@17.0.0-rc.0
|
|
1348
|
+
- @objectstack/core@17.0.0-rc.0
|
|
1349
|
+
|
|
1350
|
+
## 16.1.0
|
|
1351
|
+
|
|
1352
|
+
### Patch Changes
|
|
1353
|
+
|
|
1354
|
+
- Updated dependencies [9e45b63]
|
|
1355
|
+
- Updated dependencies [b20201f]
|
|
1356
|
+
- @objectstack/spec@16.1.0
|
|
1357
|
+
- @objectstack/core@16.1.0
|
|
1358
|
+
|
|
1359
|
+
## 16.0.0
|
|
1360
|
+
|
|
1361
|
+
### Minor Changes
|
|
1362
|
+
|
|
1363
|
+
- a9459e6: Analytics drill metadata now snapshots raw grouped values for totals/subtotal rows too (#3214). The ADR-0021 D2 drill sidecar (`drillRawRows`, #2080) only covered `result.rows`, but the totals rows added in #1753 carry dimension values and go through the same label resolution — which overwrote their stored value (select option value, lookup/master_detail FK id) with the display label, leaving a subtotal drill nothing to exact-match on.
|
|
1364
|
+
|
|
1365
|
+
`queryDataset` now also emits `drillRawTotals`, aligned to `result.totals` by index (`drillRawTotals[i][j]` ↔ `result.totals[i].rows[j]`), captured in the same pre-label-resolution pass. Each map is restricted to the drillable dimensions the grouping actually groups by, so the grand-total grouping (`[]`) contributes an empty map per row. Purely additive result props (same as #2080) — no spec-contract change.
|
|
1366
|
+
|
|
1367
|
+
- dd9f223: feat(analytics): scope a datetime date-bucket drill to the reference-tz midnight instants (#1752 follow-up)
|
|
1368
|
+
|
|
1369
|
+
Closes the one gap left by the initial #1752 change: a `datetime` date dimension
|
|
1370
|
+
bucketed under a **non-UTC reference timezone** previously fell back to a superset
|
|
1371
|
+
drill (its bucket boundary is that tz's midnight _instant_, which `YYYY-MM-DD`
|
|
1372
|
+
calendar bounds can't express).
|
|
1373
|
+
|
|
1374
|
+
- **`@objectstack/core`** adds `zonedDateStartToUtcMs(ymd, tz)` — the UTC instant
|
|
1375
|
+
at which a calendar day begins in a reference timezone (the inverse of
|
|
1376
|
+
`calendarPartsInTz`). DST-safe: the offset is read from the platform tz
|
|
1377
|
+
database via `Intl`, with a two-pass resolution for the rare offset-boundary
|
|
1378
|
+
case; an unset/`'UTC'`/invalid zone returns plain UTC midnight.
|
|
1379
|
+
- **`@objectstack/service-analytics`** now emits `drillRanges` bounds per the
|
|
1380
|
+
field's temporal type (ADR-0053): a `datetime` field → ISO **instant** bounds
|
|
1381
|
+
at the reference tz's midnight (works under any tz, incl. DST); a `date` field
|
|
1382
|
+
→ `YYYY-MM-DD` calendar bounds (tz-naive, exact under any tz). An unknown field
|
|
1383
|
+
type is still emitted only under UTC and omitted (superset) under a non-UTC tz.
|
|
1384
|
+
|
|
1385
|
+
No objectui change is needed — the client already forwards whatever bound values
|
|
1386
|
+
the server sends into the drill filter and the `filter[field][gte|lt]` URL.
|
|
1387
|
+
|
|
1388
|
+
- 290e2f0: feat(analytics): emit a half-open date-range drill scope for granularity-bucketed date dimensions (#1752)
|
|
1389
|
+
|
|
1390
|
+
A report/dashboard cell grouped by a `dateGranularity` date dimension ("2026-Q2")
|
|
1391
|
+
covers a SPAN of records, so drilling it needs a range (`>= start AND < nextStart`),
|
|
1392
|
+
which the equality drill contract (`drillRawRows`) can't express — date dims were
|
|
1393
|
+
therefore excluded from drill metadata and a drill landed on an unscoped superset.
|
|
1394
|
+
|
|
1395
|
+
- **`@objectstack/core`** adds `bucketKeyToCalendarRange(key, granularity)`, the
|
|
1396
|
+
inverse of `bucketDateValue`: it turns a canonical bucket key into its half-open
|
|
1397
|
+
`[start, end)` calendar span (`YYYY-MM-DD`, `end` exclusive). Pure, timezone-naive
|
|
1398
|
+
calendar arithmetic; returns `null` for unbucketable / out-of-range keys so the
|
|
1399
|
+
caller falls back to an unscoped (superset) drill rather than emit a wrong bound.
|
|
1400
|
+
- **`@objectstack/service-analytics`** emits a `drillRanges` sidecar (aligned to
|
|
1401
|
+
`rows` by index — the range companion to `drillRawRows`) for `date` +
|
|
1402
|
+
`dateGranularity` dimensions, computed from the canonical bucket key in the
|
|
1403
|
+
pre-label-resolution snapshot pass. A `datetime` field under a non-UTC reference
|
|
1404
|
+
timezone is omitted (host drills a superset) until instant-boundary support
|
|
1405
|
+
lands; a tz-naive `date` field is exact under any timezone (ADR-0053).
|
|
1406
|
+
|
|
1407
|
+
Consumed by objectui's report drill-through to scope the drilled record list to the
|
|
1408
|
+
clicked time bucket.
|
|
1409
|
+
|
|
1410
|
+
### Patch Changes
|
|
1411
|
+
|
|
1412
|
+
- Updated dependencies [f972574]
|
|
1413
|
+
- Updated dependencies [6289ec3]
|
|
1414
|
+
- Updated dependencies [22013aa]
|
|
1415
|
+
- Updated dependencies [3ad3dd5]
|
|
1416
|
+
- Updated dependencies [8efa395]
|
|
1417
|
+
- Updated dependencies [3a18b60]
|
|
1418
|
+
- Updated dependencies [a8aa34c]
|
|
1419
|
+
- Updated dependencies [e057f42]
|
|
1420
|
+
- Updated dependencies [a3823b2]
|
|
1421
|
+
- Updated dependencies [43a3efb]
|
|
1422
|
+
- Updated dependencies [524696a]
|
|
1423
|
+
- Updated dependencies [bfa3c3f]
|
|
1424
|
+
- Updated dependencies [5e3301d]
|
|
1425
|
+
- Updated dependencies [dd9f223]
|
|
1426
|
+
- Updated dependencies [46e876c]
|
|
1427
|
+
- Updated dependencies [5f05de2]
|
|
1428
|
+
- Updated dependencies [021ba4c]
|
|
1429
|
+
- Updated dependencies [158aa14]
|
|
1430
|
+
- Updated dependencies [62a2117]
|
|
1431
|
+
- Updated dependencies [d2723e2]
|
|
1432
|
+
- Updated dependencies [fefcd54]
|
|
1433
|
+
- Updated dependencies [beaf2de]
|
|
1434
|
+
- Updated dependencies [369eb6e]
|
|
1435
|
+
- Updated dependencies [06ff734]
|
|
1436
|
+
- Updated dependencies [b659111]
|
|
1437
|
+
- Updated dependencies [5754a23]
|
|
1438
|
+
- Updated dependencies [6c270a6]
|
|
1439
|
+
- Updated dependencies [290e2f0]
|
|
1440
|
+
- Updated dependencies [668dd17]
|
|
1441
|
+
- Updated dependencies [8abf133]
|
|
1442
|
+
- Updated dependencies [e0859b1]
|
|
1443
|
+
- Updated dependencies [04ecd4e]
|
|
1444
|
+
- Updated dependencies [4d5a892]
|
|
1445
|
+
- Updated dependencies [16cebeb]
|
|
1446
|
+
- Updated dependencies [86d30af]
|
|
1447
|
+
- Updated dependencies [8923843]
|
|
1448
|
+
- Updated dependencies [a2795f6]
|
|
1449
|
+
- Updated dependencies [f16b492]
|
|
1450
|
+
- Updated dependencies [4b6fde8]
|
|
1451
|
+
- Updated dependencies [2018df9]
|
|
1452
|
+
- Updated dependencies [fc5a3a2]
|
|
1453
|
+
- Updated dependencies [8ff9210]
|
|
1454
|
+
- @objectstack/spec@16.0.0
|
|
1455
|
+
- @objectstack/core@16.0.0
|
|
1456
|
+
|
|
1457
|
+
## 16.0.0-rc.1
|
|
1458
|
+
|
|
1459
|
+
### Patch Changes
|
|
1460
|
+
|
|
1461
|
+
- Updated dependencies [6289ec3]
|
|
1462
|
+
- Updated dependencies [8efa395]
|
|
1463
|
+
- Updated dependencies [bfa3c3f]
|
|
1464
|
+
- Updated dependencies [62a2117]
|
|
1465
|
+
- Updated dependencies [06ff734]
|
|
1466
|
+
- @objectstack/spec@16.0.0-rc.1
|
|
1467
|
+
- @objectstack/core@16.0.0-rc.1
|
|
1468
|
+
|
|
1469
|
+
## 16.0.0-rc.0
|
|
1470
|
+
|
|
1471
|
+
### Minor Changes
|
|
1472
|
+
|
|
1473
|
+
- a9459e6: Analytics drill metadata now snapshots raw grouped values for totals/subtotal rows too (#3214). The ADR-0021 D2 drill sidecar (`drillRawRows`, #2080) only covered `result.rows`, but the totals rows added in #1753 carry dimension values and go through the same label resolution — which overwrote their stored value (select option value, lookup/master_detail FK id) with the display label, leaving a subtotal drill nothing to exact-match on.
|
|
1474
|
+
|
|
1475
|
+
`queryDataset` now also emits `drillRawTotals`, aligned to `result.totals` by index (`drillRawTotals[i][j]` ↔ `result.totals[i].rows[j]`), captured in the same pre-label-resolution pass. Each map is restricted to the drillable dimensions the grouping actually groups by, so the grand-total grouping (`[]`) contributes an empty map per row. Purely additive result props (same as #2080) — no spec-contract change.
|
|
1476
|
+
|
|
1477
|
+
- dd9f223: feat(analytics): scope a datetime date-bucket drill to the reference-tz midnight instants (#1752 follow-up)
|
|
1478
|
+
|
|
1479
|
+
Closes the one gap left by the initial #1752 change: a `datetime` date dimension
|
|
1480
|
+
bucketed under a **non-UTC reference timezone** previously fell back to a superset
|
|
1481
|
+
drill (its bucket boundary is that tz's midnight _instant_, which `YYYY-MM-DD`
|
|
1482
|
+
calendar bounds can't express).
|
|
1483
|
+
|
|
1484
|
+
- **`@objectstack/core`** adds `zonedDateStartToUtcMs(ymd, tz)` — the UTC instant
|
|
1485
|
+
at which a calendar day begins in a reference timezone (the inverse of
|
|
1486
|
+
`calendarPartsInTz`). DST-safe: the offset is read from the platform tz
|
|
1487
|
+
database via `Intl`, with a two-pass resolution for the rare offset-boundary
|
|
1488
|
+
case; an unset/`'UTC'`/invalid zone returns plain UTC midnight.
|
|
1489
|
+
- **`@objectstack/service-analytics`** now emits `drillRanges` bounds per the
|
|
1490
|
+
field's temporal type (ADR-0053): a `datetime` field → ISO **instant** bounds
|
|
1491
|
+
at the reference tz's midnight (works under any tz, incl. DST); a `date` field
|
|
1492
|
+
→ `YYYY-MM-DD` calendar bounds (tz-naive, exact under any tz). An unknown field
|
|
1493
|
+
type is still emitted only under UTC and omitted (superset) under a non-UTC tz.
|
|
1494
|
+
|
|
1495
|
+
No objectui change is needed — the client already forwards whatever bound values
|
|
1496
|
+
the server sends into the drill filter and the `filter[field][gte|lt]` URL.
|
|
1497
|
+
|
|
1498
|
+
- 290e2f0: feat(analytics): emit a half-open date-range drill scope for granularity-bucketed date dimensions (#1752)
|
|
1499
|
+
|
|
1500
|
+
A report/dashboard cell grouped by a `dateGranularity` date dimension ("2026-Q2")
|
|
1501
|
+
covers a SPAN of records, so drilling it needs a range (`>= start AND < nextStart`),
|
|
1502
|
+
which the equality drill contract (`drillRawRows`) can't express — date dims were
|
|
1503
|
+
therefore excluded from drill metadata and a drill landed on an unscoped superset.
|
|
1504
|
+
|
|
1505
|
+
- **`@objectstack/core`** adds `bucketKeyToCalendarRange(key, granularity)`, the
|
|
1506
|
+
inverse of `bucketDateValue`: it turns a canonical bucket key into its half-open
|
|
1507
|
+
`[start, end)` calendar span (`YYYY-MM-DD`, `end` exclusive). Pure, timezone-naive
|
|
1508
|
+
calendar arithmetic; returns `null` for unbucketable / out-of-range keys so the
|
|
1509
|
+
caller falls back to an unscoped (superset) drill rather than emit a wrong bound.
|
|
1510
|
+
- **`@objectstack/service-analytics`** emits a `drillRanges` sidecar (aligned to
|
|
1511
|
+
`rows` by index — the range companion to `drillRawRows`) for `date` +
|
|
1512
|
+
`dateGranularity` dimensions, computed from the canonical bucket key in the
|
|
1513
|
+
pre-label-resolution snapshot pass. A `datetime` field under a non-UTC reference
|
|
1514
|
+
timezone is omitted (host drills a superset) until instant-boundary support
|
|
1515
|
+
lands; a tz-naive `date` field is exact under any timezone (ADR-0053).
|
|
1516
|
+
|
|
1517
|
+
Consumed by objectui's report drill-through to scope the drilled record list to the
|
|
1518
|
+
clicked time bucket.
|
|
1519
|
+
|
|
1520
|
+
### Patch Changes
|
|
1521
|
+
|
|
1522
|
+
- Updated dependencies [f972574]
|
|
1523
|
+
- Updated dependencies [22013aa]
|
|
1524
|
+
- Updated dependencies [3ad3dd5]
|
|
1525
|
+
- Updated dependencies [3a18b60]
|
|
1526
|
+
- Updated dependencies [a8aa34c]
|
|
1527
|
+
- Updated dependencies [e057f42]
|
|
1528
|
+
- Updated dependencies [a3823b2]
|
|
1529
|
+
- Updated dependencies [43a3efb]
|
|
1530
|
+
- Updated dependencies [524696a]
|
|
1531
|
+
- Updated dependencies [5e3301d]
|
|
1532
|
+
- Updated dependencies [dd9f223]
|
|
1533
|
+
- Updated dependencies [46e876c]
|
|
1534
|
+
- Updated dependencies [5f05de2]
|
|
1535
|
+
- Updated dependencies [021ba4c]
|
|
1536
|
+
- Updated dependencies [158aa14]
|
|
1537
|
+
- Updated dependencies [d2723e2]
|
|
1538
|
+
- Updated dependencies [fefcd54]
|
|
1539
|
+
- Updated dependencies [beaf2de]
|
|
1540
|
+
- Updated dependencies [369eb6e]
|
|
1541
|
+
- Updated dependencies [b659111]
|
|
1542
|
+
- Updated dependencies [5754a23]
|
|
1543
|
+
- Updated dependencies [6c270a6]
|
|
1544
|
+
- Updated dependencies [290e2f0]
|
|
1545
|
+
- Updated dependencies [668dd17]
|
|
1546
|
+
- Updated dependencies [8abf133]
|
|
1547
|
+
- Updated dependencies [e0859b1]
|
|
1548
|
+
- Updated dependencies [04ecd4e]
|
|
1549
|
+
- Updated dependencies [4d5a892]
|
|
1550
|
+
- Updated dependencies [16cebeb]
|
|
1551
|
+
- Updated dependencies [86d30af]
|
|
1552
|
+
- Updated dependencies [8923843]
|
|
1553
|
+
- Updated dependencies [a2795f6]
|
|
1554
|
+
- Updated dependencies [f16b492]
|
|
1555
|
+
- Updated dependencies [4b6fde8]
|
|
1556
|
+
- Updated dependencies [2018df9]
|
|
1557
|
+
- Updated dependencies [fc5a3a2]
|
|
1558
|
+
- @objectstack/spec@16.0.0-rc.0
|
|
1559
|
+
- @objectstack/core@16.0.0-rc.0
|
|
1560
|
+
|
|
1561
|
+
## 15.1.1
|
|
1562
|
+
|
|
1563
|
+
### Patch Changes
|
|
1564
|
+
|
|
1565
|
+
- @objectstack/spec@15.1.1
|
|
1566
|
+
- @objectstack/core@15.1.1
|
|
1567
|
+
|
|
1568
|
+
## 15.1.0
|
|
1569
|
+
|
|
1570
|
+
### Patch Changes
|
|
1571
|
+
|
|
1572
|
+
- Updated dependencies [f531a26]
|
|
1573
|
+
- Updated dependencies [f531a26]
|
|
1574
|
+
- Updated dependencies [f531a26]
|
|
1575
|
+
- Updated dependencies [f531a26]
|
|
1576
|
+
- Updated dependencies [f531a26]
|
|
1577
|
+
- Updated dependencies [f531a26]
|
|
1578
|
+
- Updated dependencies [3fe9df1]
|
|
1579
|
+
- Updated dependencies [f531a26]
|
|
1580
|
+
- Updated dependencies [f531a26]
|
|
1581
|
+
- Updated dependencies [f531a26]
|
|
1582
|
+
- Updated dependencies [f531a26]
|
|
1583
|
+
- Updated dependencies [f531a26]
|
|
1584
|
+
- Updated dependencies [f531a26]
|
|
1585
|
+
- Updated dependencies [f531a26]
|
|
1586
|
+
- Updated dependencies [f531a26]
|
|
1587
|
+
- Updated dependencies [f531a26]
|
|
1588
|
+
- Updated dependencies [f531a26]
|
|
1589
|
+
- Updated dependencies [f531a26]
|
|
1590
|
+
- Updated dependencies [f531a26]
|
|
1591
|
+
- Updated dependencies [4109153]
|
|
1592
|
+
- Updated dependencies [f531a26]
|
|
1593
|
+
- Updated dependencies [f531a26]
|
|
1594
|
+
- Updated dependencies [f531a26]
|
|
1595
|
+
- Updated dependencies [f531a26]
|
|
1596
|
+
- Updated dependencies [f531a26]
|
|
1597
|
+
- Updated dependencies [f531a26]
|
|
1598
|
+
- Updated dependencies [627f225]
|
|
1599
|
+
- Updated dependencies [f531a26]
|
|
1600
|
+
- Updated dependencies [f531a26]
|
|
1601
|
+
- Updated dependencies [f531a26]
|
|
1602
|
+
- @objectstack/spec@15.1.0
|
|
1603
|
+
- @objectstack/core@15.1.0
|
|
1604
|
+
|
|
1605
|
+
## 15.0.0
|
|
1606
|
+
|
|
1607
|
+
### Patch Changes
|
|
1608
|
+
|
|
1609
|
+
- Updated dependencies [28b7c28]
|
|
1610
|
+
- Updated dependencies [13749ec]
|
|
1611
|
+
- Updated dependencies [e62c233]
|
|
1612
|
+
- Updated dependencies [ed61c9b]
|
|
1613
|
+
- Updated dependencies [31d04d4]
|
|
1614
|
+
- @objectstack/spec@15.0.0
|
|
1615
|
+
- @objectstack/core@15.0.0
|
|
1616
|
+
|
|
1617
|
+
## 14.8.0
|
|
1618
|
+
|
|
1619
|
+
### Patch Changes
|
|
1620
|
+
|
|
1621
|
+
- Updated dependencies [16b4bf6]
|
|
1622
|
+
- Updated dependencies [16b4bf6]
|
|
1623
|
+
- Updated dependencies [10e8983]
|
|
1624
|
+
- Updated dependencies [607aaf4]
|
|
1625
|
+
- Updated dependencies [bb71321]
|
|
1626
|
+
- @objectstack/spec@14.8.0
|
|
1627
|
+
- @objectstack/core@14.8.0
|
|
1628
|
+
|
|
1629
|
+
## 14.7.0
|
|
1630
|
+
|
|
1631
|
+
### Patch Changes
|
|
1632
|
+
|
|
1633
|
+
- Updated dependencies [d6a72eb]
|
|
1634
|
+
- @objectstack/spec@14.7.0
|
|
1635
|
+
- @objectstack/core@14.7.0
|
|
1636
|
+
|
|
1637
|
+
## 14.6.0
|
|
1638
|
+
|
|
1639
|
+
### Patch Changes
|
|
1640
|
+
|
|
1641
|
+
- Updated dependencies [609cb13]
|
|
1642
|
+
- Updated dependencies [ce6d151]
|
|
1643
|
+
- @objectstack/spec@14.6.0
|
|
1644
|
+
- @objectstack/core@14.6.0
|
|
1645
|
+
|
|
1646
|
+
## 14.5.0
|
|
1647
|
+
|
|
1648
|
+
### Patch Changes
|
|
1649
|
+
|
|
1650
|
+
- Updated dependencies [526805e]
|
|
1651
|
+
- Updated dependencies [d79ca07]
|
|
1652
|
+
- Updated dependencies [33ebd34]
|
|
1653
|
+
- Updated dependencies [c044f08]
|
|
1654
|
+
- Updated dependencies [01274eb]
|
|
1655
|
+
- @objectstack/spec@14.5.0
|
|
1656
|
+
- @objectstack/core@14.5.0
|
|
1657
|
+
|
|
1658
|
+
## 14.4.0
|
|
1659
|
+
|
|
1660
|
+
### Patch Changes
|
|
1661
|
+
|
|
1662
|
+
- Updated dependencies [7953832]
|
|
1663
|
+
- Updated dependencies [82e745e]
|
|
1664
|
+
- Updated dependencies [f3035bd]
|
|
1665
|
+
- Updated dependencies [82c0d94]
|
|
1666
|
+
- Updated dependencies [7449476]
|
|
1667
|
+
- @objectstack/spec@14.4.0
|
|
1668
|
+
- @objectstack/core@14.4.0
|
|
1669
|
+
|
|
1670
|
+
## 14.3.0
|
|
1671
|
+
|
|
1672
|
+
### Patch Changes
|
|
1673
|
+
|
|
1674
|
+
- Updated dependencies [2a71f48]
|
|
1675
|
+
- Updated dependencies [02f6af4]
|
|
1676
|
+
- Updated dependencies [c1064f1]
|
|
1677
|
+
- @objectstack/spec@14.3.0
|
|
1678
|
+
- @objectstack/core@14.3.0
|
|
1679
|
+
|
|
1680
|
+
## 14.2.0
|
|
1681
|
+
|
|
1682
|
+
### Patch Changes
|
|
1683
|
+
|
|
1684
|
+
- Updated dependencies [ac8f029]
|
|
1685
|
+
- Updated dependencies [4ab9958]
|
|
1686
|
+
- @objectstack/spec@14.2.0
|
|
1687
|
+
- @objectstack/core@14.2.0
|
|
1688
|
+
|
|
1689
|
+
## 14.1.0
|
|
1690
|
+
|
|
1691
|
+
### Patch Changes
|
|
1692
|
+
|
|
1693
|
+
- Updated dependencies [5a8465f]
|
|
1694
|
+
- Updated dependencies [7f8620b]
|
|
1695
|
+
- Updated dependencies [82ba3a6]
|
|
1696
|
+
- @objectstack/spec@14.1.0
|
|
1697
|
+
- @objectstack/core@14.1.0
|
|
1698
|
+
|
|
1699
|
+
## 14.0.0
|
|
1700
|
+
|
|
1701
|
+
### Patch Changes
|
|
1702
|
+
|
|
1703
|
+
- Updated dependencies [0a8e685]
|
|
1704
|
+
- Updated dependencies [afa8115]
|
|
1705
|
+
- Updated dependencies [80f12ca]
|
|
1706
|
+
- Updated dependencies [e2fa074]
|
|
1707
|
+
- Updated dependencies [23c8668]
|
|
1708
|
+
- Updated dependencies [29f017d]
|
|
1709
|
+
- Updated dependencies [216fa9a]
|
|
1710
|
+
- Updated dependencies [6c22b12]
|
|
1711
|
+
- @objectstack/spec@14.0.0
|
|
1712
|
+
- @objectstack/core@14.0.0
|
|
1713
|
+
|
|
1714
|
+
## 13.0.0
|
|
1715
|
+
|
|
1716
|
+
### Patch Changes
|
|
1717
|
+
|
|
1718
|
+
- Updated dependencies [6d83431]
|
|
1719
|
+
- Updated dependencies [01917c2]
|
|
1720
|
+
- Updated dependencies [b271691]
|
|
1721
|
+
- Updated dependencies [a5a1e41]
|
|
1722
|
+
- Updated dependencies [466adf6]
|
|
1723
|
+
- Updated dependencies [5be00c3]
|
|
1724
|
+
- Updated dependencies [466adf6]
|
|
1725
|
+
- Updated dependencies [2bee609]
|
|
1726
|
+
- Updated dependencies [fc7e7f7]
|
|
1727
|
+
- @objectstack/spec@13.0.0
|
|
1728
|
+
- @objectstack/core@13.0.0
|
|
1729
|
+
|
|
1730
|
+
## 12.6.0
|
|
1731
|
+
|
|
1732
|
+
### Patch Changes
|
|
1733
|
+
|
|
1734
|
+
- Updated dependencies [6cebf22]
|
|
1735
|
+
- Updated dependencies [21420d9]
|
|
1736
|
+
- @objectstack/spec@12.6.0
|
|
1737
|
+
- @objectstack/core@12.6.0
|
|
1738
|
+
|
|
1739
|
+
## 12.5.0
|
|
1740
|
+
|
|
1741
|
+
### Patch Changes
|
|
1742
|
+
|
|
1743
|
+
- Updated dependencies [8b3d363]
|
|
1744
|
+
- @objectstack/spec@12.5.0
|
|
1745
|
+
- @objectstack/core@12.5.0
|
|
1746
|
+
|
|
1747
|
+
## 12.4.0
|
|
1748
|
+
|
|
1749
|
+
### Patch Changes
|
|
1750
|
+
|
|
1751
|
+
- Updated dependencies [60dc3ba]
|
|
1752
|
+
- @objectstack/spec@12.4.0
|
|
1753
|
+
- @objectstack/core@12.4.0
|
|
1754
|
+
|
|
1755
|
+
## 12.3.0
|
|
1756
|
+
|
|
1757
|
+
### Patch Changes
|
|
1758
|
+
|
|
1759
|
+
- Updated dependencies [e7eceec]
|
|
1760
|
+
- @objectstack/spec@12.3.0
|
|
1761
|
+
- @objectstack/core@12.3.0
|
|
1762
|
+
|
|
1763
|
+
## 12.2.0
|
|
1764
|
+
|
|
1765
|
+
### Patch Changes
|
|
1766
|
+
|
|
1767
|
+
- Updated dependencies [fce8ff4]
|
|
1768
|
+
- Updated dependencies [3962023]
|
|
1769
|
+
- Updated dependencies [2bb193d]
|
|
1770
|
+
- Updated dependencies [0426d27]
|
|
1771
|
+
- Updated dependencies [da807f7]
|
|
1772
|
+
- Updated dependencies [4f5b791]
|
|
1773
|
+
- @objectstack/spec@12.2.0
|
|
1774
|
+
- @objectstack/core@12.2.0
|
|
1775
|
+
|
|
1776
|
+
## 12.1.0
|
|
1777
|
+
|
|
1778
|
+
### Patch Changes
|
|
1779
|
+
|
|
1780
|
+
- Updated dependencies [93e6d02]
|
|
1781
|
+
- @objectstack/spec@12.1.0
|
|
1782
|
+
- @objectstack/core@12.1.0
|
|
1783
|
+
|
|
1784
|
+
## 12.0.0
|
|
1785
|
+
|
|
1786
|
+
### Patch Changes
|
|
1787
|
+
|
|
1788
|
+
- Updated dependencies [a8df396]
|
|
1789
|
+
- Updated dependencies [e695fe0]
|
|
1790
|
+
- Updated dependencies [7c09621]
|
|
1791
|
+
- Updated dependencies [7709db4]
|
|
1792
|
+
- Updated dependencies [2082109]
|
|
1793
|
+
- Updated dependencies [7c09621]
|
|
1794
|
+
- Updated dependencies [9860de4]
|
|
1795
|
+
- Updated dependencies [069c205]
|
|
1796
|
+
- @objectstack/spec@12.0.0
|
|
1797
|
+
- @objectstack/core@12.0.0
|
|
1798
|
+
|
|
1799
|
+
## 11.10.0
|
|
1800
|
+
|
|
1801
|
+
### Patch Changes
|
|
1802
|
+
|
|
1803
|
+
- Updated dependencies [6a9397e]
|
|
1804
|
+
- Updated dependencies [c0efe5d]
|
|
1805
|
+
- @objectstack/spec@11.10.0
|
|
1806
|
+
- @objectstack/core@11.10.0
|
|
1807
|
+
|
|
1808
|
+
## 11.9.0
|
|
1809
|
+
|
|
1810
|
+
### Patch Changes
|
|
1811
|
+
|
|
1812
|
+
- Updated dependencies [d3595d9]
|
|
1813
|
+
- @objectstack/spec@11.9.0
|
|
1814
|
+
- @objectstack/core@11.9.0
|
|
1815
|
+
|
|
1816
|
+
## 11.8.0
|
|
1817
|
+
|
|
1818
|
+
### Patch Changes
|
|
1819
|
+
|
|
1820
|
+
- @objectstack/spec@11.8.0
|
|
1821
|
+
- @objectstack/core@11.8.0
|
|
1822
|
+
|
|
1823
|
+
## 11.7.0
|
|
1824
|
+
|
|
1825
|
+
### Patch Changes
|
|
1826
|
+
|
|
1827
|
+
- Updated dependencies [5178906]
|
|
1828
|
+
- @objectstack/spec@11.7.0
|
|
1829
|
+
- @objectstack/core@11.7.0
|
|
1830
|
+
|
|
1831
|
+
## 11.6.0
|
|
1832
|
+
|
|
1833
|
+
### Patch Changes
|
|
1834
|
+
|
|
1835
|
+
- @objectstack/spec@11.6.0
|
|
1836
|
+
- @objectstack/core@11.6.0
|
|
1837
|
+
|
|
1838
|
+
## 11.5.0
|
|
1839
|
+
|
|
1840
|
+
### Patch Changes
|
|
1841
|
+
|
|
1842
|
+
- Updated dependencies [6ee4f04]
|
|
1843
|
+
- Updated dependencies [c1e3a65]
|
|
1844
|
+
- @objectstack/spec@11.5.0
|
|
1845
|
+
- @objectstack/core@11.5.0
|
|
1846
|
+
|
|
1847
|
+
## 11.4.0
|
|
1848
|
+
|
|
1849
|
+
### Patch Changes
|
|
1850
|
+
|
|
1851
|
+
- Updated dependencies [5821c51]
|
|
1852
|
+
- Updated dependencies [a0fce3f]
|
|
1853
|
+
- @objectstack/spec@11.4.0
|
|
1854
|
+
- @objectstack/core@11.4.0
|
|
1855
|
+
|
|
1856
|
+
## 11.3.0
|
|
1857
|
+
|
|
1858
|
+
### Patch Changes
|
|
1859
|
+
|
|
1860
|
+
- Updated dependencies [58e8e31]
|
|
1861
|
+
- Updated dependencies [b4a5df0]
|
|
1862
|
+
- @objectstack/spec@11.3.0
|
|
1863
|
+
- @objectstack/core@11.3.0
|
|
1864
|
+
|
|
1865
|
+
## 11.2.0
|
|
1866
|
+
|
|
1867
|
+
### Patch Changes
|
|
1868
|
+
|
|
1869
|
+
- Updated dependencies [d0f4b13]
|
|
1870
|
+
- Updated dependencies [302bdab]
|
|
1871
|
+
- @objectstack/spec@11.2.0
|
|
1872
|
+
- @objectstack/core@11.2.0
|
|
1873
|
+
|
|
1874
|
+
## 11.1.0
|
|
1875
|
+
|
|
1876
|
+
### Patch Changes
|
|
1877
|
+
|
|
1878
|
+
- Updated dependencies [ce0b4f6]
|
|
1879
|
+
- Updated dependencies [9ccfcd6]
|
|
1880
|
+
- Updated dependencies [ecf193f]
|
|
1881
|
+
- Updated dependencies [51bec81]
|
|
1882
|
+
- Updated dependencies [3e593a7]
|
|
1883
|
+
- Updated dependencies [63d5403]
|
|
1884
|
+
- @objectstack/core@11.1.0
|
|
1885
|
+
- @objectstack/spec@11.1.0
|
|
1886
|
+
|
|
1887
|
+
## 11.0.0
|
|
1888
|
+
|
|
1889
|
+
### Minor Changes
|
|
1890
|
+
|
|
1891
|
+
- 5eef4cf: feat(analytics): multi-hop relationship joins for datasets (ADR-0071)
|
|
1892
|
+
|
|
1893
|
+
A dataset's `include` and dimension/measure `field` paths may now traverse up to
|
|
1894
|
+
3 to-one relationship hops (`account.owner.region`), not just one. The compiler
|
|
1895
|
+
expands each declared path into the ordered join chain (one `cube.join` per path
|
|
1896
|
+
prefix, aliased dot-free as `account__owner` so it stays a single valid SQL
|
|
1897
|
+
identifier), and the NativeSQLStrategy emits the chained `LEFT JOIN`s. Per-hop
|
|
1898
|
+
tenant/RLS read-scope is enforced for EVERY object in the chain — the
|
|
1899
|
+
alias-driven scope loop already generalizes, so no security path is rewritten.
|
|
1900
|
+
|
|
1901
|
+
Restricted to **to-one** (lookup / master_detail) relationships, which never fan
|
|
1902
|
+
out — aggregates stay correct with no symmetric-aggregate machinery; to-many
|
|
1903
|
+
traversal is out of scope. Single-hop datasets are byte-for-byte unchanged (the
|
|
1904
|
+
dot-free alias is a no-op for a single segment). Undeclared paths are still
|
|
1905
|
+
rejected (ADR-0021 D-C); paths beyond 3 hops are rejected at both parse and
|
|
1906
|
+
compile time.
|
|
1907
|
+
|
|
1908
|
+
### Patch Changes
|
|
1909
|
+
|
|
1910
|
+
- 910a8f0: fix(analytics): compare boolean filters/group-by against the real boolean, not stringified '1'
|
|
1911
|
+
|
|
1912
|
+
The analytics filter normalizer stringified boolean `true` → `'1'`, which the
|
|
1913
|
+
ObjectQL strategy then coerced back to the number `1` before calling
|
|
1914
|
+
`engine.aggregate`. Boolean fields hold a real `true`/`false`, so `1 !== true`
|
|
1915
|
+
never matched: a metric widget filtered on a boolean field (e.g.
|
|
1916
|
+
`{ is_critical: true }`) always returned 0, and pie/donut/bar charts grouped by
|
|
1917
|
+
a boolean dimension failed to bucket. `stringifyForCube` now serializes booleans
|
|
1918
|
+
as the tokens `'true'`/`'false'`, and a new `coerceFilterValueForObjectQL`
|
|
1919
|
+
recovers a real boolean for the ObjectQL engine while the SQL path keeps binding
|
|
1920
|
+
`1`/`0` (better-sqlite3 cannot bind a JS boolean).
|
|
1921
|
+
|
|
1922
|
+
- 715d667: fix(analytics): qualify base-object columns in joined dataset queries
|
|
1923
|
+
|
|
1924
|
+
A dataset that joins a related object (`include` + a `relationship.field`
|
|
1925
|
+
dimension/measure) emitted BARE base-table columns in SELECT/GROUP BY while the
|
|
1926
|
+
joined columns were alias-qualified. When the base and joined tables share a
|
|
1927
|
+
column name (e.g. both have `status`), the query failed at runtime with
|
|
1928
|
+
"ambiguous column name". `NativeSQLStrategy` now qualifies plain base-column
|
|
1929
|
+
identifiers with the base table when the cube has joins; single-object cubes
|
|
1930
|
+
are unchanged (byte-for-byte identical SQL).
|
|
1931
|
+
|
|
1932
|
+
- Updated dependencies [ab5718a]
|
|
1933
|
+
- Updated dependencies [4845c12]
|
|
1934
|
+
- Updated dependencies [c1a754a]
|
|
1935
|
+
- Updated dependencies [6fbe91f]
|
|
1936
|
+
- Updated dependencies [715d667]
|
|
1937
|
+
- Updated dependencies [5eef4cf]
|
|
1938
|
+
- Updated dependencies [72759e1]
|
|
1939
|
+
- Updated dependencies [6c4fbd9]
|
|
1940
|
+
- Updated dependencies [ef3ed67]
|
|
1941
|
+
- Updated dependencies [cd51229]
|
|
1942
|
+
- Updated dependencies [7697a0e]
|
|
1943
|
+
- Updated dependencies [e7e04f1]
|
|
1944
|
+
- Updated dependencies [cfd5ac4]
|
|
1945
|
+
- Updated dependencies [2be5c1f]
|
|
1946
|
+
- Updated dependencies [ad143ce]
|
|
1947
|
+
- Updated dependencies [5c4a8c8]
|
|
1948
|
+
- Updated dependencies [3afaeed]
|
|
1949
|
+
- Updated dependencies [8801c02]
|
|
1950
|
+
- Updated dependencies [3d04e06]
|
|
1951
|
+
- Updated dependencies [4a84c98]
|
|
1952
|
+
- Updated dependencies [c715d25]
|
|
1953
|
+
- Updated dependencies [aa33b02]
|
|
1954
|
+
- Updated dependencies [d980f0d]
|
|
1955
|
+
- Updated dependencies [a658523]
|
|
1956
|
+
- Updated dependencies [82ff91c]
|
|
1957
|
+
- Updated dependencies [638f472]
|
|
1958
|
+
- @objectstack/spec@11.0.0
|
|
1959
|
+
- @objectstack/core@11.0.0
|
|
1960
|
+
|
|
1961
|
+
## 10.3.0
|
|
1962
|
+
|
|
1963
|
+
### Patch Changes
|
|
1964
|
+
|
|
1965
|
+
- f73d40a: fix(analytics): log scalar auto-inferred cubes at debug, not warn
|
|
1966
|
+
|
|
1967
|
+
Scalar metric queries (measures only, no `dimensions`/`timeDimensions`) over an
|
|
1968
|
+
unregistered cube — the first-class `object-metric` "metric over an object" path
|
|
1969
|
+
— auto-infer a trivial count/sum cube by design. That auto-infer now logs at
|
|
1970
|
+
`debug` instead of `warn`, so boot/render no longer spams
|
|
1971
|
+
`No cube registered for "..."` for a non-problem. Grouped queries (explicit
|
|
1972
|
+
dimension / time bucket) over an unregistered cube keep the `warn`, where a
|
|
1973
|
+
forgotten cube registration is a real mistake.
|
|
1974
|
+
|
|
1975
|
+
- @objectstack/spec@10.3.0
|
|
1976
|
+
- @objectstack/core@10.3.0
|
|
1977
|
+
|
|
1978
|
+
## 10.2.0
|
|
1979
|
+
|
|
1980
|
+
### Patch Changes
|
|
1981
|
+
|
|
1982
|
+
- Updated dependencies [b496498]
|
|
1983
|
+
- @objectstack/spec@10.2.0
|
|
1984
|
+
- @objectstack/core@10.2.0
|
|
1985
|
+
|
|
1986
|
+
## 10.1.0
|
|
1987
|
+
|
|
1988
|
+
### Minor Changes
|
|
1989
|
+
|
|
1990
|
+
- 49da36e: feat(analytics): correct analytics over federated objects (ADR-0062 Phase 3, D6)
|
|
1991
|
+
|
|
1992
|
+
Analytics over an external (federated) object now aggregates against the
|
|
1993
|
+
**correct** remote table instead of silently querying the wrong one. The
|
|
1994
|
+
`NativeSQLStrategy` hand-compiles `FROM "<object>"` and bare column references,
|
|
1995
|
+
which bypass the driver's physical-table resolution (`external.remoteName` /
|
|
1996
|
+
`remoteSchema` / `columnMap`). It now **declines** any query whose base or joined
|
|
1997
|
+
object is federated, routing it to the `ObjectQLStrategy` — whose
|
|
1998
|
+
`engine.aggregate()` goes through the driver's `getBuilder` and already honours
|
|
1999
|
+
`remoteName`/`remoteSchema` (#2138/#2149). This "reuses the driver's resolution"
|
|
2000
|
+
(D6) rather than re-implementing it.
|
|
2001
|
+
|
|
2002
|
+
Adds an optional `StrategyContext.isExternalObject(objectName)` hook (reported by
|
|
2003
|
+
the analytics plugin from the object's `external` block). Purely additive — with
|
|
2004
|
+
no hook, behavior is unchanged for managed objects.
|
|
2005
|
+
|
|
2006
|
+
### Patch Changes
|
|
2007
|
+
|
|
2008
|
+
- Updated dependencies [49da36e]
|
|
2009
|
+
- Updated dependencies [ac79f16]
|
|
2010
|
+
- @objectstack/spec@10.1.0
|
|
2011
|
+
- @objectstack/core@10.1.0
|
|
2012
|
+
|
|
2013
|
+
## 10.0.0
|
|
2014
|
+
|
|
2015
|
+
### Minor Changes
|
|
2016
|
+
|
|
2017
|
+
- 70609af: Resolve a monetary measure's display currency via the field→tenant chain.
|
|
2018
|
+
|
|
2019
|
+
A dataset measure-currency now resolves through: explicit measure `currency` →
|
|
2020
|
+
source-field `currencyConfig.defaultCurrency` → tenant default (`ctx.currency`).
|
|
2021
|
+
A measure is monetary iff it declares a currency or aggregates a `currency`-type
|
|
2022
|
+
field, so count/avg-of-number measures never receive a code. Wires a
|
|
2023
|
+
`measureCurrency` field-metadata resolver from the data engine's object schema.
|
|
2024
|
+
|
|
2025
|
+
- 3187952: Dataset analytics enrich **dimension** result fields with their display label (so report/dashboard table headers read "Status" instead of the raw field name) and expose drill-through metadata on the dataset query result: the base `object`, a drillable dimension→field map, and a parallel `drillRawRows` array of each row's raw grouped values (captured before label resolution). This lets a host drill a grouped bucket back to its underlying records with an exact-match filter built from the stored value, not the display label. Date dimensions are excluded (a humanized bucket can't be exact-matched).
|
|
2026
|
+
- a581385: Propagate a dataset measure's declared currency to the analytics result field.
|
|
2027
|
+
|
|
2028
|
+
Adds an optional `DatasetMeasure.currency` (ISO 4217) on the semantic layer and
|
|
2029
|
+
carries it onto each measure result field alongside `label`/`format`, so a
|
|
2030
|
+
currency-aware client (Intl symbol) can render `¥1,234` / `$616,000` from a real
|
|
2031
|
+
currency code instead of a plain number or a `$` baked into `format`. Additive
|
|
2032
|
+
and optional — existing datasets are unaffected.
|
|
2033
|
+
|
|
2034
|
+
### Patch Changes
|
|
2035
|
+
|
|
2036
|
+
- Updated dependencies [d7ff626]
|
|
2037
|
+
- Updated dependencies [2a1b16b]
|
|
2038
|
+
- Updated dependencies [e16f2a8]
|
|
2039
|
+
- Updated dependencies [e411a82]
|
|
2040
|
+
- Updated dependencies [a581385]
|
|
2041
|
+
- Updated dependencies [d5f6d29]
|
|
2042
|
+
- Updated dependencies [220ce5b]
|
|
2043
|
+
- Updated dependencies [3efe334]
|
|
2044
|
+
- Updated dependencies [feead7e]
|
|
2045
|
+
- Updated dependencies [6ca20b3]
|
|
2046
|
+
- Updated dependencies [5f875fe]
|
|
2047
|
+
- Updated dependencies [b469950]
|
|
2048
|
+
- @objectstack/spec@10.0.0
|
|
2049
|
+
- @objectstack/core@10.0.0
|
|
2050
|
+
|
|
2051
|
+
## 9.11.0
|
|
2052
|
+
|
|
2053
|
+
### Patch Changes
|
|
2054
|
+
|
|
2055
|
+
- Updated dependencies [e7f6539]
|
|
2056
|
+
- Updated dependencies [2365d07]
|
|
2057
|
+
- Updated dependencies [6595b53]
|
|
2058
|
+
- Updated dependencies [fa8964d]
|
|
2059
|
+
- Updated dependencies [36138c7]
|
|
2060
|
+
- Updated dependencies [a8e4f3b]
|
|
2061
|
+
- Updated dependencies [4c213c2]
|
|
2062
|
+
- Updated dependencies [2afb612]
|
|
2063
|
+
- @objectstack/spec@9.11.0
|
|
2064
|
+
- @objectstack/core@9.11.0
|
|
2065
|
+
|
|
2066
|
+
## 9.10.0
|
|
2067
|
+
|
|
2068
|
+
### Patch Changes
|
|
2069
|
+
|
|
2070
|
+
- db02bd5: Fix dashboard time-series charts / "last N months" KPIs that filter or group by a `Field.datetime` column silently returning "No rows".
|
|
2071
|
+
|
|
2072
|
+
The analytics `NativeSQLStrategy` compiles dashboard relative-date tokens (`{12_months_ago}`, `{today}`, …) to ISO date strings and binds them directly into raw SQL, bypassing the driver's own filter coercion. Under better-sqlite3 a `Field.datetime` column is stored as an INTEGER epoch (ms), so `assessed_at >= '2025-06-18'` became a TEXT-vs-INTEGER affinity compare that is always false — an empty result even though the rows exist. `Field.date` columns store ISO TEXT and were unaffected.
|
|
2073
|
+
|
|
2074
|
+
The strategy now coerces a temporal comparand to the column's on-disk storage form via a new optional `StrategyContext.coerceTemporalFilterValue` hook, wired to the driver's public `SqlDriver.temporalFilterValue` (the single source of truth for the storage convention). Coercion is dialect-correct: SQLite `Field.datetime` → epoch ms; `Field.date` text and native-timestamp dialects (Postgres/MySQL) are left unchanged, so Postgres is never handed an epoch integer. Applied to `gte`/`lte`/`gt`/`lt`/`equals`, `in`/`notIn`, and the `dateRange`/timeDimension `BETWEEN` path.
|
|
2075
|
+
|
|
2076
|
+
- fd07027: fix(analytics): make organization timezone actually drive date-dimension bucketing (ADR-0053 Phase 2, #1982)
|
|
2077
|
+
|
|
2078
|
+
Date-bucketed analytics silently ignored the reference timezone end-to-end. Three independent seams were broken:
|
|
2079
|
+
|
|
2080
|
+
- **service-analytics** — `NativeSQLStrategy` (priority 10) won every cube/dataset query on a SQL driver, but it groups by the raw column (no `date_trunc`) and ignores `timezone`, so a date dimension never bucketed (one row per raw timestamp) and a non-UTC zone was dropped. It now declines queries that carry a `timeDimensions[].granularity`, handing them to `ObjectQLStrategy` → `engine.aggregate` (native bucketing when UTC-safe, uniform in-memory bucketing when non-UTC).
|
|
2081
|
+
- **objectql** — the in-memory `count` aggregation treated the `*` count-all sentinel (the Cube `count` measure / a fieldless dataset `count`, both compiled to `sql: '*'`) as a column name, counting non-null of a non-existent property → `0` for every bucket. The driver's `COUNT(*)` masked it; the in-memory path (non-UTC date buckets, `driver-rest`/`driver-memory`) returned zeros. `*` is now counted as all rows.
|
|
2082
|
+
- **rest** — `resolveExecCtx` never resolved the localization timezone/locale, so `/analytics/dataset/query` always ran with `timezone: 'UTC'`. It now resolves them through the `settings` service (honouring the 4-tier cascade incl. the `OS_LOCALIZATION_TIMEZONE` env override), mirroring the dispatcher path.
|
|
2083
|
+
|
|
2084
|
+
- Updated dependencies [db02bd5]
|
|
2085
|
+
- Updated dependencies [641675d]
|
|
2086
|
+
- Updated dependencies [94e9040]
|
|
2087
|
+
- Updated dependencies [1f88fd9]
|
|
2088
|
+
- Updated dependencies [1f88fd9]
|
|
2089
|
+
- @objectstack/spec@9.10.0
|
|
2090
|
+
- @objectstack/core@9.10.0
|
|
2091
|
+
|
|
2092
|
+
## 9.9.1
|
|
2093
|
+
|
|
2094
|
+
### Patch Changes
|
|
2095
|
+
|
|
2096
|
+
- @objectstack/spec@9.9.1
|
|
2097
|
+
- @objectstack/core@9.9.1
|
|
2098
|
+
|
|
2099
|
+
## 9.9.0
|
|
2100
|
+
|
|
2101
|
+
### Minor Changes
|
|
2102
|
+
|
|
2103
|
+
- 9afeb2d: feat(settings): `localization` settings — platform default timezone, language & formats (ADR-0053 Phase 2)
|
|
2104
|
+
|
|
2105
|
+
Adds a `localization` SettingsManifest, the missing keystone that makes the Phase 2 reference-timezone actually configurable end-to-end. One declaration gives the full settings stack for free: platform built-in default → `global` → `tenant` cascade, a permission-gated settings page, and i18n.
|
|
2106
|
+
|
|
2107
|
+
**Keys** (organization-level; per-user overrides intentionally out of scope for v1): `timezone` (UTC), `locale` (en-US), `default_country`, `date_format`, `time_format`, `number_format`, `first_day_of_week`, `currency` (USD), `fiscal_year_start`. Benchmarked against Salesforce/Workday "Company Information + Locale".
|
|
2108
|
+
|
|
2109
|
+
**Resolver 收编** — `resolveExecutionContext` now resolves `timezone` **and** `locale` from the `localization` settings via the `settings` service (canonical 4-tier cascade), falling back to a direct tenant-scoped `sys_setting` read, then `UTC` / `en-US`. This replaces the hand-rolled `sys_user_preference` + tenant-only `sys_setting` path from #1978 (which bypassed the settings abstraction and is dropped along with the per-user tier). New `ExecutionContext.locale`.
|
|
2110
|
+
|
|
2111
|
+
**Consumer wiring** — analytics date bucketing now picks up the resolved org timezone: `DatasetExecutor` threads `ExecutionContext.timezone` into the query (precedence: explicit selection tz → request tz → UTC), so #1982's tz-aware buckets fire for a configured org without callers passing a zone. Formula `today()`/`datetime` were already wired (#1979/#1980).
|
|
2112
|
+
|
|
2113
|
+
Email `datetime` rendering (`SendTemplateInput.timezone`, shipped in #1981) is intentionally **not** wired here: the only current `sendTemplate` callers are pre-session auth emails with no org context; business-notification callers can pass the zone when they appear.
|
|
2114
|
+
|
|
2115
|
+
- 601cc11: feat(analytics): timezone-aware date bucketing (ADR-0053 Phase 2)
|
|
2116
|
+
|
|
2117
|
+
Analytics day/week/month/quarter/year buckets now resolve on a **reference timezone's** calendar days, so a row near a tz day-boundary lands in the bucket a user in that zone would expect — identically on SQLite and Postgres.
|
|
2118
|
+
|
|
2119
|
+
Per ADR-0053 decision **D2**, bucketing is done **in-memory, uniformly** for non-UTC zones rather than emitting dialect-specific `date_trunc … AT TIME ZONE` (SQLite has no tz database and MySQL needs tz tables loaded, so splitting by dialect would shift bucket boundaries for the same data). `engine.aggregate({ timezone })` therefore forces the in-memory aggregation path when a non-UTC reference tz is set — the date-range `where` still goes to the driver, so only matching rows are fetched. **UTC / unset keeps the native driver fast path unchanged.**
|
|
2120
|
+
|
|
2121
|
+
- New shared `calendarPartsInTz` / `calendarPartsInTzOrUtc` util in `@objectstack/core` (DST-safe via `Intl.DateTimeFormat`, never hand-rolled offset math; falls back to UTC for an unset/`'UTC'`/invalid zone).
|
|
2122
|
+
- `EngineAggregateOptions` and the analytics `executeAggregate` bridge / `ObjectQLStrategy` thread the reference timezone (sourced from the dataset selection / `ExecutionContext`) through to `applyInMemoryAggregation` → `bucketDateValue`, and the draft-preview evaluator's `bucketDate`.
|
|
2123
|
+
- `formatDateBucket` (dimension labels) stays UTC-only by design: it re-labels values that were _already_ bucketed upstream, so re-applying a timezone there would shift a correct bucket by a day.
|
|
2124
|
+
|
|
2125
|
+
### Patch Changes
|
|
2126
|
+
|
|
2127
|
+
- Updated dependencies [84249a4]
|
|
2128
|
+
- Updated dependencies [11af299]
|
|
2129
|
+
- Updated dependencies [d5774b5]
|
|
2130
|
+
- Updated dependencies [134043a]
|
|
2131
|
+
- Updated dependencies [90108e0]
|
|
2132
|
+
- Updated dependencies [9afeb2d]
|
|
2133
|
+
- Updated dependencies [6bec07e]
|
|
2134
|
+
- Updated dependencies [601cc11]
|
|
2135
|
+
- Updated dependencies [575448d]
|
|
2136
|
+
- @objectstack/spec@9.9.0
|
|
2137
|
+
- @objectstack/core@9.9.0
|
|
2138
|
+
|
|
2139
|
+
## 9.8.0
|
|
2140
|
+
|
|
2141
|
+
### Patch Changes
|
|
2142
|
+
|
|
2143
|
+
- Updated dependencies [97c55b3]
|
|
2144
|
+
- Updated dependencies [1b1f490]
|
|
2145
|
+
- @objectstack/spec@9.8.0
|
|
2146
|
+
- @objectstack/core@9.8.0
|
|
2147
|
+
|
|
2148
|
+
## 9.7.0
|
|
2149
|
+
|
|
2150
|
+
### Patch Changes
|
|
2151
|
+
|
|
2152
|
+
- @objectstack/spec@9.7.0
|
|
2153
|
+
- @objectstack/core@9.7.0
|
|
2154
|
+
|
|
2155
|
+
## 9.6.0
|
|
2156
|
+
|
|
2157
|
+
### Patch Changes
|
|
2158
|
+
|
|
2159
|
+
- Updated dependencies [d1e930a]
|
|
2160
|
+
- Updated dependencies [71578f2]
|
|
2161
|
+
- Updated dependencies [5e3a301]
|
|
2162
|
+
- Updated dependencies [5db2742]
|
|
2163
|
+
- @objectstack/spec@9.6.0
|
|
2164
|
+
- @objectstack/core@9.6.0
|
|
2165
|
+
|
|
2166
|
+
## 9.5.1
|
|
2167
|
+
|
|
2168
|
+
### Patch Changes
|
|
2169
|
+
|
|
2170
|
+
- Updated dependencies [ee72aae]
|
|
2171
|
+
- @objectstack/spec@9.5.1
|
|
2172
|
+
- @objectstack/core@9.5.1
|
|
2173
|
+
|
|
2174
|
+
## 9.5.0
|
|
2175
|
+
|
|
2176
|
+
### Patch Changes
|
|
2177
|
+
|
|
2178
|
+
- Updated dependencies [d08551c]
|
|
2179
|
+
- Updated dependencies [707aeed]
|
|
2180
|
+
- Updated dependencies [7a103d4]
|
|
2181
|
+
- Updated dependencies [4b01250]
|
|
2182
|
+
- @objectstack/spec@9.5.0
|
|
2183
|
+
- @objectstack/core@9.5.0
|
|
2184
|
+
|
|
2185
|
+
## 9.4.0
|
|
2186
|
+
|
|
2187
|
+
### Patch Changes
|
|
2188
|
+
|
|
2189
|
+
- Updated dependencies [060467a]
|
|
2190
|
+
- Updated dependencies [0856476]
|
|
2191
|
+
- Updated dependencies [b678d8c]
|
|
2192
|
+
- Updated dependencies [b678d8c]
|
|
2193
|
+
- Updated dependencies [b678d8c]
|
|
2194
|
+
- @objectstack/spec@9.4.0
|
|
2195
|
+
- @objectstack/core@9.4.0
|
|
2196
|
+
|
|
2197
|
+
## 9.3.0
|
|
2198
|
+
|
|
2199
|
+
### Minor Changes
|
|
2200
|
+
|
|
2201
|
+
- b4765be: Server-side totals for matrix reports (#1753). `queryDataset` selections accept `totals: { groupings: string[][] }` — each grouping a subset of `selection.dimensions` to additionally aggregate by (`[]` = grand total); the marginal rows come back on `AnalyticsResult.totals` in request order. Each subtotal/grand total re-runs the full executor pipeline (measure-scoped filters, derived measures, compareTo) grouped only by that subset, so totals use each measure's true aggregate over the underlying rows — an `avg` total is the average of all rows, never an average of bucket averages (the ADR-0021 line that forbids client-side re-aggregation). Dimension display labels resolve on totals rows the same as the primary grid. A matrix report renderer asks for `{ groupings: [rowDims, columnDims, []] }` and renders the supplied totals row/column.
|
|
2202
|
+
|
|
2203
|
+
### Patch Changes
|
|
2204
|
+
|
|
2205
|
+
- Updated dependencies [1ada658]
|
|
2206
|
+
- Updated dependencies [3219191]
|
|
2207
|
+
- Updated dependencies [290f631]
|
|
2208
|
+
- Updated dependencies [50b7b47]
|
|
2209
|
+
- Updated dependencies [f15d6f6]
|
|
2210
|
+
- Updated dependencies [f8684ea]
|
|
2211
|
+
- Updated dependencies [b4765be]
|
|
2212
|
+
- @objectstack/spec@9.3.0
|
|
2213
|
+
- @objectstack/core@9.3.0
|
|
2214
|
+
|
|
2215
|
+
## 9.2.0
|
|
2216
|
+
|
|
2217
|
+
### Patch Changes
|
|
2218
|
+
|
|
2219
|
+
- Updated dependencies [2f57b75]
|
|
2220
|
+
- Updated dependencies [2f57b75]
|
|
2221
|
+
- @objectstack/spec@9.2.0
|
|
2222
|
+
- @objectstack/core@9.2.0
|
|
2223
|
+
|
|
2224
|
+
## 9.1.0
|
|
2225
|
+
|
|
2226
|
+
### Patch Changes
|
|
2227
|
+
|
|
2228
|
+
- Updated dependencies [b9062c9]
|
|
2229
|
+
- @objectstack/spec@9.1.0
|
|
2230
|
+
- @objectstack/core@9.1.0
|
|
2231
|
+
|
|
2232
|
+
## 9.0.1
|
|
2233
|
+
|
|
2234
|
+
### Patch Changes
|
|
2235
|
+
|
|
2236
|
+
- Updated dependencies [1817845]
|
|
2237
|
+
- @objectstack/spec@9.0.1
|
|
2238
|
+
- @objectstack/core@9.0.1
|
|
2239
|
+
|
|
2240
|
+
## 9.0.0
|
|
2241
|
+
|
|
2242
|
+
### Minor Changes
|
|
2243
|
+
|
|
2244
|
+
- 4a0736b: Analytics now renders date dimensions as human bucket labels instead of raw
|
|
2245
|
+
epoch millis, and buckets them by their declared granularity.
|
|
2246
|
+
|
|
2247
|
+
- A date dimension with an explicit `dateGranularity` is now grouped by that
|
|
2248
|
+
bucket (the executor promotes it to a time dimension), so a "monthly" trend
|
|
2249
|
+
chart shows one point per month rather than one per raw timestamp.
|
|
2250
|
+
- Grouped date values are formatted to a sort-stable label per granularity
|
|
2251
|
+
(`year` → `2026`, `quarter` → `2026-Q2`, `month` → `2026-04`, `day`/`week`
|
|
2252
|
+
→ `2026-04-15`), so charts no longer show `1777632968596`.
|
|
2253
|
+
|
|
2254
|
+
Pairs with the dimension display-label resolution (select option labels / lookup
|
|
2255
|
+
names) shipped previously.
|
|
2256
|
+
|
|
2257
|
+
- 2c6864f: Analytics dimensions now render human display labels instead of raw stored
|
|
2258
|
+
values. A `select` dimension shows its option `label` (e.g. `Backlog` rather than
|
|
2259
|
+
`backlog`), and a `lookup`/`master_detail` dimension shows the related record's
|
|
2260
|
+
display name (e.g. an account's name rather than its FK id). `queryDataset`
|
|
2261
|
+
resolves these server-side, so every dashboard/report chart benefits with no
|
|
2262
|
+
frontend change. Date/number/string dimensions are unaffected, and unresolved
|
|
2263
|
+
values are left as-is.
|
|
2264
|
+
- 0bf39f1: `queryDataset` now carries each measure's display `label` and `format` on the
|
|
2265
|
+
result `fields`, so presentations can show "Tasks" / "$616,000" instead of the
|
|
2266
|
+
raw measure name "task_count" / "616000".
|
|
2267
|
+
|
|
2268
|
+
- `AnalyticsResult.fields[]` gains optional `label?` and `format?`.
|
|
2269
|
+
- The dataset executor enriches measure columns from the dataset's measure
|
|
2270
|
+
definitions (matching `<name>` and `<name>__compare`).
|
|
2271
|
+
|
|
2272
|
+
The format can't be baked into the numeric row value (charts need the raw
|
|
2273
|
+
number), so the renderer applies it at display time.
|
|
2274
|
+
|
|
2275
|
+
### Patch Changes
|
|
2276
|
+
|
|
2277
|
+
- Updated dependencies [4c3f693]
|
|
2278
|
+
- Updated dependencies [0bf39f1]
|
|
2279
|
+
- Updated dependencies [f533f42]
|
|
2280
|
+
- Updated dependencies [1c83ee8]
|
|
2281
|
+
- @objectstack/spec@9.0.0
|
|
2282
|
+
- @objectstack/core@9.0.0
|
|
2283
|
+
|
|
2284
|
+
## 8.0.1
|
|
2285
|
+
|
|
2286
|
+
### Patch Changes
|
|
2287
|
+
|
|
2288
|
+
- @objectstack/spec@8.0.1
|
|
2289
|
+
- @objectstack/core@8.0.1
|
|
2290
|
+
|
|
2291
|
+
## 8.0.0
|
|
2292
|
+
|
|
2293
|
+
### Patch Changes
|
|
2294
|
+
|
|
2295
|
+
- Updated dependencies [a46c017]
|
|
2296
|
+
- Updated dependencies [b990b89]
|
|
2297
|
+
- Updated dependencies [99111ec]
|
|
2298
|
+
- Updated dependencies [d5a8161]
|
|
2299
|
+
- Updated dependencies [5cf1f1b]
|
|
2300
|
+
- Updated dependencies [9ef89d4]
|
|
2301
|
+
- Updated dependencies [3306d2f]
|
|
2302
|
+
- Updated dependencies [c262301]
|
|
2303
|
+
- Updated dependencies [bc44195]
|
|
2304
|
+
- Updated dependencies [9e2e229]
|
|
2305
|
+
- @objectstack/spec@8.0.0
|
|
2306
|
+
- @objectstack/core@8.0.0
|
|
2307
|
+
|
|
2308
|
+
## 7.9.0
|
|
2309
|
+
|
|
2310
|
+
### Patch Changes
|
|
2311
|
+
|
|
2312
|
+
- @objectstack/spec@7.9.0
|
|
2313
|
+
- @objectstack/core@7.9.0
|
|
2314
|
+
|
|
2315
|
+
## 7.8.0
|
|
2316
|
+
|
|
2317
|
+
### Patch Changes
|
|
2318
|
+
|
|
2319
|
+
- Updated dependencies [06f2bbb]
|
|
2320
|
+
- Updated dependencies [36719db]
|
|
2321
|
+
- Updated dependencies [424ab26]
|
|
2322
|
+
- @objectstack/spec@7.8.0
|
|
2323
|
+
- @objectstack/core@7.8.0
|
|
2324
|
+
|
|
2325
|
+
## 7.7.0
|
|
2326
|
+
|
|
2327
|
+
### Patch Changes
|
|
2328
|
+
|
|
2329
|
+
- Updated dependencies [b391955]
|
|
2330
|
+
- Updated dependencies [f06b64e]
|
|
2331
|
+
- Updated dependencies [023bf93]
|
|
2332
|
+
- @objectstack/spec@7.7.0
|
|
2333
|
+
- @objectstack/core@7.7.0
|
|
2334
|
+
|
|
2335
|
+
## 7.6.0
|
|
2336
|
+
|
|
2337
|
+
### Patch Changes
|
|
2338
|
+
|
|
2339
|
+
- Updated dependencies [955d4c8]
|
|
2340
|
+
- Updated dependencies [c4a4cbd]
|
|
2341
|
+
- Updated dependencies [b046ec2]
|
|
2342
|
+
- Updated dependencies [2170ad9]
|
|
2343
|
+
- Updated dependencies [02d6359]
|
|
2344
|
+
- Updated dependencies [7648242]
|
|
2345
|
+
- Updated dependencies [8fa1e7f]
|
|
2346
|
+
- Updated dependencies [55866f5]
|
|
2347
|
+
- Updated dependencies [60f9c45]
|
|
2348
|
+
- @objectstack/spec@7.6.0
|
|
2349
|
+
- @objectstack/core@7.6.0
|
|
2350
|
+
|
|
2351
|
+
## 7.5.0
|
|
2352
|
+
|
|
2353
|
+
### Patch Changes
|
|
2354
|
+
|
|
2355
|
+
- @objectstack/spec@7.5.0
|
|
2356
|
+
- @objectstack/core@7.5.0
|
|
2357
|
+
|
|
2358
|
+
## 7.4.1
|
|
2359
|
+
|
|
2360
|
+
### Patch Changes
|
|
2361
|
+
|
|
2362
|
+
- @objectstack/spec@7.4.1
|
|
2363
|
+
- @objectstack/core@7.4.1
|
|
2364
|
+
|
|
2365
|
+
## 7.4.0
|
|
2366
|
+
|
|
2367
|
+
### Patch Changes
|
|
2368
|
+
|
|
2369
|
+
- Updated dependencies [23c7107]
|
|
2370
|
+
- Updated dependencies [c72daad]
|
|
2371
|
+
- Updated dependencies [f115182]
|
|
2372
|
+
- Updated dependencies [2faf9f2]
|
|
2373
|
+
- Updated dependencies [2faf9f2]
|
|
2374
|
+
- Updated dependencies [2faf9f2]
|
|
2375
|
+
- Updated dependencies [58b450b]
|
|
2376
|
+
- Updated dependencies [82eb6cf]
|
|
2377
|
+
- Updated dependencies [13d8653]
|
|
2378
|
+
- Updated dependencies [ff3d006]
|
|
2379
|
+
- Updated dependencies [5e831de]
|
|
2380
|
+
- @objectstack/spec@7.4.0
|
|
2381
|
+
- @objectstack/core@7.4.0
|
|
2382
|
+
|
|
2383
|
+
## 7.3.0
|
|
2384
|
+
|
|
2385
|
+
### Patch Changes
|
|
2386
|
+
|
|
2387
|
+
- Updated dependencies [5e7c554]
|
|
2388
|
+
- @objectstack/spec@7.3.0
|
|
2389
|
+
- @objectstack/core@7.3.0
|
|
2390
|
+
|
|
2391
|
+
## 7.2.1
|
|
2392
|
+
|
|
2393
|
+
### Patch Changes
|
|
2394
|
+
|
|
2395
|
+
- @objectstack/spec@7.2.1
|
|
2396
|
+
- @objectstack/core@7.2.1
|
|
2397
|
+
|
|
2398
|
+
## 7.2.0
|
|
2399
|
+
|
|
2400
|
+
### Patch Changes
|
|
2401
|
+
|
|
2402
|
+
- @objectstack/spec@7.2.0
|
|
2403
|
+
- @objectstack/core@7.2.0
|
|
2404
|
+
|
|
2405
|
+
## 7.1.0
|
|
2406
|
+
|
|
2407
|
+
### Patch Changes
|
|
2408
|
+
|
|
2409
|
+
- Updated dependencies [47a92f4]
|
|
2410
|
+
- @objectstack/spec@7.1.0
|
|
2411
|
+
- @objectstack/core@7.1.0
|
|
2412
|
+
|
|
2413
|
+
## 7.0.0
|
|
2414
|
+
|
|
2415
|
+
### Patch Changes
|
|
2416
|
+
|
|
2417
|
+
- Updated dependencies [74470ad]
|
|
2418
|
+
- Updated dependencies [d29617e]
|
|
2419
|
+
- Updated dependencies [dc72172]
|
|
2420
|
+
- @objectstack/spec@7.0.0
|
|
2421
|
+
- @objectstack/core@7.0.0
|
|
2422
|
+
|
|
2423
|
+
## 6.9.0
|
|
2424
|
+
|
|
2425
|
+
### Patch Changes
|
|
2426
|
+
|
|
2427
|
+
- @objectstack/spec@6.9.0
|
|
2428
|
+
- @objectstack/core@6.9.0
|
|
2429
|
+
|
|
2430
|
+
## 6.8.1
|
|
2431
|
+
|
|
2432
|
+
### Patch Changes
|
|
2433
|
+
|
|
2434
|
+
- @objectstack/spec@6.8.1
|
|
2435
|
+
- @objectstack/core@6.8.1
|
|
2436
|
+
|
|
2437
|
+
## 6.8.0
|
|
2438
|
+
|
|
2439
|
+
### Patch Changes
|
|
2440
|
+
|
|
2441
|
+
- Updated dependencies [6e88f77]
|
|
2442
|
+
- Updated dependencies [c8b9f57]
|
|
2443
|
+
- @objectstack/spec@6.8.0
|
|
2444
|
+
- @objectstack/core@6.8.0
|
|
2445
|
+
|
|
2446
|
+
## 6.7.1
|
|
2447
|
+
|
|
2448
|
+
### Patch Changes
|
|
2449
|
+
|
|
2450
|
+
- @objectstack/spec@6.7.1
|
|
2451
|
+
- @objectstack/core@6.7.1
|
|
2452
|
+
|
|
2453
|
+
## 6.7.0
|
|
2454
|
+
|
|
2455
|
+
### Patch Changes
|
|
2456
|
+
|
|
2457
|
+
- Updated dependencies [430067b]
|
|
2458
|
+
- Updated dependencies [4f9e9d4]
|
|
2459
|
+
- @objectstack/spec@6.7.0
|
|
2460
|
+
- @objectstack/core@6.7.0
|
|
2461
|
+
|
|
2462
|
+
## 6.6.0
|
|
2463
|
+
|
|
2464
|
+
### Patch Changes
|
|
2465
|
+
|
|
2466
|
+
- Updated dependencies [a49cfc2]
|
|
2467
|
+
- @objectstack/spec@6.6.0
|
|
2468
|
+
- @objectstack/core@6.6.0
|
|
2469
|
+
|
|
2470
|
+
## 6.5.1
|
|
2471
|
+
|
|
2472
|
+
### Patch Changes
|
|
2473
|
+
|
|
2474
|
+
- @objectstack/spec@6.5.1
|
|
2475
|
+
- @objectstack/core@6.5.1
|
|
2476
|
+
|
|
2477
|
+
## 6.5.0
|
|
2478
|
+
|
|
2479
|
+
### Patch Changes
|
|
2480
|
+
|
|
2481
|
+
- @objectstack/spec@6.5.0
|
|
2482
|
+
- @objectstack/core@6.5.0
|
|
2483
|
+
|
|
2484
|
+
## 6.4.0
|
|
2485
|
+
|
|
2486
|
+
### Patch Changes
|
|
2487
|
+
|
|
2488
|
+
- Updated dependencies [f8651cc]
|
|
2489
|
+
- Updated dependencies [f8651cc]
|
|
2490
|
+
- Updated dependencies [0bf6f9a]
|
|
2491
|
+
- @objectstack/spec@6.4.0
|
|
2492
|
+
- @objectstack/core@6.4.0
|
|
2493
|
+
|
|
2494
|
+
## 6.3.0
|
|
2495
|
+
|
|
2496
|
+
### Patch Changes
|
|
2497
|
+
|
|
2498
|
+
- @objectstack/spec@6.3.0
|
|
2499
|
+
- @objectstack/core@6.3.0
|
|
2500
|
+
|
|
2501
|
+
## 6.2.0
|
|
2502
|
+
|
|
2503
|
+
### Patch Changes
|
|
2504
|
+
|
|
2505
|
+
- Updated dependencies [b4c74a9]
|
|
2506
|
+
- @objectstack/spec@6.2.0
|
|
2507
|
+
- @objectstack/core@6.2.0
|
|
2508
|
+
|
|
2509
|
+
## 6.1.1
|
|
2510
|
+
|
|
2511
|
+
### Patch Changes
|
|
2512
|
+
|
|
2513
|
+
- @objectstack/spec@6.1.1
|
|
2514
|
+
- @objectstack/core@6.1.1
|
|
2515
|
+
|
|
2516
|
+
## 6.1.0
|
|
2517
|
+
|
|
2518
|
+
### Patch Changes
|
|
2519
|
+
|
|
2520
|
+
- Updated dependencies [93c0589]
|
|
2521
|
+
- @objectstack/spec@6.1.0
|
|
2522
|
+
- @objectstack/core@6.1.0
|
|
2523
|
+
|
|
2524
|
+
## 6.0.0
|
|
2525
|
+
|
|
2526
|
+
### Patch Changes
|
|
2527
|
+
|
|
2528
|
+
- Updated dependencies [629a716]
|
|
2529
|
+
- Updated dependencies [dbc4f7d]
|
|
2530
|
+
- Updated dependencies [944f187]
|
|
2531
|
+
- @objectstack/spec@6.0.0
|
|
2532
|
+
- @objectstack/core@6.0.0
|
|
2533
|
+
|
|
2534
|
+
## 5.2.0
|
|
2535
|
+
|
|
2536
|
+
### Patch Changes
|
|
2537
|
+
|
|
2538
|
+
- Updated dependencies [bab2b20]
|
|
2539
|
+
- Updated dependencies [fa011d8]
|
|
2540
|
+
- Updated dependencies [b806f58]
|
|
2541
|
+
- @objectstack/spec@5.2.0
|
|
2542
|
+
- @objectstack/core@5.2.0
|
|
2543
|
+
|
|
2544
|
+
## 5.1.0
|
|
2545
|
+
|
|
2546
|
+
### Patch Changes
|
|
2547
|
+
|
|
2548
|
+
- Updated dependencies [75f4ee6]
|
|
2549
|
+
- Updated dependencies [823d559]
|
|
2550
|
+
- @objectstack/spec@5.1.0
|
|
2551
|
+
- @objectstack/core@5.1.0
|
|
2552
|
+
|
|
2553
|
+
## 5.0.0
|
|
2554
|
+
|
|
2555
|
+
### Patch Changes
|
|
2556
|
+
|
|
2557
|
+
- Updated dependencies [2f9073a]
|
|
2558
|
+
- @objectstack/spec@5.0.0
|
|
2559
|
+
- @objectstack/core@5.0.0
|
|
2560
|
+
|
|
2561
|
+
## 4.2.0
|
|
2562
|
+
|
|
2563
|
+
### Patch Changes
|
|
2564
|
+
|
|
2565
|
+
- Updated dependencies [2869891]
|
|
2566
|
+
- @objectstack/spec@4.2.0
|
|
2567
|
+
- @objectstack/core@4.2.0
|
|
2568
|
+
|
|
2569
|
+
## 4.1.1
|
|
2570
|
+
|
|
2571
|
+
### Patch Changes
|
|
2572
|
+
|
|
2573
|
+
- @objectstack/spec@4.1.1
|
|
2574
|
+
- @objectstack/core@4.1.1
|
|
2575
|
+
|
|
2576
|
+
## 4.1.0
|
|
2577
|
+
|
|
2578
|
+
### Patch Changes
|
|
2579
|
+
|
|
2580
|
+
- Updated dependencies [2108c30]
|
|
2581
|
+
- Updated dependencies [23db640]
|
|
2582
|
+
- @objectstack/spec@4.1.0
|
|
2583
|
+
- @objectstack/core@4.1.0
|
|
2584
|
+
|
|
2585
|
+
## 4.0.5
|
|
2586
|
+
|
|
2587
|
+
### Patch Changes
|
|
2588
|
+
|
|
2589
|
+
- 15e0df6: chore: unify all package versions to a single patch release
|
|
2590
|
+
- Updated dependencies [15e0df6]
|
|
2591
|
+
- @objectstack/spec@4.0.5
|
|
2592
|
+
- @objectstack/core@4.0.5
|
|
2593
|
+
|
|
2594
|
+
## 4.0.4
|
|
2595
|
+
|
|
2596
|
+
### Patch Changes
|
|
2597
|
+
|
|
2598
|
+
- Updated dependencies [326b66b]
|
|
2599
|
+
- @objectstack/spec@4.0.4
|
|
2600
|
+
- @objectstack/core@4.0.4
|
|
2601
|
+
|
|
2602
|
+
## 4.0.3
|
|
2603
|
+
|
|
2604
|
+
### Patch Changes
|
|
2605
|
+
|
|
2606
|
+
- @objectstack/spec@4.0.3
|
|
2607
|
+
- @objectstack/core@4.0.3
|
|
2608
|
+
|
|
2609
|
+
## 4.0.2
|
|
2610
|
+
|
|
2611
|
+
### Patch Changes
|
|
2612
|
+
|
|
2613
|
+
- Updated dependencies [5f659e9]
|
|
2614
|
+
- @objectstack/spec@4.0.2
|
|
2615
|
+
- @objectstack/core@4.0.2
|
|
2616
|
+
|
|
2617
|
+
## 4.0.0
|
|
2618
|
+
|
|
2619
|
+
### Patch Changes
|
|
2620
|
+
|
|
2621
|
+
- Updated dependencies [f08ffc3]
|
|
2622
|
+
- Updated dependencies [e0b0a78]
|
|
2623
|
+
- @objectstack/spec@4.0.0
|
|
2624
|
+
- @objectstack/core@4.0.0
|
|
2625
|
+
|
|
2626
|
+
## 3.3.1
|
|
2627
|
+
|
|
2628
|
+
### Patch Changes
|
|
2629
|
+
|
|
2630
|
+
- @objectstack/spec@3.3.1
|
|
2631
|
+
- @objectstack/core@3.3.1
|
|
2632
|
+
|
|
2633
|
+
## 3.2.10
|
|
2634
|
+
|
|
2635
|
+
### Patch Changes
|
|
2636
|
+
|
|
2637
|
+
- @objectstack/spec@3.3.0
|
|
2638
|
+
- @objectstack/core@3.3.0
|
|
2639
|
+
|
|
2640
|
+
All notable changes to this package will be documented in this file.
|
|
2641
|
+
|
|
2642
|
+
## [3.2.9] — 2026-03-22
|
|
2643
|
+
|
|
2644
|
+
### Added
|
|
2645
|
+
|
|
2646
|
+
- Initial implementation of `@objectstack/service-analytics`
|
|
2647
|
+
- `AnalyticsService` orchestrator implementing `IAnalyticsService`
|
|
2648
|
+
- Strategy pattern with priority chain:
|
|
2649
|
+
- **P1 — NativeSQLStrategy**: Pushes queries as native SQL to SQL-capable drivers (Postgres, MySQL, etc.)
|
|
2650
|
+
- **P2 — ObjectQLStrategy**: Translates analytics queries into ObjectQL `engine.aggregate()` calls
|
|
2651
|
+
- **P3 — InMemoryStrategy**: Delegates to any registered `IAnalyticsService` (e.g., `MemoryAnalyticsService`)
|
|
2652
|
+
- `CubeRegistry` for auto-discovery and registration of cubes from manifest definitions and object schema inference
|
|
2653
|
+
- `AnalyticsServicePlugin` for kernel plugin lifecycle integration
|
|
2654
|
+
- `queryCapabilities()` driver capability probing for strategy selection
|
|
2655
|
+
- `generateSql()` dry-run SQL generation across all strategies
|
|
2656
|
+
- Unit tests covering all strategy branches
|