@object-ui/data-objectstack 16.1.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +638 -1
- package/README.md +9 -4
- package/dist/index.cjs +417 -91
- package/dist/index.d.cts +294 -21
- package/dist/index.d.ts +294 -21
- package/dist/index.js +412 -90
- package/package.json +5 -4
- package/src/aggregate-capability.test.ts +177 -0
- package/src/cache/MetadataCache.ts +12 -3
- package/src/errors.test.ts +12 -12
- package/src/errors.ts +23 -5
- package/src/exportDownload.test.ts +42 -0
- package/src/filter-entry-translation.test.ts +360 -0
- package/src/filter-operator-ast-parity.test.ts +71 -0
- package/src/index.ts +736 -120
- package/src/metadata-client.ts +17 -3
- package/src/onMutation.test.ts +35 -35
- package/src/onWriteWarning.test.ts +172 -0
- package/src/orderby-serialization.test.ts +115 -0
- package/src/queryDataset.test.ts +50 -1
- package/src/searchAll.test.ts +120 -0
- package/src/security.ts +18 -5
- package/src/spec-symbol-batch6.test.ts +303 -0
- package/src/userState.test.ts +25 -4
- package/src/userState.ts +10 -6
- package/src/validation-error.test.ts +94 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,642 @@
|
|
|
1
1
|
# @object-ui/data-objectstack
|
|
2
2
|
|
|
3
|
+
## 17.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c5ccbd5: Stop declaring 12 `@object-ui/data-objectstack` / `@object-ui/plugin-chatbot` /
|
|
8
|
+
`@object-ui/plugin-list` symbols under names `@objectstack/spec` owns
|
|
9
|
+
(objectui#3160, objectstack#4115 batch 6). All three packages leave the ledger.
|
|
10
|
+
|
|
11
|
+
**Breaking for importers of `@object-ui/data-objectstack`** — four exported
|
|
12
|
+
names changed, because the spec exports the same name for a _different_ thing:
|
|
13
|
+
|
|
14
|
+
| was | now | what the spec's same-named export actually is |
|
|
15
|
+
| :-------------------- | :-------------------------- | :--------------------------------------------------------------------------------------- |
|
|
16
|
+
| `CacheStats` | `MetadataCacheStats` | the platform `ICacheService` counters (`keyCount`, `memoryUsage`) |
|
|
17
|
+
| `MetadataSaveOptions` | `MetadataClientSaveOptions` | options for writing a metadata item to a **file** (`format`, `path`, `indent`, `atomic`) |
|
|
18
|
+
| `SecurityPolicy` | `SecurityManagerPolicy` | the package supply-chain policy (`autoScan`, licences, code signing, sandbox) |
|
|
19
|
+
| `ValidationError` | `DataApiValidationError` | a plain `{ field, message, code? }` entry in a validation report |
|
|
20
|
+
|
|
21
|
+
Each pair is disjoint or nearly so — `MetadataSaveOptions` and `SecurityPolicy`
|
|
22
|
+
share not one key with the spec type whose name they wore — so none of them was
|
|
23
|
+
a dialect to reconcile; they were four unrelated concepts squatting on spec
|
|
24
|
+
names. `DataApiValidationError` follows the `<what was validated>Validation<Error|Result>`
|
|
25
|
+
convention registered on objectstack#4115 (`@object-ui/core` took
|
|
26
|
+
`SchemaNodeValidationError` in batch 4). Its **runtime** `name` deliberately
|
|
27
|
+
stays `'ValidationError'`: `normaliseClientError` and `@object-ui/react`'s
|
|
28
|
+
error-message helper both sniff `err.name`, so that string is a wire contract,
|
|
29
|
+
not a symbol.
|
|
30
|
+
|
|
31
|
+
**Breaking for importers of `@object-ui/plugin-chatbot`** — `PendingActionRow`
|
|
32
|
+
and `PendingActionStatus` are now re-exported from `@objectstack/spec/contracts`
|
|
33
|
+
instead of hand-transcribed, which narrows them. The copies had drifted three
|
|
34
|
+
ways, and each drift had **disabled a compile-time check** rather than merely
|
|
35
|
+
differed from one:
|
|
36
|
+
|
|
37
|
+
- `status: PendingActionStatus | string` — a union with `string` absorbs the
|
|
38
|
+
literals, so that annotation carried no information at all;
|
|
39
|
+
- `[key: string]: unknown` — the objectstack#4075 mechanism: an index signature
|
|
40
|
+
makes every structural comparison against the spec answer "identical", however
|
|
41
|
+
far the copy has drifted;
|
|
42
|
+
- `created_at` / `updated_at`, which the service contract does not carry and no
|
|
43
|
+
consumer in this repo reads.
|
|
44
|
+
|
|
45
|
+
**Breaking for importers of `@object-ui/plugin-list`** — `ViewTab` is derived from the spec's `ViewTabSchema`
|
|
46
|
+
— from its **input** side, because `pinned` / `isDefault` / `visible` carry
|
|
47
|
+
`.default()`s and this component is handed authored metadata, not parsed output.
|
|
48
|
+
That removes a renderer-side tolerance the copy carried: `visible` accepted
|
|
49
|
+
`string | boolean` and the tab bar compared it against the literal `'false'`, a
|
|
50
|
+
spelling no producer emits. `label` also stops being required (the spec makes it
|
|
51
|
+
optional; `name` is the identifier) and `filter` stops being `any`.
|
|
52
|
+
|
|
53
|
+
`ListView` and `UserFilters` keep their names as declared dialects: both are the
|
|
54
|
+
React **renderers** of the spec types whose names they share, and each takes that
|
|
55
|
+
spec type as a prop (`ListViewProps.schema`, `UserFiltersProps.config`) rather
|
|
56
|
+
than restating its shape. `Tool` and `MessageContent` in `plugin-chatbot` are
|
|
57
|
+
vendored Vercel AI Elements / Shadcn primitives — upstream's component API, not
|
|
58
|
+
objectui's authored surface — so the guard now skips that directory the same way
|
|
59
|
+
it already skips `components/src/ui/`, with a test that fails if any file there
|
|
60
|
+
stops carrying its vendor banner.
|
|
61
|
+
|
|
62
|
+
Scored `minor`, not `major`, per this repo's fixed-group rule — objectui's major
|
|
63
|
+
tracks `@objectstack`, so breaking changes of our own ship as minor with the
|
|
64
|
+
semantics spelled out above (see AGENTS.md §版本号策略). A `major` here would carry
|
|
65
|
+
all 39 packages of the fixed group to `18.0.0` and off objectstack's 17.x line.
|
|
66
|
+
|
|
67
|
+
- d3584c6: Bring the whole `@objectstack` family to `17.0.0-rc.1`, so the dependency graph resolves a
|
|
68
|
+
single copy of `@objectstack/spec`.
|
|
69
|
+
|
|
70
|
+
#3178 bumped **only** `@objectstack/spec` to `17.0.0-rc.1`. The rest of the family —
|
|
71
|
+
`client`, `core`, `formula`, `lint` (and `sdui-parser`, reached through `lint`) — stayed on
|
|
72
|
+
`17.0.0-rc.0`, and each of them depends on spec at an **exact** version rather than a
|
|
73
|
+
caret:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
@objectstack/client@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
77
|
+
@objectstack/core@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
78
|
+
@objectstack/formula@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
79
|
+
@objectstack/lint@17.0.0-rc.0 -> spec "17.0.0-rc.0"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
So `main` carried **two** spec copies: objectui's own code read `17.0.0-rc.1` while every
|
|
83
|
+
`@objectstack/*` package read `17.0.0-rc.0` from its own nested `node_modules`. That breaks
|
|
84
|
+
the single-contract invariant this repo's guards are built on, and it breaks them
|
|
85
|
+
_silently_ — the affected checks depend on identity, not on version strings:
|
|
86
|
+
|
|
87
|
+
- `spec-subschema-parity.test.ts` distinguishes a genuine re-export from a fork by
|
|
88
|
+
**reference identity** of the zod schema object. Two spec copies make every schema a
|
|
89
|
+
distinct object, so a real re-export starts reading as a fork (or a fork slips through,
|
|
90
|
+
depending on which copy each side resolved).
|
|
91
|
+
- `scripts/check-spec-symbol-derivation.mjs` and `spec-symbol-parity.test.ts` use
|
|
92
|
+
`createRequire` to resolve spec's `.d.ts` and run it through the TS checker. With two
|
|
93
|
+
copies installed, _which_ declaration file the checker sees is a function of resolution
|
|
94
|
+
order rather than of intent.
|
|
95
|
+
|
|
96
|
+
The declared ranges were already `^17.0.0-rc.0`, which technically admits rc.1 — the pin
|
|
97
|
+
lived in the lockfile. Raising the remaining ranges to `^17.0.0-rc.1` makes the floor
|
|
98
|
+
explicit and forbids a future install from silently sliding back onto a family member that
|
|
99
|
+
drags rc.0 along with it. The rc.1 family members pin spec at `17.0.0-rc.1` exactly, so the
|
|
100
|
+
graph now converges on one copy by construction, not by luck.
|
|
101
|
+
|
|
102
|
+
No product behaviour changes here. `check:spec-symbols` reconciliation was already
|
|
103
|
+
completed by #3178 and stays green under the unified graph; this changeset is `minor`
|
|
104
|
+
per the repo's fixed-group version policy.
|
|
105
|
+
|
|
106
|
+
### Patch Changes
|
|
107
|
+
|
|
108
|
+
- Updated dependencies [4ae0ac4]
|
|
109
|
+
- Updated dependencies [696e3c1]
|
|
110
|
+
- Updated dependencies [bca45cc]
|
|
111
|
+
- Updated dependencies [4bf612c]
|
|
112
|
+
- Updated dependencies [335041c]
|
|
113
|
+
- Updated dependencies [b414983]
|
|
114
|
+
- Updated dependencies [256f8cc]
|
|
115
|
+
- Updated dependencies [d9668a7]
|
|
116
|
+
- Updated dependencies [cb82705]
|
|
117
|
+
- Updated dependencies [f572849]
|
|
118
|
+
- Updated dependencies [d3584c6]
|
|
119
|
+
- Updated dependencies [a8ad6c0]
|
|
120
|
+
- Updated dependencies [444457c]
|
|
121
|
+
- Updated dependencies [850033c]
|
|
122
|
+
- Updated dependencies [022e4c3]
|
|
123
|
+
- Updated dependencies [009e25d]
|
|
124
|
+
- Updated dependencies [726b89c]
|
|
125
|
+
- @object-ui/types@17.2.0
|
|
126
|
+
- @object-ui/core@17.2.0
|
|
127
|
+
|
|
128
|
+
## 17.1.0
|
|
129
|
+
|
|
130
|
+
### Minor Changes
|
|
131
|
+
|
|
132
|
+
- 9b773f9: fix(analytics): a missing analytics capability no longer renders as an empty KPI — objectstack#3891
|
|
133
|
+
|
|
134
|
+
The framework retired its degraded in-kernel analytics fallback (objectstack#3891):
|
|
135
|
+
it dropped the caller's RLS/tenant scope and ignored the contract filter, so it
|
|
136
|
+
answered `200` with over-broad numbers. `@objectstack/service-analytics` is now
|
|
137
|
+
the only implementation, and a deployment without it answers `404` on
|
|
138
|
+
`/analytics/query` (objectstack#4019 stops mounting the routes) or `501` on
|
|
139
|
+
`/analytics/dataset/query`.
|
|
140
|
+
|
|
141
|
+
Three things were wrong on this side of that boundary:
|
|
142
|
+
|
|
143
|
+
**① A KPI on such a deployment rendered a confident zero.** `aggregate()`'s
|
|
144
|
+
`catch` promises a client-side fallback, and the fallback is correct — but the
|
|
145
|
+
adapter never got there for the most likely failure. It now classifies the
|
|
146
|
+
failure (`classifyAnalyticsFailure`) instead of treating every error alike:
|
|
147
|
+
capability-absent (404/501) degrades to a client-side aggregate over a
|
|
148
|
+
**server-scoped** `find()` — same rows, same filter, RLS still applied — and
|
|
149
|
+
says so **once per adapter** in the console, naming the package to install,
|
|
150
|
+
rather than once per widget or not at all.
|
|
151
|
+
|
|
152
|
+
**② A rejected query was answered with plausible numbers.** The framework
|
|
153
|
+
validates `/analytics/query` at the entry now (objectstack#4010), so a `400
|
|
154
|
+
VALIDATION_FAILED` means _this adapter_ sent an off-contract body. Degrading
|
|
155
|
+
there would bury our own bug behind output from a different code path — the
|
|
156
|
+
misdirection objectstack#3878 documented. It now throws
|
|
157
|
+
`AnalyticsQueryRejectedError` and never falls back. Transient failures (5xx,
|
|
158
|
+
network) degrade exactly as before.
|
|
159
|
+
|
|
160
|
+
**③ The dataset preview blamed the author for a missing capability.**
|
|
161
|
+
`queryDataset` mapped `501`/`404` to `Dataset query failed: 501 Not Implemented
|
|
162
|
+
— …`; it now throws the typed `AnalyticsNotInstalledError`
|
|
163
|
+
(`code: 'ANALYTICS_NOT_INSTALLED'`) with a message a UI can render verbatim, and
|
|
164
|
+
`DatasetPreview` shows it as a "analytics capability not installed" empty state
|
|
165
|
+
instead of a red error banner. A real compile error (e.g. "relationship not
|
|
166
|
+
declared in include") keeps its server detail and its banner.
|
|
167
|
+
|
|
168
|
+
New exports from `@object-ui/data-objectstack`: `AnalyticsNotInstalledError`,
|
|
169
|
+
`AnalyticsQueryRejectedError`, `isAnalyticsNotInstalledError`,
|
|
170
|
+
`classifyAnalyticsFailure`.
|
|
171
|
+
|
|
172
|
+
- 1cf0de7: fix(detail): finish the approval-lock story, and warn on silently stripped fields (framework#3794)
|
|
173
|
+
|
|
174
|
+
The Console reported record writability wrong in both directions during an
|
|
175
|
+
approval, so a user had nothing to go on: what they _could_ edit said "locked",
|
|
176
|
+
and what they _couldn't_ said "updated successfully".
|
|
177
|
+
|
|
178
|
+
**The lock band told the truth; the Edit button did not.** objectui#2902 split
|
|
179
|
+
the band into "in approval · editable" vs locked, but the header **Edit** CTA
|
|
180
|
+
still keyed off nothing at all — on a genuinely locked record it stayed live, so
|
|
181
|
+
the user opened the form, filled a screen, and got `RECORD_LOCKED` back on Save.
|
|
182
|
+
It is now `disabled` on a locked record: visible-but-off, with the band beside it
|
|
183
|
+
saying why. This is the LOCK, not the mere presence of an approval — a
|
|
184
|
+
`lockRecord: false` node keeps Edit live, which is the point of that setting.
|
|
185
|
+
|
|
186
|
+
**And the band could still re-lock itself.** `DetailView` OR-ed the record's own
|
|
187
|
+
`approval_status` mirror into `isLocked` unconditionally. That mirror is written
|
|
188
|
+
on submit by any flow configuring an `approvalStatusField`, _regardless of_
|
|
189
|
+
`lockRecord` — so on a `lockRecord: false` node the host correctly resolved "not
|
|
190
|
+
locked" from the request's `lock_record` while the mirror dragged the band back
|
|
191
|
+
to "Locked for approval", with the pencils live and saves landing underneath it.
|
|
192
|
+
The host is now authoritative whenever it threads `approvalPending`; the mirror
|
|
193
|
+
is consulted only for bare/legacy `DetailView` hosts that thread nothing, where
|
|
194
|
+
it still reads as locked (no node granularity — the safe direction).
|
|
195
|
+
|
|
196
|
+
Recall's tooltip no longer promises to unlock a record the node never locked
|
|
197
|
+
(`detail.cancelApprovalTooltipUnlocked`).
|
|
198
|
+
|
|
199
|
+
**Silently stripped fields now surface on the record form's save path.** The
|
|
200
|
+
adapter emitted a write-warning for `create`/`update` responses carrying
|
|
201
|
+
`droppedFields`, but not for `batchTransaction` — which is how the record form
|
|
202
|
+
saves a master-detail record, i.e. the one surface where a user actually edits a
|
|
203
|
+
`readonlyWhen`-locked field. `batchTransaction` now emits one warning per event,
|
|
204
|
+
resolving each back to its operation via the response's `index`.
|
|
205
|
+
|
|
206
|
+
The toast itself was hardcoded English and called every strip "read-only". It is
|
|
207
|
+
now localized (`detail.writeStripped*`, ten locales) and worded by reason:
|
|
208
|
+
`readonly_when` says the field is not editable _in this record's current state_,
|
|
209
|
+
which is what actually happened — the field is editable in other states and the
|
|
210
|
+
form rendered it as an ordinary input, so "read-only" sent the user hunting for a
|
|
211
|
+
permission problem that does not exist.
|
|
212
|
+
|
|
213
|
+
**And it stopped crying wolf.** `createObjectStackUserStateAdapter` hand-stamped
|
|
214
|
+
the server-managed `updated_at` on every recents/favorites write, which the
|
|
215
|
+
server strips and reports — so the console popped "Some fields were not saved"
|
|
216
|
+
about a field no user ever touched, on page loads, drowning the signal the toast
|
|
217
|
+
exists for. It no longer sends the column; the server stamps it anyway.
|
|
218
|
+
|
|
219
|
+
- 0ded602: fix(form): a server rejection that names fields now marks those fields (objectstack#3896)
|
|
220
|
+
|
|
221
|
+
The server has always said which field it rejected. `@objectstack/objectql`'s
|
|
222
|
+
validators throw `VALIDATION_FAILED` with `fields[]` — one entry per offending
|
|
223
|
+
field, each with a human `message` — and both the REST layer and the runtime
|
|
224
|
+
dispatcher serve that as a 400 with the entries intact.
|
|
225
|
+
|
|
226
|
+
Every form dropped them. The submit handler caught the rejection, ran the
|
|
227
|
+
message through `extractWriteErrorMessage`, and showed **one undirected toast**:
|
|
228
|
+
the user was told something was wrong but not _what_, on a surface that already
|
|
229
|
+
knows how to mark an input — and already does exactly that for client-side
|
|
230
|
+
validation. On a long form the offending field was often off-screen, so "创建"
|
|
231
|
+
appeared to do nothing.
|
|
232
|
+
|
|
233
|
+
**Now the two failures behave identically, because they share one
|
|
234
|
+
implementation.** The per-field marking, the toast naming the fields, and the
|
|
235
|
+
scroll-and-focus of the first offender (#2793) were extracted from the
|
|
236
|
+
client-side invalid handler; the server path calls the same function. As far as
|
|
237
|
+
the person filling in the form is concerned these are the same event — only the
|
|
238
|
+
referee differs.
|
|
239
|
+
|
|
240
|
+
Three layers, each of which was dropping the detail:
|
|
241
|
+
|
|
242
|
+
- **`@object-ui/react`** — new `extractFieldErrors(err)` (exported alongside
|
|
243
|
+
`extractWriteErrorMessage` / `isPermissionError`) normalises the three shapes
|
|
244
|
+
the error can arrive in: a typed `ValidationError` from the ObjectStack
|
|
245
|
+
adapter, the raw `@objectstack/client` error (whose `details` falls back to the
|
|
246
|
+
whole response body, which is where `fields[]` lands), and a hand-rolled error
|
|
247
|
+
carrying `fields` directly — the server duck-types that shape identically, so
|
|
248
|
+
the client must not be pickier than the server. Entries with no usable `field`
|
|
249
|
+
are **dropped rather than guessed at**: marking an innocent input is worse than
|
|
250
|
+
the generic toast.
|
|
251
|
+
- **`@object-ui/data-objectstack`** — `normaliseClientError` now maps a 400
|
|
252
|
+
`VALIDATION_FAILED` onto the `ValidationError` class that has sat in
|
|
253
|
+
`errors.ts` since the package was written, exported and **never once
|
|
254
|
+
constructed**. Its `validationErrors: Array<{ field, message }>` shape was
|
|
255
|
+
already exactly right. `create` also now normalises at all: only `update` did,
|
|
256
|
+
so a rejected insert reached callers as the raw client error — and a create is
|
|
257
|
+
the path that most often trips required-field validation.
|
|
258
|
+
- **`@object-ui/components`** — the form renderer maps the entries onto
|
|
259
|
+
`form.setError` and takes over the failure, **but only when every rejected
|
|
260
|
+
field has a visible input to carry it**. If the server also rejected something
|
|
261
|
+
the form does not render, it falls through to the banner, whose top-level
|
|
262
|
+
message concatenates every field's reason — so the part the user cannot see
|
|
263
|
+
inline is still said out loud instead of silently dropped.
|
|
264
|
+
|
|
265
|
+
This also removes the need for the client-side predicate mirroring added in
|
|
266
|
+
#2962: a form no longer has to guess what the server will reject in order to
|
|
267
|
+
warn about it beforehand, and mirrored predicates drift.
|
|
268
|
+
|
|
269
|
+
Non-field failures (403 / permission denials / anything without `fields[]`) take
|
|
270
|
+
exactly the path they took before.
|
|
271
|
+
|
|
272
|
+
### Patch Changes
|
|
273
|
+
|
|
274
|
+
- 4952edf: fix(errors): error-code branches survive the framework's ADR-0112 rename — objectstack#3841
|
|
275
|
+
|
|
276
|
+
Framework ADR-0112 renamed the whole `error.code` vocabulary from lowercase
|
|
277
|
+
`snake_case` to `SCREAMING_SNAKE` (`destructive_change` → `DESTRUCTIVE_CHANGE`).
|
|
278
|
+
Eleven places compared `err.code` against the old spelling with `===`, so against
|
|
279
|
+
a swept server they simply stopped matching — and nothing threw. The affordance
|
|
280
|
+
each branch guards just vanished and the user got the generic error toast instead:
|
|
281
|
+
|
|
282
|
+
- the destructive-change confirm dialog (resource editor, permission matrix)
|
|
283
|
+
- the "create a writable package first" hint
|
|
284
|
+
- field-scoped validation issues on embedded item saves
|
|
285
|
+
- the all-or-nothing publish summary naming the causal item
|
|
286
|
+
- unknown-object tolerance in the app header and in record search
|
|
287
|
+
- the marketplace's local-install messages for conflict / auth / unavailable
|
|
288
|
+
- `isNotFoundError` in the data layer
|
|
289
|
+
|
|
290
|
+
`RECORD_NOT_FOUND` had already been renamed a release earlier, so that branch was
|
|
291
|
+
already dead before this fix.
|
|
292
|
+
|
|
293
|
+
New `errorCodeIs` / `errorCodeIsAnyOf` in `@object-ui/types` compare
|
|
294
|
+
case-insensitively, so the console keeps working against servers on either side
|
|
295
|
+
of the rename — the console ships separately from the server it talks to. Every
|
|
296
|
+
call site now passes the catalog (SCREAMING) spelling, and `error-code.ts` is the
|
|
297
|
+
single file to delete once no supported server emits the old vocabulary.
|
|
298
|
+
|
|
299
|
+
- 7f0252e: fix(list,data-objectstack,types): exporting a searched list no longer downloads the unsearched superset
|
|
300
|
+
|
|
301
|
+
The server-streamed export mirrored the view's `filter` and `sort`, and the
|
|
302
|
+
code comment claimed that made the file match the screen:
|
|
303
|
+
|
|
304
|
+
> Mirrors the active view's filter + sort so the exported file matches what the
|
|
305
|
+
> user sees.
|
|
306
|
+
|
|
307
|
+
It mirrored one half. There was no way to carry the term a user had typed into
|
|
308
|
+
the search box — `ExportDownloadRequest` had no field for one — so exporting
|
|
309
|
+
during a search produced **more rows than the list showed**, in a file that
|
|
310
|
+
looks authoritative, with nothing indicating the difference. The client-side
|
|
311
|
+
fallback was always correct (it serializes the already-searched `data`); only
|
|
312
|
+
the server path was wrong, and it is the one that handles xlsx.
|
|
313
|
+
|
|
314
|
+
Same family as a dropped filter (objectstack#3948, objectstack#4181): a
|
|
315
|
+
plausible answer that is quietly broader than the one asked for.
|
|
316
|
+
|
|
317
|
+
- `ExportDownloadRequest` gains `search` / `searchFields`.
|
|
318
|
+
- `ObjectStackAdapter.exportDownload` sends them as `search=` / `searchFields=`,
|
|
319
|
+
trimming the term and omitting both when it is blank (`searchFields` alone
|
|
320
|
+
means nothing).
|
|
321
|
+
- `ListView` passes the active `searchTerm` and the view's `searchableFields`,
|
|
322
|
+
and both are now in the export callback's dependency array — a stale closure
|
|
323
|
+
would export the wrong row set.
|
|
324
|
+
|
|
325
|
+
Requires a server with objectstack#4230. Older servers ignore unknown query
|
|
326
|
+
params on this route, so they keep today's behaviour rather than erroring.
|
|
327
|
+
|
|
328
|
+
**Also: the filter merge is no longer written twice.** The three filter sources
|
|
329
|
+
(view filter, filter-panel group, per-field user filters) were merged by
|
|
330
|
+
verbatim copies in the data fetch and in the export — two copies that must
|
|
331
|
+
agree, deciding respectively what the user _sees_ and what they _download_.
|
|
332
|
+
Both now call `buildEffectiveFilter`. This is a pure extraction: the copies did
|
|
333
|
+
agree, and the four parity tests added for it pass against the old code too.
|
|
334
|
+
They exist to keep it that way — the adapter's duplicated filter-shape check
|
|
335
|
+
had already drifted apart unnoticed (#3072).
|
|
336
|
+
|
|
337
|
+
- 7d35010: fix(data-objectstack): a view's own filter no longer disappears when the user adds one
|
|
338
|
+
|
|
339
|
+
`ObjectStackAdapter` translated object-form filter entries
|
|
340
|
+
(`[{ field, operator, value }, ...]`) only at the **top level** of a `$filter`.
|
|
341
|
+
The moment a list has both a stored view filter and a user filter, it builds
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
[
|
|
345
|
+
"and",
|
|
346
|
+
[{ field: "stage", operator: "eq", value: "won" }],
|
|
347
|
+
[["amount", ">", 1]],
|
|
348
|
+
];
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
whose head is the string `and`, so the old check called the whole thing
|
|
352
|
+
"already AST" and shipped the rules untranslated. Both server answers to that
|
|
353
|
+
are wrong:
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
isFilterAST(above); // false — a bare rule object is not an AST child
|
|
357
|
+
parseFilterAST(above); // { amount: { $gt: 1 } } ← `stage = won` is GONE
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Since objectstack#4121 the `isFilterAST` gate turns it into a **400 and the
|
|
361
|
+
list fails to load**. Before it — or anywhere `parseFilterAST` is reached
|
|
362
|
+
without that gate — **the view's own condition is dropped without a word** and
|
|
363
|
+
the list returns records the view exists to exclude.
|
|
364
|
+
|
|
365
|
+
Translation is now recursive through `and`/`or` nodes and legacy flat child
|
|
366
|
+
arrays, so the shape reaches the server as a valid AST
|
|
367
|
+
(`{$and: [{stage: 'won'}, {amount: {$gt: 1}}]}`).
|
|
368
|
+
|
|
369
|
+
Three related fixes in the same code:
|
|
370
|
+
|
|
371
|
+
- **An untranslatable entry is now an error, not an omission.** Entries that
|
|
372
|
+
failed to translate were dropped, and dropping one conjunct of an `and`
|
|
373
|
+
returns a _superset_ of the rows asked for — dropping the last one sent no
|
|
374
|
+
`filter=` at all, so the whole table came back. `find()` now throws
|
|
375
|
+
`MalformedFilterError`, carrying `code: 'INVALID_FILTER'` / `httpStatus: 400`
|
|
376
|
+
so a failed list renders "the filter is malformed" rather than "check your
|
|
377
|
+
connection". A rule with a blank `field` passes `ViewFilterRuleSchema`
|
|
378
|
+
(`z.string()` admits `''`), so this is reachable from real stored metadata.
|
|
379
|
+
A _mixed_ array (`[{ field, operator, value }, ['amount', '>', 1]]`) now
|
|
380
|
+
keeps both halves instead of dropping the tuple — that case was a lost
|
|
381
|
+
condition, not a malformed one.
|
|
382
|
+
- **The two `find()` routes can no longer disagree.** The "is this object
|
|
383
|
+
form?" test existed twice — once in `translateFilterToAST`, once inline in
|
|
384
|
+
`convertQueryParams` — and the copies had already drifted: the inline one
|
|
385
|
+
omitted a `!== null` guard, so a `$filter` of `[null]` threw a `TypeError` on
|
|
386
|
+
the plain route while the same value was handled on the `$expand` route. One
|
|
387
|
+
definition now serves both.
|
|
388
|
+
- **Dropped an unreachable `entry.name` fallback.** `objectFilterEntryToAST`
|
|
389
|
+
read `entry.field ?? entry.name` while the shape check keyed on `field`
|
|
390
|
+
alone, so the `name` half was dead from the commit that introduced it. The
|
|
391
|
+
spec agrees it is not a real shape — `ViewFilterRuleSchema.field` is
|
|
392
|
+
required, so such a rule cannot be saved as view metadata.
|
|
393
|
+
|
|
394
|
+
Refs objectstack#3948, objectstack#4121, #2945
|
|
395
|
+
|
|
396
|
+
- c4d7b20: fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist
|
|
397
|
+
|
|
398
|
+
Sweeping the other `$filter` producers after #3078 turned up two live defects in
|
|
399
|
+
`ObjectView`, which fetches its own data for calendar / kanban / gallery /
|
|
400
|
+
timeline (grid delegates to `ObjectGrid`).
|
|
401
|
+
|
|
402
|
+
**1. An object filter was dropped, and only for non-grid views.**
|
|
403
|
+
`table.defaultFilters` is declared `Record<string, any>`, and the merge tested
|
|
404
|
+
`baseFilter.length > 0` — `undefined > 0` for an object. So the filter vanished
|
|
405
|
+
and the view returned **every record**. `ObjectGrid` assigns the same value
|
|
406
|
+
straight to `params.$filter`, so one view definition filtered correctly as a
|
|
407
|
+
grid and returned everything as a calendar.
|
|
408
|
+
|
|
409
|
+
**2. Rule objects were spread into the `and`, not wrapped.**
|
|
410
|
+
`['and', ...baseFilter, ...userFilter]` is only correct when the source is an
|
|
411
|
+
array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
|
|
412
|
+
spreading put bare rule objects where the AST expects nodes:
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
isFilterAST([
|
|
416
|
+
"and",
|
|
417
|
+
{ field: "stage", operator: "eq", value: "won" },
|
|
418
|
+
["owner", "=", "me"],
|
|
419
|
+
]);
|
|
420
|
+
// false → 400 since objectstack#4121
|
|
421
|
+
parseFilterAST(same);
|
|
422
|
+
// {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
That second line is a predicate over three columns named `field`, `operator`
|
|
426
|
+
and `value` — which don't exist.
|
|
427
|
+
|
|
428
|
+
> **Correction.** The first version of this note said the spread was "reachable
|
|
429
|
+
> whenever a view with a filter meets a user filter value". That was wrong for
|
|
430
|
+
> `ObjectView`: the branch required a non-empty user filter, and nothing ever
|
|
431
|
+
> wrote the state it was built from, so it could never run. The shape is
|
|
432
|
+
> genuinely broken — a live server answers it with a 400 — and the adapter-level
|
|
433
|
+
> defence added alongside is still warranted for any producer that emits it, but
|
|
434
|
+
> **this particular site was dead code, not a live defect.** Defect 1 above was
|
|
435
|
+
> live: it sat on the always-taken path. The dead machinery behind the wrong
|
|
436
|
+
> claim is removed in a follow-up.
|
|
437
|
+
|
|
438
|
+
New in `@object-ui/core`: `toFilterNode` normalizes one source (rule array / AST
|
|
439
|
+
/ MongoDB object) and `mergeFilterNodes` combines sources as siblings under one
|
|
440
|
+
`and`. `ObjectView` and `ListView.buildEffectiveFilter` both use them, so the
|
|
441
|
+
three filter shapes are reconciled in one place instead of by hand at each
|
|
442
|
+
renderer.
|
|
443
|
+
|
|
444
|
+
`ObjectStackAdapter` also now translates a bare rule object sitting directly
|
|
445
|
+
under a logical node — the chokepoint defence for any producer still emitting
|
|
446
|
+
the spread shape. Only rule-_shaped_ objects are touched; a child with no
|
|
447
|
+
`field` is a genuine MongoDB condition and passes through untouched.
|
|
448
|
+
|
|
449
|
+
**Correcting a comment shipped in #3078.** `buildEffectiveFilter` documented the
|
|
450
|
+
dropped-object case as unreachable, "nothing in this repo produces one for a
|
|
451
|
+
list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
|
|
452
|
+
that schema's `filter`, and its last fallback is `table.defaultFilters`. The
|
|
453
|
+
case is now handled rather than explained away.
|
|
454
|
+
|
|
455
|
+
Verified with 19 tests across the four packages; reverting each source file
|
|
456
|
+
fails the ones that cover it. Emitted filters are asserted against the spec's
|
|
457
|
+
own `isFilterAST` / `parseFilterAST`, including an executable pin on what the
|
|
458
|
+
old spread shape produced.
|
|
459
|
+
|
|
460
|
+
- ad0183a: fix(data-objectstack,core): an object filter no longer depends on whether the query expands a lookup
|
|
461
|
+
|
|
462
|
+
#3072 single-sourced the ARRAY branch of the adapter's two `find()` routes. The
|
|
463
|
+
object branch was left as it was: `convertQueryParams` converted a MongoDB-style
|
|
464
|
+
filter to AST while `translateFilterToAST` returned it verbatim — so the same
|
|
465
|
+
`$filter` went out in two formats, decided by whether the query happened to
|
|
466
|
+
expand a lookup.
|
|
467
|
+
|
|
468
|
+
Measured across 21 operator shapes, **four diverged**. Most of the gap turned
|
|
469
|
+
out to be harmless — `{$and: […]}` survives the plain route as a
|
|
470
|
+
`['$and','=',[…]]` comparison that `parseFilterAST` reads back as a real `$and`,
|
|
471
|
+
and `$exists` vs `$null` is a difference the server treats identically. Two were
|
|
472
|
+
not harmless:
|
|
473
|
+
|
|
474
|
+
- **The unknown-operator guard only ran on one route.** `convertFiltersToAST`
|
|
475
|
+
throws on an unrecognised operator, with a comment saying it does so "to avoid
|
|
476
|
+
silent failure" — but the expanded route never called it, so a typo'd operator
|
|
477
|
+
threw on a plain read and shipped silently whenever a lookup was expanded.
|
|
478
|
+
- **`$regex` was silently rewritten to `contains`.** `$regex: 'a.c'` matches
|
|
479
|
+
"abc"; `contains 'a.c'` matches only those three literal characters. That is a
|
|
480
|
+
_different question_, not a weaker version of the same one, and neither result
|
|
481
|
+
looks wrong on screen. The rewrite sat behind a `console.warn`, which is not
|
|
482
|
+
an error channel in a deployed app — and the function's own unknown-operator
|
|
483
|
+
message never listed `$regex` among the supported set. The spec has no
|
|
484
|
+
`$regex` (`FILTER_OPERATORS`, `data/filter.zod.ts`), so there is nothing to
|
|
485
|
+
translate it into: it is now refused, the same treatment the neighbouring
|
|
486
|
+
unknown operator already got. Nothing in the repo depended on the conversion.
|
|
487
|
+
|
|
488
|
+
Both refusals now throw `FilterOperatorError`, carrying `code: 'INVALID_FILTER'`
|
|
489
|
+
/ `httpStatus: 400`. The pre-existing unknown-operator throw was a bare `Error`,
|
|
490
|
+
which `classifyLoadError` classifies as a network fault — so a malformed filter
|
|
491
|
+
told the user to check their connection (#3066), the one thing it definitely
|
|
492
|
+
was not.
|
|
493
|
+
|
|
494
|
+
- a17ef09: fix(data-objectstack): a string `$orderby` reaches the server as a sort instead of a list of character indices — #3106
|
|
495
|
+
|
|
496
|
+
`QueryParams['$orderby']` declares four shapes — `string`, `string[]`,
|
|
497
|
+
`SortNode[]`, `Record<field, direction>`. Both of this adapter's `find()` routes
|
|
498
|
+
(`convertQueryParams` for a plain read, `rawFindWithPopulate` for one carrying
|
|
499
|
+
`$expand`/`$search`) carried their own copy of the fold that serializes it, and
|
|
500
|
+
both copies handled the same three. The bare string fell through to the
|
|
501
|
+
`Record` branch, where `Object.entries('name asc')` enumerates the string's
|
|
502
|
+
character indices — so the request went out as `sort=0,1,2,3,4,5,6,7`.
|
|
503
|
+
|
|
504
|
+
Since `objectstack#4226` the server refuses a sort it cannot read
|
|
505
|
+
(`400 INVALID_SORT`) rather than dropping it silently, so this was not a
|
|
506
|
+
degraded ordering but a list that failed to load outright — and `"${field}
|
|
507
|
+
${order}"` is exactly the shape `ObjectGrid` builds from its view metadata's
|
|
508
|
+
`sort`, making every standalone grid with a configured sort a broken one.
|
|
509
|
+
|
|
510
|
+
Both routes now share one exported `serializeOrderBy`, for the same reason the
|
|
511
|
+
filter path already shares one: two copies of a fold can only agree by
|
|
512
|
+
inspection, and these two did not.
|
|
513
|
+
|
|
514
|
+
- Updated dependencies [62311b6]
|
|
515
|
+
- Updated dependencies [9e7349e]
|
|
516
|
+
- Updated dependencies [8864971]
|
|
517
|
+
- Updated dependencies [b41f401]
|
|
518
|
+
- Updated dependencies [19e9fa0]
|
|
519
|
+
- Updated dependencies [95b7214]
|
|
520
|
+
- Updated dependencies [7d9734d]
|
|
521
|
+
- Updated dependencies [6ae818e]
|
|
522
|
+
- Updated dependencies [746dd00]
|
|
523
|
+
- Updated dependencies [aebfa4f]
|
|
524
|
+
- Updated dependencies [38ca8be]
|
|
525
|
+
- Updated dependencies [4952edf]
|
|
526
|
+
- Updated dependencies [7f0252e]
|
|
527
|
+
- Updated dependencies [c4d7b20]
|
|
528
|
+
- Updated dependencies [7639a61]
|
|
529
|
+
- Updated dependencies [94e63ef]
|
|
530
|
+
- Updated dependencies [02aef0c]
|
|
531
|
+
- Updated dependencies [6f29aa5]
|
|
532
|
+
- Updated dependencies [c4db402]
|
|
533
|
+
- Updated dependencies [5319bf1]
|
|
534
|
+
- Updated dependencies [49e5671]
|
|
535
|
+
- Updated dependencies [b5b97e2]
|
|
536
|
+
- Updated dependencies [f59f2c1]
|
|
537
|
+
- Updated dependencies [4874117]
|
|
538
|
+
- Updated dependencies [ad0183a]
|
|
539
|
+
- Updated dependencies [ce08d55]
|
|
540
|
+
- Updated dependencies [aa1240a]
|
|
541
|
+
- Updated dependencies [2374a49]
|
|
542
|
+
- Updated dependencies [390c071]
|
|
543
|
+
- Updated dependencies [d10f526]
|
|
544
|
+
- Updated dependencies [2d5d594]
|
|
545
|
+
- Updated dependencies [ea7f477]
|
|
546
|
+
- Updated dependencies [7f23cd0]
|
|
547
|
+
- Updated dependencies [24e0e0a]
|
|
548
|
+
- Updated dependencies [3a6cf24]
|
|
549
|
+
- Updated dependencies [aa35561]
|
|
550
|
+
- Updated dependencies [03bd53b]
|
|
551
|
+
- Updated dependencies [3c1f321]
|
|
552
|
+
- Updated dependencies [a045a32]
|
|
553
|
+
- Updated dependencies [912496d]
|
|
554
|
+
- Updated dependencies [9867281]
|
|
555
|
+
- @object-ui/core@17.1.0
|
|
556
|
+
- @object-ui/types@17.1.0
|
|
557
|
+
|
|
558
|
+
## 17.0.0
|
|
559
|
+
|
|
560
|
+
### Minor Changes
|
|
561
|
+
|
|
562
|
+
- d62fb1f: feat(app-shell): toast when a save silently dropped read-only fields (framework #3431/#3455)
|
|
563
|
+
|
|
564
|
+
The framework now reports fields it LEGALLY stripped from a write (a non-system
|
|
565
|
+
caller can't seed a `readonly` field, a `readonlyWhen` predicate locked it, …)
|
|
566
|
+
via a `droppedFields` payload on the create/update response. Previously the
|
|
567
|
+
console discarded it: a value the user typed into a locked field just vanished on
|
|
568
|
+
save with a success toast and no explanation.
|
|
569
|
+
|
|
570
|
+
- **data-objectstack:** `ObjectStackAdapter` now emits a `WriteWarningEvent`
|
|
571
|
+
after a create/update whose response carried `droppedFields`, exposed through a
|
|
572
|
+
new `onWriteWarning(cb)` subscription (mirrors the existing `onMutation` bus).
|
|
573
|
+
Reads the field structurally, so an older client or a backend that never drops
|
|
574
|
+
is a no-op. New exported types: `WriteWarningEvent`, `WriteWarningListener`,
|
|
575
|
+
`DroppedFieldsEvent`.
|
|
576
|
+
- **app-shell:** `AdapterProvider` subscribes and raises a `toast.warning`
|
|
577
|
+
("Some fields were not saved — the read-only field … could not be changed"),
|
|
578
|
+
so the strip is visible instead of silent. The write itself still succeeded;
|
|
579
|
+
status/behaviour are unchanged.
|
|
580
|
+
|
|
581
|
+
### Patch Changes
|
|
582
|
+
|
|
583
|
+
- 8ecf5a6: Command palette (⌘K) now surfaces record search hits from the platform's global
|
|
584
|
+
search endpoint (`GET /api/v1/search`).
|
|
585
|
+
|
|
586
|
+
Previously the palette only ran a per-object `find({ $search })` fanout (the
|
|
587
|
+
metadata-driven ADR-0061 search), which misses records that only the global
|
|
588
|
+
search index knows about — so typing a well-known record name returned no
|
|
589
|
+
records even though `/api/v1/search` served them. `ObjectStackAdapter` now
|
|
590
|
+
exposes a `searchAll(query, { limit, objects })` method that calls the unified
|
|
591
|
+
endpoint, `useRecordSearch` prefers it when present (falling back to the fanout
|
|
592
|
+
otherwise), and the palette renders the resulting record hits grouped by object.
|
|
593
|
+
|
|
594
|
+
- 6e8fd3c: fix(charts): a fieldless `count` aggregate keyed its value column `undefined`, so the chart plotted nothing (framework#3701)
|
|
595
|
+
|
|
596
|
+
framework#3701 pinned down what an OBJECT-bound chart aggregate names its result
|
|
597
|
+
columns — the raw field names it was given (`groupBy` for the category, `field`
|
|
598
|
+
for the value; no `sum_`-style decoration, unlike a dataset measure), plus the
|
|
599
|
+
literal `count` when a `count` omits `field`, which is the alias the engine
|
|
600
|
+
projects `COUNT(*)` under. `os validate` now lints page sources against that
|
|
601
|
+
convention, so the paths that build these rows have to honour it exactly.
|
|
602
|
+
|
|
603
|
+
Three of the four did. The odd one out was `count` — the one function that may
|
|
604
|
+
legitimately omit `field` — because every row builder read `params.field`
|
|
605
|
+
directly:
|
|
606
|
+
|
|
607
|
+
- `aggregateRecords` / `ObjectDataSource.aggregateClientSide` emitted
|
|
608
|
+
`{ [groupBy]: key, [undefined]: value }`, i.e. a column literally named
|
|
609
|
+
`undefined` that no axis binding could ever name;
|
|
610
|
+
- the legacy analytics path was worse: it remapped the server's `count` measure
|
|
611
|
+
onto `params.field` and **deleted** the original key, so the value the server
|
|
612
|
+
did return was thrown away before the chart saw it.
|
|
613
|
+
|
|
614
|
+
All of them now resolve the column through one helper (`aggregateValueKey`) so a
|
|
615
|
+
fieldless count lands under `count`, matching the framework contract. The
|
|
616
|
+
comparison-overlay column is derived from the same key (`count__comparison`
|
|
617
|
+
instead of `undefined__comparison`), and `aggregate.field` is typed optional to
|
|
618
|
+
match the spec's `ChartAggregateSchema`. Charts that name a field are unchanged.
|
|
619
|
+
|
|
620
|
+
- Updated dependencies [1767124]
|
|
621
|
+
- Updated dependencies [8ecf5a6]
|
|
622
|
+
- Updated dependencies [7b35e4b]
|
|
623
|
+
- Updated dependencies [e16ed2d]
|
|
624
|
+
- Updated dependencies [f9bbddb]
|
|
625
|
+
- Updated dependencies [dfd3705]
|
|
626
|
+
- Updated dependencies [2735de6]
|
|
627
|
+
- Updated dependencies [6dee2cb]
|
|
628
|
+
- Updated dependencies [c7cff19]
|
|
629
|
+
- Updated dependencies [cd09a7b]
|
|
630
|
+
- Updated dependencies [f1abf0e]
|
|
631
|
+
- Updated dependencies [f05b84e]
|
|
632
|
+
- Updated dependencies [662bdf9]
|
|
633
|
+
- Updated dependencies [059a052]
|
|
634
|
+
- Updated dependencies [53642d4]
|
|
635
|
+
- Updated dependencies [8aae006]
|
|
636
|
+
- Updated dependencies [d147a13]
|
|
637
|
+
- @object-ui/types@17.0.0
|
|
638
|
+
- @object-ui/core@17.0.0
|
|
639
|
+
|
|
3
640
|
## 16.1.0
|
|
4
641
|
|
|
5
642
|
### Minor Changes
|
|
@@ -80,7 +717,7 @@
|
|
|
80
717
|
(`visibleWhen`/`readonlyWhen`/`requiredWhen` via `fieldRules.ts`, which delegates
|
|
81
718
|
to `@objectstack/formula`'s `ExpressionEngine`) now tracks the 15.x engine — and
|
|
82
719
|
will pick up the framework's `dateField == today()` equality fix
|
|
83
|
-
(objectstack-ai/
|
|
720
|
+
(objectstack-ai/objectstack#3205) automatically at the next 15.x release via the
|
|
84
721
|
caret range. Renderer/action `visible`/`disabled` predicates are unaffected (they
|
|
85
722
|
use the home-grown JS evaluator — tracked separately in #2661).
|
|
86
723
|
|