@objectstack/service-analytics 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,449 @@
1
1
  # Changelog — @objectstack/service-analytics
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 57e4571: **BREAKING**: `/analytics/query` now refuses a cross-object filter nested inside a
8
+ combinator on the ObjectQL path, instead of silently answering the wrong number
9
+ (#10759).
10
+
11
+ `ObjectQLStrategy` runs one cross-object envelope check, from two call sites.
12
+ `generateSql()` (the `/analytics/sql` preview) asked it about every member the
13
+ `where` touches, flattened out of the filter tree. `execute()` asked it about the
14
+ built engine filter — where an AND-ed leaf sits at the top level and is seen, but
15
+ anything structural (an `$or`, a `$not`, a nested `$and` that cannot merge) has
16
+ been folded into `filter.$and`, so the only key readable for it was the literal
17
+ `$and`, which is never a field name.
18
+
19
+ One query therefore got two answers, measured over one fixture in one run:
20
+
21
+ ```
22
+ where: { $or: [{ 'account.region': 'West' }, { stage: 'won' }] }
23
+
24
+ before /analytics/sql 400 INVALID_FIELD cross-object filter "account.region"
25
+ /analytics/query 200, rows
26
+ after both 400 INVALID_FIELD cross-object filter "account.region"
27
+ ```
28
+
29
+ `engine.aggregate` cannot join. The half that returned rows was not answering the
30
+ cross-object query: the disjunct naming a column the base object does not have
31
+ can never match, so the query silently collapsed to its remaining branches and
32
+ reported a narrower figure as if it were the answer. Both call sites now derive
33
+ the member list from one shared view, so the invariant the strategy already
34
+ stated for itself — the preview accepts and rejects the same set the execution
35
+ door does — holds by construction rather than by two call sites agreeing.
36
+
37
+ Who is affected: a deployment whose driver reports `objectqlAggregate` but not
38
+ `nativeSql` (Mongo, the memory driver), running an analytics query that puts a
39
+ related object's field inside `$or` or `$not`. Such a query now returns
40
+ `400 INVALID_FIELD` naming the member. The refusal already existed and already
41
+ had these words; what changed is that the execution door reaches it too. Nothing
42
+ an author writes in metadata changes, no stored shape is affected, and queries
43
+ whose combinators name only base-object fields are untouched — that set is pinned
44
+ in `crossobject-conjunct-refusal.test.ts` alongside the new refusal, because a
45
+ fix that refused every combinator would have looked identical from the refusal
46
+ side alone.
47
+
48
+ The remedy for an affected query is the one the error message has always carried:
49
+ run it on a native-SQL driver, which can join, or drop the cross-object member
50
+ from the filter.
51
+
52
+ <!-- adr-0087: not-required (no-migration-prescription) A runtime query-shape refusal on /analytics/query, not a metadata surface: no authorable key, export or config field is removed or renamed, so `objectstack migrate meta` has nothing to rewrite and an upgrader has no stored shape to convert. The affected input is an ad-hoc request body, and the error itself names the member and the two ways out. -->
53
+ - 13a3dca: **BREAKING**: on the ObjectQL path, a compiled dataset whose definition-level
54
+ `filter` is itself cross-object is now refused by both analytics doors instead
55
+ of reaching `engine.aggregate` with a predicate it cannot join (#10861).
56
+
57
+ PR #10758 gave the dataset's own definition-level `filter` a route onto this
58
+ door for the first time. That route was outside the member view the cross-object
59
+ envelope check judges, so nothing ever saw it:
60
+
61
+ ```
62
+ dataset: object 'opportunity', include: ['account'],
63
+ filter: { 'account.region': 'West' }
64
+
65
+ before /analytics/query 200, rows -> engine.aggregate received
66
+ {"$and":[{"account.region":"West"}]}
67
+ /analytics/sql 200, SQL
68
+ after both 400 INVALID_FIELD, member "account.region",
69
+ cube "<dataset>"; the engine is never reached
70
+ ```
71
+
72
+ `engine.aggregate` cannot join. `account.region` is not a column of
73
+ `opportunity`, so on any driver that evaluates the predicate honestly it matches
74
+ nothing, and the widget answered a number that was neither the scoped number nor
75
+ the unscoped one — with no error anywhere. That is the silent mis-bucket #3654's
76
+ loud refusal exists to prevent, arriving through a producer #3654 predates.
77
+
78
+ **Breaking, and argued rather than assumed.** A query that returns `200` with
79
+ rows today starts answering `400`, on a *saved* dataset rather than on anything
80
+ in the request — a dashboard that renders today can start showing an error. That
81
+ is the strongest reading of "breaking" and it is why this is called out here
82
+ rather than filed as a quiet fix. What is *not* lost is any correct answer: the
83
+ rows that stop being served were already wrong, and wrong in the way that hides
84
+ itself. The refusal names the member, names the dataset, and says the same
85
+ definition is valid on a native-SQL deployment, so the operator has somewhere to
86
+ go; the previous behaviour gave them a plausible number and nothing to notice.
87
+ Rejecting the dataset at compile time in `dataset-compiler.ts` was considered and
88
+ not taken (maintainer ruling, 2026-08-22): the compiler cannot see which driver
89
+ will serve the dataset, and the same definition is legal on a native-SQL one.
90
+
91
+ Who is affected: a deployment whose driver reports `objectqlAggregate` but not
92
+ `nativeSql` (Mongo, the memory driver), serving a dataset whose definition-level
93
+ `filter` names a field on a related object. Nothing an author writes changes
94
+ shape, no stored document is rewritten, and an **ordinary** dataset scope
95
+ (`filter: { is_deleted: false }`) still passes both doors and still reaches the
96
+ engine carrying its predicate — that direction is pinned one character away from
97
+ the new refusal in `crossobject-conjunct-refusal.test.ts`, because an
98
+ implementation that refused *every* dataset scope would look identical from the
99
+ refusal side alone and would break every scoped dataset shipping today.
100
+
101
+ <!-- adr-0087: not-required (no-migration-prescription) No authorable surface is
102
+ retired, renamed or re-shaped: `DatasetSchema`'s `filter` key stays exactly as it
103
+ is, every stored dataset document stays valid as written, and the very same
104
+ document remains correct on a native-SQL deployment. There is therefore nothing
105
+ `objectstack migrate meta` could rewrite — a mechanical rewrite would have to
106
+ know which driver will serve the dataset, which is precisely the capability the
107
+ 2026-08-22 ruling records as invisible to the compile-time placement. This is a
108
+ query-time refusal on one driver family, not a surface retirement, so the ledger
109
+ has no entry to carry and the upgrade guide has no prescription to print. -->
110
+
111
+ ### Patch Changes
112
+
113
+ - 7bf3fb7: Point every documentation link in these packages' published READMEs — and in
114
+ the project `create-objectstack` scaffolds — at the canonical docs origin
115
+ `https://objectstack.ai`, replacing the `docs.objectstack.ai` spelling.
116
+
117
+ Both spellings reach the same pages (the alias redirects to the apex,
118
+ path-preserving), so no link was broken. The reason it needs a release rather
119
+ than an in-repo fix alone: a README ships inside the npm tarball, so the
120
+ version already on npm keeps showing the old host to every reader of the
121
+ package page until a new one is published.
122
+ - 112a8c6: Apply a dataset's definition-level `filter` on the ObjectQL analytics path
123
+ (#10413, phase 1). `/api/v1/analytics/query` served by a driver that reports
124
+ `objectqlAggregate` but not `nativeSql` (MongoDB, the memory driver) reached
125
+ `engine.aggregate` with no `filter` key at all: the dataset's own scope — a
126
+ `filter: { is_deleted: false }` on the dataset definition — was dropped, so
127
+ every measure aggregated the whole table while the dashboard door, on the same
128
+ cube and the same measure names, answered the scoped numbers. The scope is now
129
+ ANDed into the strategy's whole-call filter (never merged key-by-key, so a
130
+ caller's own `where` and the time windows cannot be overwritten by it), and the
131
+ representative SQL echo renders it too.
132
+
133
+ Per-MEASURE `filter`s on this path are still not applied: an
134
+ `engine.aggregate` aggregation is `{ field, method, alias }` and cannot carry a
135
+ predicate of its own. Widening that contract is #10576; lowering the measure
136
+ filters into it is phase 2 of #10413. The native-SQL path already applies both
137
+ (#10298).
138
+ - 6439f8b: Analytics measures are now compiled from everything they declare — `aggregate`, `field` **and** `filter` — on both the dashboard path and `POST /api/v1/analytics/query`.
139
+
140
+ **Reported figures change, and the new ones are the declared ones.** Two corrections, both of which move numbers a dashboard or an API consumer is already reading:
141
+
142
+ - A measure written `{ aggregate: 'count', field: 'some_column' }` used to compile to `COUNT(*)` and count **rows**. It now compiles to `COUNT("some_column")` and counts **non-null values**. Any such measure will report the same number as before or a **smaller** one, and a rate built on top of it (a numerator over a total) will drop accordingly — a "100%" tile whose column was mostly empty was reading its own denominator.
143
+ - `POST /api/v1/analytics/query` used to drop every per-measure `filter`, and the dataset's definition-level `filter` with it, returning unfiltered aggregates under the author's measure names. It now applies both, so the endpoint answers what the dashboard already answered for the same cube. Figures pulled through the API — agent tools, exports, downstream reports — will move to the filtered values; a measure declaring `filter: { stage: 'closed_won' }` stops counting every row.
144
+
145
+ Measures that declare no `field` still compile to `COUNT(*)`, and a cube that is not a compiled dataset (an inferred or manifest cube) emits byte-for-byte the statement it did before. Measure filters lower to portable `CASE WHEN` conditional aggregates rather than `FILTER (WHERE …)`, which MySQL does not have.
146
+
147
+ If a saved figure or a screenshot disagrees with what the platform now reports, the new number is the one the metadata declares.
148
+ - Updated dependencies [6936d07]
149
+ - Updated dependencies [59eb04d]
150
+ - Updated dependencies [9f05b7d]
151
+ - Updated dependencies [3b2af5e]
152
+ - Updated dependencies [7d2d112]
153
+ - Updated dependencies [5fa0d72]
154
+ - Updated dependencies [02b3b07]
155
+ - Updated dependencies [46d34ab]
156
+ - Updated dependencies [914c413]
157
+ - Updated dependencies [55809a0]
158
+ - Updated dependencies [ee2ff45]
159
+ - Updated dependencies [47cd3ec]
160
+ - Updated dependencies [52db1d1]
161
+ - Updated dependencies [5649efb]
162
+ - Updated dependencies [9d7d2de]
163
+ - Updated dependencies [c815c50]
164
+ - Updated dependencies [795ea05]
165
+ - Updated dependencies [2306a76]
166
+ - Updated dependencies [e5ea701]
167
+ - Updated dependencies [a40dcc1]
168
+ - Updated dependencies [def0d3e]
169
+ - Updated dependencies [8d0bb79]
170
+ - Updated dependencies [5acb58d]
171
+ - Updated dependencies [2e3cf95]
172
+ - Updated dependencies [4c93387]
173
+ - Updated dependencies [504c8d5]
174
+ - Updated dependencies [a037f7c]
175
+ - Updated dependencies [3ee8ddf]
176
+ - Updated dependencies [16cef97]
177
+ - Updated dependencies [a79bd35]
178
+ - Updated dependencies [6ceaa4b]
179
+ - Updated dependencies [15ea214]
180
+ - Updated dependencies [de19489]
181
+ - Updated dependencies [c684d00]
182
+ - Updated dependencies [923c424]
183
+ - Updated dependencies [1ec36b7]
184
+ - Updated dependencies [5f2e54c]
185
+ - Updated dependencies [189373b]
186
+ - Updated dependencies [35ad101]
187
+ - Updated dependencies [ceb33a9]
188
+ - Updated dependencies [73d9795]
189
+ - Updated dependencies [8012960]
190
+ - Updated dependencies [f34f56b]
191
+ - Updated dependencies [f399618]
192
+ - Updated dependencies [75e9301]
193
+ - Updated dependencies [2810695]
194
+ - @objectstack/spec@17.2.0
195
+ - @objectstack/core@17.2.0
196
+ - @objectstack/types@17.2.0
197
+
198
+ ## 17.1.0
199
+
200
+ ### Patch Changes
201
+
202
+ - d09d0fd: Source the comparand-type allow-list and the accepted-set refusal sentence from the shared `@objectstack/spec/data` door instead of re-spelling them locally.
203
+
204
+ `comparand-shape.ts`'s `isBindableComparand` / `isRenderableTextComparand` spelled the same six accepted comparand types (`string | number | bigint | boolean | null | Date`) that `isAcceptedFilterComparand` single-sources for the SQL driver family, and two refusal messages hand-copied the accepted-set sentence. Both predicates now delegate the type membership to the door and quote `ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE`, matching how `driver-sql` and `driver-turso` consume it.
205
+
206
+ No comparand is accepted or refused differently: the local copies already agreed with the door, and the full accept/refuse matrix is pinned end to end at both analytics filter doors, in three comparand positions each, measured before the change and re-run unchanged after it.
207
+
208
+ One user-visible wording correction falls out of removing the copy: the hand-copied sentence omitted `bigint`, a type both predicates have always accepted and both doors have always compiled, so a refusal message under-described the values it accepts. The message now names the full set. The package-local extras — a binary bindable, and the `undefined` arm both doors already refuse upstream — are unchanged and recorded at their use sites.
209
+ - 0425db9: Published READMEs link to the docs site in the one form that works on npm, on GitHub and on the docs site (#9632)
210
+
211
+ **Seven docs links in these READMEs pointed nowhere.** They were spelled as a repo
212
+ path rooted at `/` — `[Flows](/content/docs/automation/flows.mdx)` — and a README in a
213
+ package's `files` array with `private` unset is rendered on the **npm package page** and
214
+ on **GitHub**, not only in this repository. There a root-relative href resolves against
215
+ `npmjs.com` and `github.com` respectively. It was not a docs-site route either:
216
+ `apps/docs/lib/source.ts` mounts `loader({ baseUrl: '/docs' })` over `content/docs`, so
217
+ the route for that first link is `/docs/automation/flows`, and `apps/docs/redirects.mjs`
218
+ carries no `/content` source that would rescue the written form. Every target page
219
+ existed and every one of them was reachable — only the links were not.
220
+
221
+ All seven now use the absolute form the repo had already established in
222
+ `create-objectstack`'s published READMEs: `https://docs.objectstack.ai/docs/...`, with
223
+ the path taken under `content/docs` and the page extension dropped, because the route
224
+ carries none. Each target was re-verified at the route level rather than as a file — the
225
+ two that named a **directory** (`/content/docs/automation/`,
226
+ `/content/docs/references/automation/`) resolve only because those directories carry an
227
+ `index.mdx`; a directory without one is a 404, not a section.
228
+
229
+ **Two more links in the same class were converted in the same pass.**
230
+ `service-knowledge` and `knowledge-ragflow` pointed at
231
+ `../../../content/docs/protocol/knowledge.mdx`. Those relative paths do resolve on both
232
+ GitHub and npm, so they are a milder defect than the seven — but they land the reader on
233
+ **raw MDX source** instead of the rendered page. They now point at the rendered page as
234
+ well. `service-knowledge`'s link text changed with it: it was the source filename in a
235
+ code span, which stops being an honest label once the destination is the page.
236
+
237
+ No API, behaviour or type surface changes — this is the published documentation these
238
+ packages ship.
239
+ - f01c0ee: docs: five published service READMEs stop documenting an API that does not exist (#9532)
240
+
241
+ A version bump is the point, not a side effect: these five READMEs are in their
242
+ packages' `files` arrays with `private` unset, so they are the pages npm renders —
243
+ and a docs-only fix with no bump never reaches npm at all.
244
+
245
+ Each of the five told a reader to an import of a `Service…` class from its own package
246
+ and call a static `.configure({...})` on it. Neither has ever existed: no class in
247
+ this repo exposes a static `configure`, and none of `ServiceAnalytics`,
248
+ `ServiceAutomation`, `ServiceCache`, `ServiceI18n` or `ServiceJob` is exported by
249
+ anything. A reader following any of them wrote code that could not compile. The real
250
+ entry point in every case is a kernel plugin constructed with `new`:
251
+ `AnalyticsServicePlugin`, `AutomationServicePlugin`, `CacheServicePlugin`,
252
+ `I18nServicePlugin`, `JobServicePlugin`.
253
+
254
+ ⛔ A name swap alone would not have been enough, and the gate landed in #9546 is what
255
+ proves it: substituting the genuine class while keeping `.configure(...)` turns the
256
+ import finding into a call-site finding rather than into silence. Each README is
257
+ rewritten against the package's built type surface, and each package's entry is
258
+ deleted from `scripts/published-readme-exports.baseline.json` in the same change
259
+ (the baseline is reconciled in both directions, so a stale entry fails too).
260
+
261
+ What was removed as fabricated, beyond the entry point:
262
+
263
+ - **service-analytics** — a nine-endpoint REST surface (`/analytics/count`, `/sum`,
264
+ `/avg`, `/min`, `/max`, `/group-by`, `/time-series`, `/metrics`, `/metrics/:name`)
265
+ of which none exists; the real surface is `POST /analytics/query`,
266
+ `GET /analytics/meta`, `POST /analytics/sql` and `POST /analytics/dataset/query`.
267
+ Also removed: `defineMetric`, `getMetric`, `compare`, `funnel`,
268
+ `executeDashboard`, `invalidateCache`, and an `AnalyticsServiceConfig` block whose
269
+ four keys (`defaultDriver`, `enableCaching`, `cacheTTL`, `maxMemoryResults`) are
270
+ none of the real ones.
271
+ - **service-automation** — `executeFlow`/`getFlow`/`listFlows`/`getFlowHistory`/
272
+ `registerTrigger` as the contract (the real contract is `execute(flowName, context?)`
273
+ plus `listFlows()` and a set of optional members), and a five-endpoint REST list that
274
+ matches no mounted route. The flow-authoring half of that README was already accurate
275
+ and is kept.
276
+ - **service-cache** — `mget`/`mset`/`del`/`delPattern`/`namespace`/`ttl`/`expire`/
277
+ `persist`/`incr`/`incrby`/`decr`/`getOrSet`/`invalidateTag`/`resetStats`, none of
278
+ which exist; `ICacheService` has six members. `CacheStats.keys`/`hitRate` corrected to
279
+ `keyCount` (there is no `hitRate`), and `set(key, value, { ttl })` corrected to the
280
+ real positional `set(key, value, ttl?)` in seconds.
281
+ - **service-i18n** — an `await i18n.t('ns:key')` dialect with namespaces, plural
282
+ suffixes, `context`, `returnObjects`, `setLocale`/`getLocale`, `formatDate`/
283
+ `formatNumber`/`formatRelative`, `addLocale`/`removeLocale`/`reload`, `getCoverage`/
284
+ `getMissingKeys`, and a `{{lng}}/{{ns}}` file layout. The real `t()` is synchronous
285
+ and takes the locale positionally — `t(key, locale, params?)` — over one
286
+ `{locale}.json` file per locale. The `POST /i18n/translate` endpoint does not exist.
287
+ - **service-job** — `scheduleInterval`/`scheduleOnce`/`getJob`/`stopJob`/`resumeJob`/
288
+ `deleteJob`/`runNow`/`getJobHistory`/`clearHistory`/`getLastExecution`, and a
289
+ `schedule({ name, schedule, handler })` options-object call. The real `schedule` is
290
+ positional — `schedule(name, schedule, handler, options?)` — and returns `void`.
291
+ Retry defaults corrected to the enforced ones (`maxRetries: 0`,
292
+ `backoffMultiplier: 1`).
293
+
294
+ Two capability claims are corrected rather than deleted, because the source is what
295
+ decides:
296
+
297
+ - **service-cache** advertised Redis as production support. `RedisCacheAdapter` throws
298
+ `RedisCacheAdapter not yet implemented` from every method, and
299
+ `new CacheServicePlugin({ adapter: 'redis' })` throws during `init` rather than
300
+ falling back to memory. The README now says so at the top and points at registering
301
+ a custom `ICacheService` under the slot instead.
302
+ - **service-job**'s `adapter: 'interval'` stores cron registrations that never fire.
303
+ That is now stated in the adapter table rather than left for a reader to discover.
304
+
305
+ No compliance claim (SOC 2 / HIPAA / GDPR or similar) was found in any of the five —
306
+ the shape that raised `plugin-audit`'s severity in #9517 is absent here.
307
+ - 402c125: fix(objectql): a temporal filter comparand the platform cannot interpret is refused at the engine door instead of answering 200 with zero rows (#8690)
308
+
309
+ <!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is
310
+ renamed, retired or tombstoned — no spec schema is touched at all. The change
311
+ is a new runtime refusal at the engine's filter collection point, plus the
312
+ routing decline that stops the raw-SQL analytics path bypassing it. -->
313
+
314
+ A `datetime` / `date` / `time` field filtered with a bare string the platform
315
+ cannot read — `last_30_days`, `not-a-date-at-all` — was bound **as written**
316
+ all the way to the driver, where the comparison is false for every row. The
317
+ caller received `HTTP 200`, an empty result set, and nothing to indicate the
318
+ filter was meaningless. An unknown `{placeholder}` in the same position was
319
+ already refused loudly (`FILTER_TOKEN_UNKNOWN` / 400, listing the resolvable
320
+ tokens), so one API answered two shapes of unusable comparand two different
321
+ ways.
322
+
323
+ It is concretely reachable rather than theoretical: `last_7_days` /
324
+ `last_30_days` / `last_90_days` are **declared preset names** in the dashboard
325
+ schema. The shipped console lowers them to `{N_days_ago}` macros before they
326
+ reach the API, so the console path was always safe — but a saved report, an
327
+ integration, an MCP client or an AI-authored query sends the preset name itself
328
+ and got a silent zero. An empty chart is the hardest failure to debug: it is
329
+ indistinguishable from "there is genuinely no data".
330
+
331
+ Such a comparand is now refused at the ObjectQL engine's single filter
332
+ collection point, with `code: 'INVALID_FILTER'` and `status: 400`, naming the
333
+ field, the value, the key path and the spellings that would work. That seam is
334
+ the one place holding the caller's comparand and the field's **declared type**
335
+ at the same moment, and every verb (`find` / `findOne` / `count` / `aggregate`
336
+ / `update` / `delete`) and both filter spellings (the array sugar and the
337
+ lowered condition) pass through it, so all four backends inherit one answer
338
+ rather than four. `NativeSQLStrategy` additionally **declines** such a query so
339
+ the raw-SQL analytics path falls through to that door instead of binding the
340
+ value into its own statement.
341
+
342
+ Deliberately unchanged, each by ruling: a `{placeholder}` keeps its existing
343
+ refusal one layer down (the door runs before token resolution and steps around
344
+ them, so `{30_days_ago}` still resolves normally); non-string comparands are
345
+ untouched (a number is epoch milliseconds, a `Date` is an instant); and the
346
+ **empty string** keeps today's behaviour exactly — it binds as `''` and matches
347
+ every non-null row, which is a separate question that remains its own card.
348
+ - Updated dependencies [56656aa]
349
+ - Updated dependencies [07e630e]
350
+ - Updated dependencies [2f65b1b]
351
+ - Updated dependencies [720ee95]
352
+ - Updated dependencies [f287435]
353
+ - Updated dependencies [2782805]
354
+ - Updated dependencies [e43d63a]
355
+ - Updated dependencies [9aa8890]
356
+ - Updated dependencies [7c9c1dd]
357
+ - Updated dependencies [75b7c24]
358
+ - Updated dependencies [d5552ca]
359
+ - Updated dependencies [d9813a9]
360
+ - Updated dependencies [8640fb2]
361
+ - Updated dependencies [2420641]
362
+ - Updated dependencies [2ad91c3]
363
+ - Updated dependencies [f57fb38]
364
+ - Updated dependencies [00777a0]
365
+ - Updated dependencies [d491625]
366
+ - Updated dependencies [2d0af57]
367
+ - Updated dependencies [420804d]
368
+ - Updated dependencies [716ac9b]
369
+ - Updated dependencies [a38408a]
370
+ - Updated dependencies [62b1427]
371
+ - Updated dependencies [7ea1372]
372
+ - Updated dependencies [23abe27]
373
+ - Updated dependencies [985a9cd]
374
+ - Updated dependencies [5f5e234]
375
+ - Updated dependencies [a8189ae]
376
+ - Updated dependencies [26e70fb]
377
+ - Updated dependencies [27a567d]
378
+ - Updated dependencies [42b05af]
379
+ - Updated dependencies [2b292ce]
380
+ - Updated dependencies [abcf853]
381
+ - Updated dependencies [8b9eba5]
382
+ - Updated dependencies [d575779]
383
+ - Updated dependencies [94f7ef8]
384
+ - Updated dependencies [c5ac5e4]
385
+ - Updated dependencies [a777944]
386
+ - Updated dependencies [dd88e1c]
387
+ - Updated dependencies [856527c]
388
+ - Updated dependencies [870f710]
389
+ - Updated dependencies [79c46da]
390
+ - Updated dependencies [7ff3975]
391
+ - Updated dependencies [29d055b]
392
+ - Updated dependencies [65589d6]
393
+ - Updated dependencies [2c86fe3]
394
+ - Updated dependencies [e196c6a]
395
+ - Updated dependencies [24173e9]
396
+ - Updated dependencies [4ab7523]
397
+ - Updated dependencies [19539b4]
398
+ - Updated dependencies [f8eb736]
399
+ - Updated dependencies [11b779e]
400
+ - Updated dependencies [739fe5b]
401
+ - Updated dependencies [4bfe1a5]
402
+ - Updated dependencies [2065e31]
403
+ - Updated dependencies [b69d0f5]
404
+ - Updated dependencies [4d47afe]
405
+ - Updated dependencies [e4e5c6e]
406
+ - Updated dependencies [9a56784]
407
+ - Updated dependencies [d00d2f6]
408
+ - Updated dependencies [df0c12d]
409
+ - Updated dependencies [d31785f]
410
+ - Updated dependencies [c308a4f]
411
+ - Updated dependencies [e2899f6]
412
+ - Updated dependencies [3851f87]
413
+ - Updated dependencies [2a29caa]
414
+ - Updated dependencies [09a6eee]
415
+ - Updated dependencies [1a7f907]
416
+ - Updated dependencies [cd455c8]
417
+ - Updated dependencies [e1bb0ca]
418
+ - Updated dependencies [30d3752]
419
+ - Updated dependencies [c80e7ae]
420
+ - Updated dependencies [09a9a8a]
421
+ - Updated dependencies [07026cf]
422
+ - Updated dependencies [5d4f3d5]
423
+ - Updated dependencies [4d80e8b]
424
+ - Updated dependencies [30b1c63]
425
+ - Updated dependencies [079b457]
426
+ - Updated dependencies [e43b211]
427
+ - Updated dependencies [890b38f]
428
+ - Updated dependencies [8bee54b]
429
+ - Updated dependencies [7a537ce]
430
+ - Updated dependencies [593c4bf]
431
+ - Updated dependencies [ff08691]
432
+ - Updated dependencies [60e0f90]
433
+ - Updated dependencies [90c5285]
434
+ - Updated dependencies [402c125]
435
+ - Updated dependencies [7901b2d]
436
+ - Updated dependencies [56bca91]
437
+ - Updated dependencies [79394d7]
438
+ - Updated dependencies [730fd9a]
439
+ - Updated dependencies [44bc51d]
440
+ - Updated dependencies [bbbfcfc]
441
+ - Updated dependencies [73cfddf]
442
+ - Updated dependencies [d634e66]
443
+ - @objectstack/spec@17.1.0
444
+ - @objectstack/types@17.1.0
445
+ - @objectstack/core@17.1.0
446
+
3
447
  ## 17.0.0
4
448
 
5
449
  ### Major Changes