@object-ui/data-objectstack 17.3.0 → 17.6.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 +888 -0
- package/README.md +3 -11
- package/dist/index.cjs +1511 -1115
- package/dist/index.d.cts +1222 -824
- package/dist/index.d.ts +1222 -824
- package/dist/index.js +1503 -1111
- package/package.json +5 -6
- package/src/aggregate-capability.test.ts +0 -177
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud.ts +0 -109
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -71
- package/src/getObjectSchema.test.ts +0 -145
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -3658
- package/src/integration.ts +0 -200
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -849
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -562
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -132
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -306
- package/src/studio.ts +0 -152
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -240
- package/src/validation-error.test.ts +0 -94
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,881 @@
|
|
|
1
1
|
# @object-ui/data-objectstack
|
|
2
2
|
|
|
3
|
+
## 17.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a8411ad: data-objectstack: retire the four remaining `v3.0.0 Deep Integration` modules — `IntegrationManager`, `SecurityManager`, the studio canvas helpers (`createDefaultCanvasConfig` / `snapToGrid` / `calculateAutoLayout`), and the contract helpers (`validatePluginContract` / `generateContractManifest`) — for having zero code consumers outside this package
|
|
8
|
+
|
|
9
|
+
`src/index.ts`'s `// v3.0.0 Deep Integration modules` banner introduced five
|
|
10
|
+
modules. objectui#4152 / PR #4239 already retired the first,
|
|
11
|
+
`CloudOperations`, for fabricating a plausible success against a client
|
|
12
|
+
namespace that does not exist. This closes out the other four:
|
|
13
|
+
`contracts.ts`, `integration.ts`, `security.ts`, `studio.ts`.
|
|
14
|
+
|
|
15
|
+
**Not a repeat of #4152's urgency limb.** None of these four fabricated
|
|
16
|
+
anything — `SecurityManager.generateCSPHeader()` really composes a header,
|
|
17
|
+
`snapToGrid` really snaps, `validatePluginContract` really validates. What
|
|
18
|
+
they shared with `CloudOperations` was the other limb: published surface of
|
|
19
|
+
`@object-ui/data-objectstack` with a measured **zero** code consumers outside
|
|
20
|
+
this package, across `packages/`, `apps/` and `examples/` (`.ts`/`.tsx`,
|
|
21
|
+
excluding `node_modules`). The two apparent hits on re-measurement were
|
|
22
|
+
homonyms, not consumers — `packages/plugin-designer/src/PageDesigner.tsx`
|
|
23
|
+
declares its own local `snapToGrid` callback with no import from this
|
|
24
|
+
package, and the `SecurityManager` hits outside this file are prose in
|
|
25
|
+
`CHANGELOG.md`. Under the startup-focus principle a declared capability with
|
|
26
|
+
no producer, no consumer and no business pull is retired, not kept on the
|
|
27
|
+
chance it becomes useful.
|
|
28
|
+
|
|
29
|
+
**Breaking, in FROM → TO form.** The following are no longer exported from
|
|
30
|
+
`@object-ui/data-objectstack`:
|
|
31
|
+
|
|
32
|
+
- `IntegrationManager` and its types (`IntegrationConfig`, `IntegrationTrigger`,
|
|
33
|
+
`IntegrationProvider`, `SlackIntegrationConfig`, `EmailIntegrationConfig`,
|
|
34
|
+
`WebhookIntegrationConfig`)
|
|
35
|
+
- `SecurityManager` and its types (`SecurityManagerPolicy`, `CSPConfig`,
|
|
36
|
+
`AuditLogConfig`, `AuditEventType`, `DataMaskingConfig`, `DataMaskingRule`,
|
|
37
|
+
`AuditLogEntry`)
|
|
38
|
+
- `createDefaultCanvasConfig`, `snapToGrid`, `calculateAutoLayout` and their
|
|
39
|
+
types (`StudioCanvasConfig`, `StudioPropertyEditor`,
|
|
40
|
+
`StudioThemeBuilderConfig`, `StudioColorPalette`, `StudioTypographyPreset`,
|
|
41
|
+
`StudioShadowPreset`)
|
|
42
|
+
- `validatePluginContract`, `generateContractManifest` and their types
|
|
43
|
+
(`PluginContract`, `PluginExport`, `PluginAPIContract`,
|
|
44
|
+
`ContractValidationResult`, `ContractValidationError`)
|
|
45
|
+
|
|
46
|
+
It is a `minor` under this repo's version policy (objectui's own breaking
|
|
47
|
+
changes never declare `major`). Nothing broke that was working: the only
|
|
48
|
+
in-repo construction sites were this package's own `v3-compat.test.ts` (which
|
|
49
|
+
exercised the modules directly) and `spec-symbol-batch6.test.ts` (which only
|
|
50
|
+
guarded `SecurityManagerPolicy`'s name against colliding with the spec's
|
|
51
|
+
unrelated `SecurityPolicy` — that guard is removed along with its subject).
|
|
52
|
+
|
|
53
|
+
**No compile-compat stub was left**, for the same reason #4152 left none: with
|
|
54
|
+
no consumer to keep compiling, a stub would be a second phantom surface
|
|
55
|
+
guarding the first.
|
|
56
|
+
|
|
57
|
+
**The banner and the compat-test title stop claiming a v3.** `index.ts`'s
|
|
58
|
+
`// v3.0.0 Deep Integration modules` banner had nothing left under it once
|
|
59
|
+
these four went, so it is removed rather than retitled.
|
|
60
|
+
`v3-compat.test.ts` — titled "v3.0.0 compatibility tests for @objectstack
|
|
61
|
+
dependencies" against a resolved `@objectstack` family of `17.0.0-rc.6` even
|
|
62
|
+
before this change — is not an empty shell (one block, `PaginatedResult API`,
|
|
63
|
+
never depended on any of the five retired modules), so it stays and is
|
|
64
|
+
retitled instead of deleted.
|
|
65
|
+
|
|
66
|
+
A negative pin
|
|
67
|
+
(`src/v3-deep-integration-retired-4241.pin.test.ts`) replaces the retired
|
|
68
|
+
`v3-compat.test.ts` cases and fails if any of the thirty retired names
|
|
69
|
+
returns — reading both the runtime export list (which catches the seven
|
|
70
|
+
class/function exports) and `index.ts`'s source text (the only instrument
|
|
71
|
+
that can catch a returning `export type`).
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- 1ef236e: `@object-ui/data-objectstack` stops publishing its `src/` tree
|
|
76
|
+
|
|
77
|
+
The manifest's `files` array listed `src` alongside `dist`, so every published tarball carried all 43 source files — 38 of them `*.test.ts`. It had been that way since the package's first commit (`780a1b993`), never added for a consumer, and objectui#4006 recorded the same shape without acting on it: its scope was the `*.test.d.ts` half that the build program emitted into `dist`, and its own triage note graded this half as tarball weight rather than a break.
|
|
78
|
+
|
|
79
|
+
Nothing in the published surface reached those files, which is why no consumer changes in either direction. Measured on a cleanly rebuilt `dist`, all four ways in are closed: the `exports` map has one entry (`.`) and every condition under it targets `dist`; `main` / `module` / `types` are `./dist/index.js`, `./dist/index.js`, `./dist/index.d.ts`; the repo and the docs teach only the root specifier, and no `@object-ui/data-objectstack/src/...` deep import exists anywhere (the `src` paths in sibling `vite.config.ts` / `vitest.config.mts` files are workspace aliases resolved through `path.resolve()` against the source tree, which no `files` array shapes); and the tarball holds no sourcemap that could point back at `src`, since `tsup.config.ts` sets `sourcemap: false` and its bundled `dts` writes no `.d.ts.map` — the built `dist` contains four files, zero `.map` among them, and zero occurrences of `sourceMappingURL` or `../src/`.
|
|
80
|
+
|
|
81
|
+
`npm pack --dry-run` across the change, on the same `dist`:
|
|
82
|
+
|
|
83
|
+
| | before | after |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| entries | 51 | 8 |
|
|
86
|
+
| unpacked | 1356830 B | 719876 B |
|
|
87
|
+
| tarball | 393379 B | 222157 B |
|
|
88
|
+
|
|
89
|
+
43 files leave, none arrives, and every surviving entry is byte-identical apart from the edited `package.json`: `dist/index.{js,cjs,d.ts,d.cts}`, `README.md`, `CHANGELOG.md`, `LICENSE`, `package.json`. The 43 are the 38 tests plus the five modules they cover (`index.ts`, `errors.ts`, `metadata-client.ts`, `userState.ts`, `cache/MetadataCache.ts`), whose published form remains the bundled `dist/index.js`.
|
|
90
|
+
- cf4f8a6: `MetadataClient.layered()` now reads the three-layer view from its declared path,
|
|
91
|
+
`GET /meta/:type/:name/layers`, instead of flagging the ordinary item read.
|
|
92
|
+
|
|
93
|
+
The consumer half of objectstack#5882 (ruled B by the maintainer; the server half
|
|
94
|
+
landed in objectstack#6596 and shipped in `@objectstack/spec@17.0.0`). The layered
|
|
95
|
+
projection — packaged baseline vs tenant overlay vs merged effective, which is
|
|
96
|
+
what the Studio metadata editor's comparison tabs render — used to be reached by
|
|
97
|
+
hanging a query flag on `GET /meta/:type/:name`. One route therefore answered two
|
|
98
|
+
unrelated representations chosen by a query parameter, while `packages/spec`
|
|
99
|
+
declared only the unflagged one: anything generating a client from the route
|
|
100
|
+
table produced a parser that was simply wrong for the flagged call. The
|
|
101
|
+
projection now has a path of its own and a response schema of its own
|
|
102
|
+
(`GetMetaItemLayeredResponseSchema`).
|
|
103
|
+
|
|
104
|
+
Same body, same envelope, so nothing in the editor changes shape: `code`,
|
|
105
|
+
`overlay`, `overlayScope`, `effective`, the load-time `_diagnostics` and the full
|
|
106
|
+
ADR-0010 protection envelope all still arrive on one round trip, and `?package=`
|
|
107
|
+
(ADR-0048) is still threaded — the two entry points are served by ONE handler
|
|
108
|
+
upstream precisely so the deprecation window's promise holds. The retired
|
|
109
|
+
spelling still answers during that window, marked with RFC 9745 `Deprecation` and
|
|
110
|
+
an RFC 8288 `Link: rel="successor-version"` pointing here, so this migration is
|
|
111
|
+
safe against a lagging backend for as long as the window stays open, and it is
|
|
112
|
+
what lets the maintainer close it.
|
|
113
|
+
|
|
114
|
+
One behaviour delta rides along, and it is the server's design rather than a
|
|
115
|
+
choice made here: the retired flag FELL THROUGH to the plain item read when the
|
|
116
|
+
backend's protocol implementation had no layered support, answering the
|
|
117
|
+
`{ type, name, item }` envelope. A dedicated path refuses to answer a different
|
|
118
|
+
resource under this one's declared shape and returns 501 `NOT_IMPLEMENTED`, which
|
|
119
|
+
surfaces as a failed read instead of a comparison view whose `code` and `overlay`
|
|
120
|
+
are silently blank.
|
|
121
|
+
|
|
122
|
+
The request is built in this package rather than delegated to
|
|
123
|
+
`@objectstack/client` because the SDK expresses no layered read in either
|
|
124
|
+
spelling — the framework's REST route ledger records the route as `server-only`,
|
|
125
|
+
"consumed by objectui over plain HTTP", and whether the SDK should express it is
|
|
126
|
+
an open upstream product call. The new path expectation is derived from the
|
|
127
|
+
installed `@objectstack/spec` route table, and a ratchet keeps any shipped source
|
|
128
|
+
file or skills guide from reaching the projection by query flag again.
|
|
129
|
+
- 3d053bb: The Studio's overlay-layer badge stops printing the producer's raw scope value
|
|
130
|
+
|
|
131
|
+
objectui#4982. `MetadataLayered.overlayScope` was typed `string | null` under a comment naming its
|
|
132
|
+
vocabulary as `organization | environment | package` — three spellings the producer has never
|
|
133
|
+
emitted. The real vocabulary lives in `@objectstack/spec`'s `GetMetaItemLayeredResponseSchema`
|
|
134
|
+
(`z.enum(['org', 'env']).nullable()`), and the framework's two assignment sites write `'org'` /
|
|
135
|
+
`'env'`. Because the declared type was `string`, no compiler anywhere had an opinion, so the wrong
|
|
136
|
+
comment was the only description of the field a reader had.
|
|
137
|
+
|
|
138
|
+
`overlayScope` is now the spec union, derived by indexing the published response type rather than
|
|
139
|
+
restated locally (a restatement is the fork `check:spec-symbol-derivation` rejects); the alias ships
|
|
140
|
+
as `MetadataOverlayScope`.
|
|
141
|
+
|
|
142
|
+
User-visible half: `LayeredDiff`'s overlay badge rendered that value straight to screen while the
|
|
143
|
+
sibling artifact / none / merged badges all went through `translateConsoleValue`, and
|
|
144
|
+
`CONSOLE_VALUE_ZH.layer` had no entry for either value the field can hold. One badge therefore had
|
|
145
|
+
two languages depending on the data — a zh-CN admin opening any overlaid metadata item read `org` /
|
|
146
|
+
`env`, while an un-overlaid one read 「已设」. The badge now translates like its three siblings, with
|
|
147
|
+
「组织」/「环境」 added to the layer table. That table's overlay-scope half is keyed by the spec union,
|
|
148
|
+
so a scope the spec adds later fails `type-check` until it has a label instead of quietly reaching a
|
|
149
|
+
badge in English. `translateConsoleValue` remains zh-only for every group, as before — extending it
|
|
150
|
+
to the other locale packs is a separate decision and not part of this change.
|
|
151
|
+
- d871f8e: A view personalization overlay no longer freezes the view it was laid over. `ObjectView`'s
|
|
152
|
+
`persistViewPatch` sends `{ ...baseViewDef, ...patch }`, so a row written by a mere column
|
|
153
|
+
drag or sort change stored the view's whole body — its effective `filter`, `columns`,
|
|
154
|
+
`label`, `type`, `isDefault` — as of that moment, and the display merge
|
|
155
|
+
(`{ ...source, ...override }`) then let that snapshot outrank the source view indefinitely:
|
|
156
|
+
an admin edited a view's filter and everyone who had ever resized a column silently kept the
|
|
157
|
+
old filter, with nothing reporting it.
|
|
158
|
+
|
|
159
|
+
An overlay now contributes only the keys it owns — `rowHeight`, `sort`, `hiddenFields`,
|
|
160
|
+
`columnState`, `inlineEdit` (`VIEW_OVERLAY_OWNED_KEYS`, new export from
|
|
161
|
+
`@object-ui/data-objectstack` alongside `narrowPersonalizationOverlay`) — so a later change
|
|
162
|
+
to the source view reaches every user, including those whose stored row still carries the
|
|
163
|
+
old snapshot: rows written before this change stop shadowing the source on the next read,
|
|
164
|
+
with no migration to run and nothing rewritten at rest. A genuine saved view's own body is
|
|
165
|
+
untouched — it is classified by the same predicate `listViews()` already excludes overlay
|
|
166
|
+
rows by, so a row cannot be an overlay for one reader and a saved view for the other
|
|
167
|
+
(objectui#5233, ruled on objectstack#7494).
|
|
168
|
+
- a0b9e91: A system (code-defined) view's personalization overlay row no longer masquerades as a user-created saved view.
|
|
169
|
+
|
|
170
|
+
Toggling density / sort / hidden columns / column widths / inline-edit on a code-defined view persists a row under the same `type='view'` metadata namespace a genuinely saved view lives in, keyed by the same id (`ObjectStackAdapter.updateViewConfig`). `listViews()` previously returned that row indistinguishably from a real saved view, so `ObjectView`'s `isSystem = !saved` check flipped to `false` and the tab gained Rename / Delete / Set-default / Pin against a view that lives in code — `handleDeleteView` would even call `dataSource.deleteView` on it.
|
|
171
|
+
|
|
172
|
+
Two layers now keep the two kinds of rows apart:
|
|
173
|
+
|
|
174
|
+
- **Write side**: `updateViewConfig` — the only production writer of personalization overlays — stamps an explicit `_isOverride: true` discriminant on every row it saves, UNLESS the write targets an already-saved view's own row (see below).
|
|
175
|
+
- **Read side**: `listViews()` excludes any row carrying that marker, and (for rows already persisted before this fix shipped) a best-effort legacy shape: a flat body with a `viewKind` the platform can only have server-side-backfilled from a registry (code-defined) baseline — a genuine runtime-created saved view never has one.
|
|
176
|
+
|
|
177
|
+
`listViewOverrides()` (the reader `ObjectView` uses to merge these settings back into the live view for display) is unchanged — it is supposed to keep seeing overlay rows.
|
|
178
|
+
|
|
179
|
+
The overlay this stores is **org-wide shared view settings**, not a per-user preference (a true per-user scope is a parked platform-side v18 direction) — comments describing it as "personal" have been corrected to say so.
|
|
180
|
+
|
|
181
|
+
**Follow-up fix (same card, post-review):** `updateViewConfig`'s ONE call site (`ObjectView`'s toolbar-driven toggle) fires for a toggle on EITHER a system view OR an already-saved view — a saved view whose own toolbar the user toggles writes to that same view's own row. Stamping the overlay marker unconditionally there would flag the user's own saved view as an overlay and make `listViews()` exclude it on the very next read, i.e. the saved view would vanish from the switcher the moment its density was adjusted. `updateViewConfig` gains an optional `opts.isSavedView` parameter (also added to the `DataSource` interface in `@object-ui/types`); `ObjectView` passes it from the same `isSavedViewId` classification its readonly gate and mutating handlers already use, and the marker is withheld when it's true.
|
|
182
|
+
- Updated dependencies [88085e3]
|
|
183
|
+
- Updated dependencies [2533ec5]
|
|
184
|
+
- Updated dependencies [bbe8b86]
|
|
185
|
+
- Updated dependencies [8477be5]
|
|
186
|
+
- Updated dependencies [279fb13]
|
|
187
|
+
- Updated dependencies [ad07b65]
|
|
188
|
+
- Updated dependencies [41f498b]
|
|
189
|
+
- Updated dependencies [e1d4251]
|
|
190
|
+
- Updated dependencies [1184192]
|
|
191
|
+
- Updated dependencies [a2a9747]
|
|
192
|
+
- Updated dependencies [ac600e5]
|
|
193
|
+
- Updated dependencies [c1ef923]
|
|
194
|
+
- Updated dependencies [af5e292]
|
|
195
|
+
- Updated dependencies [7f96b10]
|
|
196
|
+
- Updated dependencies [167ec42]
|
|
197
|
+
- Updated dependencies [f1d4748]
|
|
198
|
+
- Updated dependencies [b1119ec]
|
|
199
|
+
- Updated dependencies [9f23d2b]
|
|
200
|
+
- Updated dependencies [578e025]
|
|
201
|
+
- Updated dependencies [af025ee]
|
|
202
|
+
- Updated dependencies [598c89a]
|
|
203
|
+
- Updated dependencies [b8b9af4]
|
|
204
|
+
- Updated dependencies [31676be]
|
|
205
|
+
- Updated dependencies [9ce096f]
|
|
206
|
+
- Updated dependencies [e05db88]
|
|
207
|
+
- Updated dependencies [5ffcc14]
|
|
208
|
+
- Updated dependencies [d971e51]
|
|
209
|
+
- Updated dependencies [97abb24]
|
|
210
|
+
- Updated dependencies [deb157a]
|
|
211
|
+
- Updated dependencies [d2ce342]
|
|
212
|
+
- Updated dependencies [9695da7]
|
|
213
|
+
- Updated dependencies [58b8346]
|
|
214
|
+
- Updated dependencies [dfc6975]
|
|
215
|
+
- Updated dependencies [3cf4de0]
|
|
216
|
+
- Updated dependencies [c9dc811]
|
|
217
|
+
- Updated dependencies [a0b9e91]
|
|
218
|
+
- Updated dependencies [99bd015]
|
|
219
|
+
- @object-ui/types@17.6.0
|
|
220
|
+
- @object-ui/core@17.6.0
|
|
221
|
+
|
|
222
|
+
## 17.5.0
|
|
223
|
+
|
|
224
|
+
### Minor Changes
|
|
225
|
+
|
|
226
|
+
- 932cbcd: An app you are not allowed to open now says so, instead of reporting that it may still be publishing
|
|
227
|
+
|
|
228
|
+
`GET /api/v1/meta/apps` is filtered per session server-side (`filterAppForUser`), so an app withheld by its `requiredPermissions` and an app that does not exist were byte-identical to the console: both simply absent from the list. With one fact and two conditions, `AppContent` rendered its only copy for an absent app — "This app is not available yet — it may still be publishing. Try again in a moment." — over a permanent authorization decision, under a Retry button that could never succeed.
|
|
229
|
+
|
|
230
|
+
That is not a cosmetic complaint. On a downstream acceptance round one role hit this screen while another opened the same app fine, and because the copy names a transient deployment state the finding was filed as a suspected platform defect and carried through two test batches before a clean-baseline investigation found the account was missing a permission-set binding. The gate had been working exactly as designed; the message is what sent everyone to the wrong place.
|
|
231
|
+
|
|
232
|
+
The maintainer ruling (2026-08-12) took the contract half first. objectstack#8013 made the BY-NAME route answer an explicit denial — `403` with the ADR-0112 catalog code `PERMISSION_DENIED` in the declared `{ success: false, error: { code, message } }` envelope — for an app that exists and whose `requiredPermissions` the session lacks, while the LIST route stays filtered exactly as before, with no `authorized: false` flag, so the enumeration surface is not widened past what a direct by-name probe already implies. Absence keeps answering `404 RESOURCE_NOT_FOUND`, and so do the two neighbouring refusals the same ruling deliberately left alone: an unpublished app (ADR-0045 §3 keeps it externally unobservable) and an app gated by an absent optional service (ADR-0057 D10 — nothing was denied to the caller).
|
|
233
|
+
|
|
234
|
+
This is the console half. When a requested app is missing from the list and the existing post-publish readiness re-check still cannot find it, the console asks the by-name route which of the two it is, through a new `ObjectStackAdapter.probeAppAccess(name)`. On the measured code it renders a plain authorization message with a way back to the launcher; on anything else — an absent app, an unreachable server, a host that injected a DataSource without the probe — today's publishing copy renders byte for byte, retry button included.
|
|
235
|
+
|
|
236
|
+
Two properties of that seam are load-bearing rather than incidental. It branches on the ADR-0112 **code**, never the status (objectui#4408): the two answers under test are both errors one status apart, and a status-reading implementation passes the happy path while going blind exactly where the defect lives. And only `denied` moves the copy: this bug exists because the console asserted a state it had not measured, so a probe that fails, times out or cannot be issued must leave the screen alone rather than guess in the other direction.
|
|
237
|
+
|
|
238
|
+
`probeAppAccess` is deliberately separate from `getApp` rather than a flag on it: `getApp` degrades every failure to `null` — the very conflation being undone — and memoises in the adapter's metadata cache, where a verdict about the CALLER would outlive the session it described. New public API on the adapter (`probeAppAccess`, `isAppPermissionDeniedError`, `APP_PERMISSION_DENIED_CODE`, `AppAccessVerdict`), purely additive; nothing existing changed shape. Three new `empty.*` keys ship in all ten locale packs.
|
|
239
|
+
|
|
240
|
+
- 537a0d1: `deleteView` removes every home the view has — deleting a draft-only saved view no longer silently no-ops
|
|
241
|
+
|
|
242
|
+
A view has two possible homes: the pending per-item **draft** (`DELETE /api/v1/meta/view/:name?state=draft`) and the **published** overlay (`DELETE /api/v1/meta/view/:name`). `deleteView` addressed only the second, unqualified. Deleting a view that existed only as a draft therefore fired the delete at the published overlay, the server answered `200 {"success":true,"reset":false,"message":"No view '…' found — nothing to delete."}`, the draft survived untouched, and the tab was still there after a reload — while the receipt reported `{ deleted: false }` and nothing surfaced the refusal to the user.
|
|
243
|
+
|
|
244
|
+
That is not a corner case. ADR-0034's `persistRuntimeMetadata` (app-shell) stages **every** runtime edit as a draft, and a view created from the `+` tab lives ONLY as a draft until an explicit Publish — so both "a view you just made" and "a published view you have since edited" are routinely draft-carrying.
|
|
245
|
+
|
|
246
|
+
**Why this is not the mechanical mirror of #4139.** `updateView` probes the draft first and writes back to whichever home the read resolved; that is right for an update in all cases. Copying it here would have been wrong in one: on a published+draft pair a draft-first-_only_ delete discards the draft and leaves the published row still serving the view. That is not Delete view, it is **Discard draft** — a deliberately different operation that already exists (`discardRuntimeDraft`, documented as "the published overlay is untouched"). The asymmetry has a clean statement: for an update, one home is the right home; for a delete, "remove this view" is satisfied only when _no home is left serving it_.
|
|
247
|
+
|
|
248
|
+
So both homes are now deleted, **draft first**. The order is load-bearing on the failure path: a fault between the two calls leaves the published overlay intact, so the view is still served and the delete is cleanly retryable. The reverse order would strand a draft-only view — precisely the bug above.
|
|
249
|
+
|
|
250
|
+
**Two blind calls, no probe.** Measured against the framework's `deleteMetaItem`: a missing home is reported as a **200** carrying `reset:false` (`"No pending draft for view/x."` / `"No view 'x' found — nothing to delete."`), never a 404. There is nothing for a probe to protect against, and `updateView`'s probe exists for a different reason — its read must resolve the row the merge writes back to — which has no counterpart for a delete.
|
|
251
|
+
|
|
252
|
+
**One transport, one error contract.** Both halves now go through `MetadataClient.reset()`, the transport that can express the `?state=` qualifier and the one `updateView`'s draft half already uses. The published half previously went through `client.meta.deleteItem`; measured, that issues the byte-identical request (this adapter configures no environment scoping), so routing it here changes no addressing and collapses two error shapes into one `MetadataError`.
|
|
253
|
+
|
|
254
|
+
The receipt is widened **additively**: `{ deleted }` gains optional `draft` and `published` outcomes (`removed`, plus the server's `reset` / `message`). `deleted` is true only when no home is left serving the view _and_ at least one actually held a row — a view that existed in neither home still answers `false`, unchanged. A failure of the published half after the draft was discarded now throws (matching `updateView`'s convention of surfacing a fault rather than degrading) carrying the partial state on the error's `outcome`: "draft gone, overlay left" is exactly what the old `{ deleted: boolean }` could not express, and it is never rounded up to `true`.
|
|
255
|
+
|
|
256
|
+
Cache invalidation moves into a `finally`, so `invalidateViewKeys` fires exactly once per call on **every** outcome including the throw. After a half-failure the draft row really is gone, and objectui#4363's asymmetry decides it: an unnecessary invalidation costs one refetch, a missed one costs the cache's full 5-minute TTL of stale overrides.
|
|
257
|
+
|
|
258
|
+
Minor rather than patch: this moves published behavior for existing callers and adds two exported types, the same grading objectui#4271's `get()` unwrap and objectui#4495's `find()` resolve→reject took. The `.d.ts` diff is additive only — `deleteView`'s return widens from an inline `{ deleted: boolean }` to the new `DeleteViewResult`, which still carries `deleted: boolean` — so no consumer needs a code edit to keep compiling. A repo-wide census found one call site (app-shell's `ObjectView` delete handler), which awaits the call and does not read the receipt.
|
|
259
|
+
|
|
260
|
+
- bec3e14: The `DataSource` contract carries `deleteView`'s per-home outcomes (#4564)
|
|
261
|
+
|
|
262
|
+
#4479 / PR #4562 widened the ObjectStack adapter's `deleteView` to return
|
|
263
|
+
`DeleteViewResult { deleted, draft?, published? }`, so a caller could finally tell a
|
|
264
|
+
partial delete ("draft gone, published overlay left") from a complete one. The shared
|
|
265
|
+
interface did not follow: `DataSource.deleteView?` still declared the narrow
|
|
266
|
+
`Promise<{ deleted: boolean }>`.
|
|
267
|
+
|
|
268
|
+
Nothing failed to compile, and that is exactly what made the gap invisible — a wider
|
|
269
|
+
return is assignable to a narrower declaration, so the adapter satisfied the interface
|
|
270
|
+
while every consumer reaching it **through** `DataSource` was handed a type with the
|
|
271
|
+
per-home outcomes already discarded. The one real call site today (app-shell's
|
|
272
|
+
`ObjectView` delete handler) awaits the call and reads nothing off the receipt, so the
|
|
273
|
+
loss was latent rather than broken.
|
|
274
|
+
|
|
275
|
+
`DeleteViewResult` and `ViewHomeDeleteOutcome` now live in `@object-ui/types`, beside
|
|
276
|
+
the `DataSource` interface that returns them, and `deleteView?`'s declared return is
|
|
277
|
+
`Promise<DeleteViewResult>`. The direction was forced: the dependency runs
|
|
278
|
+
`@object-ui/data-objectstack` to `@object-ui/types` and never the other way, so the
|
|
279
|
+
shapes could not be imported downward — moving them was the alternative to re-declaring
|
|
280
|
+
a structural twin in `types`, which the one-resolver rule rejects because a copy is
|
|
281
|
+
mutually assignable with the original for exactly as long as it takes to drift.
|
|
282
|
+
|
|
283
|
+
`@object-ui/data-objectstack` re-exports both names unchanged, so every importer PR
|
|
284
|
+
#4562 left pointing at it keeps compiling — and now resolves to the same declaration the
|
|
285
|
+
shared contract speaks rather than a look-alike. A repo-wide census before the move
|
|
286
|
+
found zero importers of either name outside the declaring file itself, PR #4562's own
|
|
287
|
+
suite included, so the re-export is insurance rather than a load-bearing shim.
|
|
288
|
+
|
|
289
|
+
`deleteView` stays **optional** on the interface and keeps both parameters; the growth is
|
|
290
|
+
to the return type only, and `deleted` is untouched, so a consumer reading only `deleted`
|
|
291
|
+
needs no edit.
|
|
292
|
+
|
|
293
|
+
Grading, per this repository's version-alignment convention (the major tracks
|
|
294
|
+
`@objectstack`, never an API-break count):
|
|
295
|
+
|
|
296
|
+
- `@object-ui/types` — **minor**: entry-reachable growth. Two new exported interfaces
|
|
297
|
+
plus a widened method return on `DataSource`, all reachable from the package entry.
|
|
298
|
+
- `@object-ui/data-objectstack` — **minor**, measured rather than assumed. Its emitted
|
|
299
|
+
`dist/index.d.ts` is **not** byte-identical after the swap: the two `interface` blocks
|
|
300
|
+
leave the file and are replaced by a re-export from `@object-ui/types` (121.61 KB to
|
|
301
|
+
120.25 KB). Both names remain in the public export list, so no importer breaks, but the
|
|
302
|
+
declaration genuinely moved and the emitted types now depend on `@object-ui/types` for
|
|
303
|
+
it — that is a minor, not a patch.
|
|
304
|
+
|
|
305
|
+
- 479cc7b: `MetadataClient.get()` returns the item body its docblock always promised — the field half of the permission matrix is alive again
|
|
306
|
+
|
|
307
|
+
`GET /api/v1/meta/:type/:name` answers the spec-declared envelope `{ type, name, item, …protection fields }` — one shape, for published and draft reads alike, since objectstack#5563 collapsed the read to it. `get()` handed that envelope straight back to callers while its own docblock declared it returned "the unwrapped item content". Every consumer reading `obj.fields` therefore read `undefined`.
|
|
308
|
+
|
|
309
|
+
The visible cost was the entire field-level half of the permission matrix: expanding any object in `/_console/apps/:app/metadata/permission/:set` reported "No fields registered for this object." with zero checkboxes, for every object, while the network showed that object's 21 fields arriving 200 OK. Reproduced against two objects on fresh loads, and proven not to be the read-only gate — a run with the editor fully writable (864 enabled checkboxes) still showed an empty field sub-table, which is exactly what a read resolving `undefined` predicts.
|
|
310
|
+
|
|
311
|
+
That was one symptom of nine. A census of every `get()` call site found **zero** deliberate readers of the envelope and nine consumers reading the body directly, all of them broken the same way: the RLS CEL editor's field lint and autocomplete resolved an empty field set; the dataset inspectors and the preview field/catalog hooks came back empty; the report drill-down's fallback path read `def.object` off the envelope, found nothing and silently returned; the record-page seed synthesized a default layout from an envelope instead of an object; and the Field Designer read `raw.fields` for display and then wrote `{ ...raw, fields }` back — saving the envelope over the object body. None of it was caught, because the test doubles across the repo were written against the docblock: they answered a bare `{ fields }` body, so the suite exercised the documented contract while production ran the other one.
|
|
312
|
+
|
|
313
|
+
The fix is at the producer, not the nine consumers. `get()` now unwraps the envelope once, at the client boundary — so every one of those call sites is repaired without being touched. Detection is by the PRESENCE of the three keys `GetMetaItemResponseSchema` declares (`type: string`, `name: string`, an `item` slot), never guessed from payload contents: a metadata document carrying its own `type` and `name` (a view is `{ name, type: 'grid', … }`) has no `item` and is left whole, and a document with an `item` property of its own but no envelope identity is likewise untouched. Key count is deliberately not part of the test, since a real envelope also spreads the ADR-0008 protection carriers. Anything that is not the envelope — an older server answering the bare document — passes through byte-for-byte, and 404 still reads as `null`.
|
|
314
|
+
|
|
315
|
+
`getDraft()` is unchanged and keeps returning the envelope, which its docblock declares and roughly eleven call sites depend on by reading `.item`. That asymmetry is now real rather than aspirational: the two methods share one private transport, and differ only in whether they unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft` (this package) remain the shared helpers for taking a draft body out, and both were already tolerant of either shape, so the two seams that reach a draft through `get()` keep their exact semantics — including reading an empty draft as "nothing pending".
|
|
316
|
+
|
|
317
|
+
Minor rather than patch: this moves published behavior for existing callers, the same grading `find()`'s resolve-to-reject change took. No signature changed — the `.d.ts` diff is documentation plus one private member — so nothing needs a code edit to keep compiling; a caller that had written its own `.item` compensator against the old behavior would need to drop it, and none exists in this repo.
|
|
318
|
+
|
|
319
|
+
- 2776b11: data-objectstack: retire the phantom `CloudOperations` surface — the class, its three `Cloud*` types, and the module that claimed to integrate a cloud namespace no client has ever shipped
|
|
320
|
+
|
|
321
|
+
`src/cloud.ts` exported a `CloudOperations` class with four methods, all
|
|
322
|
+
re-exported from the package entry, so this was published surface of
|
|
323
|
+
`@object-ui/data-objectstack`. Every method optional-chained into
|
|
324
|
+
`client.cloud?.…`, and no released `@objectstack/client` has ever exported a
|
|
325
|
+
`cloud` namespace. Re-measured at `17.0.0-rc.6` before deleting: the module's
|
|
326
|
+
export list is `ObjectStackClient`, `ScopedProjectClient`, `RealtimeAPI`,
|
|
327
|
+
`QueryBuilder`, `FilterBuilder`, `createQuery`, `createFilter`, and a
|
|
328
|
+
constructed client's `.cloud` is `undefined`. The nearest real namespaces on the
|
|
329
|
+
instance — `projects` (which owns `/api/v1/cloud/environments`) and `packages`
|
|
330
|
+
(which owns marketplace installs) — are not what these methods reached for.
|
|
331
|
+
|
|
332
|
+
So every call resolved `undefined` and fell through to a literal:
|
|
333
|
+
|
|
334
|
+
| method | what it returned, always |
|
|
335
|
+
| :-------------------- | :------------------------------------------------------------ |
|
|
336
|
+
| `deploy` | `{ deploymentId: 'deploy-' + Date.now(), status: 'pending' }` |
|
|
337
|
+
| `getDeploymentStatus` | `{ status: 'unknown' }` |
|
|
338
|
+
| `searchMarketplace` | `[]` |
|
|
339
|
+
| `installPlugin` | `{ success: false }` |
|
|
340
|
+
|
|
341
|
+
The maintainer's 2026-08-11 ruling removed it rather than repairing it, and named
|
|
342
|
+
the reason: `deploy()` did not degrade to an error, it **manufactured a
|
|
343
|
+
plausible success**. A caller got a well-formed `deploymentId` for an operation
|
|
344
|
+
that never left the process and then polled it forever against
|
|
345
|
+
`{ status: 'unknown' }`. That is the most dangerous shape for an AI consumer,
|
|
346
|
+
which builds downstream logic on the fake id instead of getting suspicious.
|
|
347
|
+
Under the startup-focus principle a declared capability with no producer, no
|
|
348
|
+
consumer and no business pull is retired, not stubbed.
|
|
349
|
+
|
|
350
|
+
**Breaking, in FROM → TO form.** `CloudOperations`, `CloudDeploymentConfig`,
|
|
351
|
+
`CloudHostingConfig` and `CloudMarketplaceEntry` are no longer exported from
|
|
352
|
+
`@object-ui/data-objectstack`. It is a `minor` under this repo's version policy
|
|
353
|
+
(objectui's own breaking changes never declare `major`). Nothing broke that was
|
|
354
|
+
working: the only in-repo construction site was a test, and every method's
|
|
355
|
+
observable behaviour was a fabricated constant.
|
|
356
|
+
|
|
357
|
+
**No compile-compat stub was left.** The ruling allows one — throwing loud
|
|
358
|
+
`NotImplemented` — only where a compile need is demonstrated. Measured across the
|
|
359
|
+
whole repository, the sole importers were the package's own `index.ts`,
|
|
360
|
+
`v3-compat.test.ts` (three cases asserting the fallback had the right _keys_,
|
|
361
|
+
which is how the emptiness stayed green) and objectui#3720's vocabulary pin. No
|
|
362
|
+
app, no other package, no doc. With no consumer to keep compiling, a stub would
|
|
363
|
+
be a second phantom surface guarding the first.
|
|
364
|
+
|
|
365
|
+
The false module header went with it — it read `Cloud namespace integration for
|
|
366
|
+
@objectstack/spec v3.0.0 / Replaces the legacy Hub namespace`, against a resolved
|
|
367
|
+
spec of `17.0.0-rc.6` and schemas this package never consumed.
|
|
368
|
+
|
|
369
|
+
**objectui#3720's pin retires with its subject.** `cloud-environment-vocabulary.pin.test.ts`
|
|
370
|
+
pinned the doc comment on `CloudDeploymentConfig.environment` — the deliberate
|
|
371
|
+
three-member deploy-target vocabulary and the `staging`-is-not-a-discovery-member
|
|
372
|
+
trap. Every fact it held was a claim _about_ that comment, and its spec-side
|
|
373
|
+
assertions existed only to keep those claims honest; with the type deleted they
|
|
374
|
+
would pin `@objectstack/spec`'s enums on behalf of no local reader — the same
|
|
375
|
+
phantom shape this change closes. #3720's conclusion is unaffected and now moot:
|
|
376
|
+
it found no producer-side deploy-target type to converge onto because the
|
|
377
|
+
producer did not exist, and this change removes the consumer that was waiting for
|
|
378
|
+
it. Its pending empty changeset (`cloud-deploy-environment-vocabulary-3720.md`,
|
|
379
|
+
never released) is removed too, since it announced a deliberate vocabulary on a
|
|
380
|
+
type this same release deletes.
|
|
381
|
+
|
|
382
|
+
A negative pin (`src/cloud-surface-retired-4152.pin.test.ts`) replaces the
|
|
383
|
+
retired cases and fails if any of the four names returns — reading both the
|
|
384
|
+
runtime export list (which catches the class) and `index.ts`'s source text
|
|
385
|
+
(which is the only instrument that can catch a returning `export type`).
|
|
386
|
+
|
|
387
|
+
- 2e3b0c0: fix(list): an `OBJECT_API_DISABLED` list request renders an honest cannot-work state instead of the empty state
|
|
388
|
+
|
|
389
|
+
A list pointed at an object whose `enable` block withholds the API rendered its ordinary
|
|
390
|
+
empty state, so _"this page cannot work, and never could"_ reached the user as _"you have no
|
|
391
|
+
records"_ (objectui#4408). The reported instance — `Setup › Advanced › Signing Keys`, whose
|
|
392
|
+
`sys_jwks` declares `enable.apiEnabled: false` — could not load for any persona and said so
|
|
393
|
+
to nobody. That is also why the upstream defect objectstack#7544 survived review for its
|
|
394
|
+
whole life: a merely unpopulated page invites nobody to click through.
|
|
395
|
+
|
|
396
|
+
The masking had two halves, in two packages, and neither package could see the other:
|
|
397
|
+
|
|
398
|
+
- **`@object-ui/data-objectstack`** (minor — see the grading note below) — `find()` degraded
|
|
399
|
+
**every** 404 into `{ data: [], total: 0 }` and memoised the resource, so the denial arrived
|
|
400
|
+
at the surface as a successful empty result, indistinguishable from a genuinely empty
|
|
401
|
+
object. The two `enable`-block denials are now let through instead: `OBJECT_API_DISABLED`
|
|
402
|
+
(404) and `OBJECT_API_METHOD_NOT_ALLOWED` (405). The memo skips them too — absorbing one
|
|
403
|
+
would have pinned the object to "empty" for the rest of the session.
|
|
404
|
+
- **`@object-ui/plugin-list`** — the load-error panel gained an `api-disabled` kind. The 405
|
|
405
|
+
half was never swallowed, so it already reached this panel, but classified as `network`:
|
|
406
|
+
_"check your connection and try again"_ for a condition no retry can change. It now says
|
|
407
|
+
the object is not exposed through the API, that this is a setting on the object rather than
|
|
408
|
+
a permission, and it offers **no Retry** button, because every retry re-fetches the
|
|
409
|
+
identical refusal.
|
|
410
|
+
|
|
411
|
+
Both denials are pure functions of the object's metadata — no user, no permission, no
|
|
412
|
+
context — so neither is transient or per-user, which is exactly the case where a silent empty
|
|
413
|
+
state is most misleading. Discrimination is on the ADR-0112 `code`, never the status: a
|
|
414
|
+
missing collection, a missing record and a disabled object are all 404.
|
|
415
|
+
|
|
416
|
+
**A genuinely empty object still renders the ordinary empty state**, and a backend without an
|
|
417
|
+
optional collection still degrades to empty — pinned in both directions, at the adapter, at
|
|
418
|
+
the view, and once end-to-end over a real adapter and a real `ListView`.
|
|
419
|
+
|
|
420
|
+
Also closes a code-propagation gap on the same path: `find()`'s raw `$expand`/`$search`
|
|
421
|
+
branch bypasses `@objectstack/client` and hand-rolled its own error, stamping only `status`.
|
|
422
|
+
It now carries the ADR-0112 envelope (`code` + `httpStatus`), so a denial arriving on the
|
|
423
|
+
branch a list takes whenever it expands a lookup or runs a search is no longer anonymous.
|
|
424
|
+
|
|
425
|
+
New strings: `list.loadErrorApiDisabledTitle` / `list.loadErrorApiDisabledMessage`, in the
|
|
426
|
+
`en` pack and mirrored in the list defaults map.
|
|
427
|
+
|
|
428
|
+
## Grading note — why `@object-ui/data-objectstack` is **minor** and not patch
|
|
429
|
+
|
|
430
|
+
Two independent reasons, either of which is sufficient under this repo's precedent
|
|
431
|
+
(objectui#4403 / #4177, and #4485's grading of `@object-ui/core`'s `toDomProps` lift):
|
|
432
|
+
|
|
433
|
+
1. **The emitted `.d.ts` grows two NEW exports.** `isApiAccessDeniedError(error: unknown):
|
|
434
|
+
boolean` and `API_ACCESS_DENIED_CODES` (the readonly tuple
|
|
435
|
+
`['OBJECT_API_DISABLED', 'OBJECT_API_METHOD_NOT_ALLOWED']`) are added to the package's
|
|
436
|
+
public surface. Additive surface growth is minor.
|
|
437
|
+
2. **Observable behaviour on a published API moves.** `ObjectStackDataSource.find()` now
|
|
438
|
+
**REJECTS** for the two `enable`-block denial codes where it previously **RESOLVED** with
|
|
439
|
+
`{ data: [], total: 0 }`. No signature changed and nothing was removed, but a caller that
|
|
440
|
+
relied on those two codes arriving as a successful empty result now receives a rejected
|
|
441
|
+
promise carrying `code` + `httpStatus`, and must handle it.
|
|
442
|
+
|
|
443
|
+
Deliberately unchanged, and still resolving to an empty result exactly as before: a bare 404
|
|
444
|
+
with no code, `OBJECT_NOT_FOUND` (still memoised) and `RECORD_NOT_FOUND`. The behaviour move
|
|
445
|
+
is scoped to the two denial codes named above and to nothing else.
|
|
446
|
+
|
|
447
|
+
Not major: this follows AGENTS.md's version-alignment rule — objectui's major tracks
|
|
448
|
+
`@objectstack`'s, so this repo's own breaking semantics are declared as minor with the change
|
|
449
|
+
described in the body, which is what this note is.
|
|
450
|
+
|
|
451
|
+
### Patch Changes
|
|
452
|
+
|
|
453
|
+
- d9d3463: Retire four zero-consumer declared surfaces (dead-surface sweep batch 3, #4328). Each was
|
|
454
|
+
measured as declared-but-never-read at the branch point, and each is removed rather than
|
|
455
|
+
left as an authoring surface whose values nothing acts on.
|
|
456
|
+
|
|
457
|
+
Breaking for anyone who typed against the removed declarations, marked `minor` per this
|
|
458
|
+
repository's version-alignment convention (the major tracks `@objectstack`, never an
|
|
459
|
+
API-break count):
|
|
460
|
+
|
|
461
|
+
- `@object-ui/core` no longer exports `mergeViewsIntoObjects`. It was a second copy left
|
|
462
|
+
behind by the move of that step to the provider layer, and it had drifted: it ignored a
|
|
463
|
+
view container's default `list` and keyed views by the authored bare key instead of the
|
|
464
|
+
composer's `<object>.<key>` identity. The live implementation — `MetadataProvider`'s, in
|
|
465
|
+
`@object-ui/app-shell` — is unchanged and remains the only one. (#3775)
|
|
466
|
+
- `@object-ui/types`' `RoleDefinition` no longer declares `permissions`. A role's grants
|
|
467
|
+
live in `ObjectPermissionConfig.roles`, keyed by object; that is the only home any
|
|
468
|
+
consumer reads (`resolveRoles` walks `inherits` and matches on `name`). The removed
|
|
469
|
+
field was _required_, so five fixtures across three packages had been declaring an empty
|
|
470
|
+
array for a value nothing would ever look at. Role-attached grants are now a compile
|
|
471
|
+
error rather than silently ignored data. (#4288)
|
|
472
|
+
- `@object-ui/react`'s `RecordContextValue` no longer declares `loading` / `error`. Both
|
|
473
|
+
had zero producers and zero consumers — no host passed them, no `record:*` renderer read
|
|
474
|
+
them — and only the provider's memo dependency list still named them. Record-level
|
|
475
|
+
loading and error state stays where it is actually expressed: each renderer's own data
|
|
476
|
+
source. (#3773)
|
|
477
|
+
|
|
478
|
+
No behaviour change, no request-count change:
|
|
479
|
+
|
|
480
|
+
- `@object-ui/data-objectstack` drops five `metadataCache.invalidate('views:<object>')`
|
|
481
|
+
calls across `updateViewConfig` / `createView` / `updateView` / `deleteView`. No read
|
|
482
|
+
path has ever populated that key — `listViews` fetches directly, uncached — so all five
|
|
483
|
+
were permanent no-ops. The invalidations of the keys that do have readers
|
|
484
|
+
(`view:<object>:<viewId>` for `getView`, `view-overrides:<object>` for
|
|
485
|
+
`listViewOverrides`) are untouched and now pinned. (#3778)
|
|
486
|
+
|
|
487
|
+
- c0f9a4b: Studio surfaces the runtime authoring gate's advisory findings instead of discarding them client-side
|
|
488
|
+
|
|
489
|
+
The framework's runtime authoring gate produces two kinds of verdict on a metadata write. Errors become a 422 and the author sees them. Advisories ride a **200** — the save succeeded, the row persisted, the version bumped — and until objectstack#7435 the server dropped them into a deduped `console.warn` behind a process-level set. That landing put them on the wire as an optional `advisories[]` on the save response, emitted only when non-empty, and objectui was still throwing them away one layer further out: `MetadataClient.save` parsed the body, returned it as an opaque `T`, and every call site awaited it for its side effect and discarded the value.
|
|
490
|
+
|
|
491
|
+
The measured case the fix is built on: a `nightly_purge` flow whose only defect is a `delete_record` node with `multi: true` and no filter yields `errors = 0 / advisories = 1`. The save returns 200, the flow goes live, and nothing anywhere tells the author it deletes every row. That matters most for exactly the authors Studio serves — a Studio tenant or an MCP/AI author has no `os lint` and no CLI config for `sys_metadata` overlay rows, so this gate is not the weakest of four doors, it is the only one.
|
|
492
|
+
|
|
493
|
+
`MetadataClient` now carries an `onSaveAdvisory` sink, invoked after a save whose response carried a non-empty `advisories[]`, and the console wires it in `useMetadataClient` — the one hook every app-shell write path takes its client from, so a single wiring covers `ResourceEditPage`, `StudioDesignSurface`, `EmbeddedItemEditor`, `DatasourceResourcePage`, `ObjectHooksPanel` and any future call site rather than a toast copied into twenty of them. The finding shape is re-exported from `@objectstack/spec` (`RuntimeAuthoringIssue`) rather than restated, so it cannot fork from the 422 `issues[]` it deliberately shares a declaration with.
|
|
494
|
+
|
|
495
|
+
The affordance is the warning tier and says "Saved" first. A successful save that reads as a failure is the specific defect this surface must not ship, so the toast acknowledges the write, lists `rule` + `message` + `hint` per finding with `where` as secondary context, and renders that text **verbatim** — `message` and `hint` are server prose composed by the gate's rules, not i18n keys. Only the frame around them is translated (`console.saveAdvisoryTitle`, ten packs). The sink is best-effort in both directions: a malformed finding is dropped rather than printed as blanks, and a throwing renderer cannot turn a save the server already committed into an error.
|
|
496
|
+
|
|
497
|
+
**What this does not surface yet, and why.** Studio's designer saves as a **draft** on every edit, and drafts are never gated — the framework returns at its D1 early-return (`if (args.state !== 'active') return null`) before running a single rule, so a draft save produces no findings at all rather than producing some that get withheld. The publish step that promotes a draft to active _does_ run the gate, but the publish route returns no `advisories` field until objectstack#7294 lands. So a draft-then-publish flow renders nothing today, at both of its doors, for two different reasons; the active-mode save door renders findings now. That gap is pinned as a test rather than left for a reader to rediscover.
|
|
498
|
+
|
|
499
|
+
- 605b747: The second metadata client class surfaces the runtime authoring gate's advisories instead of discarding them
|
|
500
|
+
|
|
501
|
+
objectui#4133 (PR #4236) put the gate's advisory findings — the ones that ride a **200**, where the save succeeded and the row persisted — in front of Studio authors, but it covered only one of the two client classes that write through `PUT /api/v1/meta/:type/:name`. The wiring lifts at `useMetadataClient`, which is where every app-shell path takes its `MetadataClient` from. `ObjectStackClient.meta.saveItem` — the SDK client hanging off `ObjectStackAdapter` — is a different class reaching the same door, and every one of its callers awaited the call and discarded the response, so an `advisories[]` the server attached was parsed off the wire and dropped one layer further out.
|
|
502
|
+
|
|
503
|
+
Those callers all write in **active** mode, so this is not the draft case where the gate never runs: the gate does run for them, produces findings, and the author was told nothing. The list is `MetadataService` (five saves behind the Object Manager and Field Designer), `useNavigationSync`, plugin-designer's Create/EditAppPage, and the adapter's own `updateViewConfig` / view / `updateDashboard` paths.
|
|
504
|
+
|
|
505
|
+
`ObjectStackAdapter` now carries an `onSaveAdvisory(listener)` subscription and emits on it after a metadata save whose 200 carried a non-empty `advisories[]`; `AdapterProvider` subscribes once and renders through the same `emitSaveAdvisories` the other client class already uses, so both doors produce one wording on the warning tier that says "Saved" first. The emitter is installed **once at the adapter/client seam** rather than at the call sites: every caller above reaches the save door through the adapter's own long-lived `ObjectStackClient`, so one interception covers all of them, plus any future one, without a toast copied into a dozen places — the same reasoning that put #4133's sink at one factory instead of twenty call sites.
|
|
506
|
+
|
|
507
|
+
It is a sibling of the `onWriteWarning` channel (#3431/#3455) rather than a second payload pushed down it, which is what `MetadataSaveAdvisoryEvent` already said it was modelled on. `WriteWarningEvent` is a closed shape whose required `droppedFields` means "fields the write legally stripped", so carrying advisories on it would either force every existing subscriber to grow a branch or make the event lie about what happened. The seam's shape is reused; its event type is not. `readSaveAdvisories` is shared unchanged between the two clients — one reader, two call sites — which the response envelopes make possible: the spec puts `advisories` at the save body's top level, and the SDK returns that body verbatim (it strips its `{ success, data }` envelope only when a `data` key is present, and this body has none). That measurement is pinned by tests that drive a real SDK client through a fake `fetch` rather than stubbing the method under test.
|
|
508
|
+
|
|
509
|
+
- b42558a: Renaming a freshly-created view now persists — `updateView` reads and writes the same row, instead of reading the published overlay and losing the edit into a rejected partial write
|
|
510
|
+
|
|
511
|
+
ADR-0034 stages every runtime-created view as a per-item **draft**: a view made from the `+` tab lives only in the draft row until an explicit Publish, and the UI reads it back through `?preview=draft`. `updateView` addressed neither half of that. Its read went to the published overlay (`client.meta.getItem`, no draft qualifier), which 404s for a draft-only view; a `catch {}` labelled "treat missing as create-equivalent" then substituted `current = {}`, so the read-merge-write cycle merged onto nothing. What went out was the fragment that merge produces — literally `{label, name, object}`, no `viewKind`, no `config` — which the server rejects as an invalid ViewItem (422). Nothing surfaced to the user, and the draft row still held the old label, so the rename simply did not happen. Create, pin and delete were unaffected: they never take this path.
|
|
512
|
+
|
|
513
|
+
The read now probes the draft row first and, on a hit, merges onto that body and writes it straight back with `mode: 'draft'`. Whichever row the read resolved is the row the write updates, so the two halves agree by construction rather than by coincidence. Probing the draft **before** the published overlay is what makes it correct for a view that has both: writing the published row while a draft is pending would put the edit somewhere the draft shadows, and Publish would later overwrite it with the pre-edit body — losing the change a second time, further from the cause. A draft edit stays a draft, preserving ADR-0037's guarantee that nothing the preview shows goes live until Publish. Renaming a published view with no draft pending is unchanged, published read to published write.
|
|
514
|
+
|
|
515
|
+
The silent catch is gone. A view that resolves in neither home now throws naming the view and the object (creating one is `createView`'s job — no caller of `updateView` relied on the create-equivalent behaviour), and a network, permission or server fault on either read propagates instead of degrading into the partial write that corrupted the row. This turns a class of failure that was previously invisible into an error the existing call sites already catch and surface.
|
|
516
|
+
|
|
517
|
+
Set-default and reorder drive the same read-merge-write cycle with `{isDefault}` / `{sortOrder}` patches, so they were emitting the same partial write and are fixed by the same change.
|
|
518
|
+
|
|
519
|
+
- d2f6e6b: Publishing a view from the console no longer serves a five-minute-stale override map — every writer now routes through one invalidation seam
|
|
520
|
+
|
|
521
|
+
`ObjectStackAdapter` caches two view-shaped reads: `getView` under `view:{object}:{name}` and `listViewOverrides` under `view-overrides:{object}`, with `MetadataCache`'s default 5-minute TTL. objectui#4363 made the adapter's own four write paths drop both. But the console's real create-a-view flow never calls any of them: `ObjectView.handleViewCreate` writes through the ADR-0034 metadata seam (`createRuntimeMetadata` → `metadataClient.save`), and Publish goes `RuntimeDraftBar` → `publishRuntimeMetadata` → `metadataClient.publish`. Two writers into the same `/meta/view/:name` rows; only one of them invalidated anything.
|
|
522
|
+
|
|
523
|
+
Publish is the sharp end. A create lands an invisible per-item draft, and `listViewOverrides` enumerates published rows, so the map is still honest there. Publish promotes the row into exactly the world the map describes — and nothing dropped the key, so the object page kept applying its pre-publish snapshot for the rest of the TTL. It does not self-heal: `loadViewOverrides` treats a resolved map as authoritative and deliberately does not re-probe per view (objectui#3774, correct — re-probing reinstates the 404 flurry the batch read exists to remove), so the per-view `getView` fallback that would have masked a stale map is by design unreachable.
|
|
524
|
+
|
|
525
|
+
The fix is one seam rather than a fifth copy of the key list. `ObjectStackAdapter.invalidateViewKeys(objectName, viewName)` is now the only place that knows which keys a view-row write drops; the adapter's four write paths call it instead of restating the pair, app-shell's ADR-0034 persistence module calls it for `view` saves, creates, publishes and discards, and `MetadataService.saveMetadataItem` calls it when the category is `view` (where it previously named `view:{name}`, which no reader has). Restatement is what this repo keeps paying for — objectui#3778 removed five copies of a key no reader populated, objectui#4363 fixed four copies that named half the live set, and objectui#4373 is the measured proof that a new writer forgets the list by default. A pin suite can only guard writers that exist; a seam makes the next one unable to forget.
|
|
526
|
+
|
|
527
|
+
No cache key, no read path and no public signature changed. The adapter's eight existing invalidation pins pass unchanged, which is the evidence that routing four paths through a seam changed nothing observable; two new structural guards keep the key set from being restated again — one asserting each key template appears exactly twice in the adapter (its reader, and the seam), one asserting no app-shell file spells either.
|
|
528
|
+
|
|
529
|
+
- 85a3082: Every view write path now invalidates the override map — a created, renamed or deleted view is no longer shadowed by a five-minute-stale batch read
|
|
530
|
+
|
|
531
|
+
`ObjectStackAdapter` caches two view-shaped reads: `getView` under `view:{object}:{viewId}`, and `listViewOverrides` under `view-overrides:{object}`. Four write paths touch view rows, and until now exactly one of them — `updateViewConfig` — invalidated the second key. `createView`, `updateView` and `deleteView` invalidated only the per-view key, so the batch override map kept answering from a snapshot taken up to `MetadataCache`'s default 5-minute TTL earlier.
|
|
532
|
+
|
|
533
|
+
That gap does not heal itself. `loadViewOverrides` in app-shell's `ObjectView` treats a resolved map as authoritative and deliberately does not re-probe per view — that is objectui#3774's fix, and it is correct, since re-probing reinstates the 404 flurry the batch read exists to remove. So the per-view `getView` fallback that would have masked a stale map is by design unreachable, and the stale map is served in full. Meanwhile `listViews` is uncached and answers fresh, so the view switcher could list a view whose override body came from a map written minutes earlier: the sharpest shape is the rename/pin path (`updateView`), where a user edits a view, returns to the object, and is served the pre-edit override.
|
|
534
|
+
|
|
535
|
+
All four paths now emit the same ordered pair — the per-view key, then the object's override map. The rule is uniform per method rather than per branch: `updateView`'s draft half invalidates both keys as its published half does, which is deliberate over-invalidation (both readers enumerate published rows, so a draft write stales neither) chosen because an unnecessary invalidation costs one refetch while a missed one costs the full TTL. `createView` names the per-view key too, because `saveItem` is an upsert and an explicit `spec.name` that already exists overwrites a published row a prior `getView` may hold.
|
|
536
|
+
|
|
537
|
+
No signature, no cache key and no read path changed; the only difference is which keys each write drops. The pin suite added by objectui#4328 now asserts the full invalidation key set for all five call sites, with the sweep's two pins kept as untouched controls: `listViews` stays uncached, and no write path names the retired `views:{object}` key.
|
|
538
|
+
|
|
539
|
+
- Updated dependencies [ee66e2e]
|
|
540
|
+
- Updated dependencies [ee26e65]
|
|
541
|
+
- Updated dependencies [5900ac5]
|
|
542
|
+
- Updated dependencies [f650253]
|
|
543
|
+
- Updated dependencies [3d9769a]
|
|
544
|
+
- Updated dependencies [3fc2971]
|
|
545
|
+
- Updated dependencies [aca27fa]
|
|
546
|
+
- Updated dependencies [dde7283]
|
|
547
|
+
- Updated dependencies [92876f0]
|
|
548
|
+
- Updated dependencies [f279deb]
|
|
549
|
+
- Updated dependencies [eb7f586]
|
|
550
|
+
- Updated dependencies [e901131]
|
|
551
|
+
- Updated dependencies [d9d3463]
|
|
552
|
+
- Updated dependencies [2a40f69]
|
|
553
|
+
- Updated dependencies [bec3e14]
|
|
554
|
+
- Updated dependencies [613b167]
|
|
555
|
+
- Updated dependencies [1f9b905]
|
|
556
|
+
- Updated dependencies [abb0f81]
|
|
557
|
+
- Updated dependencies [38ab505]
|
|
558
|
+
- Updated dependencies [7e4f0e5]
|
|
559
|
+
- Updated dependencies [92250d6]
|
|
560
|
+
- Updated dependencies [c1d939f]
|
|
561
|
+
- Updated dependencies [49ae9f4]
|
|
562
|
+
- Updated dependencies [2459a3e]
|
|
563
|
+
- Updated dependencies [d6aa172]
|
|
564
|
+
- Updated dependencies [fe52a04]
|
|
565
|
+
- Updated dependencies [bb68488]
|
|
566
|
+
- Updated dependencies [9461dd3]
|
|
567
|
+
- Updated dependencies [ab04728]
|
|
568
|
+
- @object-ui/core@17.5.0
|
|
569
|
+
- @object-ui/types@17.5.0
|
|
570
|
+
|
|
571
|
+
## 17.4.0
|
|
572
|
+
|
|
573
|
+
### Minor Changes
|
|
574
|
+
|
|
575
|
+
- 48132f7: Track the `@objectstack` family at `17.0.0-rc.5` (objectui#3560).
|
|
576
|
+
|
|
577
|
+
The pin moves from `^17.0.0-rc.2` to `^17.0.0-rc.5` across all 37 declarations in
|
|
578
|
+
30 `package.json` files, and the sibling `@objectstack/*` packages (`client` /
|
|
579
|
+
`formula` / `lint`) move with it — they pin `@objectstack/spec` **exactly**, so
|
|
580
|
+
leaving them behind would keep a second copy of the spec in the tree and have
|
|
581
|
+
`@objectstack/lint` validating against schemas that still accept the keys rc.3–rc.5
|
|
582
|
+
retire. `pnpm-lock.yaml` now resolves one copy of each of the six family packages
|
|
583
|
+
(`spec` / `client` / `core` / `formula` / `lint` / `sdui-parser`), all at rc.5.
|
|
584
|
+
|
|
585
|
+
Bumping the pin and repairing the fallout cannot be split: the pin alone reddens
|
|
586
|
+
CI, and the code alone targets a shape that is not in effect yet.
|
|
587
|
+
|
|
588
|
+
## A live bug this upgrade fixes
|
|
589
|
+
|
|
590
|
+
**`ObjectStackDataSource.delete()` never emitted its mutation event, and resolved
|
|
591
|
+
`undefined` instead of a boolean.** `@objectstack/client`'s `DeleteDataResult`
|
|
592
|
+
declared a key called `deleted` — a key no schema has ever declared and no server
|
|
593
|
+
path has ever returned on `DELETE /data/:object/:id`. So `result.deleted`
|
|
594
|
+
compiled and read `undefined` at runtime: the guard never fired, a successful
|
|
595
|
+
delete notified no subscriber, and every consumer's cache stayed stale.
|
|
596
|
+
objectstack#5638 corrected the interface to the schema's `success`; following the
|
|
597
|
+
rename is what restores both behaviours. Nothing in this repo had to change shape
|
|
598
|
+
for it — the code was already asking the right question of the wrong key.
|
|
599
|
+
|
|
600
|
+
## Breaking, in FROM → TO form
|
|
601
|
+
|
|
602
|
+
- **The five `@objectstack/spec/ui` interaction-config modules are gone** —
|
|
603
|
+
touch / dnd / keyboard / animation / offline, 32 defs and 64 exports
|
|
604
|
+
(objectstack#4988, PR objectstack#5321). None of them had an authoring door: no
|
|
605
|
+
metadata document could ever carry one of these blocks, so a stack that parsed
|
|
606
|
+
before the retirement parses byte-for-byte the same after it. `@object-ui/types`
|
|
607
|
+
drops the 32 `export type` re-exports. The vocabulary each one's only real
|
|
608
|
+
consumer needs is now declared by that consumer, which is the remedy the spec's
|
|
609
|
+
own retirement ledger prescribes ("declare that union locally — it is your
|
|
610
|
+
client's policy, not the platform's"):
|
|
611
|
+
|
|
612
|
+
- `@object-ui/react`'s `useOffline` owns `OfflineStrategy`, `ConflictResolution`,
|
|
613
|
+
`PersistStorageType`, `EvictionPolicyType`, `OfflineConfig`,
|
|
614
|
+
`OfflineCacheConfig`, `OfflineSyncConfig`;
|
|
615
|
+
- `@object-ui/core`'s `DndProtocol` / `KeyboardProtocol` own `DndConfig`,
|
|
616
|
+
`DragItem`, `DropZone`, `DragConstraint`, `DragHandle`, `DropEffect`,
|
|
617
|
+
`KeyboardNavigationConfig`, `KeyboardShortcut`, `FocusManagement`,
|
|
618
|
+
`FocusTrapConfig`;
|
|
619
|
+
- `@object-ui/types`' `mobile` module owns `SpecGestureConfig`,
|
|
620
|
+
`SwipeGestureConfig`, `PinchGestureConfig`, `LongPressGestureConfig`,
|
|
621
|
+
`TouchTargetConfig`, `TouchInteraction` (plus a new `SPEC_GESTURE_TYPES`
|
|
622
|
+
runtime tuple), so `@object-ui/mobile`'s import paths are unchanged.
|
|
623
|
+
|
|
624
|
+
Every shape is moved verbatim — same keys, same members, same optionality — so
|
|
625
|
+
no hook or bridge changes behaviour. Consumers importing these names from
|
|
626
|
+
`@object-ui/types` must import them from the owning package instead. Note the
|
|
627
|
+
spec's _surviving_ `ConnectorConflictResolution` (`/integration`, connector sync)
|
|
628
|
+
and `ConflictResolutionStrategy` (`/api`, route merge policy) are **different
|
|
629
|
+
concepts** — do not re-point at them.
|
|
630
|
+
|
|
631
|
+
- **`@object-ui/types` no longer re-exports `NotificationAction` or `EmbedConfig`**
|
|
632
|
+
(objectstack#5015, PR objectstack#5300). Both were published `ui` vocabulary with
|
|
633
|
+
no authoring door; no notification action was ever parsed from metadata and no
|
|
634
|
+
iframe route ever read an embed config. The presentation enums
|
|
635
|
+
(`NotificationType` / `NotificationSeverity` / `NotificationPosition`) and
|
|
636
|
+
`SharingConfig` **survive** and are untouched — public form sharing still gates
|
|
637
|
+
the anonymous endpoints on `allowAnonymous` + `publicLink`.
|
|
638
|
+
`@object-ui/core`'s `SharingProtocol` keeps `resolveEmbedConfig` /
|
|
639
|
+
`generateEmbedCode` against a locally declared `EmbedConfig`, so its surface is
|
|
640
|
+
unchanged.
|
|
641
|
+
- **`ThemeEngine` stops emitting nine retired CSS variable groups**
|
|
642
|
+
(objectstack#5021 option 2, PR objectstack#5289). `theme.animation`,
|
|
643
|
+
`theme.zIndex` and five typography groups (`fontSize` / `fontWeight` /
|
|
644
|
+
`lineHeight` / `letterSpacing`, plus `fontFamily.heading` / `fontFamily.mono`)
|
|
645
|
+
are tombstones the schema now rejects by name, so `--duration-*`, `--timing-*`,
|
|
646
|
+
`--z-*`, `--font-size-*`, `--font-weight-*`, `--line-height-*`,
|
|
647
|
+
`--letter-spacing-*`, `--font-heading` and `--font-mono` had become structurally
|
|
648
|
+
dead code — no author could produce the input that reached them.
|
|
649
|
+
`generateAnimationVars` and `generateZIndexVars` are removed from
|
|
650
|
+
`@object-ui/core`, and `@object-ui/types` drops `Animation` / `ZIndex` /
|
|
651
|
+
`AnimationSchema` / `ZIndexSchema`. **`theme.customVars` is the declared — and
|
|
652
|
+
since #5021 the only — door**: each entry is emitted verbatim as
|
|
653
|
+
`--<key>: <value>`, so a `--z-modal` or a `--duration-fast` goes there now.
|
|
654
|
+
LIVE emission is untouched byte for byte: `colors`, `borderRadius`, `shadows`,
|
|
655
|
+
`typography.fontFamily.base` (→ `--font-sans`) and `customVars`.
|
|
656
|
+
- **`@object-ui/types`' `HttpMethodSchema` now binds the spec's
|
|
657
|
+
`HttpMethodSubsetSchema`, and `HttpMethod` binds `HttpMethodSubset`**
|
|
658
|
+
(objectstack#5832, PR objectstack#5976 — objectui#3499). The spec renamed its
|
|
659
|
+
5-value UI subset because `schemaNameFromExportKey` strips the `Schema` suffix,
|
|
660
|
+
so the 5-value and 7-value enums both published as `shared/HttpMethod` and the
|
|
661
|
+
later write won — the emitted JSON Schema and reference page described only one
|
|
662
|
+
of them. **The runtime domain is unchanged and this repo's exported names are
|
|
663
|
+
unchanged**; this follows the rename without touching cross-package semantics.
|
|
664
|
+
Deliberately NOT re-pointed at the spec's bare `HttpMethod`: that is the 7-value
|
|
665
|
+
enum, and widening to it would let `method: 'HEAD'` compile and then throw in
|
|
666
|
+
`HttpRequestSchema.parse()`.
|
|
667
|
+
- **`dashboard.widgets[].actionUrl` / `actionType` / `actionIcon` / `aria` are
|
|
668
|
+
refused, not stripped** (objectstack#5010, ADR-0049 enforce-or-remove). A
|
|
669
|
+
dashboard widget has no action button and never had one — every action the
|
|
670
|
+
dashboard dispatches comes from `header.actions[]` — and no renderer ever applied
|
|
671
|
+
the widget `aria`, so it promised accessibility compliance it did not deliver.
|
|
672
|
+
A stale dashboard now gets a named error telling it where the affordance moved,
|
|
673
|
+
instead of silently losing it. Run `os migrate meta --from 16` to rewrite.
|
|
674
|
+
|
|
675
|
+
### Patch Changes
|
|
676
|
+
|
|
677
|
+
- 3765678: data-objectstack: pass the server's `drillRanges` date-bucket drill scope through `queryDataset` (restores date drill-through)
|
|
678
|
+
|
|
679
|
+
`queryDataset` rebuilds its result by **hand-picking** keys off the REST payload,
|
|
680
|
+
and `drillRanges` was never in the list — so the analytics service's date-range
|
|
681
|
+
drill sidecar (framework#1752) was dropped by the only real adapter in this repo,
|
|
682
|
+
while five consumer call sites were already reading it (`DatasetWidget.tsx:471`
|
|
683
|
+
and `:593`, `DatasetReportRenderer.tsx:316`, `:431`, `:855`).
|
|
684
|
+
|
|
685
|
+
The user-visible effect was not a degraded drill but a missing one. A
|
|
686
|
+
`dateGranularity` dimension groups a **span** of records into one bucket, which
|
|
687
|
+
equality filters cannot express, so `service-analytics` deliberately excludes
|
|
688
|
+
date dimensions from `dimensionFields`/`drillRawRows` and sends a parallel
|
|
689
|
+
half-open `[gte, lt)` range per row instead. For a chart or report grouped **only
|
|
690
|
+
by time** that makes `drillRanges` the _only_ thing that can make
|
|
691
|
+
`canDrill = !!object && (drillDims.length > 0 || !!drillRanges?.length)` true —
|
|
692
|
+
with the key dropped, the entire drill entry point disappeared. A mixed
|
|
693
|
+
date + non-date grouping kept its drill but built a filter with no time bound, so
|
|
694
|
+
clicking June's bar opened every month (a superset).
|
|
695
|
+
|
|
696
|
+
Neither side's tests could see it: the dashboard and report tests mock their own
|
|
697
|
+
data source and feed `drillRanges` in directly, and the adapter's own suite never
|
|
698
|
+
asserted the key. The new adapter-level tests therefore mock the **envelope the
|
|
699
|
+
server actually sends** — bare (`res.json(result)`, no `{ success, data }`
|
|
700
|
+
wrapper), carrying `sql`, and for a date-only grouping carrying `object` +
|
|
701
|
+
`drillRanges` and _no_ `dimensionFields`/`drillRawRows` — then assert the key
|
|
702
|
+
arrives verbatim and row-aligned, that the consumers' own `canDrill` predicate is
|
|
703
|
+
true, and that `buildDatasetDrillFilter` (the shared builder both surfaces call)
|
|
704
|
+
scopes the drilled list to the clicked bucket.
|
|
705
|
+
|
|
706
|
+
The declared entry type is `@object-ui/core`'s `DatasetDrillRange` **by
|
|
707
|
+
reference**, per the objectui#3613/#3752 discipline: it is the single in-repo
|
|
708
|
+
declaration of this shape (what the filter builder accepts and what both
|
|
709
|
+
renderers type their state with), and nothing in `@objectstack/spec` owns it yet,
|
|
710
|
+
so restating `{ field, gte, lt }` locally would create a third dialect of it.
|
|
711
|
+
|
|
712
|
+
`drillRawTotals` (the totals-row companion, framework#3214) is deliberately
|
|
713
|
+
**not** added: it has zero consumers in this repo, so passing it through would
|
|
714
|
+
add a declared-but-unexercised return key with no user-facing effect — it belongs
|
|
715
|
+
in the change that lands a totals-row drill and can test it.
|
|
716
|
+
|
|
717
|
+
- d83f6b3: data-objectstack: type `queryDataset`'s result `fields[]` as the spec's `AnalyticsResult.fields[]` element instead of a hand-written copy
|
|
718
|
+
|
|
719
|
+
The return-value half of the drift objectui#3613 fixed on the parameter side. The
|
|
720
|
+
adapter hand-listed five keys for a result column
|
|
721
|
+
(`name`/`type`/`label`/`format`/`currency`) and, like every restatement, stopped
|
|
722
|
+
at the contract of the day it was written: it never grew **`percentScale`**,
|
|
723
|
+
which `@objectstack/spec@17.0.0-rc.5` carries on
|
|
724
|
+
`AnalyticsResult.fields[]` and documents as mandatory reading for renderers —
|
|
725
|
+
"renderers that receive it must scale by it instead of guessing from the value"
|
|
726
|
+
(objectui#3136).
|
|
727
|
+
|
|
728
|
+
That omission was not cosmetic. `percentScale` is the server's answer to a
|
|
729
|
+
question a `%` format string cannot express (is the stored number a 0–1 fraction,
|
|
730
|
+
or already percentage points?), and objectui#3136 exists because guessing from
|
|
731
|
+
the value's magnitude printed a ratio of exactly `1` as "1.0%". Three in-repo
|
|
732
|
+
consumers read the field through their own local types
|
|
733
|
+
(`DatasetResultField` in `@object-ui/core`), so nothing was red here — but any
|
|
734
|
+
author reading columns through the adapter's **declared** return type got
|
|
735
|
+
`Property 'percentScale' does not exist`, i.e. the declaration actively steered
|
|
736
|
+
them back to the guess the spec bans.
|
|
737
|
+
|
|
738
|
+
`fields` is now the spec type by reference, so there is nothing left to re-sync;
|
|
739
|
+
the change is additive for existing consumers (one more optional key).
|
|
740
|
+
`queryDataset.test.ts` pins structural identity with the spec element, pins
|
|
741
|
+
`percentScale` as the `'fraction' | 'whole'` union rather than a widened
|
|
742
|
+
`string`, keeps a negative pin against the five-key shape, and adds a runtime
|
|
743
|
+
test that reads `percentScale` off a result column **through the declared type**.
|
|
744
|
+
|
|
745
|
+
The rest of the envelope stays locally declared, deliberately. It is the REST
|
|
746
|
+
envelope, not an `AnalyticsResult`: the route adds ADR-0021 D2 drill metadata
|
|
747
|
+
(`object` / `dimensionFields` / `drillRawRows`) on top of the spec result, and
|
|
748
|
+
this method rebuilds its own object from the payload without copying `sql` — so
|
|
749
|
+
declaring the envelope as `AnalyticsResult & { … }` would advertise a key the
|
|
750
|
+
adapter structurally cannot return. A pin records that too.
|
|
751
|
+
|
|
752
|
+
- 5f08c05: data-objectstack: type `queryDataset(selection)` as the spec's `DatasetSelection` instead of a hand-written copy
|
|
753
|
+
|
|
754
|
+
The adapter restated the selection contract inline, field by field, and the copy
|
|
755
|
+
had drifted three ways from the pinned `@objectstack/spec@17.0.0-rc.5`:
|
|
756
|
+
|
|
757
|
+
- **`compareTo.dimension` was required.** It has been optional since
|
|
758
|
+
objectstack#5011, _because the executor resolves it_: exactly one time
|
|
759
|
+
dimension carrying a `dateRange` is the one shifted, and zero or several
|
|
760
|
+
raises a loud error naming the candidates. Requiring it made the compiler
|
|
761
|
+
demand from every typed caller precisely the consumer-side dimension guess
|
|
762
|
+
that change forbids — trading a loud executor error for a silently wrong
|
|
763
|
+
comparison window. No runtime path hit this yet (the dashboard's
|
|
764
|
+
`DatasetWidget` passes `selection` as `unknown`), but a declaration is a live
|
|
765
|
+
instruction to anyone calling this client from TypeScript.
|
|
766
|
+
- **`timeDimensions` was widened to `unknown[]`**, erasing the very entry shape
|
|
767
|
+
the executor's resolution reads (`{ dimension, granularity?, dateRange? }`),
|
|
768
|
+
and **`runtimeFilter` to `Record<string, unknown>`**, erasing the
|
|
769
|
+
`$and`/`$or`/`$not` vocabulary the server parses.
|
|
770
|
+
- **`dateGranularity` was missing entirely** — the copy had simply stopped at
|
|
771
|
+
whatever the contract looked like the day it was written, so a typed caller
|
|
772
|
+
could not bucket a trend by month at all.
|
|
773
|
+
|
|
774
|
+
The parameter is now the spec type by reference, so there is nothing left to
|
|
775
|
+
re-sync. The fix is the removal of the dialect rather than a correction to it:
|
|
776
|
+
restating a contract owned elsewhere creates a second de-facto dialect of it, and
|
|
777
|
+
drift is then only a matter of time (AGENTS.md #0/#0.1). `queryDataset.test.ts`
|
|
778
|
+
pins structural identity with `DatasetSelection` plus each of the three drifts
|
|
779
|
+
individually, checked by this package's `tsc --noEmit`; a runtime test pins that
|
|
780
|
+
a dimension-less `compareTo` reaches the server untouched, so the adapter can
|
|
781
|
+
never start guessing on the executor's behalf.
|
|
782
|
+
|
|
783
|
+
The response type is deliberately left alone — it is the REST envelope
|
|
784
|
+
(`object` / `dimensionFields` / `drillRawRows`), not a restatement of
|
|
785
|
+
`AnalyticsResult`.
|
|
786
|
+
|
|
787
|
+
- 41d6022: The console no longer reads `/meta/*` before it knows whether it has a session, and a failed request now says which request failed
|
|
788
|
+
|
|
789
|
+
Opening a logged-out console painted ~30 red `HTTP request failed` lines before
|
|
790
|
+
the login form was drawn. Two independent causes, fixed independently
|
|
791
|
+
(objectui#4042).
|
|
792
|
+
|
|
793
|
+
**1. Requests fired before the session was known.** `ConnectedShellInner` now
|
|
794
|
+
withholds the metadata tree until `GET /auth/get-session` resolves, so
|
|
795
|
+
`meta/object` / `meta/view` / `meta/app` are never issued blind. `useAuth()`
|
|
796
|
+
outside an `AuthProvider` reports `isLoading: false`, so an embed with no auth
|
|
797
|
+
provider is unaffected, and every protected route already sat behind an
|
|
798
|
+
`AuthGuard` that resolves auth first — the signed-in data flow is unchanged.
|
|
799
|
+
|
|
800
|
+
The console's landing route (`<Route path="/">`) was the actual entry point for
|
|
801
|
+
the burst: it mounted `ConnectedShell` with no guard above it, so simply opening
|
|
802
|
+
`/_console/` mounted the whole data layer as an anonymous visitor. It is now
|
|
803
|
+
guarded, which also means an unauthenticated visitor reaches `/login` without a
|
|
804
|
+
single doomed request. `examples/console-starter` had the same shape and got the
|
|
805
|
+
same fix.
|
|
806
|
+
|
|
807
|
+
**2. Two requests per type, per mount — not an unauthenticated artefact.**
|
|
808
|
+
Consumers read metadata during the FIRST render (`useActionModal` reads
|
|
809
|
+
`objects`, whose getter kicks `ensureType('object')` and `ensureType('view')`
|
|
810
|
+
from the render phase), before any effect runs. `MetadataProvider`'s preview-mode
|
|
811
|
+
effect then cleared the whole cache on mount, discarding those two entries while
|
|
812
|
+
their requests were in flight; the next render found them `idle` and refetched
|
|
813
|
+
both. The effect now skips its mount run — on mount the cache is empty and there
|
|
814
|
+
was never anything to drop; it only ever meant something on a later
|
|
815
|
+
`previewDrafts` change. That halved `meta/object` and `meta/view` on **every**
|
|
816
|
+
mount, signed in included.
|
|
817
|
+
|
|
818
|
+
A second duplicate only appeared once a read had failed: `entry.promise`
|
|
819
|
+
collapses callers that arrive while a request is in flight, but callers arriving
|
|
820
|
+
just after a failure each started a fresh attempt. A failed type now stays
|
|
821
|
+
un-retried for ~1s, which collapses one mount's burst of callers into a single
|
|
822
|
+
attempt. This is deliberately not the 5-minute `ttlMs` — later callers still
|
|
823
|
+
retry on their own, and `refresh()` / `invalidate()` retry immediately and
|
|
824
|
+
unconditionally, so no explicit recovery path changes.
|
|
825
|
+
|
|
826
|
+
**3. `HTTP request failed` now identifies the request.** `@objectstack/client`
|
|
827
|
+
reports every non-2xx as
|
|
828
|
+
`logger.error("HTTP request failed", undefined, { method, url, status, error })`,
|
|
829
|
+
and the console's logger forwarded that verbatim — so the identifying fields
|
|
830
|
+
lived only in the third argument, and anything that flattens a console record to
|
|
831
|
+
text rendered them `[object Object]` / `Object`. A screenful of failures could
|
|
832
|
+
not tell you a single URL or status. The message string now carries them:
|
|
833
|
+
|
|
834
|
+
```text
|
|
835
|
+
HTTP request failed: GET /api/v1/meta/object -> 401 [UNAUTHORIZED]
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
The structured bag is still passed alongside for DevTools to expand — text for
|
|
839
|
+
the flatteners, object for the inspectors, neither at the other's expense. The
|
|
840
|
+
formatter is exported as `formatHttpFailureMessage`, and `createQuietHttpLogger`
|
|
841
|
+
is now exported too so an app wiring its own `ObjectStackClient` gets the same
|
|
842
|
+
identified failures.
|
|
843
|
+
|
|
844
|
+
Nothing is newly silenced. The only demotion remains 404-on-an-optional-
|
|
845
|
+
collection (`sys_presence`, `sys_activity`), which is an expected outcome of a
|
|
846
|
+
request we still mean to make; a 401 that survives the session gate — a
|
|
847
|
+
mid-session expiry, say — stays a visible, fully-identified error. The cure for
|
|
848
|
+
doomed requests is not issuing them, never hiding them once issued.
|
|
849
|
+
|
|
850
|
+
- 7e2b7e9: Fix saved list-view preferences never reading back (density, column widths, sort, hidden columns, inline edit)
|
|
851
|
+
|
|
852
|
+
`listViewOverrides` in the ObjectStack adapter enumerated `GET /api/v1/meta/{objectName}` — putting the object name in the metadata **type** slot — while `updateViewConfig` persists under `type='view'`. The two key spaces are disjoint, so the batch map came back empty for every object and every personalization a user saved on a list view was written to the server but never read back, showing up as "the setting didn't save".
|
|
853
|
+
|
|
854
|
+
The read now enumerates `type='view'` once and narrows to the object client-side, through the same accessor `listViews()` uses over the same rows — the metadata index is name-only, so there is no server-side `?object=` filter to push it into.
|
|
855
|
+
|
|
856
|
+
Second half: the batch read no longer swallows its own failures into an empty map. An empty map is an authoritative "this object has no overrides" and callers may still trust it and skip the per-view reads (the batch optimization is intact), but a transport failure now rejects, so the per-view `getView` fallback it was silently disabling becomes reachable again. `DataSource.listViewOverrides` documents both terms so other adapters implement the same contract.
|
|
857
|
+
|
|
858
|
+
- Updated dependencies [6719877]
|
|
859
|
+
- Updated dependencies [56ff091]
|
|
860
|
+
- Updated dependencies [d229dfa]
|
|
861
|
+
- Updated dependencies [4bc6c23]
|
|
862
|
+
- Updated dependencies [c3b01a7]
|
|
863
|
+
- Updated dependencies [e06810e]
|
|
864
|
+
- Updated dependencies [ab3ad4f]
|
|
865
|
+
- Updated dependencies [c2fd122]
|
|
866
|
+
- Updated dependencies [48132f7]
|
|
867
|
+
- Updated dependencies [1d723e3]
|
|
868
|
+
- Updated dependencies [0109f54]
|
|
869
|
+
- Updated dependencies [7e5bb5d]
|
|
870
|
+
- Updated dependencies [fbc23e0]
|
|
871
|
+
- Updated dependencies [e6fdbdc]
|
|
872
|
+
- Updated dependencies [6bb454a]
|
|
873
|
+
- Updated dependencies [523be48]
|
|
874
|
+
- Updated dependencies [7e2b7e9]
|
|
875
|
+
- Updated dependencies [c1e1e6b]
|
|
876
|
+
- @object-ui/core@17.4.0
|
|
877
|
+
- @object-ui/types@17.4.0
|
|
878
|
+
|
|
3
879
|
## 17.3.0
|
|
4
880
|
|
|
5
881
|
### Minor Changes
|
|
@@ -1022,6 +1898,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1022
1898
|
objects+fields were editable in Studio; this reworks both surfaces.
|
|
1023
1899
|
|
|
1024
1900
|
**Setup (assign + read-only):**
|
|
1901
|
+
|
|
1025
1902
|
- The six facets (`object_permissions`, `field_permissions`, `system_permissions`,
|
|
1026
1903
|
`row_level_security`, `tab_permissions`, `admin_scope`) now render read-only on
|
|
1027
1904
|
the `sys_permission_set` record page as a compact summary (counts, or capability
|
|
@@ -1036,6 +1913,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1036
1913
|
|
|
1037
1914
|
**Studio (design every facet):** the permission matrix editor gains structured
|
|
1038
1915
|
editors for the facets that were JSON-only —
|
|
1916
|
+
|
|
1039
1917
|
- **System Capabilities**: a multi-select over the live `sys_capability` registry
|
|
1040
1918
|
(scope-grouped, labelled chips).
|
|
1041
1919
|
- **Row-Level Security**: per-policy rows (object · operation · enabled) with CEL
|
|
@@ -1052,6 +1930,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1052
1930
|
record the Setup summary reads, so a fresh Studio edit isn’t reflected in Setup’s
|
|
1053
1931
|
read-only view until the projection refreshes — tracked as a framework follow-up
|
|
1054
1932
|
(enforcement reads the authoritative metadata).
|
|
1933
|
+
|
|
1055
1934
|
- @object-ui/types@13.2.0
|
|
1056
1935
|
- @object-ui/core@13.2.0
|
|
1057
1936
|
|
|
@@ -1359,6 +2238,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1359
2238
|
|
|
1360
2239
|
**`@object-ui/plugin-designer`** — two new route-ready pages that
|
|
1361
2240
|
together close the "Data Model" management loop in the Setup app:
|
|
2241
|
+
|
|
1362
2242
|
- `MetadataObjectsPage` — lists every object schema (via
|
|
1363
2243
|
`MetadataClient.list('object')`), renders the existing
|
|
1364
2244
|
`ObjectManager`, and persists edits/deletes through PUT/DELETE on
|
|
@@ -1389,6 +2269,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1389
2269
|
the Setup app → _All Metadata Types_.
|
|
1390
2270
|
|
|
1391
2271
|
### New: `@object-ui/app-shell` views/metadata-admin
|
|
2272
|
+
|
|
1392
2273
|
- **`MetadataDirectoryPage`** — auto-grouped tile directory by domain, with
|
|
1393
2274
|
free-text search, domain chips, and a _Writable only_ filter.
|
|
1394
2275
|
- **`MetadataResourceListPage` / `MetadataResourceEditPage` / `…CreatePage` / `…HistoryPage`** —
|
|
@@ -1412,6 +2293,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1412
2293
|
`t(key)` helper.
|
|
1413
2294
|
|
|
1414
2295
|
### New routing variant
|
|
2296
|
+
|
|
1415
2297
|
- App nav now supports `{ type: 'component', componentRef, params? }` items.
|
|
1416
2298
|
`AppContent` resolves them through the existing `ComponentRegistry`.
|
|
1417
2299
|
- Built-in components registered: `metadata:directory`, `metadata:resource`,
|
|
@@ -1420,12 +2302,14 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1420
2302
|
/ page.
|
|
1421
2303
|
|
|
1422
2304
|
### Plugin-designer
|
|
2305
|
+
|
|
1423
2306
|
- Lazy-exported `ObjectManager`, `FieldDesigner`, `ObjectViewConfigurator`,
|
|
1424
2307
|
`DashboardEditor`, `PageCanvasEditor`, `MetadataObjectsPage`, and
|
|
1425
2308
|
`MetadataFieldsPage` so the engine can mount them on demand.
|
|
1426
2309
|
|
|
1427
2310
|
The temporary `/dev/meta` route is removed. Setup app navigation flows
|
|
1428
2311
|
through the new component routes.
|
|
2312
|
+
|
|
1429
2313
|
- @object-ui/types@6.2.0
|
|
1430
2314
|
- @object-ui/core@6.2.0
|
|
1431
2315
|
|
|
@@ -1533,6 +2417,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1533
2417
|
|
|
1534
2418
|
Extract the bulk-vs-per-row decision into a reusable
|
|
1535
2419
|
`executeBulkBatch(input, ops)` helper in `@object-ui/core`:
|
|
2420
|
+
|
|
1536
2421
|
- Single decision tree shared by both update and delete fast paths.
|
|
1537
2422
|
- Bulk success → no per-row pass.
|
|
1538
2423
|
- Bulk partial-count → aggregate batch error.
|
|
@@ -1576,6 +2461,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1576
2461
|
multi-user editing scenarios.
|
|
1577
2462
|
|
|
1578
2463
|
**`@object-ui/data-objectstack`**
|
|
2464
|
+
|
|
1579
2465
|
- Exports `ConcurrentUpdateError` (carries `currentVersion` and
|
|
1580
2466
|
`currentRecord`) and `isConcurrentUpdateError()` type guard.
|
|
1581
2467
|
- `update()` / `delete()` accept `opts.ifMatch` and forward it via the
|
|
@@ -1588,6 +2474,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1588
2474
|
from conflicts without parsing the wire format.
|
|
1589
2475
|
|
|
1590
2476
|
**`@object-ui/core`**
|
|
2477
|
+
|
|
1591
2478
|
- `ApiDataSource.update()` and `.delete()` accept `opts.ifMatch` and emit
|
|
1592
2479
|
the `If-Match` HTTP header.
|
|
1593
2480
|
|
|
@@ -1630,6 +2517,7 @@ ${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
|
1630
2517
|
their settings.
|
|
1631
2518
|
|
|
1632
2519
|
The adapter now defaults to:
|
|
2520
|
+
|
|
1633
2521
|
- `resource`: `sys_user_preference`
|
|
1634
2522
|
- field shape: `(user_id, key, value)` instead of `(user_id, kind, payload)`
|
|
1635
2523
|
- option name: **`key`** instead of `kind`
|