@object-ui/data-objectstack 17.6.0 → 18.0.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 +2460 -0
- package/README.md +727 -11
- package/dist/index.cjs +2213 -405
- package/dist/index.d.cts +1971 -211
- package/dist/index.d.ts +1971 -211
- package/dist/index.js +2199 -406
- package/package.json +9 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,62 +1,171 @@
|
|
|
1
1
|
import { ObjectStackClient } from '@objectstack/client';
|
|
2
|
-
import { DroppedFieldsEvent } from '@objectstack/spec/data';
|
|
2
|
+
import { DroppedFieldsEvent, EngineAggregateOptions } from '@objectstack/spec/data';
|
|
3
3
|
export { DroppedFieldsEvent } from '@objectstack/spec/data';
|
|
4
|
-
import {
|
|
4
|
+
import { ListViewGroupHeaderRow } from '@objectstack/spec/ui';
|
|
5
|
+
import { GetMetaItemResponse, GetMetaItemLayeredResponse, ListDraftsResponse, RuntimeAuthoringIssue, GetMetaItemsResponse, PublishPackageDraftsResponse, ValidateDataIssue } from '@objectstack/spec/api';
|
|
5
6
|
export { RuntimeAuthoringIssue } from '@objectstack/spec/api';
|
|
6
7
|
import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts';
|
|
7
|
-
import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
|
|
8
|
-
export { DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types';
|
|
8
|
+
import { DataSource, ConnectionState, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
|
|
9
|
+
export { ConnectionState, DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types';
|
|
9
10
|
import { DatasetDrillRange } from '@object-ui/core';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Take the body out of a served draft envelope (`{ type, name, item }`) and
|
|
14
|
+
* remove the framework's own READ DECORATIONS from it — or answer `null` when
|
|
15
|
+
* there is nothing pending.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* ## Why this function exists at all (objectui#8181)
|
|
18
|
+
*
|
|
19
|
+
* It was implemented three separate times — once in `ResourceEditPage`, once in
|
|
20
|
+
* `StudioDesignSurface`, once in `PackageOwdOverviewPanel` — plus a fourth
|
|
21
|
+
* hand-rolled copy in `ObjectHooksPanel` under a different name. objectui#7603
|
|
22
|
+
* taught exactly ONE of those copies to strip. That is the defect this file
|
|
23
|
+
* closes, and it is a different defect from "N consumers forgot": four
|
|
24
|
+
* byte-identical copies of a rule mean the next copy is free to omit it again,
|
|
25
|
+
* so the cure is one function rather than four strips.
|
|
26
|
+
*
|
|
27
|
+
* ## Why it lives HERE and not in `app-shell`
|
|
28
|
+
*
|
|
29
|
+
* Next to the {@link MetadataClient.getDraft} that produces the envelope this
|
|
30
|
+
* decodes. `getDraft` deliberately hands back the wire envelope rather than the
|
|
31
|
+
* body (objectui#4271), so the unwrap is part of that method's contract, not a
|
|
32
|
+
* detail of any one view. `app-shell` already depends on this package, and the
|
|
33
|
+
* one non-app-shell consumer (`updateView`, in this package) is a caller too.
|
|
34
|
+
*
|
|
35
|
+
* ## The verdict runs BEFORE the strip, never after
|
|
36
|
+
*
|
|
37
|
+
* What counts as a pending draft is `getDraft`'s answer. A draft whose only
|
|
38
|
+
* keys are decorations is still a served draft, so the emptiness verdict is
|
|
39
|
+
* taken on the body as it arrived; the strip may return an EMPTY object, but it
|
|
40
|
+
* may never turn a served draft into "no draft". Pinned in
|
|
41
|
+
* `draft-envelope.test.ts`.
|
|
42
|
+
*
|
|
43
|
+
* ## Which keys, and which keys deliberately survive
|
|
44
|
+
*
|
|
45
|
+
* The list is the SPEC'S (`METADATA_READ_DECORATIONS`), reached through its own
|
|
46
|
+
* exported helper — the same one `MetadataService.saveFields` uses on the write
|
|
47
|
+
* side. Never a second hand-maintained `['_diagnostics', '_draft']` in this
|
|
48
|
+
* repo: a local copy goes stale the next time the framework adds a decoration,
|
|
49
|
+
* and a decoration this code does not know to remove is precisely the defect.
|
|
50
|
+
*
|
|
51
|
+
* The ADR-0010 protection envelope (`_lock`, `_provenance`, `_packageId`,
|
|
52
|
+
* `_packageVersion`, ...) shares the underscore spelling and is deliberately
|
|
53
|
+
* NOT on that list: those keys are declared by the closed schemas so provenance
|
|
54
|
+
* survives a re-parse. This strip leaves them alone, and doing otherwise would
|
|
55
|
+
* be the same "drop whatever looks internal" pass AGENTS.md #0.1 bans.
|
|
56
|
+
*
|
|
57
|
+
* ## Not a lenient fallback
|
|
58
|
+
*
|
|
59
|
+
* It removes exactly the two keys the framework ADDS AT READ TIME and never
|
|
60
|
+
* stores. A genuinely unrecognized key still fails loudly at whichever gate
|
|
61
|
+
* sees it next.
|
|
62
|
+
*
|
|
63
|
+
* @param resp the value `MetadataClient.getDraft()` resolved to.
|
|
64
|
+
* @returns the decoration-free draft body, or `null` for "nothing pending".
|
|
65
|
+
*/
|
|
66
|
+
declare function extractDraftBody(resp: unknown): Record<string, unknown> | null;
|
|
67
|
+
/**
|
|
68
|
+
* One entry of a pending draft's per-key provenance (objectui#12725): which
|
|
69
|
+
* authoring surface last changed one key path of the draft body, under which
|
|
70
|
+
* actor, and when. DERIVED from `@objectstack/spec`'s
|
|
71
|
+
* `GetMetaItemResponseSchema.draftChanges` (objectstack#24298), never
|
|
72
|
+
* restated: `path` is the key path inside `item` (an array value is one leaf,
|
|
73
|
+
* `[]` the whole body), `by` is `copilot | studio | api`, `at` an ISO instant.
|
|
17
74
|
*/
|
|
75
|
+
type DraftChange = NonNullable<GetMetaItemResponse['draftChanges']>[number];
|
|
18
76
|
/**
|
|
19
|
-
*
|
|
77
|
+
* The `draftChanges` of a served draft envelope — the value
|
|
78
|
+
* {@link MetadataClient.getDraft} resolved to — or `null` when the read
|
|
79
|
+
* recorded none.
|
|
20
80
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
81
|
+
* The strict draft read (`?state=draft`) serves the key beside `item`, and
|
|
82
|
+
* leaves it ABSENT, never `[]`, when nothing was recorded (a draft row saved
|
|
83
|
+
* before the framework's writer, objectstack#24395). So `null` here means
|
|
84
|
+
* "not recorded", and it is never folded into an empty list a caller would read
|
|
85
|
+
* as "nothing changed".
|
|
86
|
+
*
|
|
87
|
+
* @param resp the value `MetadataClient.getDraft()` resolved to.
|
|
88
|
+
*/
|
|
89
|
+
declare function readDraftChanges(resp: unknown): DraftChange[] | null;
|
|
90
|
+
/**
|
|
91
|
+
* The entry that answers for the key at `path`: of the entries whose path is
|
|
92
|
+
* `path` or one of its ancestors, the LONGEST — the framework's own rule, since
|
|
93
|
+
* a draft save replaces every entry at or under each path it changes, so the
|
|
94
|
+
* deepest recorded ancestor is the last write that changed this key. `[]` (the
|
|
95
|
+
* whole body) is everyone's ancestor. `null` when no entry is an ancestor, and
|
|
96
|
+
* when `changes` is `null` ("not recorded").
|
|
28
97
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
98
|
+
* Paths compare segment by segment, never as a joined string: a segment may
|
|
99
|
+
* itself contain a dot (a permission set's `fields` map is keyed
|
|
100
|
+
* `object.field`).
|
|
101
|
+
*/
|
|
102
|
+
declare function governingDraftChange(changes: readonly DraftChange[] | null | undefined, path: readonly string[]): DraftChange | null;
|
|
103
|
+
/**
|
|
104
|
+
* The entries strictly UNDER `path`: changes to keys inside the value at
|
|
105
|
+
* `path` (`["fields", "x", "label"]` under `["fields", "x"]`). The governing
|
|
106
|
+
* entry ({@link governingDraftChange}) answers for the value as a whole; these
|
|
107
|
+
* answer for its parts, which a later write may have changed through another
|
|
108
|
+
* surface.
|
|
109
|
+
*/
|
|
110
|
+
declare function draftChangesUnder(changes: readonly DraftChange[] | null | undefined, path: readonly string[]): DraftChange[];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* ObjectUI
|
|
114
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
38
115
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* type, mirroring the framework's "single Zod source per type" rule.
|
|
116
|
+
* This source code is licensed under the MIT license found in the
|
|
117
|
+
* LICENSE file in the root directory of this source tree.
|
|
42
118
|
*/
|
|
43
119
|
|
|
44
120
|
/**
|
|
45
|
-
* Emitted after a
|
|
46
|
-
*
|
|
47
|
-
*
|
|
121
|
+
* Emitted after a metadata WRITE whose response carried a non-empty
|
|
122
|
+
* `advisories` array. The write SUCCEEDED — the row persisted and the server
|
|
123
|
+
* returned 200 — so this is advisory, never a failure.
|
|
48
124
|
*
|
|
49
125
|
* Deliberately the same shape of seam as `ObjectStackAdapter.onWriteWarning`
|
|
50
|
-
* (#3431
|
|
126
|
+
* (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455): a successful write whose response carries something the author
|
|
51
127
|
* needs to be told, surfaced to the shell as an event so the data layer never
|
|
52
128
|
* imports a toaster. The difference is only which door produced it — that one
|
|
53
|
-
* is record CRUD,
|
|
129
|
+
* is record CRUD, these are the metadata write doors.
|
|
130
|
+
*
|
|
131
|
+
* ## Both write doors report (#5026)
|
|
132
|
+
*
|
|
133
|
+
* The objectstack-ai/objectstack#4463 runtime authoring gate runs on BOTH metadata write doors by its D1
|
|
134
|
+
* ruling — a draft→active promotion is gated exactly as a direct active save —
|
|
135
|
+
* and its non-blocking findings ride the 2xx of whichever door earned them.
|
|
136
|
+
* `SaveMetaItemResponseSchema` has carried the key since objectstack-ai/objectstack#4717 and
|
|
137
|
+
* `PublishMetaItemResponseSchema` since objectstack#9176; both declare it at
|
|
138
|
+
* the response's TOP level, under the same name and with the same
|
|
139
|
+
* `RuntimeAuthoringIssueSchema` element type, which is what lets one reader and
|
|
140
|
+
* one event serve both. Measured against the installed `@objectstack/spec`
|
|
141
|
+
* rather than assumed — see the PR for the probe.
|
|
142
|
+
*
|
|
143
|
+
* ## And the BATCH publish door (`ce986aafc`)
|
|
144
|
+
*
|
|
145
|
+
* `PublishPackageDraftsResponseSchema` declares the same key with the same
|
|
146
|
+
* element type, riding EACH `published[]` element rather than a parallel
|
|
147
|
+
* top-level map (objectstack#9343's ruled shape). So the reader is still one
|
|
148
|
+
* reader — the object it is handed is one `published[]` element instead of a
|
|
149
|
+
* whole response — and the event is still one event per promoted item.
|
|
150
|
+
*
|
|
151
|
+
* The name keeps its `Save` prefix because it is public API of this package and
|
|
152
|
+
* renaming it would break consumers for no behavioural gain; {@link door} is
|
|
153
|
+
* what says which write produced the event.
|
|
54
154
|
*/
|
|
55
155
|
interface MetadataSaveAdvisoryEvent {
|
|
56
156
|
/** Metadata type saved (e.g. `'flow'`). */
|
|
57
157
|
type: string;
|
|
58
158
|
/** Item name saved. */
|
|
59
159
|
name: string;
|
|
160
|
+
/**
|
|
161
|
+
* The bundle member the write named (objectui#12413), when it named one:
|
|
162
|
+
* set by {@link MetadataClient.publishDraft} from its `options.member`, so
|
|
163
|
+
* an advisory raised on one language's publish of an `email_template` is
|
|
164
|
+
* attributed to that member and not to the whole template. Absent for every
|
|
165
|
+
* write that names no member. Typed from {@link MetadataDraftHeader}'s
|
|
166
|
+
* `member`, the value the caller read off the draft row.
|
|
167
|
+
*/
|
|
168
|
+
member?: MetadataDraftHeader['member'];
|
|
60
169
|
/**
|
|
61
170
|
* The save mode the call used. `'draft'` writes are **never gated** — the
|
|
62
171
|
* framework returns at `runtime-authoring-gate.ts`'s D1 early-return
|
|
@@ -65,15 +174,65 @@ interface MetadataSaveAdvisoryEvent {
|
|
|
65
174
|
* consumer reading the event can tell which door it came through.
|
|
66
175
|
*/
|
|
67
176
|
mode: 'draft' | 'publish';
|
|
177
|
+
/**
|
|
178
|
+
* Which write door produced this event (#5026).
|
|
179
|
+
*
|
|
180
|
+
* - `'save'` — `PUT /meta/:type/:name` ({@link MetadataClient.save}, and the
|
|
181
|
+
* SDK's `meta.saveItem` behind `ObjectStackAdapter`).
|
|
182
|
+
* - `'publish'` — the promotion doors: `POST /meta/:type/:name/publish`
|
|
183
|
+
* ({@link MetadataClient.publish} and {@link MetadataClient.publishDraft}),
|
|
184
|
+
* and the batch `POST /packages/:id/publish-drafts`
|
|
185
|
+
* ({@link MetadataClient.publishPackageDrafts}), which emits ONE event per
|
|
186
|
+
* `published[]` element carrying that element's own `type` / `name`.
|
|
187
|
+
*
|
|
188
|
+
* ## Why the batch route is not a third value (`ce986aafc`)
|
|
189
|
+
*
|
|
190
|
+
* The batch door promotes drafts to active — the author pressed Publish, and
|
|
191
|
+
* every item this event names really was published. What `door` decides is
|
|
192
|
+
* the frame's VERB, and "Published" is the true one for all three routes; a
|
|
193
|
+
* third value would have to render the same word. The per-item identity the
|
|
194
|
+
* author needs to act on is already carried by {@link type} / {@link name},
|
|
195
|
+
* one event per item, so nothing about the batch is lost by sharing the
|
|
196
|
+
* value. ⛔ It is NOT a claim that "one call = one event": the batch emits as
|
|
197
|
+
* many events as it has advised items, which is why each one names its own.
|
|
198
|
+
*
|
|
199
|
+
* Distinct from {@link mode}, which cannot answer this: a direct active save
|
|
200
|
+
* and a draft promotion both report `mode: 'publish'` because both land the
|
|
201
|
+
* body in the active overlay. The renderer needs the DOOR, because the author
|
|
202
|
+
* pressed either Save or Publish and a toast that says "Saved" after a
|
|
203
|
+
* Publish tells them their change is still a draft — the opposite of what
|
|
204
|
+
* happened, and in this product Save and Publish are two different buttons
|
|
205
|
+
* with two different meanings.
|
|
206
|
+
*
|
|
207
|
+
* REQUIRED rather than optional-with-a-default so a future third door cannot
|
|
208
|
+
* be wired without stating which one it is: an omitted discriminator would
|
|
209
|
+
* silently render the save wording.
|
|
210
|
+
*/
|
|
211
|
+
door: 'save' | 'publish';
|
|
68
212
|
/** The findings. Never empty — the event is not emitted otherwise. */
|
|
69
213
|
advisories: RuntimeAuthoringIssue[];
|
|
70
214
|
}
|
|
71
215
|
/** Event listener type for save-advisory events. */
|
|
72
216
|
type MetadataSaveAdvisoryListener = (event: MetadataSaveAdvisoryEvent) => void;
|
|
73
217
|
/**
|
|
74
|
-
* Read the `advisories` array off a
|
|
218
|
+
* Read the `advisories` array off a metadata write response, defensively.
|
|
75
219
|
*
|
|
76
|
-
*
|
|
220
|
+
* Serves EVERY write door unchanged (#5026, `ce986aafc`):
|
|
221
|
+
* `SaveMetaItemResponseSchema` and `PublishMetaItemResponseSchema` declare the
|
|
222
|
+
* key at the same top level, and `PublishPackageDraftsResponseSchema` declares
|
|
223
|
+
* it on each `published[]` element — under the same name, with the same element
|
|
224
|
+
* schema. So there is one reader, not a per-door copy that could drift; the
|
|
225
|
+
* batch caller hands it one element, which is the object that carries the key
|
|
226
|
+
* there.
|
|
227
|
+
*
|
|
228
|
+
* ⛔ It reads the `advisories` key of the object it is GIVEN and never
|
|
229
|
+
* traverses. Choosing the object — a response body at the single-item doors,
|
|
230
|
+
* one `published[]` element at the batch door — is the caller's job precisely
|
|
231
|
+
* because the place is declared per route: a reader that went hunting for
|
|
232
|
+
* findings wherever they might be would be inventing a contract the server
|
|
233
|
+
* never stated.
|
|
234
|
+
*
|
|
235
|
+
* The server omits the key entirely on a clean write, so `undefined` is the
|
|
77
236
|
* common case and means "nothing to say". Anything that is not an array of
|
|
78
237
|
* objects carrying the six required keys is dropped rather than rendered: a
|
|
79
238
|
* half-shaped finding would print blanks at the author, and this channel must
|
|
@@ -105,9 +264,13 @@ interface MetadataClientConfig {
|
|
|
105
264
|
previewDrafts?: boolean;
|
|
106
265
|
/**
|
|
107
266
|
* Called after a {@link MetadataClient.save} whose 2xx response carried a
|
|
108
|
-
* non-empty `advisories` array (objectstack#7435).
|
|
109
|
-
*
|
|
110
|
-
*
|
|
267
|
+
* non-empty `advisories` array (objectstack#7435). Since #5026 the SAME sink
|
|
268
|
+
* also receives the publish door's findings (objectstack#9176), and since
|
|
269
|
+
* `ce986aafc` the batch publish door's too, one event per advised
|
|
270
|
+
* `published[]` element (objectstack#9343) — read `event.door` to tell a
|
|
271
|
+
* save from a promotion, and `event.type` / `event.name` for which item. The
|
|
272
|
+
* write already succeeded; this is how the shell learns there is something to
|
|
273
|
+
* tell the author instead of the findings being discarded client-side.
|
|
111
274
|
*
|
|
112
275
|
* Set on the CONFIG rather than exposed as a `subscribe()` method on purpose:
|
|
113
276
|
* console metadata clients are minted per-component by `useMetadataClient`,
|
|
@@ -124,18 +287,92 @@ interface MetadataListOptions {
|
|
|
124
287
|
/** Filter by source package id (matches the `package` query param). */
|
|
125
288
|
packageId?: string;
|
|
126
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* A list read as {@link MetadataClient.listResponse} returns it: the items, and
|
|
292
|
+
* what the server serves BESIDE them, typed by the spec's
|
|
293
|
+
* `GetMetaItemsResponse` rather than restated (its `type` key is not kept).
|
|
294
|
+
*
|
|
295
|
+
* `registrations` is the automation engine's verdict on each listed flow,
|
|
296
|
+
* keyed by the item's `name` (`FlowRegistrationSchema`, objectstack#23316):
|
|
297
|
+
* `registered`, `refused` with the engine's `reason` verbatim, or
|
|
298
|
+
* `unregistered`. It is absent when the server sent none — a type other than
|
|
299
|
+
* `flow`, a server with no automation service, or one that predates the key —
|
|
300
|
+
* and absence means no engine judged the flow, ⛔ never `registered`. A name
|
|
301
|
+
* the map has no entry for is unjudged in the same way.
|
|
302
|
+
*/
|
|
303
|
+
type MetadataListAnswer<T = unknown> = {
|
|
304
|
+
items: T[];
|
|
305
|
+
} & Pick<GetMetaItemsResponse, 'registrations'>;
|
|
127
306
|
/**
|
|
128
307
|
* A pending DRAFT metadata item (ADR-0033), as returned by {@link MetadataClient.listDrafts}.
|
|
129
308
|
* Light header — no body — carrying the owning package so the console can group
|
|
130
309
|
* pending changes by app package.
|
|
310
|
+
*
|
|
311
|
+
* DERIVED from the spec's row, `ListDraftsResponseSchema.drafts[]`, rather than
|
|
312
|
+
* re-spelled (objectui#12413): the key names and their types come from the
|
|
313
|
+
* spec symbol, so a key the spec renames or retypes fails the type-check here
|
|
314
|
+
* instead of drifting past it.
|
|
315
|
+
*
|
|
316
|
+
* - `member` — the bundle member this draft is (objectstack#23073): present on
|
|
317
|
+
* every draft of a type whose identity is a pair (today only
|
|
318
|
+
* `email_template`, `(name, locale)`), absent for a type identified by its
|
|
319
|
+
* name alone. It is the value the by-name doors take back as `?member=`
|
|
320
|
+
* ({@link MetadataClient.publishDraft}'s `options.member`).
|
|
321
|
+
*
|
|
322
|
+
* ⛔ The pick is deliberate: the five keys this interface declared before keep
|
|
323
|
+
* their exact types, and `member` is the one key added. The spec row's `label`
|
|
324
|
+
* is not picked here: it is declared REQUIRED there, and this client talks to
|
|
325
|
+
* runtimes of several vintages, so claiming it would be this client asserting
|
|
326
|
+
* a server version it cannot check.
|
|
131
327
|
*/
|
|
132
|
-
interface MetadataDraftHeader {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
packageId: string | null;
|
|
136
|
-
updatedAt: string | null;
|
|
137
|
-
updatedBy: string | null;
|
|
328
|
+
interface MetadataDraftHeader extends Pick<ListDraftsRow, 'type' | 'name' | 'packageId' | 'updatedAt' | 'updatedBy'> {
|
|
329
|
+
/** The bundle member, typed by the spec row's own `member` (see above). */
|
|
330
|
+
member?: ListDraftsRow['member'];
|
|
138
331
|
}
|
|
332
|
+
/** One element of `ListDraftsResponseSchema.drafts`, the spec row `MetadataDraftHeader` derives from. */
|
|
333
|
+
type ListDraftsRow = ListDraftsResponse['drafts'][number];
|
|
334
|
+
/**
|
|
335
|
+
* What {@link MetadataClient.publishPackageDrafts} resolves with — the "publish
|
|
336
|
+
* whole app" body, DERIVED from `PublishPackageDraftsResponseSchema` rather
|
|
337
|
+
* than re-spelled (`ce986aafc`).
|
|
338
|
+
*
|
|
339
|
+
* Derived, and then widened in exactly two ways, each for a measured reason:
|
|
340
|
+
*
|
|
341
|
+
* - `Partial<…>` because this client talks to runtimes of several vintages and
|
|
342
|
+
* the spec's six required keys are what TODAY's producer sets. Declaring them
|
|
343
|
+
* required would be this client asserting a server version it cannot check —
|
|
344
|
+
* and an older runtime that omits one would be a type lie, not a compile
|
|
345
|
+
* error. Every consumer here already reads these keys defensively.
|
|
346
|
+
* - The index signature because the REST door adds keys on the way out
|
|
347
|
+
* (ADR-0045 visibility receipts, the `metadata:reloaded` announce receipt),
|
|
348
|
+
* and because a body this client hands back must stay readable by a caller
|
|
349
|
+
* that knows about a key this package has never heard of.
|
|
350
|
+
*
|
|
351
|
+
* ⛔ What it is NOT is a second definition of the batch response: the key names
|
|
352
|
+
* and element shapes come from the spec symbol, so a key the spec adds, renames
|
|
353
|
+
* or retypes arrives here with no edit.
|
|
354
|
+
*/
|
|
355
|
+
type MetadataPublishPackageDraftsResult = Partial<PublishPackageDraftsResponse> & Record<string, unknown>;
|
|
356
|
+
/**
|
|
357
|
+
* What {@link MetadataClient.publishPackageDrafts} may ask of the batch door
|
|
358
|
+
* beyond the package id (objectui#12929): today the protocol's opt-in
|
|
359
|
+
* `backfill` (objectstack#24253), and nothing else.
|
|
360
|
+
*
|
|
361
|
+
* Picked off `@objectstack/client`'s own `packages.publishDrafts` signature
|
|
362
|
+
* rather than re-spelled, so the element shape (`{ object, field }`) is the
|
|
363
|
+
* client's declaration. The request has no spec schema of its own; the
|
|
364
|
+
* client's signature is where the protocol declares it. `actor` is not
|
|
365
|
+
* picked: the door writes as the signed-in caller, and no console surface
|
|
366
|
+
* names another.
|
|
367
|
+
*
|
|
368
|
+
* `backfill` names fields this publish ADDS whose literal `defaultValue` the
|
|
369
|
+
* server writes into the object's existing rows, as the caller, after the
|
|
370
|
+
* batch goes live; the receipt comes back as `backfillApplied`. A field that
|
|
371
|
+
* is not new, or declares no literal default, refuses the WHOLE request with a
|
|
372
|
+
* 400 before anything is published, so a caller offers only fields it has
|
|
373
|
+
* judged by the same rule.
|
|
374
|
+
*/
|
|
375
|
+
type MetadataPublishPackageDraftsOptions = Pick<NonNullable<Parameters<ObjectStackClient['packages']['publishDrafts']>[1]>, 'backfill'>;
|
|
139
376
|
/**
|
|
140
377
|
* Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
|
|
141
378
|
* `/api/v1/meta/:type/:name`.
|
|
@@ -146,17 +383,41 @@ interface MetadataDraftHeader {
|
|
|
146
383
|
* metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
|
|
147
384
|
* `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
|
|
148
385
|
* exists here, and not one of these exists there: this is the REST client's
|
|
149
|
-
* request envelope — optimistic concurrency (`ifMatch` → `If-Match
|
|
150
|
-
*
|
|
151
|
-
* owning package. Same words, different
|
|
386
|
+
* request envelope — optimistic concurrency (`ifMatch` → `If-Match`,
|
|
387
|
+
* `ifNoneMatch` → `If-None-Match`), the destructive-change override, the
|
|
388
|
+
* ADR-0033 draft/publish mode, and the owning package. Same words, different
|
|
389
|
+
* layer.
|
|
152
390
|
*/
|
|
153
391
|
interface MetadataClientSaveOptions {
|
|
154
392
|
/**
|
|
155
|
-
* Optimistic concurrency
|
|
156
|
-
*
|
|
157
|
-
*
|
|
393
|
+
* Optimistic concurrency (ADR-0008): the `version` token of the stored row
|
|
394
|
+
* this write was built on, sent as the `If-Match` header, so a write over a
|
|
395
|
+
* row that has moved since is refused `409 METADATA_CONFLICT` instead of
|
|
396
|
+
* overwriting it.
|
|
397
|
+
*
|
|
398
|
+
* The `/meta` doors serve that token in three places, each naming the row a
|
|
399
|
+
* save to the SAME address (type, name, `?package=`, lifecycle) compares
|
|
400
|
+
* against: the item read's `version` (`?state=draft` for the draft row,
|
|
401
|
+
* declared on `GetMetaItemResponseSchema`), a save receipt's `version`, and
|
|
402
|
+
* a version conflict's `currentVersion` (`MetadataConflictErrorSchema`). A
|
|
403
|
+
* served `null` means no such row: pin with {@link ifNoneMatch} instead.
|
|
404
|
+
*
|
|
405
|
+
* ⛔ Never together with {@link ifNoneMatch}: {@link MetadataClient.save}
|
|
406
|
+
* throws before sending a request that carries both.
|
|
158
407
|
*/
|
|
159
408
|
ifMatch?: string;
|
|
409
|
+
/**
|
|
410
|
+
* The first-write pin, sent as `If-None-Match: *`: write only where no row of
|
|
411
|
+
* this write's lifecycle exists at its address (the draft row, for
|
|
412
|
+
* `mode: 'draft'`). A create sends it, and so does the first draft save of an
|
|
413
|
+
* item with no pending draft; the door refuses it `409 METADATA_CONFLICT`,
|
|
414
|
+
* carrying the existing row's `currentVersion`, when such a row is there.
|
|
415
|
+
*
|
|
416
|
+
* `'*'` alone: the door reads no other value. ⛔ Never together with
|
|
417
|
+
* {@link ifMatch} — the door refuses that pair `400`, and
|
|
418
|
+
* {@link MetadataClient.save} throws before sending it.
|
|
419
|
+
*/
|
|
420
|
+
ifNoneMatch?: '*';
|
|
160
421
|
/**
|
|
161
422
|
* Bypass destructive-change protection (Phase 3a). The server returns
|
|
162
423
|
* `409 destructive_change` with `issues[]` if a write would drop or
|
|
@@ -177,6 +438,19 @@ interface MetadataClientSaveOptions {
|
|
|
177
438
|
* Studio package workspace. Omit for an env-local overlay.
|
|
178
439
|
*/
|
|
179
440
|
packageId?: string;
|
|
441
|
+
/**
|
|
442
|
+
* The authoring surface this save is made through (objectui#12725), sent as
|
|
443
|
+
* `?surface=`. A draft save stamps it as `by` on every key path it changes,
|
|
444
|
+
* the entries the strict draft read serves as `draftChanges`; absent, the
|
|
445
|
+
* framework stamps `api`. Studio's own saves send `'studio'`, so a person's
|
|
446
|
+
* edit there is told apart from the AI copilot's (objectstack#24395).
|
|
447
|
+
*
|
|
448
|
+
* DERIVED from the spec's authoring-surface set (the one `draftChanges[].by`
|
|
449
|
+
* reports, `MetadataAuthoringSurface`), minus `'copilot'`: the HTTP doors
|
|
450
|
+
* refuse `?surface=copilot` `400` (only an in-process tool may state it), so
|
|
451
|
+
* this client cannot send it. Omit for any other caller.
|
|
452
|
+
*/
|
|
453
|
+
surface?: Exclude<DraftChange['by'], 'copilot'>;
|
|
180
454
|
}
|
|
181
455
|
interface MetadataGetOptions {
|
|
182
456
|
/**
|
|
@@ -193,7 +467,7 @@ interface MetadataGetOptions {
|
|
|
193
467
|
*/
|
|
194
468
|
packageId?: string;
|
|
195
469
|
}
|
|
196
|
-
interface MetadataDeleteOptions extends MetadataClientSaveOptions {
|
|
470
|
+
interface MetadataDeleteOptions extends Omit<MetadataClientSaveOptions, 'surface'> {
|
|
197
471
|
/**
|
|
198
472
|
* Target state. `'draft'` discards the pending draft (keeps the
|
|
199
473
|
* published overlay intact). Omit to reset the active overlay back
|
|
@@ -202,12 +476,13 @@ interface MetadataDeleteOptions extends MetadataClientSaveOptions {
|
|
|
202
476
|
state?: 'active' | 'draft';
|
|
203
477
|
}
|
|
204
478
|
/**
|
|
205
|
-
* Which layer an overlay was saved at — `'
|
|
206
|
-
*
|
|
479
|
+
* Which layer an overlay was saved at — `'env'` for an environment-level one,
|
|
480
|
+
* `null` exactly when `overlay` is null. Every metadata read is environment →
|
|
481
|
+
* code since objectstack#15206 S5, which retired the `'org'` tenant scope.
|
|
207
482
|
*
|
|
208
483
|
* DERIVED from `@objectstack/spec`, not restated: the vocabulary lives once, in
|
|
209
484
|
* `GetMetaItemLayeredResponseSchema`'s `overlayScope`
|
|
210
|
-
* (`z.enum(['
|
|
485
|
+
* (`z.enum(['env']).nullable()`), and this alias indexes the published
|
|
211
486
|
* response type so a scope the spec adds arrives here with no edit. Restating
|
|
212
487
|
* the union locally is the fork `scripts/check-spec-symbol-derivation.mjs`
|
|
213
488
|
* exists to reject, and the mirror-image mistake — a consumer re-spelling a
|
|
@@ -246,9 +521,13 @@ type MetadataOverlayScope = GetMetaItemLayeredResponse['overlayScope'];
|
|
|
246
521
|
* the cross-repo half of the drift, not just the in-repo half.
|
|
247
522
|
*
|
|
248
523
|
* ⚠️ This types what this repo may WRITE. It does NOT constrain what a server
|
|
249
|
-
* may SEND
|
|
250
|
-
*
|
|
251
|
-
*
|
|
524
|
+
* may SEND, and it never will — {@link MetadataClient.layered} forwards the
|
|
525
|
+
* wire value rather than dropping it, so every reader must still handle a value
|
|
526
|
+
* outside these four. What changed in objectui#5676 is that the value is no
|
|
527
|
+
* longer forwarded *silently*: the boundary validates it against the producer's
|
|
528
|
+
* own schema and names the field in {@link MetadataLayered._unrecognized} when
|
|
529
|
+
* it does not match. The lock banner in `ResourceEditPage` is the worked
|
|
530
|
+
* example of a reader that labels such a value instead of rendering it blank.
|
|
252
531
|
*/
|
|
253
532
|
type MetadataLockState = GetMetaItemLayeredResponse['lock'];
|
|
254
533
|
/**
|
|
@@ -299,6 +578,21 @@ interface MetadataLayered<T = unknown> {
|
|
|
299
578
|
deletable?: boolean;
|
|
300
579
|
/** True when "Reset to package default" applies (has overlay + artifact). */
|
|
301
580
|
resettable?: boolean;
|
|
581
|
+
/**
|
|
582
|
+
* Protection-envelope keys whose wire value did NOT match `packages/spec`'s
|
|
583
|
+
* schema for them (objectui#5676). Computed HERE, at the boundary — the
|
|
584
|
+
* server never sends this key, and {@link MetadataClient.layered} builds the
|
|
585
|
+
* returned object field by field, so a server that tried could not smuggle
|
|
586
|
+
* one in.
|
|
587
|
+
*
|
|
588
|
+
* Absent when everything the server sent parsed, so its presence is the
|
|
589
|
+
* signal. The offending values are still forwarded on the fields themselves:
|
|
590
|
+
* this is "pass through and label", the treatment objectui#5672 chose for
|
|
591
|
+
* `lock` alone, applied to the whole ADR-0010 envelope. Dropping them would
|
|
592
|
+
* be the reject semantics that card refused — turning a wrong render into no
|
|
593
|
+
* render is a behaviour change for every consumer of this client.
|
|
594
|
+
*/
|
|
595
|
+
_unrecognized?: readonly string[];
|
|
302
596
|
}
|
|
303
597
|
/**
|
|
304
598
|
* One row in the metadata protection-audit trail
|
|
@@ -443,7 +737,7 @@ declare class MetadataClient {
|
|
|
443
737
|
private readonly headers;
|
|
444
738
|
/** ADR-0037: when true, reads render the draft-overlaid world. */
|
|
445
739
|
readonly previewDrafts: boolean;
|
|
446
|
-
/** #4133 — sink for
|
|
740
|
+
/** #4133 / #5026 — sink for every write door's advisory findings; see the config field. */
|
|
447
741
|
private readonly onSaveAdvisory;
|
|
448
742
|
constructor(config: MetadataClientConfig);
|
|
449
743
|
/** Update the client's environment scope at runtime. */
|
|
@@ -454,10 +748,51 @@ declare class MetadataClient {
|
|
|
454
748
|
* renderer tree into ADR-0037 preview mode off one URL flag.
|
|
455
749
|
*/
|
|
456
750
|
withPreviewDrafts(previewDrafts: boolean): MetadataClient;
|
|
751
|
+
/**
|
|
752
|
+
* Share one in-flight GET among the callers asking the same method for the
|
|
753
|
+
* same URL with the same headers through the same transport (objectui#11797).
|
|
754
|
+
*
|
|
755
|
+
* Opening a Studio package mounts several readers that ask the same
|
|
756
|
+
* question at once, and each one sent its own request. The first call for a
|
|
757
|
+
* key sends `read()`; a call that arrives while it is pending waits for that
|
|
758
|
+
* request instead of sending another.
|
|
759
|
+
*
|
|
760
|
+
* ⛔ Not a response cache, deliberately — the rules `findOne` keeps on the
|
|
761
|
+
* data adapter (objectui#11699):
|
|
762
|
+
* - the entry goes the moment the read settles, so a call made after an
|
|
763
|
+
* answer always reaches the network (there is no reuse window: a client
|
|
764
|
+
* cannot see every write that changes metadata, an agent's server-side
|
|
765
|
+
* edits among them, and a window would answer the read after such a write
|
|
766
|
+
* with the state from before it);
|
|
767
|
+
* - a rejection reaches every caller already waiting and is not remembered;
|
|
768
|
+
* - a write sent through the same transport drops every pending read when
|
|
769
|
+
* it lands (see {@link sendWrite}), so a read asked after a save or a
|
|
770
|
+
* publish is never answered by a request sent before it;
|
|
771
|
+
* - every caller, the first one included, gets an answer no other caller
|
|
772
|
+
* holds (see {@link handOut}).
|
|
773
|
+
*/
|
|
774
|
+
private shareRead;
|
|
775
|
+
/**
|
|
776
|
+
* Send a write, then drop every read still pending on this transport
|
|
777
|
+
* (objectui#11797): a read on the wire when the write lands was asked before
|
|
778
|
+
* it, so a caller asking after the write must not join it. Dropped however
|
|
779
|
+
* the write ends — a refused or interrupted write may still have changed
|
|
780
|
+
* something. The callers already holding a pending read keep its answer.
|
|
781
|
+
*/
|
|
782
|
+
private sendWrite;
|
|
457
783
|
/** List all registered metadata types (returns the registry rows). */
|
|
458
784
|
listTypes(): Promise<unknown[]>;
|
|
459
785
|
/** List items of a metadata type (e.g. `object`, `field`, `view`). */
|
|
460
786
|
list<T = unknown>(type: string, options?: MetadataListOptions): Promise<T[]>;
|
|
787
|
+
/**
|
|
788
|
+
* The same list read as {@link list}, with what the server serves beside
|
|
789
|
+
* the items (objectui#12349): today that is the automation engine's
|
|
790
|
+
* `registrations` on a `flow` list — see {@link MetadataListAnswer}.
|
|
791
|
+
*
|
|
792
|
+
* One request for both methods: `list` is this read with the items picked
|
|
793
|
+
* out, so a caller asking both at once shares one in-flight GET.
|
|
794
|
+
*/
|
|
795
|
+
listResponse<T = unknown>(type: string, options?: MetadataListOptions): Promise<MetadataListAnswer<T>>;
|
|
461
796
|
/**
|
|
462
797
|
* List pending DRAFT items (ADR-0033) — what an AI authored but nobody
|
|
463
798
|
* published yet. `list()` only sees published/active metadata, so a
|
|
@@ -517,12 +852,71 @@ declare class MetadataClient {
|
|
|
517
852
|
* {@link get} is deliberate and long-standing (the draft envelope's identity
|
|
518
853
|
* and protection carriers are part of what a draft reader inspects), so it
|
|
519
854
|
* is preserved by reading the transport directly instead of going through
|
|
520
|
-
* `get()`'s unwrap.
|
|
521
|
-
*
|
|
855
|
+
* `get()`'s unwrap.
|
|
856
|
+
*
|
|
857
|
+
* ⛔ Do NOT hand `.item` to a gate or a write yourself: the body this serves
|
|
858
|
+
* is DECORATED (`_draft`, then `_diagnostics` from `decorateMetadataItem`),
|
|
859
|
+
* and the spec says a served body is not a valid input to the schema that
|
|
860
|
+
* produced it until those come off. {@link extractDraftBody} is the one
|
|
861
|
+
* reader for this method's answer — it takes the presence verdict and then
|
|
862
|
+
* the strip, in that order. That function used to be copy-pasted into four
|
|
863
|
+
* views and only one copy knew the rule (objectui#8181).
|
|
864
|
+
*
|
|
865
|
+
* Its tolerant siblings exist for the OTHER client method: `unwrapDraftBody`
|
|
866
|
+
* (app-shell) and `unwrapViewDraft` (this package) read a draft that arrived
|
|
867
|
+
* through {@link get}, which already unwrapped the envelope. They split on
|
|
868
|
+
* which method feeds them, not on what they mean — all three strip.
|
|
869
|
+
*
|
|
870
|
+
* objectui#11799 — whether a draft exists is answered by the pending-drafts
|
|
871
|
+
* ledger first (see {@link draftLedgerMayHold}), and `?state=draft` is sent
|
|
872
|
+
* only when the ledger lists the name. The item read answers 404 when there
|
|
873
|
+
* is no draft, so opening every item in Studio used to log one error per
|
|
874
|
+
* item for an answer the ledger gives with a 200.
|
|
522
875
|
*/
|
|
523
876
|
getDraft<T = unknown>(type: string, name: string, options?: {
|
|
524
877
|
packageId?: string;
|
|
525
878
|
}): Promise<T | null>;
|
|
879
|
+
/**
|
|
880
|
+
* Whether the pending-drafts ledger (`GET /meta/_drafts`) may hold a draft
|
|
881
|
+
* named `name` (objectui#11799).
|
|
882
|
+
*
|
|
883
|
+
* Read through {@link listDrafts}, the shared read objectui#11797 made: a
|
|
884
|
+
* question asked while a ledger read is pending waits for it, and a write
|
|
885
|
+
* through this transport drops it when the write lands ({@link sendWrite}),
|
|
886
|
+
* so a draft saved a moment ago is never reported absent by a read sent
|
|
887
|
+
* before the save.
|
|
888
|
+
*
|
|
889
|
+
* The WHOLE ledger, matched by name. The `?state=draft` read finds a row
|
|
890
|
+
* under either spelling of the type, and, when it names a package, that
|
|
891
|
+
* package's row or the package-less one. The ledger's `type` and `packageId`
|
|
892
|
+
* filters are exact, so a filtered ledger could miss a draft the item read
|
|
893
|
+
* would serve, and the editor would open the published body over a pending
|
|
894
|
+
* draft. Unfiltered, every miss here is a real absence; a same-name row of
|
|
895
|
+
* another type or package only costs the item read that was always sent.
|
|
896
|
+
*
|
|
897
|
+
* A ledger that cannot be read (403 without an authoring capability, 501 on
|
|
898
|
+
* a kernel without drafts, a network fault) is UNKNOWN, not empty: the item
|
|
899
|
+
* read is sent, as before.
|
|
900
|
+
*/
|
|
901
|
+
private draftLedgerMayHold;
|
|
902
|
+
/**
|
|
903
|
+
* Report the runtime authoring gate's advisory findings for a write the
|
|
904
|
+
* server has ALREADY committed (#4133 for the save door, #5026 for the
|
|
905
|
+
* publish door).
|
|
906
|
+
*
|
|
907
|
+
* Everything here is best-effort by construction, and that is the whole
|
|
908
|
+
* contract: the row is committed server-side before this runs, so neither a
|
|
909
|
+
* malformed body, nor a missing key, nor a throwing sink may change what the
|
|
910
|
+
* calling method returns or whether it throws. The server emits `advisories`
|
|
911
|
+
* ONLY when non-empty, so a clean write costs one absent-key check.
|
|
912
|
+
*
|
|
913
|
+
* One helper rather than a copy per door: every door's response declares the
|
|
914
|
+
* key identically, so a second inline copy could only ever drift. The batch
|
|
915
|
+
* door (`ce986aafc`) calls it once per `published[]` element, handing it
|
|
916
|
+
* the element — the object that carries the key there — and the same
|
|
917
|
+
* best-effort contract covers that loop unchanged.
|
|
918
|
+
*/
|
|
919
|
+
private emitAdvisories;
|
|
526
920
|
/**
|
|
527
921
|
* Save (PUT) a metadata item. The framework accepts both the bare
|
|
528
922
|
* item payload and the `{ item: ... }` / `{ metadata: ... }`
|
|
@@ -541,8 +935,77 @@ declare class MetadataClient {
|
|
|
541
935
|
* the rows and reports under `seedApplied` — a data problem never fails the
|
|
542
936
|
* publish, so callers should check `seedApplied?.success` and warn the user
|
|
543
937
|
* rather than assume the data went live.
|
|
938
|
+
*
|
|
939
|
+
* Same door as {@link publish} (`POST /meta/:type/:name/publish`), so it
|
|
940
|
+
* reports the gate's advisories the same way (#5026).
|
|
941
|
+
*
|
|
942
|
+
* The BATCH door (`POST /packages/:id/publish-drafts`) is a different route
|
|
943
|
+
* with a method of its own — {@link publishPackageDrafts} — and it reports
|
|
944
|
+
* too. It did not when this paragraph was first written: it discarded
|
|
945
|
+
* per-draft advisories server-side, which was objectstack#9343, and that card
|
|
946
|
+
* has since landed with a ruling that each `published[]` element carries
|
|
947
|
+
* them (`ce986aafc` is the client half).
|
|
948
|
+
*
|
|
949
|
+
* What survives that change is the READING RULE, which is about this method
|
|
950
|
+
* rather than about the other route: it reads the top level of the
|
|
951
|
+
* single-item body and nothing else. A batch-shaped body arriving HERE still
|
|
952
|
+
* reports nothing, because `PublishMetaItemResponse` declares no
|
|
953
|
+
* `published[]` and a client that went looking for findings wherever they
|
|
954
|
+
* might be would be inventing a contract instead of reading one.
|
|
955
|
+
*
|
|
956
|
+
* ## Why there is no `{ success, data }` unwrapping here (`7b433197d`)
|
|
957
|
+
*
|
|
958
|
+
* This method used to unwrap a dispatcher-shaped envelope before returning,
|
|
959
|
+
* while {@link publish} — the same route, a couple of hundred lines down —
|
|
960
|
+
* did not. Two spellings of one wire contract, and nothing said which was
|
|
961
|
+
* right. The tolerance is gone because the route was MEASURED, at the
|
|
962
|
+
* producer rather than inferred from the response schema:
|
|
963
|
+
*
|
|
964
|
+
* - The only mount of `POST /api/v1/meta/:type/:name/publish` is the REST
|
|
965
|
+
* server's own route-manager registration, which answers
|
|
966
|
+
* `res.json(await protocol.publishMetaItem(...))` — the protocol's object
|
|
967
|
+
* verbatim, with no envelope branch. The project-scoped mirror
|
|
968
|
+
* (`/api/v1/environments/:environmentId/...`) re-mounts that same handler.
|
|
969
|
+
* - The `{ success, data }` envelope has exactly one producer, the runtime
|
|
970
|
+
* `HttpDispatcher`, and it does not serve this route: its `/meta` domain
|
|
971
|
+
* has no publish branch, the dispatcher's own route ledger has no row for
|
|
972
|
+
* it, and a three-segment `/meta` path that is not `/published` terminates
|
|
973
|
+
* in a located `routeNotFound`. So no composition — REST server, the
|
|
974
|
+
* dispatcher-only Hono adapter, or both — puts an envelope on this body.
|
|
975
|
+
* - `packages/spec` states the same split in the two docstrings side by
|
|
976
|
+
* side: `PublishMetaItemResponseSchema` describes "the FULL body" this
|
|
977
|
+
* route returns and records that the REST route hands the producer's
|
|
978
|
+
* object to `res.json()` verbatim, while its batch sibling
|
|
979
|
+
* `PublishPackageDraftsResponseSchema` describes a body answered
|
|
980
|
+
* explicitly "inside the dispatcher's `{ success, data }` envelope".
|
|
981
|
+
*
|
|
982
|
+
* ⛔ So an enveloped body on THIS route is not a shape the platform emits,
|
|
983
|
+
* and unwrapping one is not a kindness: it is Commandment #0.1's lenient
|
|
984
|
+
* fallback, a second de-facto contract for a route that declares one. It
|
|
985
|
+
* also fails in the direction that hides the problem — a body that arrived
|
|
986
|
+
* enveloped is one this door did not actually serve, and unwrapping it
|
|
987
|
+
* would present that as a successful promotion.
|
|
988
|
+
*
|
|
989
|
+
* ⚠️ Sibling tolerances in this file are NOT this defect and must stay:
|
|
990
|
+
* {@link listDrafts} reads `data?.data?.drafts` because `GET /meta/_drafts`
|
|
991
|
+
* IS a dispatcher route, and `publishHealthFromResponse` unwraps because the
|
|
992
|
+
* batch door really is enveloped. The rule is per-route, not per-file.
|
|
993
|
+
*
|
|
994
|
+
* ## The bundle member (objectui#12402)
|
|
995
|
+
*
|
|
996
|
+
* Pass `member` to publish one member of a type whose identity is a pair
|
|
997
|
+
* (today only `email_template`, `(name, locale)`), sent as `?member=` — the
|
|
998
|
+
* selector the by-name doors read since objectstack `038d4c958`, and the
|
|
999
|
+
* value a `GET /meta/_drafts` row carries as `member`. A publish that names
|
|
1000
|
+
* none promotes the CANONICAL member's draft, so two languages of one
|
|
1001
|
+
* template drafted outside a package cannot both be published by name
|
|
1002
|
+
* without it. The parameter is OMITTED, never sent empty, when the caller
|
|
1003
|
+
* has no member: the door refuses a blank selector, and refuses any
|
|
1004
|
+
* selector on a type with no members.
|
|
544
1005
|
*/
|
|
545
|
-
publishDraft(type: string, name: string
|
|
1006
|
+
publishDraft(type: string, name: string, options?: {
|
|
1007
|
+
member?: string;
|
|
1008
|
+
}): Promise<{
|
|
546
1009
|
success?: boolean;
|
|
547
1010
|
seedApplied?: {
|
|
548
1011
|
success: boolean;
|
|
@@ -552,6 +1015,63 @@ declare class MetadataClient {
|
|
|
552
1015
|
errors?: unknown[];
|
|
553
1016
|
};
|
|
554
1017
|
} & Record<string, unknown>>;
|
|
1018
|
+
/**
|
|
1019
|
+
* Publish EVERY pending draft bound to one package — Studio's "publish whole
|
|
1020
|
+
* app" door, `POST /api/v1/packages/:id/publish-drafts` (ADR-0033; the route
|
|
1021
|
+
* that orders structure-before-seeds server-side and runs the ADR-0038 L3
|
|
1022
|
+
* runtime probes).
|
|
1023
|
+
*
|
|
1024
|
+
* ## Why it is expressed here at all (`ce986aafc`)
|
|
1025
|
+
*
|
|
1026
|
+
* It was not, and that was the defect. Two app-shell call sites fired this
|
|
1027
|
+
* route with a bare `fetch` / a page-private `apiJson`, outside the seam that
|
|
1028
|
+
* covers every other metadata write — so when objectstack#9343 landed and the
|
|
1029
|
+
* server began sending per-draft advisories, the author publishing a whole
|
|
1030
|
+
* app was told nothing, while the SAME button's client-side capability lint
|
|
1031
|
+
* still raised a toast. A door that cannot report is not a door the gate can
|
|
1032
|
+
* reach, however loudly the server speaks.
|
|
1033
|
+
*
|
|
1034
|
+
* ## What it reports, and what it refuses to invent
|
|
1035
|
+
*
|
|
1036
|
+
* `PublishPackageDraftsResponseSchema` declares `advisories` on EACH
|
|
1037
|
+
* `published[]` element — objectstack#9343's ruled shape, the same element
|
|
1038
|
+
* type and the same omitted-when-empty discipline as the single-item door,
|
|
1039
|
+
* and explicitly not a parallel top-level map. So this method emits one
|
|
1040
|
+
* {@link MetadataSaveAdvisoryEvent} per advised element, each naming that
|
|
1041
|
+
* element's own `type` / `name`, through the same sink, event and renderer
|
|
1042
|
+
* the save and single-item publish doors use.
|
|
1043
|
+
*
|
|
1044
|
+
* ⛔ It renders only what the server sent where the schema says it sits:
|
|
1045
|
+
*
|
|
1046
|
+
* - `advisories` is read off the element, by the one shared reader, which
|
|
1047
|
+
* drops anything that is not a complete finding.
|
|
1048
|
+
* - An element that does not carry the `type` and `name` the schema states as
|
|
1049
|
+
* REQUIRED is skipped rather than reported under invented identity — the
|
|
1050
|
+
* event names the item the author has to go fix, and an event that cannot
|
|
1051
|
+
* name it truthfully is worse than silence.
|
|
1052
|
+
* - Nothing is derived, counted or summarised from the batch: no advisory
|
|
1053
|
+
* exists here that the server did not put in the body.
|
|
1054
|
+
*
|
|
1055
|
+
* ## The failure shapes stay the callers'
|
|
1056
|
+
*
|
|
1057
|
+
* Non-2xx throws {@link MetadataError}, like every other method. A 2xx is
|
|
1058
|
+
* RETURNED unexamined — `success: false` is not a failure on this route
|
|
1059
|
+
* (`outcome: 'nothing_to_publish'` answers it too, and the spec's own text
|
|
1060
|
+
* says to read `outcome`, not the boolean), and the two callers have their
|
|
1061
|
+
* own, different rules for the refusal and rolled-back cases. Deciding that
|
|
1062
|
+
* here would change behaviour this card is not about.
|
|
1063
|
+
*
|
|
1064
|
+
* ## The opt-in backfill (objectui#12929)
|
|
1065
|
+
*
|
|
1066
|
+
* `options.backfill` is sent as the body's `backfill` key, as given, and only
|
|
1067
|
+
* when the caller passes one ({@link MetadataPublishPackageDraftsOptions}).
|
|
1068
|
+
* Without it the body is `{}`, byte for byte what every caller sent before,
|
|
1069
|
+
* so a caller that names nothing writes nothing into existing rows. The
|
|
1070
|
+
* receipt is `backfillApplied` on the returned body, unexamined like the rest
|
|
1071
|
+
* of a 2xx: one element per named field with the rows written, and `error`
|
|
1072
|
+
* on a field that stopped short (the publish itself stands).
|
|
1073
|
+
*/
|
|
1074
|
+
publishPackageDrafts(packageId: string, options?: MetadataPublishPackageDraftsOptions): Promise<MetadataPublishPackageDraftsResult>;
|
|
555
1075
|
/**
|
|
556
1076
|
* Get the 3-state layered view of a metadata item: `code` (the packaged
|
|
557
1077
|
* artifact baseline), `overlay` (the tenant customisation row alone) and
|
|
@@ -607,6 +1127,12 @@ declare class MetadataClient {
|
|
|
607
1127
|
* Idempotent: returns the result even when no overlay row existed.
|
|
608
1128
|
* Pass `state: 'draft'` to discard the pending draft only (keeps the
|
|
609
1129
|
* published overlay intact) — useful for a Studio "Discard draft" button.
|
|
1130
|
+
*
|
|
1131
|
+
* Pass `packageId` to name the package whose row the delete removes, sent
|
|
1132
|
+
* as `?package=`. A delete that names none addresses the one package that
|
|
1133
|
+
* holds the item; when two or more packages hold it the door refuses it
|
|
1134
|
+
* `409 RESOURCE_CONFLICT`, deletes nothing, and names `?package=` as the
|
|
1135
|
+
* remedy. That refusal is thrown like any other {@link MetadataError}.
|
|
610
1136
|
*/
|
|
611
1137
|
reset<T = unknown>(type: string, name: string, options?: MetadataDeleteOptions): Promise<T>;
|
|
612
1138
|
/**
|
|
@@ -615,9 +1141,16 @@ declare class MetadataClient {
|
|
|
615
1141
|
* `{ success, version, seq, message }`. Throws a `404 no_draft` if
|
|
616
1142
|
* nothing is pending and `409 metadata_conflict` if the published
|
|
617
1143
|
* overlay moved while the draft was sitting.
|
|
1144
|
+
*
|
|
1145
|
+
* Reports the runtime authoring gate's advisory findings through
|
|
1146
|
+
* {@link MetadataClientConfig.onSaveAdvisory} when the response carries them
|
|
1147
|
+
* (#5026 / objectstack#9176), with `door: 'publish'`. This is the door the
|
|
1148
|
+
* Studio designer takes on every edit, so it is the one whose findings an
|
|
1149
|
+
* author actually sees.
|
|
618
1150
|
*/
|
|
619
1151
|
publish<T = unknown>(type: string, name: string, options?: {
|
|
620
1152
|
message?: string;
|
|
1153
|
+
packageId?: string;
|
|
621
1154
|
}): Promise<T>;
|
|
622
1155
|
/**
|
|
623
1156
|
* Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
|
|
@@ -877,6 +1410,35 @@ interface MetadataCacheStats {
|
|
|
877
1410
|
* As soon as the backend supports `sys_user_preference`, persistence
|
|
878
1411
|
* "lights up" with no code change.
|
|
879
1412
|
*
|
|
1413
|
+
* ## One read per load (objectui#12187)
|
|
1414
|
+
*
|
|
1415
|
+
* The console builds one adapter per slot (favorites, recent items, flow
|
|
1416
|
+
* palette recents) over the same data source and user, and their providers
|
|
1417
|
+
* call `load()` in the same effects pass. Those loads share ONE request: the
|
|
1418
|
+
* first `load()` opens a read for its (data source, resource, user), every
|
|
1419
|
+
* `load()` of that scope issued before the next microtask adds its key to it,
|
|
1420
|
+
* and the read is then sent once and each adapter takes its own row from the
|
|
1421
|
+
* answer. A read that collected one key sends the per-key query this adapter
|
|
1422
|
+
* always sent; several keys go as one `key in (...)` query.
|
|
1423
|
+
*
|
|
1424
|
+
* Nothing is kept past that: the read closes the moment it is sent, so a later
|
|
1425
|
+
* `load()` (a new adapter after a language or account switch) asks the server
|
|
1426
|
+
* again, and a different user or data source never joins another's read.
|
|
1427
|
+
* `save()` does not use it: a write still finds and writes its own row.
|
|
1428
|
+
*
|
|
1429
|
+
* ## A family of keys in the same read (objectui#12418)
|
|
1430
|
+
*
|
|
1431
|
+
* Some state is one row per subject the console cannot name at load time: the
|
|
1432
|
+
* list state of every object a user has opened is a row per object,
|
|
1433
|
+
* `ui.list.<object>`, and which objects those are is only known from the rows
|
|
1434
|
+
* themselves. {@link createObjectStackUserPreferenceFamily} loads such a family
|
|
1435
|
+
* by its key PREFIX, and joins the same shared read: the read then asks for its
|
|
1436
|
+
* keys OR any key starting with the prefix, still one request. A prefix bounds
|
|
1437
|
+
* nothing the way the unique index bounds a key, so the read asks for at most
|
|
1438
|
+
* {@link PREFERENCE_FAMILY_ROW_LIMIT} family rows on top of its keys; an
|
|
1439
|
+
* answer that fills that limit may have left a key's row out, and such a key
|
|
1440
|
+
* asks for its own row instead (the same fallback an untagged row takes).
|
|
1441
|
+
*
|
|
880
1442
|
* @module
|
|
881
1443
|
*/
|
|
882
1444
|
|
|
@@ -899,10 +1461,31 @@ interface ObjectStackUserStateAdapterOptions {
|
|
|
899
1461
|
*/
|
|
900
1462
|
onError?: (where: 'load' | 'save', error: unknown) => void;
|
|
901
1463
|
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Generic persistence adapter for a user-scoped list.
|
|
1466
|
+
*
|
|
1467
|
+
* Implementations must be safe to call concurrently and should *never*
|
|
1468
|
+
* throw — failures should be swallowed so the hosting provider can degrade
|
|
1469
|
+
* to localStorage-only mode without crashing the UI.
|
|
1470
|
+
*
|
|
1471
|
+
* This is the one declaration of the name: `@object-ui/app-shell`'s
|
|
1472
|
+
* `UserStateAdaptersProvider` injects adapters of this type and re-exports it
|
|
1473
|
+
* (objectui#6349, batch 9).
|
|
1474
|
+
*/
|
|
902
1475
|
interface UserDataAdapter<T> {
|
|
1476
|
+
/** Load the persisted list for the current user. Resolve to [] when absent. */
|
|
903
1477
|
load(): Promise<T[]>;
|
|
1478
|
+
/** Persist the full list (debounced upstream). Errors are silently ignored. */
|
|
904
1479
|
save(items: T[]): Promise<void>;
|
|
905
1480
|
}
|
|
1481
|
+
/**
|
|
1482
|
+
* How many rows of one key family a shared read asks for (objectui#12418).
|
|
1483
|
+
* The unique index bounds a KEY to one row; nothing bounds a prefix, so the
|
|
1484
|
+
* read's `$top` is its key count plus this many rows per family. An answer
|
|
1485
|
+
* that reaches its `$top` is read as possibly cut short: see
|
|
1486
|
+
* {@link SharedAnswer.saturated}.
|
|
1487
|
+
*/
|
|
1488
|
+
declare const PREFERENCE_FAMILY_ROW_LIMIT = 200;
|
|
906
1489
|
/**
|
|
907
1490
|
* Build a `UserDataAdapter<T>` backed by ObjectStack.
|
|
908
1491
|
*
|
|
@@ -910,6 +1493,290 @@ interface UserDataAdapter<T> {
|
|
|
910
1493
|
* one per slot you want to persist.
|
|
911
1494
|
*/
|
|
912
1495
|
declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
|
|
1496
|
+
interface ObjectStackUserPreferenceFamilyOptions {
|
|
1497
|
+
/** Connected data source (usually the one provided by `<AdapterProvider>`). */
|
|
1498
|
+
dataSource: DataSource<any>;
|
|
1499
|
+
/** Authenticated user id. */
|
|
1500
|
+
userId: string;
|
|
1501
|
+
/**
|
|
1502
|
+
* The family's key prefix, dotted and ending in a dot, e.g. `ui.list.`: the
|
|
1503
|
+
* family is every key that starts with it, one row per subject
|
|
1504
|
+
* (`ui.list.account`, `ui.list.contact`, …).
|
|
1505
|
+
*/
|
|
1506
|
+
prefix: string;
|
|
1507
|
+
/** Override the storage object name. Defaults to `"sys_user_preference"`. */
|
|
1508
|
+
resource?: string;
|
|
1509
|
+
/** Optional diagnostics hook; defaults to noop. */
|
|
1510
|
+
onError?: (where: 'load' | 'save', error: unknown) => void;
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* A user's rows of one key family: every `sys_user_preference` row whose key
|
|
1514
|
+
* starts with the family's prefix, each holding one JSON object.
|
|
1515
|
+
*
|
|
1516
|
+
* Same contract as {@link UserDataAdapter}: never throws; a failed read loads
|
|
1517
|
+
* as no rows and a failed write is reported to `onError` and swallowed, so
|
|
1518
|
+
* the hosting provider keeps its localStorage state.
|
|
1519
|
+
*/
|
|
1520
|
+
interface UserPreferenceFamily<T> {
|
|
1521
|
+
/** The key prefix this family covers. */
|
|
1522
|
+
readonly prefix: string;
|
|
1523
|
+
/**
|
|
1524
|
+
* Load every row of the family, joining the shared read of the load
|
|
1525
|
+
* (one request with the slots' keys). Resolves to `{ [key]: value }`, with
|
|
1526
|
+
* only the rows whose value is a JSON object; `{}` when there are none.
|
|
1527
|
+
*/
|
|
1528
|
+
load(): Promise<Record<string, T>>;
|
|
1529
|
+
/**
|
|
1530
|
+
* Upsert one key's row. A key outside the family is refused (reported to
|
|
1531
|
+
* `onError`, nothing written). Writes to one key run in order.
|
|
1532
|
+
*/
|
|
1533
|
+
save(key: string, value: T): Promise<void>;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Build a {@link UserPreferenceFamily} backed by ObjectStack: one row per key,
|
|
1537
|
+
* loaded together by prefix in the shared read (see the module header), each
|
|
1538
|
+
* written with the same upsert a single-key adapter uses.
|
|
1539
|
+
*/
|
|
1540
|
+
declare function createObjectStackUserPreferenceFamily<T extends object = Record<string, unknown>>(options: ObjectStackUserPreferenceFamilyOptions): UserPreferenceFamily<T>;
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* The served form of a picklist-bound field, turned back into the authored
|
|
1544
|
+
* form before a writer that seeded its body from a served read PUTs it
|
|
1545
|
+
* (objectui#10202, objectui#11692).
|
|
1546
|
+
*
|
|
1547
|
+
* ## Why the writer has to do this
|
|
1548
|
+
*
|
|
1549
|
+
* A select field may name a shared list instead of carrying its own options:
|
|
1550
|
+
* `picklist: 'industry'` (`FieldSchema.picklist`, `@objectstack/spec`
|
|
1551
|
+
* `data/picklist.zod.ts`). The runtime serves such a field with BOTH keys:
|
|
1552
|
+
* `picklist`, still naming the list, and `options`, RESOLVED from that list and
|
|
1553
|
+
* from every `picklistExtensions` entry other packages add to it
|
|
1554
|
+
* (`PicklistServedFieldSchema`). Every object read carries that served form —
|
|
1555
|
+
* the object list `GET /meta/object`, `GET /meta/object/NAME`, and the
|
|
1556
|
+
* `effective` layer of `GET /meta/object/NAME/layers` alike, because the
|
|
1557
|
+
* runtime resolves the binding in the one fold all three share.
|
|
1558
|
+
*
|
|
1559
|
+
* The authoring door refuses the two keys together: `FieldSchema` adds an issue
|
|
1560
|
+
* at `fields.FIELD.options` whose prescription is "Keep `picklist` and delete
|
|
1561
|
+
* `options`". So a writer that sends its seed back unchanged gets
|
|
1562
|
+
* `422 INVALID_METADATA` for the whole object, whatever the author edited —
|
|
1563
|
+
* an OWD batch edit, a relabel, a field reorder, anything.
|
|
1564
|
+
* The resolved list belongs to the picklist, not to the field: the producer of
|
|
1565
|
+
* the PUT drops it (objectstack's producer obligation, pinned server-side in
|
|
1566
|
+
* `protocol-picklist-served-roundtrip.test.ts`).
|
|
1567
|
+
*
|
|
1568
|
+
* ## What this changes, and what it never touches
|
|
1569
|
+
*
|
|
1570
|
+
* EVERY field that names a picklist leaves without `options` — not only the one
|
|
1571
|
+
* the author edited, because the whole document is re-sent and any bound field
|
|
1572
|
+
* in it is refused. A field that names no picklist is not touched at all: its
|
|
1573
|
+
* inline `options` stay byte-identical. No other key of any field, and no key of
|
|
1574
|
+
* the document, is touched.
|
|
1575
|
+
*
|
|
1576
|
+
* "Names a picklist" is the spec's own test, `picklist !== undefined` (the
|
|
1577
|
+
* `FieldSchema` refinement), so this drops `options` on exactly the fields the
|
|
1578
|
+
* door would refuse for carrying both.
|
|
1579
|
+
*
|
|
1580
|
+
* Both `fields` shapes a document can carry are handled: the spec's record keyed
|
|
1581
|
+
* by field name, and the array `readFields` / `writeFields` round-trip for a
|
|
1582
|
+
* document that arrived that way.
|
|
1583
|
+
*
|
|
1584
|
+
* ⛔ Not at the transport. `MetadataClient.save` and the other metadata-write
|
|
1585
|
+
* doors cannot tell a served copy from an author who wrote both keys, and the
|
|
1586
|
+
* second must stay a loud refusal with its prescription. The writer can tell:
|
|
1587
|
+
* it seeded its body from the served read. So this is exported for WRITERS —
|
|
1588
|
+
* the opposite of `assertObjectMetadataWritable`, which is exported for doors —
|
|
1589
|
+
* and the rule each writer applies is one sentence: a body seeded from a served
|
|
1590
|
+
* object read goes through this function on its way to the PUT; a body built
|
|
1591
|
+
* from the author's own input does not.
|
|
1592
|
+
*
|
|
1593
|
+
* ## Why it lives in this package (objectui#11692)
|
|
1594
|
+
*
|
|
1595
|
+
* The writers that seed from a served read are not all in one package: the
|
|
1596
|
+
* Studio and metadata-admin surfaces live in `@object-ui/app-shell`, and the
|
|
1597
|
+
* Setup fields and objects pages in `@object-ui/plugin-designer`, which does
|
|
1598
|
+
* not depend on `app-shell`. Both depend on this package, which also owns the
|
|
1599
|
+
* client that serves the read and carries the write — the same reason
|
|
1600
|
+
* `extractDraftBody` lives here. One function, not one copy per package: a
|
|
1601
|
+
* second copy is free to drift from the spec's served shape on its own.
|
|
1602
|
+
*
|
|
1603
|
+
* Pure: the input is never mutated, and a document with nothing to drop comes
|
|
1604
|
+
* back BY REFERENCE, so a caller can tell "unchanged" by identity.
|
|
1605
|
+
*/
|
|
1606
|
+
declare function dropServedPicklistOptions<T extends Record<string, unknown>>(body: T): T;
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* One issue → a single field-anchored line: `• fields.amount.type — Required`.
|
|
1610
|
+
*
|
|
1611
|
+
* Exported so a caller that formats a LIST of failures — `app-shell`'s publish
|
|
1612
|
+
* formatter, whose `failed[]` entries carry their own `issues` — writes each
|
|
1613
|
+
* issue in this same grammar instead of keeping a second copy of it.
|
|
1614
|
+
*/
|
|
1615
|
+
declare function formatMetadataIssue(i: MetadataValidationIssue): string;
|
|
1616
|
+
/**
|
|
1617
|
+
* Format a caught save/publish error for a banner/toast. When the error carries
|
|
1618
|
+
* spec-validation issues, list them (one field per line) so the user sees the
|
|
1619
|
+
* offending fields and what to do about them; otherwise fall back to the plain
|
|
1620
|
+
* message.
|
|
1621
|
+
*/
|
|
1622
|
+
declare function formatMetadataError(e: unknown): string;
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* The object-metadata write invariant, placed at the DOOR rather than at the
|
|
1626
|
+
* writers (objectui#8676).
|
|
1627
|
+
*
|
|
1628
|
+
* ## Why this module exists — the enumeration, not the count
|
|
1629
|
+
*
|
|
1630
|
+
* objectui#7714 ruled one client behaviour: *a half-filled relationship stays
|
|
1631
|
+
* client-side and the PUT body never carries a `lookup` without a non-empty
|
|
1632
|
+
* `reference`*. Its PR implemented that ruling by naming the writers it knew
|
|
1633
|
+
* about — two of them — and putting the assertion inside each one's array-to-map
|
|
1634
|
+
* conversion (`toFieldsMap`). The very failure the ruling was written against
|
|
1635
|
+
* was then reproduced on a THIRD writer neither conversion covers, in that
|
|
1636
|
+
* card's own required dogfood (fixed in `9073cf018`). objectui#8676 swept and
|
|
1637
|
+
* found nine more.
|
|
1638
|
+
*
|
|
1639
|
+
* The durable half of that card is a sentence about method, not a number:
|
|
1640
|
+
*
|
|
1641
|
+
* > a ruling that enumerates writers is only as good as the enumeration.
|
|
1642
|
+
*
|
|
1643
|
+
* A hand list of writers is stale the next time somebody adds one, and nothing
|
|
1644
|
+
* says so. So this invariant is not attached to writers at all. It is attached
|
|
1645
|
+
* to the DOORS — the places that put bytes on the wire — because the doors are a
|
|
1646
|
+
* CLOSED set that this repository owns, while the writers are an OPEN set it
|
|
1647
|
+
* does not. Three in-repo transports can PUT `/meta/:type/:name`, and
|
|
1648
|
+
* `scripts/check-object-metadata-write-doors.mjs` derives that set on every run
|
|
1649
|
+
* rather than restating it:
|
|
1650
|
+
*
|
|
1651
|
+
* 1. `MetadataClient.save` `packages/data-objectstack/src/metadata-client.ts`
|
|
1652
|
+
* 2. `importObjectDraft` `packages/app-shell/src/views/metadata-admin/external/api.ts`
|
|
1653
|
+
* 3. the `@objectstack/client` SDK's `meta.saveItem`, reached in this repo
|
|
1654
|
+
* through `MetadataService`'s one seam
|
|
1655
|
+
*
|
|
1656
|
+
* Every writer in the repo reaches the server through one of those three. Guard
|
|
1657
|
+
* them and the writer count stops mattering; add a fourth transport and the gate
|
|
1658
|
+
* turns red naming it. That is the whole design: ⛔ do not re-open this by
|
|
1659
|
+
* adding a writer list anywhere.
|
|
1660
|
+
*
|
|
1661
|
+
* ## What it asserts, and the two things it deliberately does NOT
|
|
1662
|
+
*
|
|
1663
|
+
* It asserts two field-level invariants, and nothing else:
|
|
1664
|
+
*
|
|
1665
|
+
* - objectui#7714's: a relationship (`lookup` / `master_detail`) carries a
|
|
1666
|
+
* usable `reference`;
|
|
1667
|
+
* - objectui#11253's: a choice (`select` / `radio`) carries an option source,
|
|
1668
|
+
* a non-empty `options` list or a shared `picklist`. This is the objectui
|
|
1669
|
+
* branch of the maintainer's ruling A on objectstack#20827, whose door
|
|
1670
|
+
* refuses "a `select` / `radio` with neither `options` nor `picklist`".
|
|
1671
|
+
*
|
|
1672
|
+
* ⛔ **NOT a client-side revalidation of the document.** Parsing the whole body
|
|
1673
|
+
* through `ObjectSchema` before the PUT would refuse plugin-registered keys the
|
|
1674
|
+
* SERVER accepts, and it would promote a client PREDICTION into a block — which
|
|
1675
|
+
* is exactly what objectui#4306 / objectui#6980 ruled against for the designer's
|
|
1676
|
+
* live Zod pass, on the stated ground that a schema issue on a draft the server
|
|
1677
|
+
* ACCEPTS would dead-bolt Save with no on-screen editor able to clear it.
|
|
1678
|
+
*
|
|
1679
|
+
* Neither invariant forecloses anything the server would have taken, and that
|
|
1680
|
+
* is the whole of the dead-bolt argument for both:
|
|
1681
|
+
*
|
|
1682
|
+
* - The relationship refusal: the same body is refused one layer down with a
|
|
1683
|
+
* 422 on `fields.NAME.reference`, so the guard only moves an identical
|
|
1684
|
+
* refusal earlier, where it can name the field while it is still on screen.
|
|
1685
|
+
* - The choice refusal: the same body is refused one layer down at
|
|
1686
|
+
* `fields.NAME.options`, an empty list included, since `@objectstack/spec`
|
|
1687
|
+
* 17.7.0 shipped the door (objectstack#21390, the ruling's own door). Until
|
|
1688
|
+
* that release the installed spec still accepted a choice with no option
|
|
1689
|
+
* source and this refusal was deliberately AHEAD of the server, by the
|
|
1690
|
+
* ruling's execution parameter (2): "if Studio saves a `select` before its
|
|
1691
|
+
* options exist, the objectui side changes its order first (a card in that
|
|
1692
|
+
* lane) and the door closes after". The door has closed, the order changed
|
|
1693
|
+
* first, and the refusal collapsed into the relationship form at
|
|
1694
|
+
* objectui#11717's bump. The editors still clear it on screen: the Studio
|
|
1695
|
+
* data page and the metadata-admin object editor carry an options editor in
|
|
1696
|
+
* that field's inspector (both pinned by their
|
|
1697
|
+
* `*.choiceWithoutOptions-11253` suites); plugin-designer's drawer has no
|
|
1698
|
+
* options editor, so it stops offering the choice types, a stored `select`
|
|
1699
|
+
* keeps its type there and can be retyped or removed, and a stored `radio`
|
|
1700
|
+
* is among the types that page carries through unedited and sends to
|
|
1701
|
+
* metadata-admin, as it already does for a target-less `master_detail`.
|
|
1702
|
+
* `object-metadata-write-guard.derivation.test.ts` re-measures "refused one
|
|
1703
|
+
* layer down" for both invariants on every run.
|
|
1704
|
+
*
|
|
1705
|
+
* ⛔ **NOT strip-and-report-saved.** Dropping the offending field and reporting
|
|
1706
|
+
* success would trade a visible refusal for an invisible deletion —
|
|
1707
|
+
* objectstack#4001's shape, ruled out for this family in objectui#7714 and again
|
|
1708
|
+
* in `9073cf018`. It throws; the caller surfaces the message.
|
|
1709
|
+
*
|
|
1710
|
+
* ## The type lists are pinned to the contract, not to memory
|
|
1711
|
+
*
|
|
1712
|
+
* {@link RELATIONSHIP_TYPES_REQUIRING_REFERENCE} and
|
|
1713
|
+
* {@link CHOICE_TYPES_REQUIRING_OPTIONS} are the two lists this module keeps,
|
|
1714
|
+
* and they are the same class of hazard the module exists to close, so neither
|
|
1715
|
+
* gets to be a remembered list either.
|
|
1716
|
+
* `object-metadata-write-guard.derivation.test.ts` DERIVES both from the
|
|
1717
|
+
* installed `@objectstack/spec`:
|
|
1718
|
+
*
|
|
1719
|
+
* - the relationship set from every member of `FieldType`, parsed as
|
|
1720
|
+
* `{ type, label }` through `FieldSchema`, keeping those refused at path
|
|
1721
|
+
* `reference`;
|
|
1722
|
+
* - the choice set from the same members run through `checkFieldCompleteness`
|
|
1723
|
+
* (ADR-0078's author-time rule, which the ruling's door adopts), keeping
|
|
1724
|
+
* those it reports as `field/choice-without-options` at `error` severity.
|
|
1725
|
+
* The door itself shipped in 17.7.0, and the pin holds the two answers equal.
|
|
1726
|
+
*
|
|
1727
|
+
* A spec release that changes either set turns that pin red instead of leaving
|
|
1728
|
+
* a guard that quietly stopped covering the contract.
|
|
1729
|
+
*
|
|
1730
|
+
* The derivation is a TEST and not a runtime probe on purpose: parsing 50
|
|
1731
|
+
* field-type documents on the way to every save is a cost paid on the hot path
|
|
1732
|
+
* to re-learn something that changes at most once per spec release, and a probe
|
|
1733
|
+
* that throws at runtime has to choose between blocking writes and failing open.
|
|
1734
|
+
* A pin has neither problem — it fails at CI time, loudly, with nothing at stake.
|
|
1735
|
+
*/
|
|
1736
|
+
/**
|
|
1737
|
+
* Field types whose `reference` — the target object a relationship links to —
|
|
1738
|
+
* `@objectstack/spec` requires to be present and non-empty.
|
|
1739
|
+
*
|
|
1740
|
+
* Derived from the installed artifact by the pin named above, never recalled.
|
|
1741
|
+
*/
|
|
1742
|
+
declare const RELATIONSHIP_TYPES_REQUIRING_REFERENCE: readonly string[];
|
|
1743
|
+
/**
|
|
1744
|
+
* Field types that offer nothing to choose without an option source — a
|
|
1745
|
+
* non-empty `options` list or a shared `picklist` (objectui#11253, the objectui
|
|
1746
|
+
* branch of the maintainer's ruling A on objectstack#20827).
|
|
1747
|
+
*
|
|
1748
|
+
* Derived from the installed artifact by the pin named above, never recalled.
|
|
1749
|
+
*/
|
|
1750
|
+
declare const CHOICE_TYPES_REQUIRING_OPTIONS: readonly string[];
|
|
1751
|
+
/** The metadata type whose documents carry the `fields` map this guard reads. */
|
|
1752
|
+
declare const OBJECT_METADATA_TYPE = "object";
|
|
1753
|
+
/**
|
|
1754
|
+
* Refuse an object-metadata write whose body carries a relationship field with
|
|
1755
|
+
* no usable target, or a choice field with no option source.
|
|
1756
|
+
*
|
|
1757
|
+
* A no-op for every other metadata type and for any body with no readable
|
|
1758
|
+
* `fields` member — this door serves `view`, `app`, `flow`, `permission`,
|
|
1759
|
+
* `hook` and `dashboard` writes too, and has no opinion about them.
|
|
1760
|
+
*
|
|
1761
|
+
* ## The message is the same at every door, and names no code (objectui#11785)
|
|
1762
|
+
*
|
|
1763
|
+
* The thrown text reaches authors: Studio keeps it under a refusal's Details,
|
|
1764
|
+
* and the other surfaces that save through a door show it as their banner. So
|
|
1765
|
+
* it names the field, its type and the remedy, and ⛔ no class, package or
|
|
1766
|
+
* tracker id. The rationale for each rule lives in this module's docblock, not
|
|
1767
|
+
* in the runtime string. The door's label is not printed either, which makes the
|
|
1768
|
+
* text a function of the body alone: a caller that kept the body it sent can
|
|
1769
|
+
* re-run this guard on it and get the identical message, which is how Studio
|
|
1770
|
+
* tells this refusal apart from any other failure without parsing the prose.
|
|
1771
|
+
*
|
|
1772
|
+
* @param type the metadata type segment of the write (`'object'`, `'view'`, …)
|
|
1773
|
+
* @param item the body about to be serialised onto the wire
|
|
1774
|
+
* @param _writer the door's label. Every door still names itself at the call,
|
|
1775
|
+
* and the stack trace says where the write stopped; the message
|
|
1776
|
+
* does not print it (see above)
|
|
1777
|
+
* @throws Error naming the offending field, before any request is issued
|
|
1778
|
+
*/
|
|
1779
|
+
declare function assertObjectMetadataWritable(type: unknown, item: unknown, _writer: string): void;
|
|
913
1780
|
|
|
914
1781
|
/**
|
|
915
1782
|
* Map human-readable filter operator names produced by SDUI view configs
|
|
@@ -947,6 +1814,156 @@ declare class MalformedFilterError extends Error {
|
|
|
947
1814
|
}
|
|
948
1815
|
/** Detect the malformed-filter refusal, whether raised here or by the server. */
|
|
949
1816
|
declare function isMalformedFilterError(error: unknown): boolean;
|
|
1817
|
+
/**
|
|
1818
|
+
* A `where` that reached `aggregate()`'s SPEC-SHAPE branch as an array the spec
|
|
1819
|
+
* cannot read as a filter — an unlowered `ViewFilterRule[]` above all.
|
|
1820
|
+
*
|
|
1821
|
+
* WHY THIS IS A REFUSAL AND NOT A LOWERING (objectui#6825, maintainer ruling
|
|
1822
|
+
* 2026-08-30, option A). The two branches of `aggregate()` take a filter by
|
|
1823
|
+
* different names: the analytics branch takes `filter` and lowers it through
|
|
1824
|
+
* `translateFilterArray` (objectui#6302), the spec-shape branch takes `where`
|
|
1825
|
+
* and posts it to `POST /data/:object/query` verbatim. Lowering here too was
|
|
1826
|
+
* option B and was REFUSED: it is the tolerant-consumer direction AGENTS.md
|
|
1827
|
+
* #0.1 exists to stop, and it would bless as supported a params shape
|
|
1828
|
+
* `AggregateParams` does not even declare. Retiring the branch was option D and
|
|
1829
|
+
* was refused as the riskier move — this repo has no producer that can reach it
|
|
1830
|
+
* (`ObjectChart`'s gate requires a non-array `aggregate.groupBy`, and all three
|
|
1831
|
+
* in-tree constructors pass a string), but `ObjectChart`'s props are `any` and
|
|
1832
|
+
* an out-of-repo host may already send this shape. So the branch stays, and it
|
|
1833
|
+
* says no out loud.
|
|
1834
|
+
*
|
|
1835
|
+
* What that buys is the FAILURE MODE, not a failure count. The receiving engine
|
|
1836
|
+
* already refuses these arrays ("is not a filter" — objectstack's
|
|
1837
|
+
* `engine-filter-array-lowering.test.ts`), so nothing that reaches this point
|
|
1838
|
+
* could have produced a correct number; the pre-existing behaviour was to find
|
|
1839
|
+
* that out on the wire, or worse, to have the predicate quietly dropped while
|
|
1840
|
+
* the chart rendered confident, wrong figures with no signal to their author.
|
|
1841
|
+
* This moves the same refusal to the producer, where the author can act on it.
|
|
1842
|
+
*
|
|
1843
|
+
* Carries the `INVALID_FILTER` / 400 pair its sibling {@link MalformedFilterError}
|
|
1844
|
+
* carries, so `isMalformedFilterError()` recognises it and a failed widget renders
|
|
1845
|
+
* "this filter is malformed" rather than "check your connection" (objectui#3066).
|
|
1846
|
+
*/
|
|
1847
|
+
declare class UnloweredAggregateWhereError extends Error {
|
|
1848
|
+
readonly code = "INVALID_FILTER";
|
|
1849
|
+
readonly httpStatus = 400;
|
|
1850
|
+
/** The value as received, so a caller can log what its producer actually built. */
|
|
1851
|
+
readonly where: unknown;
|
|
1852
|
+
/** The object `aggregate()` was called for. */
|
|
1853
|
+
readonly resource: string;
|
|
1854
|
+
constructor(where: unknown, resource: string);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* The keys `aggregate()`'s ANALYTICS branch reads and its SPEC-SHAPE branch does
|
|
1858
|
+
* not: `filter`, `field`, `function`.
|
|
1859
|
+
*
|
|
1860
|
+
* Written out once, and read by both the refusal below and its message, so the
|
|
1861
|
+
* set a caller is told about cannot drift from the set that is checked.
|
|
1862
|
+
*
|
|
1863
|
+
* ⛔ Deliberately NOT "every key the spec-shape branch does not read". A refusal
|
|
1864
|
+
* scoped that widely would reject `orderBy`, a future spec key, or any harmless
|
|
1865
|
+
* extra a host happens to carry, and it would satisfy a naive "the spec-shape
|
|
1866
|
+
* branch refuses `filter`" test while breaking traffic nobody complained about.
|
|
1867
|
+
* These three are named one by one because they are the OTHER branch's own
|
|
1868
|
+
* parameters: their presence says the params were built for the analytics wire
|
|
1869
|
+
* and arrived on the spec-shape one, which is a producer defect with a name.
|
|
1870
|
+
*/
|
|
1871
|
+
declare const ANALYTICS_ONLY_AGGREGATE_KEYS: readonly ["filter", "field", "function"];
|
|
1872
|
+
type AnalyticsOnlyAggregateKey = (typeof ANALYTICS_ONLY_AGGREGATE_KEYS)[number];
|
|
1873
|
+
/**
|
|
1874
|
+
* Analytics-branch params — `filter`, `field`, `function` — that reached
|
|
1875
|
+
* `aggregate()`'s SPEC-SHAPE branch, which reads none of them.
|
|
1876
|
+
*
|
|
1877
|
+
* WHY A REFUSAL AND NOT A DROP (`503cd8b89`). This applies the maintainer
|
|
1878
|
+
* ruling of 2026-08-30 on objectui#6825 — option A, REFUSE at the producer — to
|
|
1879
|
+
* the rest of the same branch. That ruling's reason was that a shape the spec's
|
|
1880
|
+
* own gate would reject is off-contract at the PRODUCER, so the adapter says no
|
|
1881
|
+
* instead of degrading leniently; the reason lands harder here, because a
|
|
1882
|
+
* silent drop is precisely the disposition it refused for `where`. Leaving one
|
|
1883
|
+
* key on this branch refusing while three others vanish without a word would be
|
|
1884
|
+
* harder to explain than the old behaviour.
|
|
1885
|
+
*
|
|
1886
|
+
* ⭐ AND THE DROP HERE IS WORSE THAN THE `where` HALF. `filter` is not the only
|
|
1887
|
+
* casualty: `field` and `function` are the analytics branch's whole measure, and
|
|
1888
|
+
* the spec-shape branch copies a measure only out of `aggregations`. So the
|
|
1889
|
+
* legacy shape `{ field, function, groupBy, filter }` whose `groupBy` happens to
|
|
1890
|
+
* be an ARRAY produced a `queryAst` carrying a `groupBy` and NO `aggregations`
|
|
1891
|
+
* at all — a grouping with no measure — with the author's predicate gone too.
|
|
1892
|
+
* The chart still rendered, the numbers were wrong, and there was nothing to
|
|
1893
|
+
* look at.
|
|
1894
|
+
*
|
|
1895
|
+
* HOW A LEGACY CALLER GETS HERE, measured in this repo on 2026-09-08.
|
|
1896
|
+
* `Array.isArray(params.groupBy)` is one of `looksLikeSpecShape`'s three
|
|
1897
|
+
* disjuncts, and `ObjectChart`'s `runAggregate` gates only on the STRUCTURED
|
|
1898
|
+
* node shape (`gb && typeof gb === 'object' && !Array.isArray(gb)`), so an
|
|
1899
|
+
* ARRAY `aggregate.groupBy` falls through to its legacy call — `{ field,
|
|
1900
|
+
* function, groupBy, filter }` — and that call lands here.
|
|
1901
|
+
* `ObjectMetricWidget.computeOne` forwards `aggregate.groupBy` the same way.
|
|
1902
|
+
* Both take the value from authored widget metadata across an `any` seam
|
|
1903
|
+
* (`isObjectProvider`'s `aggregate?: any`, `ds: any`), so the type system
|
|
1904
|
+
* cannot refuse it and this runtime refusal is what an author gets instead.
|
|
1905
|
+
*
|
|
1906
|
+
* ⛔ WHAT THIS DELIBERATELY DOES NOT DO. It does not ROUTE the legacy shape back
|
|
1907
|
+
* to the analytics branch: that is the tolerant-consumer direction AGENTS.md
|
|
1908
|
+
* §0.1 exists to stop, it is the option #6825 refused for `where`, and it could
|
|
1909
|
+
* not work anyway — the analytics branch posts `dimensions: [params.groupBy]`,
|
|
1910
|
+
* so an array `groupBy` would go out nested. It does not widen `AggregateParams`
|
|
1911
|
+
* (`@object-ui/types`), which is a separate contract question. And it does not
|
|
1912
|
+
* refuse a key that is present but nullish: `undefined`/`null` carries nothing
|
|
1913
|
+
* for the branch to drop, and both `ObjectChart` and `ObjectMetricWidget` build
|
|
1914
|
+
* their params by spreading possibly-absent authored values.
|
|
1915
|
+
*
|
|
1916
|
+
* Carries the `INVALID_FILTER` / 400 pair its siblings {@link MalformedFilterError}
|
|
1917
|
+
* and {@link UnloweredAggregateWhereError} carry, so `isMalformedFilterError()`
|
|
1918
|
+
* recognises it and a failed widget renders "this filter is malformed" rather
|
|
1919
|
+
* than "check your connection" (objectui#3066). One branch, one envelope: a
|
|
1920
|
+
* caller can catch `aggregate()`'s producer-side refusals with one predicate.
|
|
1921
|
+
*/
|
|
1922
|
+
declare class AnalyticsKeysOnSpecShapeError extends Error {
|
|
1923
|
+
readonly code = "INVALID_FILTER";
|
|
1924
|
+
readonly httpStatus = 400;
|
|
1925
|
+
/** The offending keys, in the order {@link ANALYTICS_ONLY_AGGREGATE_KEYS} lists them. */
|
|
1926
|
+
readonly keys: readonly AnalyticsOnlyAggregateKey[];
|
|
1927
|
+
/** What each offending key carried, so a producer is identifiable from a log. */
|
|
1928
|
+
readonly received: Record<string, unknown>;
|
|
1929
|
+
/** The object `aggregate()` was called for. */
|
|
1930
|
+
readonly resource: string;
|
|
1931
|
+
constructor(keys: readonly AnalyticsOnlyAggregateKey[], params: any, resource: string);
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* An ARRAY `filter` that reached `aggregate()`'s ANALYTICS branch and that the
|
|
1935
|
+
* protocol's lowering sink cannot turn into a `FilterCondition` — an infix join
|
|
1936
|
+
* (`[condA, 'or', condB]`) above all.
|
|
1937
|
+
*
|
|
1938
|
+
* WHY A REFUSAL AND NOT A DROP (objectui#7752; maintainer ruling of 2026-09-05
|
|
1939
|
+
* on objectstack#15828, which reads that card as a frontend defect: the
|
|
1940
|
+
* protocol is right and the adapter was wrong). `parseFilterAST` answers
|
|
1941
|
+
* `undefined` for an array it cannot read, and `undefined` here would post a
|
|
1942
|
+
* body with NO `where` at all: the aggregate would then be computed over every
|
|
1943
|
+
* row and returned as a confident number, with the author's predicate gone and
|
|
1944
|
+
* nothing on the wire to say it went. That is the same silent over-fetch
|
|
1945
|
+
* {@link MalformedFilterError} exists to stop, one door further along.
|
|
1946
|
+
*
|
|
1947
|
+
* Deliberately NOT {@link UnloweredAggregateWhereError}, whose meaning is not
|
|
1948
|
+
* widened to cover this: that one guards the SPEC-SHAPE branch, whose `where`
|
|
1949
|
+
* is posted verbatim and must therefore have been lowered by its PRODUCER
|
|
1950
|
+
* (objectui#6825, ruling 2026-08-30 option A — refuse, never lower). This
|
|
1951
|
+
* branch's `filter` is authoring input and IS lowered here, at the door the
|
|
1952
|
+
* protocol names; this error covers only the input that sink cannot lower.
|
|
1953
|
+
*
|
|
1954
|
+
* Carries the `INVALID_FILTER` / 400 pair both siblings carry, so
|
|
1955
|
+
* `isMalformedFilterError()` recognises it and a failed widget renders "this
|
|
1956
|
+
* filter is malformed" rather than "check your connection" (objectui#3066).
|
|
1957
|
+
*/
|
|
1958
|
+
declare class UnlowerableAnalyticsFilterError extends Error {
|
|
1959
|
+
readonly code = "INVALID_FILTER";
|
|
1960
|
+
readonly httpStatus = 400;
|
|
1961
|
+
/** The filter as received, so a caller can log what its producer actually built. */
|
|
1962
|
+
readonly filter: unknown;
|
|
1963
|
+
/** The object `aggregate()` was called for. */
|
|
1964
|
+
readonly resource: string;
|
|
1965
|
+
constructor(filter: unknown, resource: string, detail?: string);
|
|
1966
|
+
}
|
|
950
1967
|
/**
|
|
951
1968
|
* Serialize a `$orderby` to the server's `sort` shorthand
|
|
952
1969
|
* (`field,-other_field`), for every shape `QueryParams['$orderby']` declares.
|
|
@@ -979,7 +1996,7 @@ declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise<unkn
|
|
|
979
1996
|
declare function clearSharedDiscoveryCache(): void;
|
|
980
1997
|
/**
|
|
981
1998
|
* Read the cross-object atomic-batch capability from a `discovery` document
|
|
982
|
-
* (
|
|
1999
|
+
* (objectstack-ai/objectstack#3298 / objectui #2693). The server advertises it hierarchically
|
|
983
2000
|
* under `capabilities.transactionalBatch.enabled`; the published
|
|
984
2001
|
* `@objectstack/client` also accepts the flat `capabilities.transactionalBatch:
|
|
985
2002
|
* boolean` form and normalizes the two — mirror that here so the adapter reads
|
|
@@ -992,7 +2009,7 @@ declare function clearSharedDiscoveryCache(): void;
|
|
|
992
2009
|
* real error.
|
|
993
2010
|
* - `false` — the backend explicitly does NOT (route absent, or a runtime that
|
|
994
2011
|
* can't open a transaction).
|
|
995
|
-
* - `undefined` — the capability is absent, i.e. the backend predates #3298;
|
|
2012
|
+
* - `undefined` — the capability is absent, i.e. the backend predates objectstack-ai/objectstack#3298;
|
|
996
2013
|
* the caller must keep the legacy runtime-probe fallback (we can't tell
|
|
997
2014
|
* whether `/batch` exists without trying it).
|
|
998
2015
|
*/
|
|
@@ -1045,22 +2062,52 @@ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_
|
|
|
1045
2062
|
declare function isApiAccessDeniedError(error: unknown): boolean;
|
|
1046
2063
|
/**
|
|
1047
2064
|
* What the by-name meta app route said about THIS session's access to an app
|
|
1048
|
-
* (objectui#4252 / objectstack#8013).
|
|
2065
|
+
* (objectui#4252 / objectstack#8013, widened by objectui#9262).
|
|
1049
2066
|
*
|
|
1050
|
-
* - `granted`
|
|
1051
|
-
* - `denied`
|
|
1052
|
-
* The only verdict a caller may render as an
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
2067
|
+
* - `granted` — the route served the app document.
|
|
2068
|
+
* - `denied` — the app EXISTS and the session lacks its
|
|
2069
|
+
* `requiredPermissions`. The only verdict a caller may render as an
|
|
2070
|
+
* authorization refusal.
|
|
2071
|
+
* - `not_found` — the route answered, and what it answered is that it has no
|
|
2072
|
+
* app at this name for this session. WHICH absence it is — never created, a
|
|
2073
|
+
* typo, an unpublished draft, an app gated by an absent optional service —
|
|
2074
|
+
* is deliberately NOT distinguished: the control plane answers the same way
|
|
2075
|
+
* for every app the caller may not see, and the 2026-08-12 ruling keeps it
|
|
2076
|
+
* that way. A caller may say the app cannot be opened; it may never say why.
|
|
2077
|
+
* - `unreachable` — the probe could not obtain an answer at all: the transport
|
|
2078
|
+
* failed, the server erred, the request timed out. Says nothing about the
|
|
2079
|
+
* app.
|
|
2080
|
+
* - `unknown` — nothing was measured, because nothing could be asked: a
|
|
2081
|
+
* host DataSource that does not implement this probe (the console is
|
|
2082
|
+
* protocol-agnostic — AGENTS #1), or no app name to ask about.
|
|
1057
2083
|
*
|
|
1058
|
-
*
|
|
1059
|
-
*
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
2084
|
+
* ## Why `not_found` and `unreachable` are separate members (objectui#9262)
|
|
2085
|
+
*
|
|
2086
|
+
* They were one `unknown` before, and the screen above them therefore asserted
|
|
2087
|
+
* a transient publish state over both. The distinction is not new information
|
|
2088
|
+
* to obtain — it is already on the wire, and folding it away was the defect.
|
|
2089
|
+
* The maintainer ruling (2026-09-13) named the two members; this type is the
|
|
2090
|
+
* two-member widening that ruling declared.
|
|
2091
|
+
*
|
|
2092
|
+
* ⚠️ `unknown` no longer means "absent". A consumer that treats `unknown` as a
|
|
2093
|
+
* positive absence is reading a verdict that now means the opposite: that the
|
|
2094
|
+
* question was never put.
|
|
2095
|
+
*/
|
|
2096
|
+
type AppAccessVerdict = 'granted' | 'denied' | 'not_found' | 'unreachable' | 'unknown';
|
|
2097
|
+
/**
|
|
2098
|
+
* What {@link ObjectStackAdapter.probeMetaItemAccess} obtained about one item
|
|
2099
|
+
* of any type the by-name meta route gates per caller (objectui#12112). The
|
|
2100
|
+
* verdicts mean what they mean for an app ({@link AppAccessVerdict}).
|
|
1062
2101
|
*/
|
|
1063
|
-
|
|
2102
|
+
interface MetaItemAccess {
|
|
2103
|
+
readonly verdict: AppAccessVerdict;
|
|
2104
|
+
/**
|
|
2105
|
+
* The refusal's own `message`, as the server wrote it — for a dashboard,
|
|
2106
|
+
* `You do not have permission to open the 'NAME' dashboard.` Only on
|
|
2107
|
+
* `denied`, and absent when the server wrote none.
|
|
2108
|
+
*/
|
|
2109
|
+
readonly message?: string;
|
|
2110
|
+
}
|
|
1064
2111
|
/**
|
|
1065
2112
|
* The ADR-0112 standard catalog code the by-name meta app route answers with
|
|
1066
2113
|
* when an app exists and the session lacks its `requiredPermissions`
|
|
@@ -1084,9 +2131,32 @@ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
|
|
|
1084
2131
|
* are errors.
|
|
1085
2132
|
*/
|
|
1086
2133
|
declare function isAppPermissionDeniedError(error: unknown): boolean;
|
|
2134
|
+
/**
|
|
2135
|
+
* The ADR-0112 standard catalog code the metadata routes answer with when the
|
|
2136
|
+
* named item is not there for this caller (`sendError(res, 404,
|
|
2137
|
+
* 'RESOURCE_NOT_FOUND', …)` in `packages/rest/src/rest-server.ts`).
|
|
2138
|
+
*
|
|
2139
|
+
* One code covers every absence on purpose: a nonexistent name, an unpublished
|
|
2140
|
+
* app (ADR-0045 §3 keeps it externally unobservable), and an app gated by an
|
|
2141
|
+
* absent optional service (ADR-0057 D10 — nothing was denied to the CALLER) are
|
|
2142
|
+
* one answer by the 2026-08-12 ruling's design, not three that happen to
|
|
2143
|
+
* collide. Consumers may report the absence; they may not report a reason.
|
|
2144
|
+
*/
|
|
2145
|
+
declare const META_ITEM_ABSENT_CODE = "RESOURCE_NOT_FOUND";
|
|
2146
|
+
/**
|
|
2147
|
+
* True when `error` is a metadata route's "not here for you" answer.
|
|
2148
|
+
*
|
|
2149
|
+
* Discriminates on the ADR-0112 `code`, never on the status, for the same
|
|
2150
|
+
* reason {@link isAppPermissionDeniedError} does (objectui#4408): a 404 is a
|
|
2151
|
+
* transport fact many conditions share — a mis-typed route, a proxy, an
|
|
2152
|
+
* appliance in front of the server — while the code is the contract. A 404 that
|
|
2153
|
+
* carries no declared code is NOT an absence: nobody said the item was missing,
|
|
2154
|
+
* so the honest reading is that no answer was obtained.
|
|
2155
|
+
*/
|
|
2156
|
+
declare function isMetaItemAbsentError(error: unknown): boolean;
|
|
1087
2157
|
/**
|
|
1088
2158
|
* Thrown when the deployment has no analytics capability installed
|
|
1089
|
-
* (framework#3891 / #4019).
|
|
2159
|
+
* (framework#3891 / objectstack-ai/objectstack#4019).
|
|
1090
2160
|
*
|
|
1091
2161
|
* The framework retired its degraded in-kernel analytics fallback — it dropped
|
|
1092
2162
|
* the caller's RLS/tenant scope and ignored the contract filter, so it answered
|
|
@@ -1194,6 +2264,41 @@ declare class AnalyticsUnauthenticatedError extends Error {
|
|
|
1194
2264
|
serverMessage?: string;
|
|
1195
2265
|
});
|
|
1196
2266
|
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Thrown when the dataset query was refused because this user may not READ an
|
|
2269
|
+
* object the dataset queries — `403` + ADR-0112 `PERMISSION_DENIED`, which
|
|
2270
|
+
* `@objectstack/service-analytics`' read admission (`readAdmissionDeniedError`)
|
|
2271
|
+
* answers with the same code and status the data API gives the same user for
|
|
2272
|
+
* the same object (objectui#10899).
|
|
2273
|
+
*
|
|
2274
|
+
* The FOURTH branch. It used to fall through to the generic
|
|
2275
|
+
* `Dataset query failed: 403 Forbidden — [Analytics] Access denied: …` string,
|
|
2276
|
+
* which a dashboard tile then rendered verbatim: a raw English exception where
|
|
2277
|
+
* the list view over the same object shows its localized "no access" state.
|
|
2278
|
+
* That generic string names a transport status where a person needs a fact
|
|
2279
|
+
* ("you can't see this data"), and it is the one case a renderer must be able
|
|
2280
|
+
* to tell apart — so it is typed, and it carries `httpStatus` + a `code` the
|
|
2281
|
+
* shared `classifyLoadError` reads as `forbidden` without any string matching.
|
|
2282
|
+
*
|
|
2283
|
+
* `serverMessage` is kept for diagnostics only. The producer deliberately
|
|
2284
|
+
* names the object and nothing else, but it is still platform prose, not text
|
|
2285
|
+
* addressed to the end user.
|
|
2286
|
+
*/
|
|
2287
|
+
declare class AnalyticsForbiddenError extends Error {
|
|
2288
|
+
readonly code = "PERMISSION_DENIED";
|
|
2289
|
+
readonly httpStatus = 403;
|
|
2290
|
+
/** The server's own ADR-0112 code — the field this branch was chosen BY. */
|
|
2291
|
+
readonly serverCode?: string;
|
|
2292
|
+
/** The server's own message, verbatim (diagnostics only). */
|
|
2293
|
+
readonly serverMessage?: string;
|
|
2294
|
+
/** The dataset the refused query asked for, when it named one. */
|
|
2295
|
+
readonly datasetName?: string;
|
|
2296
|
+
constructor(opts?: {
|
|
2297
|
+
serverCode?: string;
|
|
2298
|
+
serverMessage?: string;
|
|
2299
|
+
datasetName?: string;
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
1197
2302
|
/**
|
|
1198
2303
|
* Thrown when the server REJECTED the analytics query body (HTTP 400 —
|
|
1199
2304
|
* `VALIDATION_FAILED` since framework#4010 validates `/analytics/query` at the
|
|
@@ -1210,6 +2315,49 @@ declare class AnalyticsQueryRejectedError extends Error {
|
|
|
1210
2315
|
readonly serverCode?: string;
|
|
1211
2316
|
constructor(detail?: string, serverCode?: string);
|
|
1212
2317
|
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Thrown by `aggregate()` when `client.analytics.query` resolves to anything
|
|
2320
|
+
* other than the post-unwrap `AnalyticsResult` — concretely, when the value it
|
|
2321
|
+
* hands back has no `rows` array (objectui#7028).
|
|
2322
|
+
*
|
|
2323
|
+
* ONE SPELLING, BY RULING. `@objectstack/client` 17.3.0 converged
|
|
2324
|
+
* `analytics.query` on `unwrapResponse` (objectstack#13079, maintainer ruling
|
|
2325
|
+
* 2026-08-31, option A): the method resolves to the payload, and the caller
|
|
2326
|
+
* reads `result.rows`. The same ruling ordered this adapter's tolerant row
|
|
2327
|
+
* ladder tightened in the same wave, so that defensive code is not read as a
|
|
2328
|
+
* contract. The ladder also read a bare array, `data` as an array,
|
|
2329
|
+
* `data.data.rows` and `results`; `rows` is now the only spelling read, and
|
|
2330
|
+
* everything else lands here.
|
|
2331
|
+
*
|
|
2332
|
+
* WHY A THROW AND NOT `[]`. The ladder answered every shape it did not
|
|
2333
|
+
* recognise with an empty array, and an empty array is a RESULT: a KPI renders
|
|
2334
|
+
* a confident zero and a chart reads "no data" over a populated table — the
|
|
2335
|
+
* lie objectui#5954 removed on the failure side. The envelope arriving here
|
|
2336
|
+
* means the SDK in front of this call predates the convergence (it is older
|
|
2337
|
+
* than 17.3.0), or something between it and `POST /analytics/query` wraps the
|
|
2338
|
+
* payload a second time. Either way the adapter is handed a contract it no
|
|
2339
|
+
* longer reads, and saying so is the only answer that names the repair.
|
|
2340
|
+
*
|
|
2341
|
+
* WHY NOT THE CLIENT-SIDE FALLBACK. Analytics answered; this is not a
|
|
2342
|
+
* transport failure. Degrading would put plausible numbers from a different
|
|
2343
|
+
* code path over a contract violation — the misdirection
|
|
2344
|
+
* {@link AnalyticsQueryRejectedError} refuses for the same reason
|
|
2345
|
+
* (framework#3878) — so `aggregate()`'s catch rethrows this before it
|
|
2346
|
+
* classifies anything.
|
|
2347
|
+
*/
|
|
2348
|
+
declare class AnalyticsResultShapeError extends Error {
|
|
2349
|
+
readonly code = "ANALYTICS_RESULT_SHAPE_INVALID";
|
|
2350
|
+
/** The object `aggregate()` was called for. */
|
|
2351
|
+
readonly resource: string;
|
|
2352
|
+
/**
|
|
2353
|
+
* True when the value is the envelope the client stopped handing back at
|
|
2354
|
+
* 17.3.0 — `{ success, data: { rows } }`, or its `{ data: { rows } }` core.
|
|
2355
|
+
*/
|
|
2356
|
+
readonly envelope: boolean;
|
|
2357
|
+
/** What the value was, so the producer is identifiable from a log. */
|
|
2358
|
+
readonly received: string;
|
|
2359
|
+
constructor(resource: string, result: unknown);
|
|
2360
|
+
}
|
|
1213
2361
|
/**
|
|
1214
2362
|
* Classify a FAILED analytics call so the caller knows whether to degrade or
|
|
1215
2363
|
* to surface the failure.
|
|
@@ -1257,11 +2405,18 @@ declare class AnalyticsQueryRejectedError extends Error {
|
|
|
1257
2405
|
* `ROUTE_NOT_FOUND` (framework#4019 stops mounting the routes at all).
|
|
1258
2406
|
* A client-side aggregate over a scoped `find()` answers the chart
|
|
1259
2407
|
* correctly, and the operator is told once that the semantic layer is off.
|
|
1260
|
-
* - **`rejected`** — *THROW*. The server refused OUR body
|
|
1261
|
-
* `VALIDATION_FAILED
|
|
1262
|
-
* entry)
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
2408
|
+
* - **`rejected`** — *THROW*. The server refused OUR body: 400
|
|
2409
|
+
* `VALIDATION_FAILED` (framework#4010 validates `/analytics/query` at the
|
|
2410
|
+
* entry) OR any other coded 400 (objectui#7755 — e.g. `service-analytics`
|
|
2411
|
+
* ships its own 400 `INVALID_FILTER` on a filter shape it refuses). The
|
|
2412
|
+
* status alone is decisive once it is 400: a producer that refuses the
|
|
2413
|
+
* body and ships a code this consumer does not enumerate is refusing it
|
|
2414
|
+
* exactly as hard as one that ships `VALIDATION_FAILED` — the refusal is
|
|
2415
|
+
* in the transport fact, not in which code spells it. Degrading would
|
|
2416
|
+
* answer our own contract violation with plausible numbers from a
|
|
2417
|
+
* different code path (`find()`'s `$filter` accepts array shapes the
|
|
2418
|
+
* analytics body does not) and bury it — the misdirection framework#3878
|
|
2419
|
+
* documented.
|
|
1265
2420
|
* - **`unauthenticated`** — *THROW*. 401 `UNAUTHENTICATED`: the request was
|
|
1266
2421
|
* refused before it ran, so it is evidence about the SESSION and none at
|
|
1267
2422
|
* all about the capability. Degrading is not merely misleading here, it is
|
|
@@ -1309,16 +2464,46 @@ declare class ConcurrentUpdateError extends Error {
|
|
|
1309
2464
|
readonly httpStatus = 409;
|
|
1310
2465
|
readonly currentVersion: string | null;
|
|
1311
2466
|
readonly currentRecord: unknown;
|
|
2467
|
+
/**
|
|
2468
|
+
* The refusal text the PRODUCER marked as addressed to the end user
|
|
2469
|
+
* (`ApiErrorSchema.userMessage`, objectstack `79c46da90`), or `null` when the
|
|
2470
|
+
* refusal carried no marking.
|
|
2471
|
+
*
|
|
2472
|
+
* Declared on the class because this error has no `details` bag: the shared
|
|
2473
|
+
* reader (`declaredUserMessage` in `@object-ui/react`) looks at the
|
|
2474
|
+
* top-level field first, so parking it here is what makes the marking
|
|
2475
|
+
* readable at the surface. The contract is status-agnostic — a 409 carries
|
|
2476
|
+
* it exactly like a 400 or a 403 does.
|
|
2477
|
+
*/
|
|
2478
|
+
readonly userMessage: string | null;
|
|
1312
2479
|
constructor(opts: {
|
|
1313
2480
|
currentVersion: string | null;
|
|
1314
2481
|
currentRecord: unknown;
|
|
1315
2482
|
message?: string;
|
|
2483
|
+
userMessage?: string | null;
|
|
1316
2484
|
});
|
|
1317
2485
|
}
|
|
1318
2486
|
/**
|
|
1319
2487
|
* Detect "concurrent update" errors raised by the platform. The wire
|
|
1320
2488
|
* shape is `409` + `code: 'CONCURRENT_UPDATE'`. The client surfaces
|
|
1321
2489
|
* extra details on `error.details` (full response body).
|
|
2490
|
+
*
|
|
2491
|
+
* Accepts EITHER that wire `code` OR `name === 'ConcurrentUpdateError'`, and
|
|
2492
|
+
* reads `httpStatus` for neither. This paragraph exists because the doc used
|
|
2493
|
+
* to name only the wire shape, which left the `name` limb reading as drift
|
|
2494
|
+
* (objectui#6375). It is not drift: it is the deliberate cross-realm
|
|
2495
|
+
* duck-check — a host that bundles this package twice (or re-throws across a
|
|
2496
|
+
* worker boundary) ends up holding two copies of the class, `instanceof`
|
|
2497
|
+
* fails, and the `name` string is the only discriminator left. That host is out of tree by construction, so an in-repo
|
|
2498
|
+
* consumer census cannot see the case the limb was written for and is not
|
|
2499
|
+
* evidence against it; `@object-ui/plugin-form` and `@object-ui/plugin-detail`
|
|
2500
|
+
* each carry their own copy of the same two-limb check for adapters they must
|
|
2501
|
+
* not depend on.
|
|
2502
|
+
*
|
|
2503
|
+
* Deliberately WIDER than {@link normaliseClientError}'s re-wrap, which keys
|
|
2504
|
+
* on the wire `code` alone: an error carrying only the class name is
|
|
2505
|
+
* recognised here and passed through there. Both accepted sets are pinned in
|
|
2506
|
+
* `occ.test.ts`.
|
|
1322
2507
|
*/
|
|
1323
2508
|
declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpdateError;
|
|
1324
2509
|
/**
|
|
@@ -1331,6 +2516,12 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
|
|
|
1331
2516
|
* - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
|
|
1332
2517
|
* server's per-field entries so a form can mark the offending inputs
|
|
1333
2518
|
* instead of showing one undirected toast.
|
|
2519
|
+
*
|
|
2520
|
+
* Both re-wraps preserve the producer's `userMessage` marking (see
|
|
2521
|
+
* {@link liftUserMessage}). Re-wrapping used to drop it, which made the
|
|
2522
|
+
* marking unreadable at every surface downstream of this boundary — nothing
|
|
2523
|
+
* threw, the typed error was otherwise correct, and the user simply got a
|
|
2524
|
+
* generic string.
|
|
1334
2525
|
*/
|
|
1335
2526
|
declare function normaliseClientError(error: unknown): unknown;
|
|
1336
2527
|
/**
|
|
@@ -1377,10 +2568,7 @@ declare function formatHttpFailureMessage(message: string, meta?: Record<string,
|
|
|
1377
2568
|
* own `ObjectStackClient` gets the same identified failures.
|
|
1378
2569
|
*/
|
|
1379
2570
|
declare function createQuietHttpLogger(): any;
|
|
1380
|
-
|
|
1381
|
-
* Connection state for monitoring
|
|
1382
|
-
*/
|
|
1383
|
-
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
|
|
2571
|
+
|
|
1384
2572
|
/**
|
|
1385
2573
|
* Connection state change event
|
|
1386
2574
|
*/
|
|
@@ -1408,20 +2596,306 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
|
|
|
1408
2596
|
*/
|
|
1409
2597
|
type BatchProgressListener = (event: BatchProgressEvent) => void;
|
|
1410
2598
|
|
|
2599
|
+
/**
|
|
2600
|
+
* The `reason` of a write-strip this bundle's spec pin cannot name
|
|
2601
|
+
* (objectui#4934).
|
|
2602
|
+
*
|
|
2603
|
+
* Deliberately NOT a spec spelling: it is namespaced so it can never collide
|
|
2604
|
+
* with an arm `@objectstack/spec` adds later (a collision would merge real
|
|
2605
|
+
* reasons into the skew bucket — the failure this whole card is about, one level
|
|
2606
|
+
* up). `droppedFieldsReason.boundary.test.ts` pins that the installed spec does
|
|
2607
|
+
* not declare it.
|
|
2608
|
+
*/
|
|
2609
|
+
declare const UNRECOGNIZED_DROP_REASON = "objectui:unrecognized-drop-reason";
|
|
2610
|
+
/**
|
|
2611
|
+
* The skew arm: one server-reported write-strip whose `reason` is outside the
|
|
2612
|
+
* enum this bundle's `@objectstack/spec` pin declares (objectui#4934).
|
|
2613
|
+
*
|
|
2614
|
+
* A deployed client normally runs BEHIND the server it talks to, so a reason
|
|
2615
|
+
* from the future is the expected skew direction, not a corrupt payload. The
|
|
2616
|
+
* boundary used to assert such an entry into {@link DroppedFieldsEvent} on shape
|
|
2617
|
+
* alone — `reason` was never read — so the interior was typed to trust a union
|
|
2618
|
+
* nothing had checked, and the next consumer to write an exhaustive-looking
|
|
2619
|
+
* table over `DroppedFieldsEvent['reason']` would have been handed a value that
|
|
2620
|
+
* type says is impossible.
|
|
2621
|
+
*
|
|
2622
|
+
* Three properties of this shape are load-bearing:
|
|
2623
|
+
*
|
|
2624
|
+
* - The entry is **kept**. Dropping it would tell the user nothing about fields
|
|
2625
|
+
* the server really did strip — precisely the silence objectui#3484 removed.
|
|
2626
|
+
* - The wire value is **preserved verbatim** in {@link unrecognizedReason},
|
|
2627
|
+
* never coerced or normalised onto a known arm: claiming `readonly` for a
|
|
2628
|
+
* reason we cannot name is a false statement about the user's data, and it is
|
|
2629
|
+
* also unfalsifiable once the original value is gone.
|
|
2630
|
+
* - `reason` carries {@link UNRECOGNIZED_DROP_REASON}, which is not assignable
|
|
2631
|
+
* to `DroppedFieldsEvent['reason']`. That is what makes the skew case visible
|
|
2632
|
+
* to `tsc` at every consumer instead of resting on N per-consumer
|
|
2633
|
+
* disciplines — and it keeps the spec type as the canonical arm rather than
|
|
2634
|
+
* widening the whole surface to `string` (objectui#3160).
|
|
2635
|
+
*/
|
|
2636
|
+
interface UnrecognizedDropReasonEvent {
|
|
2637
|
+
object?: string;
|
|
2638
|
+
fields: string[];
|
|
2639
|
+
reason: typeof UNRECOGNIZED_DROP_REASON;
|
|
2640
|
+
/** Whatever the server sent, untouched — including a non-string or nothing at all. */
|
|
2641
|
+
unrecognizedReason: unknown;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* One entry of a write-warning: either the spec type (canonical arm) or the
|
|
2645
|
+
* named skew arm above. Narrow with `entry.reason === UNRECOGNIZED_DROP_REASON`.
|
|
2646
|
+
*/
|
|
2647
|
+
type DroppedFieldsNotice = DroppedFieldsEvent | UnrecognizedDropReasonEvent;
|
|
1411
2648
|
/**
|
|
1412
2649
|
* Emitted after a create/update whose response carried `droppedFields`
|
|
1413
|
-
* (
|
|
1414
|
-
*
|
|
1415
|
-
*
|
|
2650
|
+
* (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455) or
|
|
2651
|
+
* `warnings` (objectui#12107). The write SUCCEEDED — this is a warning that
|
|
2652
|
+
* some supplied fields never landed, or that an advisory validation rule hit,
|
|
2653
|
+
* so the UI can tell the user rather than let it pass silently. Subscribe via
|
|
2654
|
+
* {@link ObjectStackAdapter.onWriteWarning}.
|
|
2655
|
+
*
|
|
2656
|
+
* `droppedFields` is the two-arm {@link DroppedFieldsNotice} and not
|
|
2657
|
+
* `DroppedFieldsEvent[]`: the wire is parsed here, and an entry whose `reason`
|
|
2658
|
+
* this bundle's spec pin cannot name arrives on the explicit skew arm rather
|
|
2659
|
+
* than being asserted into the union (objectui#4934). It stays REQUIRED and is
|
|
2660
|
+
* an empty array when the event exists only for `warnings`.
|
|
2661
|
+
*
|
|
2662
|
+
* `warnings` is the write answer's own key, typed by the spec's
|
|
2663
|
+
* `ValidateDataIssueSchema` (the element of `CreateDataResponseSchema` /
|
|
2664
|
+
* `UpdateDataResponseSchema`'s `warnings`) and parsed through it here. It is
|
|
2665
|
+
* present only when at least one entry parsed: an answer without the key —
|
|
2666
|
+
* including one from a runtime that predates it — leaves the event exactly as
|
|
2667
|
+
* it was.
|
|
1416
2668
|
*/
|
|
1417
2669
|
interface WriteWarningEvent {
|
|
1418
2670
|
operation: 'create' | 'update';
|
|
1419
2671
|
resource: string;
|
|
1420
2672
|
id?: string | number;
|
|
1421
|
-
droppedFields:
|
|
2673
|
+
droppedFields: DroppedFieldsNotice[];
|
|
2674
|
+
warnings?: ValidateDataIssue[];
|
|
1422
2675
|
}
|
|
1423
2676
|
/** Event listener type for write-warning (dropped-fields) events. */
|
|
1424
2677
|
type WriteWarningListener = (event: WriteWarningEvent) => void;
|
|
2678
|
+
/**
|
|
2679
|
+
* What a FAILED `listImportMappings` read actually was (objectui#7741).
|
|
2680
|
+
*
|
|
2681
|
+
* - `not-served` — this deployment does not serve the `mapping` kind, or has
|
|
2682
|
+
* no `/meta` door at all. QUIET: a real, supported deployment shape, and the
|
|
2683
|
+
* empty list plus a hidden selector is the correct rendering of it.
|
|
2684
|
+
* - `refused` — the server answered and declined THIS caller: anonymous,
|
|
2685
|
+
* lapsed token, missing permission, method withheld. LOUD, and the remedy
|
|
2686
|
+
* names a person: sign in again, or ask for the grant.
|
|
2687
|
+
* - `unreadable` — the read could not be completed for any other reason: a
|
|
2688
|
+
* 5xx, a network failure, a code this consumer cannot name. LOUD, and the
|
|
2689
|
+
* remedy is to retry or report.
|
|
2690
|
+
*
|
|
2691
|
+
* The last two are the same verdict for the user — *we could not find out* —
|
|
2692
|
+
* and they are separated because the sentence that helps differs. What they
|
|
2693
|
+
* share is what matters: neither is evidence that no mapping is registered.
|
|
2694
|
+
*
|
|
2695
|
+
* ⚠️ The three NAMES are shared by every classifier feeding
|
|
2696
|
+
* {@link MetadataReadWarningEvent}; the POPULATION each name covers is not, and
|
|
2697
|
+
* is decided per read door. `not-served` in particular is a strictly smaller
|
|
2698
|
+
* set on `view` than on `mapping` — see {@link classifyViewsFailure}.
|
|
2699
|
+
*/
|
|
2700
|
+
type MetadataReadFailureKind = 'not-served' | 'refused' | 'unreadable';
|
|
2701
|
+
/**
|
|
2702
|
+
* The published spelling of {@link MetadataReadFailureKind}, kept as the name
|
|
2703
|
+
* objectui#7741 shipped (`@object-ui/data-objectstack@17.2.0`).
|
|
2704
|
+
*
|
|
2705
|
+
* An ALIAS, not a second declaration: identical members, so every existing
|
|
2706
|
+
* consumer's assignability is unchanged in both directions. It was renamed
|
|
2707
|
+
* because the channel gained a second emitter (objectui#8151) and a type called
|
|
2708
|
+
* `ImportMappingsFailureKind` describing a `view` read would be a name that
|
|
2709
|
+
* lies; the old name stays because retiring a published one is not this card's
|
|
2710
|
+
* business.
|
|
2711
|
+
*/
|
|
2712
|
+
type ImportMappingsFailureKind = MetadataReadFailureKind;
|
|
2713
|
+
/**
|
|
2714
|
+
* Classify a FAILED `meta.getItems('mapping')` call so the caller knows whether
|
|
2715
|
+
* to stay quiet or to say something (objectui#7741).
|
|
2716
|
+
*
|
|
2717
|
+
* ## Why this function exists
|
|
2718
|
+
*
|
|
2719
|
+
* `listImportMappings` degrades every failure to `[]`, and the wizard hides its
|
|
2720
|
+
* saved-mapping selector on an empty list. So "the server served zero mappings"
|
|
2721
|
+
* and "the server never answered" rendered identically — the same UI, on every
|
|
2722
|
+
* deployment, with a `console.warn` as the only discriminator. That silence did
|
|
2723
|
+
* not merely hide a fault: it produced a confident WRONG diagnosis in a careful
|
|
2724
|
+
* reporter (objectstack#14026 was filed, routed and worked by two seats against
|
|
2725
|
+
* a repo whose wizard had been correct since `@object-ui/data-objectstack`
|
|
2726
|
+
* 17.1.0), and the misdiagnosis travelled further than the fault would have.
|
|
2727
|
+
*
|
|
2728
|
+
* This is the discrimination framework objectstack#13906 decision 1 option A already
|
|
2729
|
+
* adopted at the tenancy-posture seam — *a thing that could not be READ is not
|
|
2730
|
+
* a thing that is ABSENT* — applied here. It is not a new principle.
|
|
2731
|
+
*
|
|
2732
|
+
* ## It reads the ERROR, never the result
|
|
2733
|
+
*
|
|
2734
|
+
* The one thing this must never do is decide from "is the result an empty
|
|
2735
|
+
* array". An empty array is what BOTH conditions produce, so a test on it can
|
|
2736
|
+
* never fail and can never separate *served zero* from *did not serve* — a
|
|
2737
|
+
* reading that cannot fail is indistinguishable from a reading that passed.
|
|
2738
|
+
* Every branch below reads `err` itself: the ADR-0112 `code` first (the
|
|
2739
|
+
* contract), the status only where no code was declared (a transport fact many
|
|
2740
|
+
* conditions share). Same rule, same order, and for the same reasons as
|
|
2741
|
+
* {@link classifyAnalyticsFailure} (objectui#5663 / objectui#5721).
|
|
2742
|
+
*
|
|
2743
|
+
* `@objectstack/client`'s fetch wrapper hands both envelope families here
|
|
2744
|
+
* already flattened — `errorBody?.code ?? errorBody?.error?.code` onto
|
|
2745
|
+
* `error.code`, plus `error.httpStatus = res.status` — so no envelope reading
|
|
2746
|
+
* belongs in this function. Comparisons go through `errorCodeIs`/
|
|
2747
|
+
* `errorCodeIsAnyOf` so the pre- and post-ADR-0112 spellings both match.
|
|
2748
|
+
*/
|
|
2749
|
+
declare function classifyImportMappingsFailure(error: unknown): {
|
|
2750
|
+
kind: ImportMappingsFailureKind;
|
|
2751
|
+
code?: string;
|
|
2752
|
+
status?: number;
|
|
2753
|
+
message?: string;
|
|
2754
|
+
};
|
|
2755
|
+
/**
|
|
2756
|
+
* Classify a FAILED `view` metadata list read so {@link ObjectStackAdapter.listViews}
|
|
2757
|
+
* knows whether to stay quiet or to say something (objectui#8151).
|
|
2758
|
+
*
|
|
2759
|
+
* ## Why this is a SEPARATE reading and not a second caller of
|
|
2760
|
+
* {@link classifyImportMappingsFailure}
|
|
2761
|
+
*
|
|
2762
|
+
* The two methods share a defect and a remedy, not a population. The quiet arm
|
|
2763
|
+
* `listImportMappings` is built around — *an older server that does not serve
|
|
2764
|
+
* this kind* — has no members on `view`, and carrying it over would put a fresh
|
|
2765
|
+
* swallow into the method this card exists to un-swallow. Measured, on the
|
|
2766
|
+
* framework tree, three ways:
|
|
2767
|
+
*
|
|
2768
|
+
* 1. **`view` is in the platform's static spelling contract**, so the metadata
|
|
2769
|
+
* LIST door's unknown-kind refusal is unreachable for it.
|
|
2770
|
+
* `RestServer.refuseUnknownMetaListType` (framework#9488,
|
|
2771
|
+
* `packages/rest/src/rest-server.ts`) returns without writing a refusal
|
|
2772
|
+
* whenever `unrecognisedMetaTypeRefusal(urlType)` is `null`, and that
|
|
2773
|
+
* predicate answers `null` for every spelling in the contract.
|
|
2774
|
+
* `packages/spec/src/meta-spelling/meta-url-data.generated.ts` carries both
|
|
2775
|
+
* `"views": "view"` and the canonical singular `view`. So a `400`
|
|
2776
|
+
* `INVALID_REQUEST` on `GET /meta/view` is NEVER that door saying "this
|
|
2777
|
+
* deployment carries no such kind" — it is some other refusal of the
|
|
2778
|
+
* request, and reading it as kind-absence would silence a real one.
|
|
2779
|
+
* 2. **There is no "before" for `view` to be older than.** `mapping`'s quiet
|
|
2780
|
+
* arm exists because `mapping` was PROMOTED into the declared set
|
|
2781
|
+
* (framework#2611), so servers predating the promotion are a real, shipped
|
|
2782
|
+
* population. `view` is the kind the metadata surface is built around — the
|
|
2783
|
+
* compound-arity door `/meta/<object>/views/<name>`, the ADR-0017
|
|
2784
|
+
* `ViewItem` discriminant this very method filters on.
|
|
2785
|
+
* 3. **A deployment that cannot serve `view` cannot render the caller.**
|
|
2786
|
+
* `listViews` is reached only from `ObjectView`, whose `objectDef` came from
|
|
2787
|
+
* `MetadataProvider`, which reads the SAME door and lists `view` among its
|
|
2788
|
+
* `EAGER_TYPES` at mount (`packages/app-shell/src/providers/MetadataProvider.tsx`).
|
|
2789
|
+
* A server that does not answer `/meta/view` has already failed that read
|
|
2790
|
+
* before any object page exists.
|
|
2791
|
+
*
|
|
2792
|
+
* ## So the quiet set here is strictly SMALLER — it is the DOORLESS one only
|
|
2793
|
+
*
|
|
2794
|
+
* What survives as quiet is not about `view` at all: it is *this host mounted no
|
|
2795
|
+
* metadata door*. That stays silent for the reason objectui#7741 gave — a real,
|
|
2796
|
+
* supported deployment shape must not be turned into a visible fault — and it
|
|
2797
|
+
* costs nothing to keep silent here, because such a host has already failed
|
|
2798
|
+
* `MetadataProvider`'s eager `app`/`object`/`view` reads; a per-object toast
|
|
2799
|
+
* would be a fourth voice on one deployment fact, not a new one.
|
|
2800
|
+
*
|
|
2801
|
+
* Everything else is LOUD. In particular a `refused` (401/403/405,
|
|
2802
|
+
* `UNAUTHENTICATED`, `PERMISSION_DENIED`) is the condition this card was filed
|
|
2803
|
+
* for: a token that lapsed mid-session renders the object's view switcher as
|
|
2804
|
+
* though the user's own saved views did not exist.
|
|
2805
|
+
*
|
|
2806
|
+
* ## It reads the ERROR, never the result — and from BOTH of this method's doors
|
|
2807
|
+
*
|
|
2808
|
+
* `listViews` is fed by two transports, unlike its sibling's one, and they
|
|
2809
|
+
* decorate differently:
|
|
2810
|
+
*
|
|
2811
|
+
* published `client.meta.getItems('view')` -> `@objectstack/client`'s fetch
|
|
2812
|
+
* wrapper: `error.code` (flattened from either envelope family)
|
|
2813
|
+
* plus `error.httpStatus`.
|
|
2814
|
+
* drafts `MetadataClient.withPreviewDrafts(true).list('view')` (ADR-0037,
|
|
2815
|
+
* `?preview=draft`) -> this package's own `parseError`:
|
|
2816
|
+
* `err.code` plus **`err.status`**, with no `httpStatus` at all.
|
|
2817
|
+
*
|
|
2818
|
+
* The status ladder below therefore reads `httpStatus`, then `status`, then
|
|
2819
|
+
* `statusCode`, and is the reason a preview-mode failure classifies the same as
|
|
2820
|
+
* a published-mode one instead of falling through to the code-less residual.
|
|
2821
|
+
* What it must never read is "is the result an empty array": that is what BOTH
|
|
2822
|
+
* a served-zero and a refusal produce, so a test on it can never fail for the
|
|
2823
|
+
* condition it is supposed to be about. This is framework objectstack#13906 decision 1
|
|
2824
|
+
* option A — *a thing that could not be READ is not a thing that is ABSENT*.
|
|
2825
|
+
*/
|
|
2826
|
+
declare function classifyViewsFailure(error: unknown): {
|
|
2827
|
+
kind: MetadataReadFailureKind;
|
|
2828
|
+
code?: string;
|
|
2829
|
+
status?: number;
|
|
2830
|
+
message?: string;
|
|
2831
|
+
};
|
|
2832
|
+
/**
|
|
2833
|
+
* Emitted when a metadata READ was answered by the server with a failure that
|
|
2834
|
+
* is NOT the supported "this deployment does not serve that kind" shape, and
|
|
2835
|
+
* the adapter degraded it to an empty result anyway (objectui#7741).
|
|
2836
|
+
*
|
|
2837
|
+
* The degrade is deliberate and unchanged — the caller still receives `[]`, and
|
|
2838
|
+
* `listImportMappings`' published return type has not moved. This event is the
|
|
2839
|
+
* channel ALONGSIDE it: the fact that the empty result is an artefact of a
|
|
2840
|
+
* failed read, carried in a form a consumer can act on, instead of a
|
|
2841
|
+
* `console.warn` nothing in the UI points at.
|
|
2842
|
+
*
|
|
2843
|
+
* Subscribe via {@link ObjectStackAdapter.onMetadataReadWarning}. A sibling of
|
|
2844
|
+
* {@link WriteWarningEvent} and `MetadataSaveAdvisoryEvent` in SHAPE — one
|
|
2845
|
+
* long-lived instance, `subscribe → unsubscribe`, `AdapterProvider` wires it
|
|
2846
|
+
* once — and deliberately not a payload pushed down either of those: both of
|
|
2847
|
+
* them describe a write that SUCCEEDED, so carrying a failed read on one would
|
|
2848
|
+
* make the event lie about what happened.
|
|
2849
|
+
*
|
|
2850
|
+
* `operation` and `kind` are CLOSED unions on purpose. Every emitter is named
|
|
2851
|
+
* in them, so widening is an additive, reviewed change rather than something a
|
|
2852
|
+
* consumer's exhaustive switch discovers at runtime. (The same trade
|
|
2853
|
+
* `WriteWarningEvent`'s required `operation` documents, taken deliberately
|
|
2854
|
+
* here.)
|
|
2855
|
+
*
|
|
2856
|
+
* ## The widening this design was built for happened (objectui#8151)
|
|
2857
|
+
*
|
|
2858
|
+
* objectui#7741 shipped both fields as SINGLE-member unions and said in as many
|
|
2859
|
+
* words that a second emitter should arrive as a reviewed widening. It has:
|
|
2860
|
+
* `listViews` is the second, for the same defect one method over, and the
|
|
2861
|
+
* mechanism worked as designed — the consumer that renders these events
|
|
2862
|
+
* (`app-shell`'s `metadataReadWarningToast`) had a `switch` that named the one
|
|
2863
|
+
* operation, so adding the member turned "a views failure is toasted as an
|
|
2864
|
+
* import-mapping failure" into a COMPILE error instead of a runtime lie.
|
|
2865
|
+
*
|
|
2866
|
+
* ⚠️ The pair is the emitter's invariant, not the type's: `operation` and
|
|
2867
|
+
* `kind` are two independent unions, so nothing in the type stops
|
|
2868
|
+
* `{ operation: 'listViews', kind: 'mapping' }` being constructed. Each emitter
|
|
2869
|
+
* writes its own pair, one line apart, and every consumer branches on
|
|
2870
|
+
* `operation` — the adapter METHOD, which is what names the list the user is
|
|
2871
|
+
* actually looking at. Tightening this to a discriminated union would mean
|
|
2872
|
+
* turning a published `interface` into a type alias; that is a reviewable
|
|
2873
|
+
* change on its own terms and is deliberately not smuggled in here.
|
|
2874
|
+
*/
|
|
2875
|
+
interface MetadataReadWarningEvent {
|
|
2876
|
+
/** The adapter method whose read failed. */
|
|
2877
|
+
operation: 'listImportMappings' | 'listViews';
|
|
2878
|
+
/** The metadata kind it asked for. */
|
|
2879
|
+
kind: 'mapping' | 'view';
|
|
2880
|
+
/** The object the read was scoped to. */
|
|
2881
|
+
objectName: string;
|
|
2882
|
+
/**
|
|
2883
|
+
* Which loud verdict this is — see {@link MetadataReadFailureKind}. Never
|
|
2884
|
+
* `'not-served'`: that arm is the supported deployment shape and is not
|
|
2885
|
+
* emitted at all, so a subscriber never has to filter it out. ⚠️ WHICH
|
|
2886
|
+
* failures fall in that un-emitted arm differs per `operation` — it is a
|
|
2887
|
+
* strictly smaller set on `listViews` (see {@link classifyViewsFailure}).
|
|
2888
|
+
*/
|
|
2889
|
+
reason: Exclude<MetadataReadFailureKind, 'not-served'>;
|
|
2890
|
+
/** The server's own ADR-0112 code, when it declared one. */
|
|
2891
|
+
code?: string;
|
|
2892
|
+
/** The HTTP status, when the failure carried one. */
|
|
2893
|
+
status?: number;
|
|
2894
|
+
/** The server's own message, when it sent one. Rendered verbatim, not translated. */
|
|
2895
|
+
message?: string;
|
|
2896
|
+
}
|
|
2897
|
+
/** Event listener type for metadata read-warning events. */
|
|
2898
|
+
type MetadataReadWarningListener = (event: MetadataReadWarningEvent) => void;
|
|
1425
2899
|
|
|
1426
2900
|
/**
|
|
1427
2901
|
* Resolve which object a `type='view'` metadata item belongs to.
|
|
@@ -1441,8 +2915,17 @@ type WriteWarningListener = (event: WriteWarningEvent) => void;
|
|
|
1441
2915
|
*
|
|
1442
2916
|
* `object` is the identity field the write path stamps (and that the
|
|
1443
2917
|
* framework's overlay heals onto identity-less personalization rows —
|
|
1444
|
-
* objectstack#2555); `data.object` is the config's data-provider target
|
|
1445
|
-
*
|
|
2918
|
+
* objectstack#2555); `data.object` is the config's data-provider target.
|
|
2919
|
+
*
|
|
2920
|
+
* ⛔ No `objectName` leg (objectui#11013, ruling 甲 on objectstack#20051:
|
|
2921
|
+
* objectui reads a stored view row by the spec's declared spellings). The
|
|
2922
|
+
* spec declares `object` on every `view` member — required on the ViewItem
|
|
2923
|
+
* record and on both flattened overlays — and declares `objectName` on none,
|
|
2924
|
+
* so the metadata write door refuses a row that carries only `objectName`
|
|
2925
|
+
* (`viewItemObjectName.declaredSpelling-11013.test.ts` pins that door beside
|
|
2926
|
+
* this reader). Every write path in this package stamps `object` as well, so a
|
|
2927
|
+
* row the console wrote carries it; a row that carries ONLY the undeclared
|
|
2928
|
+
* spelling no longer matches any object here.
|
|
1446
2929
|
*
|
|
1447
2930
|
* **Exported** since objectui#4373, for the same one-spelling reason: a writer
|
|
1448
2931
|
* outside this module that holds a view BODY but not its object name (app-shell's
|
|
@@ -1456,13 +2939,18 @@ declare function viewItemObjectName(item: any): string | undefined;
|
|
|
1456
2939
|
* The keys a personalization overlay row legitimately OWNS (objectui#5233).
|
|
1457
2940
|
*
|
|
1458
2941
|
* One per `persistViewPatch` call site in app-shell's `ObjectView` — the ONLY
|
|
1459
|
-
* production writer of these rows
|
|
2942
|
+
* production writer of these rows, read off the tree when it was written:
|
|
1460
2943
|
* `rowHeight` (the density toggle, spec-canonical since #2890), `sort`,
|
|
1461
|
-
* `hiddenFields
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
2944
|
+
* `hiddenFields` and `columnState`. Plus `inlineEdit`, which no console call
|
|
2945
|
+
* site wrote after objectui#5144 (triage's ruling E: the inline-edit toggle
|
|
2946
|
+
* is session-only). That writer is retired (objectui#12504): no toolbar
|
|
2947
|
+
* control writes an overlay, and every key here is kept so an overlay an
|
|
2948
|
+
* earlier console wrote is still read; the rulings keep that data rather than
|
|
2949
|
+
* migrating it. Nothing else in
|
|
2950
|
+
* such a row is an opinion the user expressed; anything else it carries is a
|
|
2951
|
+
* COPY of the source view as it stood at write time, because
|
|
2952
|
+
* `persistViewPatch` USED TO send `{ ...baseViewDef, ...patch }` and this
|
|
2953
|
+
* adapter persists what it is given.
|
|
1466
2954
|
*
|
|
1467
2955
|
* That copy was the defect the maintainer ruled on (objectstack#7494, comment
|
|
1468
2956
|
* 5261754173): an overlay written by a mere column drag froze the view's
|
|
@@ -1472,20 +2960,19 @@ declare function viewItemObjectName(item: any): string | undefined;
|
|
|
1472
2960
|
* view's filter and every user who once resized a column kept the old one,
|
|
1473
2961
|
* with nothing anywhere reporting it.
|
|
1474
2962
|
*
|
|
1475
|
-
* Both halves of that ruling
|
|
1476
|
-
* unchanged by either — it
|
|
2963
|
+
* Both halves of that ruling landed, and this adapter's behaviour was
|
|
2964
|
+
* unchanged by either — it persisted what it was given:
|
|
1477
2965
|
*
|
|
1478
2966
|
* - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer
|
|
1479
2967
|
* that MERGES an overlay over a source view contributes only these keys, so
|
|
1480
|
-
* every already-stored fat row stops shadowing its
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
* the view, not a copy of one.
|
|
2968
|
+
* every already-stored fat row that carries the marker stops shadowing its
|
|
2969
|
+
* source. A fat row written before the marker is no longer narrowed
|
|
2970
|
+
* (objectui#10210, ruling B — see {@link isPersonalizationOverlayRow}).
|
|
2971
|
+
* - **write** (objectui#5233, unblocked by `columnState`'s admission to the
|
|
2972
|
+
* view-metadata surface as a runtime-only overlay key — objectstack
|
|
2973
|
+
* `d5552ca13`, released in `@objectstack/spec` 17.1.0): a *system view's*
|
|
2974
|
+
* overlay was written as the patch alone from then on, and the whole write
|
|
2975
|
+
* is retired since objectui#12504.
|
|
1489
2976
|
*
|
|
1490
2977
|
* A fat row is therefore a legacy shape, not a shape this product still
|
|
1491
2978
|
* produces; the list below is still what a reader is allowed to trust from one.
|
|
@@ -1493,9 +2980,10 @@ declare function viewItemObjectName(item: any): string | undefined;
|
|
|
1493
2980
|
* ⛔ Do not grow this list to make some other key "stick" through an overlay.
|
|
1494
2981
|
* A key that belongs to the view belongs in the view; the overlay is a patch,
|
|
1495
2982
|
* and a patch that carries the whole document is what this list exists to
|
|
1496
|
-
* stop.
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
2983
|
+
* stop. No writer of these rows is left (objectui#12504), so there is no new
|
|
2984
|
+
* key for an entry to answer; each entry is kept for reading the rows already
|
|
2985
|
+
* at rest — `inlineEdit` for rows written before objectui#5144, the other four
|
|
2986
|
+
* for rows written before objectui#12474 / objectui#12418.
|
|
1499
2987
|
*/
|
|
1500
2988
|
declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFields", "columnState", "inlineEdit"];
|
|
1501
2989
|
/**
|
|
@@ -1506,8 +2994,8 @@ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFie
|
|
|
1506
2994
|
* {@link VIEW_OVERLAY_OWNED_KEYS}). Of the three dispositions the issue names
|
|
1507
2995
|
* for them — strip on next write, migrate, tolerate on read — this is the
|
|
1508
2996
|
* third, chosen deliberately and stated here rather than left implicit,
|
|
1509
|
-
* because it is the only one that is already true for every existing
|
|
1510
|
-
* moment it ships: strip-on-next-write heals a row only when its user happens
|
|
2997
|
+
* because it is the only one that is already true for every existing marked
|
|
2998
|
+
* row the moment it ships: strip-on-next-write heals a row only when its user happens
|
|
1511
2999
|
* to touch that view again (and leaves the frozen filter live until then),
|
|
1512
3000
|
* and a migration needs a runner this product does not have for `sys_metadata`
|
|
1513
3001
|
* rows an operator may not even know exist. What the issue forbids is SILENT
|
|
@@ -1528,7 +3016,9 @@ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFie
|
|
|
1528
3016
|
* IS the view, and every key on it is an opinion its author expressed.
|
|
1529
3017
|
* Classification is {@link isPersonalizationOverlayRow}, the same predicate
|
|
1530
3018
|
* {@link ObjectStackAdapter.listViews} excludes rows by, so a row cannot be a
|
|
1531
|
-
* saved view for one reader and an overlay for the other.
|
|
3019
|
+
* saved view for one reader and an overlay for the other. It reads the marker
|
|
3020
|
+
* only (objectui#10210, ruling B): an unmarked row is returned by reference
|
|
3021
|
+
* whatever its shape, including a fat row written before the marker existed.
|
|
1532
3022
|
*/
|
|
1533
3023
|
declare function narrowPersonalizationOverlay<T>(row: T): T;
|
|
1534
3024
|
|
|
@@ -1579,12 +3069,14 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1579
3069
|
private analyticsCapabilityWarned;
|
|
1580
3070
|
private fetchImpl;
|
|
1581
3071
|
private inflightFinds;
|
|
3072
|
+
private inflightFindOnes;
|
|
1582
3073
|
private missingResources;
|
|
1583
3074
|
private batchUnsupported;
|
|
1584
3075
|
private atomicBatchCapability;
|
|
1585
3076
|
private mutationListeners;
|
|
1586
3077
|
private writeWarningListeners;
|
|
1587
3078
|
private saveAdvisoryListeners;
|
|
3079
|
+
private metadataReadWarningListeners;
|
|
1588
3080
|
constructor(config: {
|
|
1589
3081
|
baseUrl: string;
|
|
1590
3082
|
token?: string;
|
|
@@ -1600,8 +3092,18 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1600
3092
|
/**
|
|
1601
3093
|
* Ensure the client is connected to the server.
|
|
1602
3094
|
* Call this before making requests or it will auto-connect on first request.
|
|
3095
|
+
*
|
|
3096
|
+
* With `autoReconnect`, a failed discovery read is retried up to
|
|
3097
|
+
* `maxReconnectAttempts` times with exponential backoff. The promise resolves
|
|
3098
|
+
* once discovery answers and rejects with the last `ConnectionError` once the
|
|
3099
|
+
* retries are spent (objectui#12318).
|
|
1603
3100
|
*/
|
|
1604
3101
|
connect(): Promise<void>;
|
|
3102
|
+
/**
|
|
3103
|
+
* One discovery attempt, and the retry that follows it when it fails with
|
|
3104
|
+
* budget left. Runs inside the promise {@link connect} hands every caller.
|
|
3105
|
+
*/
|
|
3106
|
+
private attemptConnect;
|
|
1605
3107
|
/**
|
|
1606
3108
|
* Attempt to reconnect to the server with exponential backoff
|
|
1607
3109
|
*/
|
|
@@ -1626,6 +3128,34 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1626
3128
|
* Set connection state and notify listeners
|
|
1627
3129
|
*/
|
|
1628
3130
|
private setConnectionState;
|
|
3131
|
+
/**
|
|
3132
|
+
* Run one request through the adapter's fetch and let its outcome move the
|
|
3133
|
+
* connection state (objectui#12295). Before the first successful
|
|
3134
|
+
* {@link connect} this records nothing: `connect()` owns the state until
|
|
3135
|
+
* then, and its own discovery read passes through here too.
|
|
3136
|
+
*
|
|
3137
|
+
* - **Answered** — the fetch resolved with a Response, whatever its status.
|
|
3138
|
+
* A 4xx or 5xx is an answer: the server is reachable, so it counts the
|
|
3139
|
+
* same as a 200 and ends any outage.
|
|
3140
|
+
* - **Not answered** — the fetch rejected: no HTTP answer at all (refused,
|
|
3141
|
+
* reset, DNS, an offline browser). A rejection the caller asked for by
|
|
3142
|
+
* aborting is not evidence about the server and records nothing.
|
|
3143
|
+
*
|
|
3144
|
+
* The rejection or the Response reaches the caller unchanged.
|
|
3145
|
+
*/
|
|
3146
|
+
private observeServerAnswer;
|
|
3147
|
+
/** An answered request after the first connect: the server is reachable. */
|
|
3148
|
+
private noteServerAnswered;
|
|
3149
|
+
/**
|
|
3150
|
+
* An unanswered request after the first connect. It spends the adapter's own
|
|
3151
|
+
* retry window — the `autoReconnect` / `maxReconnectAttempts` budget that
|
|
3152
|
+
* {@link connect} grants a failed discovery — counted in the requests the
|
|
3153
|
+
* host already makes rather than in timed probes: while the budget lasts the
|
|
3154
|
+
* state is `reconnecting`, and the first unanswered request past it moves the
|
|
3155
|
+
* state to `error`. No timer and no request of the adapter's own is involved;
|
|
3156
|
+
* the next answered request, whenever it comes, restores `connected`.
|
|
3157
|
+
*/
|
|
3158
|
+
private noteServerUnanswered;
|
|
1629
3159
|
/**
|
|
1630
3160
|
* Emit batch progress event to listeners
|
|
1631
3161
|
*/
|
|
@@ -1635,6 +3165,16 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1635
3165
|
* Converts OData-style params to ObjectStack query options.
|
|
1636
3166
|
*/
|
|
1637
3167
|
find(resource: string, params?: QueryParams): Promise<QueryResult<T>>;
|
|
3168
|
+
/**
|
|
3169
|
+
* Share one in-flight read among concurrent callers asking the same
|
|
3170
|
+
* question: the first call for `key` issues `read()`, every call that
|
|
3171
|
+
* arrives while it is still pending gets that same promise, and the entry is
|
|
3172
|
+
* dropped the moment it settles — so nothing is cached past the round trip,
|
|
3173
|
+
* a rejection reaches every caller that shared it and is not remembered, and
|
|
3174
|
+
* the next call after settle issues a fresh read. Used by `find` and
|
|
3175
|
+
* `findOne` (objectui#11699).
|
|
3176
|
+
*/
|
|
3177
|
+
private shareInFlight;
|
|
1638
3178
|
/**
|
|
1639
3179
|
* Full-text search across every searchable object in a single round-trip.
|
|
1640
3180
|
*
|
|
@@ -1644,7 +3184,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1644
3184
|
* — a per-object metadata-driven search (ADR-0061) — this consults the search
|
|
1645
3185
|
* index and ranks hits across objects, so it surfaces records the per-object
|
|
1646
3186
|
* fanout misses. Global affordances (⌘K command palette, search page) prefer
|
|
1647
|
-
* this path (
|
|
3187
|
+
* this path (objectstack-ai/objectstack#3371).
|
|
1648
3188
|
*
|
|
1649
3189
|
* Returns `{ query, hits }`. A backend without the search plugin installed
|
|
1650
3190
|
* answers `404`; we treat that as "no global search here" and return an empty
|
|
@@ -1657,8 +3197,13 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1657
3197
|
}): Promise<GlobalSearchResult>;
|
|
1658
3198
|
/**
|
|
1659
3199
|
* Find a single record by ID.
|
|
3200
|
+
*
|
|
3201
|
+
* Concurrent calls for the same resource, id and params share one request,
|
|
3202
|
+
* exactly as `find` does (objectui#11699); see {@link shareInFlight}.
|
|
1660
3203
|
*/
|
|
1661
3204
|
findOne(resource: string, id: string | number, params?: QueryParams): Promise<T | null>;
|
|
3205
|
+
/** The one round trip behind {@link findOne}, unshared. */
|
|
3206
|
+
private readOne;
|
|
1662
3207
|
/**
|
|
1663
3208
|
* Create a new record.
|
|
1664
3209
|
*/
|
|
@@ -1680,14 +3225,24 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1680
3225
|
*/
|
|
1681
3226
|
private emitWriteWarning;
|
|
1682
3227
|
/**
|
|
1683
|
-
* Read `droppedFields` off a create/update response (
|
|
3228
|
+
* Read `droppedFields` off a create/update response (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455)
|
|
1684
3229
|
* and, when present, notify write-warning subscribers. Tolerant of a client
|
|
1685
3230
|
* whose response type predates `droppedFields`: the field is read structurally
|
|
1686
3231
|
* and validated, so an older client (or a backend that never drops) is a no-op.
|
|
3232
|
+
*
|
|
3233
|
+
* SHAPE decides whether an entry is an event at all (it must name at least one
|
|
3234
|
+
* field); `object`, `fields` and `reason` are then PARSED — an unrecognized
|
|
3235
|
+
* reason routed to the skew arm, a non-string field element refused, a missing
|
|
3236
|
+
* `object` healed from `resource` — never asserted into the union, and the
|
|
3237
|
+
* entry itself never dropped for them (objectui#4934, `f75810e7c`).
|
|
3238
|
+
*
|
|
3239
|
+
* The same answer's `warnings` (objectui#12107) rides this one event: it is
|
|
3240
|
+
* emitted when either list is non-empty, so a write whose only news is an
|
|
3241
|
+
* advisory rule hit still reaches the subscriber, with `droppedFields: []`.
|
|
1687
3242
|
*/
|
|
1688
3243
|
private notifyDroppedFields;
|
|
1689
3244
|
/**
|
|
1690
|
-
* Same, for the cross-object transactional batch (
|
|
3245
|
+
* Same, for the cross-object transactional batch (objectstack-ai/objectstack#3794). Its
|
|
1691
3246
|
* response hangs the events off a top-level `droppedFields` list, each tagged
|
|
1692
3247
|
* with the `index` of the operation it came from — `results` entries are bare
|
|
1693
3248
|
* record echoes with nowhere to hang a per-row list.
|
|
@@ -1701,7 +3256,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1701
3256
|
private notifyBatchDroppedFields;
|
|
1702
3257
|
/**
|
|
1703
3258
|
* Subscribe to write-warning events (a create/update dropped caller-supplied
|
|
1704
|
-
* fields — #3431
|
|
3259
|
+
* fields — objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455). Returns an unsubscribe function. The app shell uses
|
|
1705
3260
|
* this to toast the user; the write itself already succeeded.
|
|
1706
3261
|
*/
|
|
1707
3262
|
onWriteWarning(callback: WriteWarningListener): () => void;
|
|
@@ -1710,7 +3265,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1710
3265
|
* advisory findings on a save that SUCCEEDED (#4237; backend
|
|
1711
3266
|
* objectstack#7435). Returns an unsubscribe function.
|
|
1712
3267
|
*
|
|
1713
|
-
* Deliberately the same seam as {@link onWriteWarning} (#3431
|
|
3268
|
+
* Deliberately the same seam as {@link onWriteWarning} (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455), which
|
|
1714
3269
|
* is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
|
|
1715
3270
|
* was modelled on. It is a SIBLING of that channel rather than a second
|
|
1716
3271
|
* payload pushed down it: `WriteWarningEvent` is a closed shape whose
|
|
@@ -1734,6 +3289,32 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1734
3289
|
* nor starve the others.
|
|
1735
3290
|
*/
|
|
1736
3291
|
private emitSaveAdvisory;
|
|
3292
|
+
/**
|
|
3293
|
+
* Subscribe to metadata read-warning events — a metadata READ that could not
|
|
3294
|
+
* be answered and was degraded to an empty result anyway (objectui#7741).
|
|
3295
|
+
* Returns an unsubscribe function. The app shell uses this to toast the user;
|
|
3296
|
+
* the caller has already been handed the empty result.
|
|
3297
|
+
*
|
|
3298
|
+
* Deliberately the same seam as {@link onWriteWarning} and
|
|
3299
|
+
* {@link onSaveAdvisory}, and a SIBLING of them rather than a payload pushed
|
|
3300
|
+
* down either: both of those announce a write that SUCCEEDED, and an event
|
|
3301
|
+
* saying "this read did not happen" carried on one of them would make it lie
|
|
3302
|
+
* about what occurred. What is reused is the seam's SHAPE — one long-lived
|
|
3303
|
+
* instance with a `subscribe -> unsubscribe` registration that
|
|
3304
|
+
* `AdapterProvider` wires once — not its event type.
|
|
3305
|
+
*
|
|
3306
|
+
* ⛔ Nothing here changes what a caller RECEIVES. `listImportMappings` still
|
|
3307
|
+
* answers `[]` on every failure, including the loud ones; this channel is
|
|
3308
|
+
* added ALONGSIDE that return, which is why it is not a change to a surface
|
|
3309
|
+
* published since `@object-ui/data-objectstack@17.1.0`.
|
|
3310
|
+
*/
|
|
3311
|
+
onMetadataReadWarning(callback: MetadataReadWarningListener): () => void;
|
|
3312
|
+
/**
|
|
3313
|
+
* Notify all metadata read-warning subscribers. Isolated exactly like
|
|
3314
|
+
* {@link emitWriteWarning}: a throwing listener must neither break the caller
|
|
3315
|
+
* nor starve the others.
|
|
3316
|
+
*/
|
|
3317
|
+
private emitMetadataReadWarning;
|
|
1737
3318
|
/**
|
|
1738
3319
|
* Install the ONE emitter for the metadata save door (#4237).
|
|
1739
3320
|
*
|
|
@@ -1741,8 +3322,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1741
3322
|
*
|
|
1742
3323
|
* `ObjectStackClient.meta.saveItem` is the second client class that writes
|
|
1743
3324
|
* through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches
|
|
1744
|
-
* it through an adapter this class constructed — the
|
|
1745
|
-
* (
|
|
3325
|
+
* it through an adapter this class constructed — the three inside this file
|
|
3326
|
+
* (the two view paths, `updateDashboard`) via
|
|
1746
3327
|
* `this.client`, and every caller outside it via {@link getClient}, which
|
|
1747
3328
|
* hands back this same instance: `MetadataService` (app-shell, five saves),
|
|
1748
3329
|
* `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the
|
|
@@ -1771,16 +3352,60 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1771
3352
|
*
|
|
1772
3353
|
* Drafts are NEVER gated: the framework returns at its D1 early-return
|
|
1773
3354
|
* (`if (args.state !== 'active') return null`) before running a rule, so a
|
|
1774
|
-
* draft save produces no findings to withhold.
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
* the
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
3355
|
+
* draft save produces no findings to withhold. `mode` on the emitted event is
|
|
3356
|
+
* derived from the response's own `state`, not from the caller's options:
|
|
3357
|
+
* `'draft'` when the server says the row landed as a draft, `'publish'`
|
|
3358
|
+
* otherwise. The server's answer is the one that says which door the write
|
|
3359
|
+
* took, so the event cannot claim a door the request only asked for.
|
|
3360
|
+
*
|
|
3361
|
+
* ## Every argument is forwarded (objectui#12300)
|
|
3362
|
+
*
|
|
3363
|
+
* The SDK's `saveItem(type, name, item, options?)` carries the write's scope
|
|
3364
|
+
* in its fourth argument, `SaveMetaItemOptions`: `packageId` (`?package=`),
|
|
3365
|
+
* `force`, `mode` and `ifMatch`. This wrapper used to name three parameters
|
|
3366
|
+
* and call the original with three, so that bag was dropped before the SDK
|
|
3367
|
+
* saw it. Every caller reaching the SDK through {@link getClient} got a bare
|
|
3368
|
+
* `PUT` whatever it passed, while the declared signature still accepted the
|
|
3369
|
+
* bag, so nothing at the call site could tell. The wrapper now takes and
|
|
3370
|
+
* passes the original's own parameter list, so it forwards whatever the SDK
|
|
3371
|
+
* declares, today's bag and any later member, without naming one.
|
|
1782
3372
|
*/
|
|
1783
3373
|
private installSaveAdvisoryInterceptor;
|
|
3374
|
+
/**
|
|
3375
|
+
* Route this adapter's client's published view-list read through the one
|
|
3376
|
+
* {@link VIEW_LIST_KEY} (objectui#13044).
|
|
3377
|
+
*
|
|
3378
|
+
* The app-shell `MetadataProvider` reads `client.meta.getItems('view')`
|
|
3379
|
+
* through {@link getClient}; {@link listViews} and {@link listViewOverrides}
|
|
3380
|
+
* read the same `GET /api/v1/meta/view`. Each used to put its own request on
|
|
3381
|
+
* the wire, so one list-page open asked the server three times for one URL.
|
|
3382
|
+
*
|
|
3383
|
+
* A caller of the client keeps its contract, "ask the server": it is never
|
|
3384
|
+
* answered from a settled entry. It joins a request already in flight for
|
|
3385
|
+
* the same URL instead of sending a second one, and its answer is stored
|
|
3386
|
+
* under {@link VIEW_LIST_KEY} (see {@link MetadataCache.share}), which is
|
|
3387
|
+
* what {@link readViewList} answers this adapter's own readers from
|
|
3388
|
+
* (objectui#13057). Only the bare call is routed: any argument after the
|
|
3389
|
+
* type changes the URL, so such a call goes straight to the SDK.
|
|
3390
|
+
*
|
|
3391
|
+
* Every caller gets its own copy of the answer. Before this, every reader
|
|
3392
|
+
* got its own parsed response, so a reader that changed what it was handed
|
|
3393
|
+
* changed nothing anyone else read; a shared answer must not change that.
|
|
3394
|
+
*
|
|
3395
|
+
* Same wrap shape and the same bound as `installSaveAdvisoryInterceptor`:
|
|
3396
|
+
* `meta` is an own property of a client this adapter built and never shares.
|
|
3397
|
+
*/
|
|
3398
|
+
private installViewListShare;
|
|
3399
|
+
/**
|
|
3400
|
+
* The published view list for this adapter's own readers
|
|
3401
|
+
* ({@link listViews}, {@link listViewOverrides}): the answer stored under
|
|
3402
|
+
* {@link VIEW_LIST_KEY} while it holds one (objectui#13057), else the
|
|
3403
|
+
* client's read, which joins the request in flight for that key or starts
|
|
3404
|
+
* one, and stores its answer (objectui#13044). Always a copy the caller may
|
|
3405
|
+
* keep. Every path that should see a fresh list drops the key first; the
|
|
3406
|
+
* list is in {@link VIEW_LIST_KEY}'s doc.
|
|
3407
|
+
*/
|
|
3408
|
+
private readViewList;
|
|
1784
3409
|
create(resource: string, data: Partial<T>): Promise<T>;
|
|
1785
3410
|
/**
|
|
1786
3411
|
* Update an existing record.
|
|
@@ -1840,25 +3465,25 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1840
3465
|
* @returns Promise resolving to array of results
|
|
1841
3466
|
*/
|
|
1842
3467
|
/**
|
|
1843
|
-
* Cross-object transactional batch (
|
|
3468
|
+
* Cross-object transactional batch (objectstack-ai/objectstack#1604 / ADR-0034 item 4).
|
|
1844
3469
|
* Runs the operations in ONE server transaction — commit all or roll back
|
|
1845
3470
|
* all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
|
|
1846
3471
|
* created id, so a child can reference its parent created earlier in the same
|
|
1847
3472
|
* batch (master-detail).
|
|
1848
3473
|
*
|
|
1849
3474
|
* Transport: the published `@objectstack/client` SDK method
|
|
1850
|
-
* `data.batchTransaction` (
|
|
3475
|
+
* `data.batchTransaction` (objectstack-ai/objectstack#3271; shipped since client v16, our
|
|
1851
3476
|
* dependency floor). Per AGENTS.md §7 data always flows through the client —
|
|
1852
3477
|
* never a hand-rolled `fetch('/api/v1/batch')`.
|
|
1853
3478
|
*
|
|
1854
|
-
* Fallback decision — declarative capability negotiation (
|
|
3479
|
+
* Fallback decision — declarative capability negotiation (objectstack-ai/objectstack#3298 /
|
|
1855
3480
|
* objectui #2693). At connect() we read `capabilities.transactionalBatch`
|
|
1856
3481
|
* from discovery:
|
|
1857
3482
|
* - Declared `true` → the backend GUARANTEES atomicity (declared ===
|
|
1858
3483
|
* enforced). We TRUST it: any batch failure — including 404/405/501 —
|
|
1859
3484
|
* surfaces as a real error. No non-atomic client-side compensation. This
|
|
1860
3485
|
* is the path modern backends take.
|
|
1861
|
-
* - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
|
|
3486
|
+
* - Declared `false`, or ABSENT (backend predates objectstack-ai/objectstack#3298) → we can't rely on
|
|
1862
3487
|
* server atomicity, so we keep the legacy behaviour: on 404/405 (no
|
|
1863
3488
|
* endpoint) or 501 (runtime without transactions) degrade to the
|
|
1864
3489
|
* client-side, NON-atomic {@link emulateBatchTransaction} so a save is
|
|
@@ -1951,6 +3576,30 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1951
3576
|
* the caller triggers the browser download.
|
|
1952
3577
|
*/
|
|
1953
3578
|
exportDownload(resource: string, request?: ExportDownloadRequest): Promise<Blob>;
|
|
3579
|
+
/**
|
|
3580
|
+
* Download the server's import template:
|
|
3581
|
+
* `GET /api/v1/data/:object/export?template=true` (objectui#9600).
|
|
3582
|
+
*
|
|
3583
|
+
* Same route as {@link exportDownload}, with the server's template switch. It
|
|
3584
|
+
* sends only `template=true`: the server refuses the export's row parameters
|
|
3585
|
+
* on a template request, and its answer is always an xlsx workbook.
|
|
3586
|
+
*
|
|
3587
|
+
* The server writes the template's labels in the request's locale, and the
|
|
3588
|
+
* import accepts a translated label only in its own request's locale. The
|
|
3589
|
+
* import request goes through `@objectstack/client`, which stamps
|
|
3590
|
+
* `Accept-Language` with the client's locale when one is set (and the host's
|
|
3591
|
+
* `fetch` stamps its own otherwise). This request bypasses the client, so it
|
|
3592
|
+
* stamps the client's locale the same way. Both requests then carry the same
|
|
3593
|
+
* locale.
|
|
3594
|
+
*/
|
|
3595
|
+
downloadImportTemplate(resource: string): Promise<Blob>;
|
|
3596
|
+
/**
|
|
3597
|
+
* GET `/api/v1/data/:object/export` with the given query and answer the body
|
|
3598
|
+
* as a Blob. A failure throws an `Error` that carries the server's `status`
|
|
3599
|
+
* and its ADR-0112 `code`. The one request path {@link exportDownload} and
|
|
3600
|
+
* {@link downloadImportTemplate} share.
|
|
3601
|
+
*/
|
|
3602
|
+
private fetchExportBlob;
|
|
1954
3603
|
/**
|
|
1955
3604
|
* Convert ObjectUI QueryParams to ObjectStack QueryOptions.
|
|
1956
3605
|
* Maps OData-style conventions to ObjectStack conventions.
|
|
@@ -1964,15 +3613,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
1964
3613
|
* @returns Promise resolving to the object schema
|
|
1965
3614
|
*/
|
|
1966
3615
|
getObjectSchema(objectName: string): Promise<unknown>;
|
|
1967
|
-
/**
|
|
1968
|
-
* ADR-0056 P2 (epic #2398) — stamp structured-widget hints onto platform
|
|
1969
|
-
* fields whose framework type is a storage primitive (e.g. `textarea`) but
|
|
1970
|
-
* whose authoring UX should be a structured editor. Only the render `widget`
|
|
1971
|
-
* is added; the field's `type` (the storage contract) is untouched. Applied
|
|
1972
|
-
* idempotently to the cached schema so form + detail both honor it. Widget
|
|
1973
|
-
* components are registered as `field:<widget>` in `@object-ui/fields`.
|
|
1974
|
-
*/
|
|
1975
|
-
private applyFieldWidgetOverrides;
|
|
1976
3616
|
/**
|
|
1977
3617
|
* Fetch a single object's schema while always revalidating the browser cache.
|
|
1978
3618
|
*
|
|
@@ -2002,6 +3642,27 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2002
3642
|
name: string;
|
|
2003
3643
|
label?: string;
|
|
2004
3644
|
}>>;
|
|
3645
|
+
/**
|
|
3646
|
+
* List the items one metadata registry type declares —
|
|
3647
|
+
* `GET /api/v1/meta/:type`, through the client's `meta.getItems` — as
|
|
3648
|
+
* `{ name, label }` headers for the `position-ref` / `permission-ref` field
|
|
3649
|
+
* widgets (objectui#7611).
|
|
3650
|
+
*
|
|
3651
|
+
* Unlike {@link getObjects}, a failure REJECTS (the `DataSource` contract):
|
|
3652
|
+
* the transport's own error propagates, and an answer without the spec's
|
|
3653
|
+
* `items` list is refused rather than read as an empty registry, so a
|
|
3654
|
+
* failed load can never reach the widget as "this registry declares
|
|
3655
|
+
* nothing". Uncached, like `getObjects`: an item authored in Studio is
|
|
3656
|
+
* listed on the next form that opens.
|
|
3657
|
+
*
|
|
3658
|
+
* An item without a string `name` is not listed (the widget stores the
|
|
3659
|
+
* name, so there is nothing to store); a `label` that is not a string is
|
|
3660
|
+
* left off rather than coerced.
|
|
3661
|
+
*/
|
|
3662
|
+
listMetadataItems(type: string): Promise<Array<{
|
|
3663
|
+
name: string;
|
|
3664
|
+
label?: string;
|
|
3665
|
+
}>>;
|
|
2005
3666
|
/**
|
|
2006
3667
|
* Get access to the underlying ObjectStack client for advanced operations.
|
|
2007
3668
|
*/
|
|
@@ -2061,9 +3722,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2061
3722
|
* Batch-fetch all persisted view overrides for an object.
|
|
2062
3723
|
*
|
|
2063
3724
|
* Per-view runtime overrides (density, column widths, sort, hidden
|
|
2064
|
-
* columns, inlineEdit …) live in the SAME metadata namespace the
|
|
2065
|
-
*
|
|
2066
|
-
* {@link
|
|
3725
|
+
* columns, inlineEdit …) live in the SAME metadata namespace the view
|
|
3726
|
+
* writes use: `type='view'`, `name=<viewId>` (see {@link createView} /
|
|
3727
|
+
* {@link updateView}; the toolbar's `updateViewConfig` wrote overlays there
|
|
3728
|
+
* until objectui#12504 retired it). Loading them per-view fires N HTTP GETs
|
|
2067
3729
|
* that 404 for every view the user never customized — console noise on
|
|
2068
3730
|
* every page load. This batch method performs a single
|
|
2069
3731
|
* `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the
|
|
@@ -2087,9 +3749,9 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2087
3749
|
*
|
|
2088
3750
|
* Result is cached identically to {@link getView}, and EVERY view write —
|
|
2089
3751
|
* on this adapter and above it — invalidates it through the one seam,
|
|
2090
|
-
* {@link invalidateViewKeys}. For a long time only
|
|
2091
|
-
* did (objectui#4363), which left the other three
|
|
2092
|
-
* for the cache's 5-minute TTL. That gap does not self-heal: the consumer
|
|
3752
|
+
* {@link invalidateViewKeys}. For a long time only `updateViewConfig` (since
|
|
3753
|
+
* retired, objectui#12504) did (objectui#4363), which left the other three
|
|
3754
|
+
* adapter paths stale for the cache's 5-minute TTL. That gap does not self-heal: the consumer
|
|
2093
3755
|
* (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as
|
|
2094
3756
|
* authoritative and deliberately does not re-probe per view — objectui#3774,
|
|
2095
3757
|
* and correct, since re-probing reinstates the 404 flurry the batch read
|
|
@@ -2113,55 +3775,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2113
3775
|
* @returns Promise resolving to the view definition or null
|
|
2114
3776
|
*/
|
|
2115
3777
|
getView(objectName: string, viewId: string): Promise<unknown | null>;
|
|
2116
|
-
/**
|
|
2117
|
-
* Persist a toolbar-driven view config patch — density, column widths,
|
|
2118
|
-
* sort, hidden columns, inline edit. Symmetric counterpart to
|
|
2119
|
-
* {@link getView}: writes the row to the server metadata store via
|
|
2120
|
-
* `client.meta.saveItem`, then invalidates the matching cache entry so the
|
|
2121
|
-
* next {@link getView} reflects the new payload. Returns the persisted item
|
|
2122
|
-
* when the server echoes it, otherwise undefined.
|
|
2123
|
-
*
|
|
2124
|
-
* Called from exactly ONE production site — `ObjectView`'s
|
|
2125
|
-
* `persistViewPatch`, for the toolbar toggle — but that ONE call site
|
|
2126
|
-
* fires for BOTH kinds of active tab: a code-defined **system** view (no
|
|
2127
|
-
* row of its own yet) and a genuinely user-created **saved** view (already
|
|
2128
|
-
* has a row — the toggle is editing ITS OWN definition, not laying an
|
|
2129
|
-
* overlay on top of it). Which one a given call means is NOT re-derived
|
|
2130
|
-
* here from the write's shape (objectui#4227's own lesson: shape inference
|
|
2131
|
-
* on this namespace is exactly what let a system view masquerade as
|
|
2132
|
-
* saved) — the caller already knows, via the same `isSavedViewId`
|
|
2133
|
-
* classification that gates the switcher's readonly flag and its five
|
|
2134
|
-
* mutating handlers, and passes it as {@link opts.isSavedView}.
|
|
2135
|
-
*
|
|
2136
|
-
* - `isSavedView` false/omitted (system-view target, the common case and
|
|
2137
|
-
* the default for backward compatibility): stamps
|
|
2138
|
-
* {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
|
|
2139
|
-
* the original objectui#4227 fix.
|
|
2140
|
-
* - `isSavedView` true: the marker is withheld. Stamping it here would
|
|
2141
|
-
* flag the saved view's OWN row as a personalization overlay, and
|
|
2142
|
-
* `listViews()` would exclude it on the very next read — the user's own
|
|
2143
|
-
* view would vanish from the switcher the moment they toggled its
|
|
2144
|
-
* density (objectui#4227 follow-up, PM review on PR #4713, measured:
|
|
2145
|
-
* `persistViewPatch` has no gate on which kind of tab is active, and
|
|
2146
|
-
* this method writes to the exact same `(type='view', name=viewId)` key
|
|
2147
|
-
* {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
|
|
2148
|
-
* that view, so the write is an upsert onto the saved view's row, not a
|
|
2149
|
-
* new one).
|
|
2150
|
-
*
|
|
2151
|
-
* Per objectstack#7494's ruling, the overlay this writes (system-view
|
|
2152
|
-
* case) is ORG-WIDE shared view settings, not a per-user preference — a
|
|
2153
|
-
* true per-user scope is a parked v18 direction on the platform side.
|
|
2154
|
-
*
|
|
2155
|
-
* @param objectName - Object name (e.g. 'lead')
|
|
2156
|
-
* @param viewId - View identifier (e.g. 'all_leads')
|
|
2157
|
-
* @param config - Full view definition to persist
|
|
2158
|
-
* @param opts.isSavedView - Whether `viewId` already names a saved view
|
|
2159
|
-
* (vs. a system view being personalized for the first time). Omit /
|
|
2160
|
-
* `false` for the default overlay-marking behavior.
|
|
2161
|
-
*/
|
|
2162
|
-
updateViewConfig(objectName: string, viewId: string, config: Record<string, any>, opts?: {
|
|
2163
|
-
isSavedView?: boolean;
|
|
2164
|
-
}): Promise<Record<string, any> | void>;
|
|
2165
3778
|
/**
|
|
2166
3779
|
* List user-created views for a given object via the metadata overlay
|
|
2167
3780
|
* API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
|
|
@@ -2173,6 +3786,42 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2173
3786
|
* the metadata index is name-only, not field-typed, so the route has no
|
|
2174
3787
|
* `?object=` to push the filter down into. {@link listViewOverrides}
|
|
2175
3788
|
* reads the same rows through the same accessor.
|
|
3789
|
+
*
|
|
3790
|
+
* ## Every failure still degrades to an empty list — and now says which kind
|
|
3791
|
+
* ## of failure it was (objectui#8151)
|
|
3792
|
+
*
|
|
3793
|
+
* The degrade is unchanged and deliberate, exactly as it is one method over
|
|
3794
|
+
* in {@link listImportMappings}: a host with no metadata door keeps answering
|
|
3795
|
+
* `[]` rather than breaking the page. What changed is that the OTHER failures
|
|
3796
|
+
* no longer render as that one.
|
|
3797
|
+
*
|
|
3798
|
+
* This method's empty list is read as *this object has no saved views*, and
|
|
3799
|
+
* that reading reaches the user in two places measured on this tree: the list
|
|
3800
|
+
* view switcher simply shows fewer tabs, and `@object-ui/core`'s
|
|
3801
|
+
* `elementDataSourceViewNotFoundMessage` states it outright. So a session
|
|
3802
|
+
* whose token lapsed mid-read was shown an object that appears to have no
|
|
3803
|
+
* saved views AT ALL — including ones the user created — with a
|
|
3804
|
+
* `console.warn` as the only discriminator, in a console nothing in the UI
|
|
3805
|
+
* points at. That is the silence objectui#7741 removed from the sibling after
|
|
3806
|
+
* it produced a confident wrong diagnosis in objectstack#14026.
|
|
3807
|
+
*
|
|
3808
|
+
* The `catch` now asks {@link classifyViewsFailure} what the failure WAS,
|
|
3809
|
+
* reading the error's own ADR-0112 `code` and status — never "is the result
|
|
3810
|
+
* empty", which is what both conditions produce and so can never tell them
|
|
3811
|
+
* apart — and anything that is not the doorless shape is announced on
|
|
3812
|
+
* {@link onMetadataReadWarning}.
|
|
3813
|
+
*
|
|
3814
|
+
* ⛔ That classifier is a SEPARATE reading, not a second caller of the
|
|
3815
|
+
* sibling's: `view`'s quiet set is strictly smaller, because the arm
|
|
3816
|
+
* `listImportMappings` is built around (*an older server without this kind*)
|
|
3817
|
+
* has no members here. See {@link classifyViewsFailure} for the measurement.
|
|
3818
|
+
*
|
|
3819
|
+
* ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
|
|
3820
|
+
* throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
|
|
3821
|
+
* nothing new sees exactly what it saw before, including on the loud arms.
|
|
3822
|
+
* Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
|
|
3823
|
+
* READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
|
|
3824
|
+
* moving that contract.
|
|
2176
3825
|
*/
|
|
2177
3826
|
listViews(objectName: string, options?: {
|
|
2178
3827
|
previewDrafts?: boolean;
|
|
@@ -2186,11 +3835,36 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2186
3835
|
private metadataClient;
|
|
2187
3836
|
/**
|
|
2188
3837
|
* List registered import `mapping` artifacts targeting a given object
|
|
2189
|
-
* (
|
|
3838
|
+
* (objectstack-ai/objectstack#2611). Reads the `mapping` metadata kind via the overlay API
|
|
2190
3839
|
* and filters by `targetObject` client-side (the metadata index is
|
|
2191
|
-
* name-only). Feeds the import wizard's "saved mapping" selector
|
|
2192
|
-
*
|
|
2193
|
-
*
|
|
3840
|
+
* name-only). Feeds the import wizard's "saved mapping" selector.
|
|
3841
|
+
*
|
|
3842
|
+
* ## Every failure still degrades to an empty list — and now says which kind
|
|
3843
|
+
* ## of failure it was (objectui#7741)
|
|
3844
|
+
*
|
|
3845
|
+
* The degrade is unchanged and deliberate: an older server without the
|
|
3846
|
+
* `mapping` kind must not break the wizard, so it keeps answering `[]` and
|
|
3847
|
+
* the selector simply doesn't appear. That is a real, supported deployment
|
|
3848
|
+
* shape and it stays quiet.
|
|
3849
|
+
*
|
|
3850
|
+
* What changed is that the OTHER failures no longer render as that one. A
|
|
3851
|
+
* refused door and a broken one used to be indistinguishable, in the UI, from
|
|
3852
|
+
* "no mapping is registered" — both were an empty list behind a
|
|
3853
|
+
* `console.warn` nothing pointed at — and that silence produced a confident
|
|
3854
|
+
* wrong diagnosis in a careful reporter (objectstack#14026 was filed, routed
|
|
3855
|
+
* and worked by two seats against a wizard that had been correct since
|
|
3856
|
+
* 17.1.0). So the `catch` now asks {@link classifyImportMappingsFailure} what
|
|
3857
|
+
* the failure WAS, reading the error's own ADR-0112 `code` and status —
|
|
3858
|
+
* never "is the result empty", which is what both conditions produce and so
|
|
3859
|
+
* can never tell them apart — and anything that is not the supported
|
|
3860
|
+
* kind-absent shape is announced on {@link onMetadataReadWarning}.
|
|
3861
|
+
*
|
|
3862
|
+
* ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
|
|
3863
|
+
* throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
|
|
3864
|
+
* nothing new sees exactly what it saw before, including on the loud arms.
|
|
3865
|
+
* Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
|
|
3866
|
+
* READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
|
|
3867
|
+
* moving that contract.
|
|
2194
3868
|
*/
|
|
2195
3869
|
listImportMappings(objectName: string): Promise<any[]>;
|
|
2196
3870
|
/**
|
|
@@ -2311,6 +3985,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2311
3985
|
* the PUBLISHED half after the draft was discarded carries the partial
|
|
2312
3986
|
* state on the error's `outcome` — "draft gone, overlay left" is exactly
|
|
2313
3987
|
* what the old `{ deleted: boolean }` could not express.
|
|
3988
|
+
* Its message is the adapter's "NOT fully removed" sentence followed, on
|
|
3989
|
+
* the next line, by the server's own reason (objectui#12416), so
|
|
3990
|
+
* `formatMetadataError(err)` shows the remedy a refusal names; the
|
|
3991
|
+
* original error stays on `cause`.
|
|
2314
3992
|
*
|
|
2315
3993
|
* Invalidates through {@link invalidateViewKeys}: the deleted row leaves the
|
|
2316
3994
|
* batch override map too, and a ghost entry there is what the object page
|
|
@@ -2351,11 +4029,17 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2351
4029
|
*
|
|
2352
4030
|
* The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
|
|
2353
4031
|
* rather than in the list, so the enumeration surface is not widened past what
|
|
2354
|
-
* a by-name probe already implies (objectstack#8013 / PR #8135): an app that
|
|
4032
|
+
* a by-name probe already implies (objectstack#8013 / PR objectstack-ai/objectstack#8135): an app that
|
|
2355
4033
|
* exists and whose `requiredPermissions` the session lacks answers `403` with
|
|
2356
4034
|
* `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
|
|
2357
|
-
* name, an unpublished app, an app gated by an absent optional service —
|
|
2358
|
-
*
|
|
4035
|
+
* name, an unpublished app, an app gated by an absent optional service — is
|
|
4036
|
+
* one undifferentiated answer.
|
|
4037
|
+
*
|
|
4038
|
+
* ⚠️ That absence is NOT uniformly a `404`, which this docstring asserted
|
|
4039
|
+
* until objectui#9262 measured it: the 404 covers an app that EXISTS and is
|
|
4040
|
+
* withheld for a non-permission reason, while a name that resolves to nothing
|
|
4041
|
+
* answers `200` with an envelope carrying no `item`. Both are absence and both
|
|
4042
|
+
* read as `not_found`; see {@link metaEnvelopeCarriesItem} for the measurement.
|
|
2359
4043
|
*
|
|
2360
4044
|
* ## Why this is a separate method and not a flavour of {@link getApp}
|
|
2361
4045
|
*
|
|
@@ -2366,13 +4050,55 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2366
4050
|
* be cached beside a document about the APP — one grant, and a cached
|
|
2367
4051
|
* denial outlives the session it described.
|
|
2368
4052
|
*
|
|
2369
|
-
* Nothing here throws:
|
|
2370
|
-
*
|
|
2371
|
-
*
|
|
4053
|
+
* Nothing here throws: every outcome is a verdict the caller renders a screen
|
|
4054
|
+
* from, never a catch block. Only the measured `code` produces `denied` —
|
|
4055
|
+
* never a status, never a message (objectui#4408).
|
|
4056
|
+
*
|
|
4057
|
+
* ## objectui#9262 — what this method stopped folding away
|
|
4058
|
+
*
|
|
4059
|
+
* It used to answer `granted` whenever the call did not throw, and `unknown`
|
|
4060
|
+
* for everything that did. Both halves were wrong in the same direction, and
|
|
4061
|
+
* the first was measured on a real server before this change landed:
|
|
4062
|
+
*
|
|
4063
|
+
* - **an app that does not exist answers `200` with the envelope minus its
|
|
4064
|
+
* `item`**, so "did not throw" reported `granted` for a name with nothing
|
|
4065
|
+
* behind it — the shape issue objectui#9262 listed as cause 8 and expected
|
|
4066
|
+
* to be structural. It is the ordinary case: every typo, every never-created
|
|
4067
|
+
* app and every unpublished draft took it. See
|
|
4068
|
+
* {@link metaEnvelopeCarriesItem} for the measurement and the server-side
|
|
4069
|
+
* mechanism.
|
|
4070
|
+
* - a 404 absence and an unreachable server were one `unknown`, though the
|
|
4071
|
+
* transport had already told them apart.
|
|
4072
|
+
*
|
|
4073
|
+
* The verdicts now say only what was obtained: a served document, a refusal
|
|
4074
|
+
* naming permission, an answer that there is no such app here, or no answer
|
|
4075
|
+
* at all. `unknown` is left for the one case where nothing was asked.
|
|
2372
4076
|
*
|
|
2373
4077
|
* @param appName - the app name as it appears in the URL segment
|
|
2374
4078
|
*/
|
|
2375
4079
|
probeAppAccess(appName: string): Promise<AppAccessVerdict>;
|
|
4080
|
+
/**
|
|
4081
|
+
* {@link probeAppAccess} for any metadata type whose by-name read refuses a
|
|
4082
|
+
* caller (objectui#12112): why `name` is not in this session's list of
|
|
4083
|
+
* `type`, plus the server's own sentence when it refused.
|
|
4084
|
+
*
|
|
4085
|
+
* Since objectstack#22731 a dashboard and a list view carry the app's gate:
|
|
4086
|
+
* `GET /api/v1/meta/dashboard` and `/meta/view` list only what the caller may
|
|
4087
|
+
* open, so a withheld item and a missing one are the same absence there, and
|
|
4088
|
+
* the by-name read of one the caller does not hold answers `403`
|
|
4089
|
+
* `PERMISSION_DENIED`, "the app arm's whole refusal (same emitter, same
|
|
4090
|
+
* envelope)". The verdicts are therefore read exactly as for an app.
|
|
4091
|
+
*
|
|
4092
|
+
* `message` is the refusal's `message`, as the server wrote it: the one
|
|
4093
|
+
* sentence that names the item and says it was refused. Only on `denied`.
|
|
4094
|
+
*
|
|
4095
|
+
* Nothing here throws, and nothing is cached: a verdict about the CALLER
|
|
4096
|
+
* must not outlive a grant.
|
|
4097
|
+
*
|
|
4098
|
+
* @param type - the singular metadata type (`app`, `dashboard`, `view`)
|
|
4099
|
+
* @param name - the item's name, as the by-name route addresses it
|
|
4100
|
+
*/
|
|
4101
|
+
probeMetaItemAccess(type: string, name: string): Promise<MetaItemAccess>;
|
|
2376
4102
|
/**
|
|
2377
4103
|
* Get a page definition from ObjectStack.
|
|
2378
4104
|
* Uses the metadata API to fetch page layouts.
|
|
@@ -2397,9 +4123,28 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2397
4123
|
* Uses `this.client.analytics.query()` from @objectstack/client to leverage
|
|
2398
4124
|
* the SDK's built-in auth, headers, and fetch configuration.
|
|
2399
4125
|
* Falls back to client-side aggregation via find() if the analytics endpoint
|
|
2400
|
-
* is not available.
|
|
4126
|
+
* is not available. Throws {@link AnalyticsResultShapeError} when it answers
|
|
4127
|
+
* with anything but an `AnalyticsResult` carrying `rows`.
|
|
2401
4128
|
*/
|
|
2402
4129
|
aggregate(resource: string, params: any): Promise<any[]>;
|
|
4130
|
+
/**
|
|
4131
|
+
* Answer a grouped list view's GROUP HEADER query (objectui#7189) — see
|
|
4132
|
+
* `DataSource.queryGroupHeaders` in `@object-ui/types` for the contract.
|
|
4133
|
+
*
|
|
4134
|
+
* The compiled query rides the platform's EXISTING data door verbatim:
|
|
4135
|
+
* `client.data.query()` posts it to `POST /data/:object/query`, whose
|
|
4136
|
+
* `findData` routes a body carrying `groupBy` / `aggregations` to
|
|
4137
|
+
* `engine.aggregate` and answers the header rows as `records` (the door the
|
|
4138
|
+
* spec half of objectstack#14556 names, pinned platform-side by
|
|
4139
|
+
* objectstack#15330). No new route, no new wire shape.
|
|
4140
|
+
*
|
|
4141
|
+
* ⛔ No fallback, deliberately — unlike {@link aggregate}. A header query that
|
|
4142
|
+
* degraded to bucketing a fetched page would answer page slices under a name
|
|
4143
|
+
* that promises the query's own numbers, which is the defect this member
|
|
4144
|
+
* exists to end. A failure is thrown to the caller, and a body without a
|
|
4145
|
+
* `records` array is refused rather than read as "no groups".
|
|
4146
|
+
*/
|
|
4147
|
+
queryGroupHeaders(resource: string, query: EngineAggregateOptions): Promise<ListViewGroupHeaderRow[]>;
|
|
2403
4148
|
/**
|
|
2404
4149
|
* Client-side aggregation over a server-scoped `find()` — the fallback used
|
|
2405
4150
|
* whenever the analytics endpoint cannot answer (capability absent, network
|
|
@@ -2585,6 +4330,21 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
|
|
|
2585
4330
|
/**
|
|
2586
4331
|
* Factory function to create an ObjectStack data source.
|
|
2587
4332
|
*
|
|
4333
|
+
* The declared return is `ObjectStackAdapter<T>`, not the shared
|
|
4334
|
+
* `DataSource<T>` interface: the object this hands back is an
|
|
4335
|
+
* `ObjectStackAdapter`, and the adapter-only members the package README
|
|
4336
|
+
* documents — `getClient`, the metadata-cache controls, the connection-state
|
|
4337
|
+
* and batch-progress subscriptions — live on the class, not on `DataSource`.
|
|
4338
|
+
* Declaring the interface here narrowed all of them
|
|
4339
|
+
* away from the factory while leaving them on the value (objectui#7323), so
|
|
4340
|
+
* `createObjectStackAdapter(...)` and `new ObjectStackAdapter(...)` — the two
|
|
4341
|
+
* documented ways to obtain the same object — handed back different type
|
|
4342
|
+
* surfaces. ⛔ Do not narrow this back to `DataSource<T>`: a caller who wants
|
|
4343
|
+
* the narrow surface writes `const ds: DataSource = createObjectStackAdapter(…)`
|
|
4344
|
+
* and gets it, because a wider return is assignable to the narrower annotation;
|
|
4345
|
+
* the reverse is not recoverable at the call site without a cast.
|
|
4346
|
+
* `adapterFactoryReturn.types.test.ts` pins both directions.
|
|
4347
|
+
*
|
|
2588
4348
|
* @example
|
|
2589
4349
|
* ```typescript
|
|
2590
4350
|
* const dataSource = createObjectStackAdapter({
|
|
@@ -2607,6 +4367,6 @@ declare function createObjectStackAdapter<T = unknown>(config: {
|
|
|
2607
4367
|
autoReconnect?: boolean;
|
|
2608
4368
|
maxReconnectAttempts?: number;
|
|
2609
4369
|
reconnectDelay?: number;
|
|
2610
|
-
}):
|
|
4370
|
+
}): ObjectStackAdapter<T>;
|
|
2611
4371
|
|
|
2612
|
-
export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, ConcurrentUpdateError, ConnectionError, type
|
|
4372
|
+
export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsForbiddenError, AnalyticsKeysOnSpecShapeError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsResultShapeError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, CHOICE_TYPES_REQUIRING_OPTIONS, ConcurrentUpdateError, ConnectionError, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, type DraftChange, type DroppedFieldsNotice, FILTER_OPERATOR_ALIASES, type ImportMappingsFailureKind, META_ITEM_ABSENT_CODE, MalformedFilterError, type MetaItemAccess, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListAnswer, type MetadataListOptions, type MetadataLockState, MetadataNotFoundError, type MetadataOverlayScope, type MetadataReadFailureKind, type MetadataReadWarningEvent, type MetadataReadWarningListener, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, OBJECT_METADATA_TYPE, ObjectStackAdapter, ObjectStackError, type ObjectStackUserPreferenceFamilyOptions, type ObjectStackUserStateAdapterOptions, PREFERENCE_FAMILY_ROW_LIMIT, RELATIONSHIP_TYPES_REQUIRING_REFERENCE, UNRECOGNIZED_DROP_REASON, UnlowerableAnalyticsFilterError, UnloweredAggregateWhereError, type UnrecognizedDropReasonEvent, type UserDataAdapter, type UserPreferenceFamily, VIEW_OVERLAY_OWNED_KEYS, type WriteWarningEvent, type WriteWarningListener, assertObjectMetadataWritable, classifyAnalyticsFailure, classifyImportMappingsFailure, classifyViewsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserPreferenceFamily, createObjectStackUserStateAdapter, createQuietHttpLogger, draftChangesUnder, dropServedPicklistOptions, extractDraftBody, formatHttpFailureMessage, formatMetadataError, formatMetadataIssue, getSharedDiscovery, governingDraftChange, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isMetaItemAbsentError, isObjectStackError, narrowPersonalizationOverlay, normaliseClientError, readDraftChanges, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, viewItemObjectName };
|