@object-ui/data-objectstack 17.5.0 → 17.7.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 +2311 -0
- package/README.md +557 -11
- package/dist/index.cjs +1663 -463
- package/dist/index.d.cts +1780 -518
- package/dist/index.d.ts +1780 -518
- package/dist/index.js +1648 -457
- package/package.json +9 -9
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/aggregate-capability.test.ts +0 -177
- package/src/apiAccessDenied.test.ts +0 -233
- package/src/appAccessProbe.test.ts +0 -147
- package/src/bulkDelete.test.ts +0 -75
- package/src/bulkUpdate.test.ts +0 -93
- package/src/cache/MetadataCache.test.ts +0 -477
- package/src/cache/MetadataCache.ts +0 -289
- package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
- package/src/connection.test.ts +0 -188
- package/src/contracts.ts +0 -115
- package/src/deleteView.homes.test.ts +0 -389
- package/src/deleteViewContract.types.test.ts +0 -246
- package/src/errors.test.ts +0 -426
- package/src/errors.ts +0 -293
- package/src/exportDownload.test.ts +0 -141
- package/src/filter-entry-translation.test.ts +0 -360
- package/src/filter-operator-ast-parity.test.ts +0 -135
- package/src/getObjectSchema.test.ts +0 -145
- package/src/httpFailureLogging.test.ts +0 -109
- package/src/importJob.test.ts +0 -71
- package/src/index.ts +0 -4442
- package/src/integration.ts +0 -200
- package/src/listViewOverrides.test.ts +0 -241
- package/src/listViews.test.ts +0 -175
- package/src/metadata-client.get-envelope.test.ts +0 -171
- package/src/metadata-client.saveAdvisories.test.ts +0 -226
- package/src/metadata-client.test.ts +0 -303
- package/src/metadata-client.ts +0 -1046
- package/src/occ.test.ts +0 -101
- package/src/onMutation.test.ts +0 -589
- package/src/onSaveAdvisory.test.ts +0 -311
- package/src/onWriteWarning.test.ts +0 -273
- package/src/orderby-serialization.test.ts +0 -115
- package/src/queryDataset.test.ts +0 -543
- package/src/searchAll.test.ts +0 -120
- package/src/security.ts +0 -243
- package/src/spec-symbol-batch6.test.ts +0 -326
- package/src/studio.ts +0 -152
- package/src/updateView.draft.test.ts +0 -242
- package/src/userState.test.ts +0 -315
- package/src/userState.ts +0 -238
- package/src/v3-compat.test.ts +0 -229
- package/src/validation-error.test.ts +0 -94
- package/src/viewCacheInvalidation.pin.test.ts +0 -260
- package/src/viewInvalidationSeam.pin.test.ts +0 -162
package/dist/index.js
CHANGED
|
@@ -4,6 +4,71 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { ObjectStackClient } from "@objectstack/client";
|
|
7
|
+
import { VIEW_CONSOLE_ROUND_TRIP_KEYS } from "@objectstack/spec/ui";
|
|
8
|
+
import { DroppedFieldsEventSchema } from "@objectstack/spec/data";
|
|
9
|
+
import { isFilterAST, parseFilterAST } from "@objectstack/spec/data";
|
|
10
|
+
import { isRefusedTextComparand as isRefusedTextComparand2, textComparandRefusalReason as textComparandRefusalReason2 } from "@objectstack/spec/data";
|
|
11
|
+
import { stripReadDecorations as stripReadDecorations2 } from "@objectstack/spec/kernel";
|
|
12
|
+
|
|
13
|
+
// src/metadata-client.ts
|
|
14
|
+
import { GetMetaItemLayeredResponseSchema } from "@objectstack/spec/api";
|
|
15
|
+
|
|
16
|
+
// src/object-metadata-write-guard.ts
|
|
17
|
+
var RELATIONSHIP_TYPES_REQUIRING_REFERENCE = ["lookup", "master_detail"];
|
|
18
|
+
var CHOICE_TYPES_REQUIRING_OPTIONS = ["select", "radio"];
|
|
19
|
+
var OBJECT_METADATA_TYPE = "object";
|
|
20
|
+
function isUsableTarget(reference) {
|
|
21
|
+
return typeof reference === "string" && reference.trim() !== "";
|
|
22
|
+
}
|
|
23
|
+
function describeTarget(reference) {
|
|
24
|
+
if (reference === void 0) return "no `reference` key at all";
|
|
25
|
+
if (reference === null) return "a `null` `reference`";
|
|
26
|
+
if (typeof reference !== "string") return `a \`reference\` of type \`${typeof reference}\``;
|
|
27
|
+
if (reference === "") return "an empty `reference`";
|
|
28
|
+
return "a whitespace-only `reference`";
|
|
29
|
+
}
|
|
30
|
+
function hasOptionSource(def) {
|
|
31
|
+
return Array.isArray(def.options) && def.options.length > 0 || typeof def.picklist === "string";
|
|
32
|
+
}
|
|
33
|
+
function describeOptions(options) {
|
|
34
|
+
if (options === void 0) return "no `options` key and no `picklist`";
|
|
35
|
+
if (Array.isArray(options)) return "an empty `options` list and no `picklist`";
|
|
36
|
+
if (options === null) return "a `null` `options` and no `picklist`";
|
|
37
|
+
return `an \`options\` value of type \`${typeof options}\` and no \`picklist\``;
|
|
38
|
+
}
|
|
39
|
+
function fieldEntries(fields) {
|
|
40
|
+
if (Array.isArray(fields)) {
|
|
41
|
+
return fields.flatMap((raw, index) => {
|
|
42
|
+
if (!raw || typeof raw !== "object") return [];
|
|
43
|
+
const record = raw;
|
|
44
|
+
const name = typeof record.name === "string" && record.name ? record.name : `[${index}]`;
|
|
45
|
+
return [{ name, def: record }];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (fields && typeof fields === "object") {
|
|
49
|
+
return Object.entries(fields).flatMap(
|
|
50
|
+
([name, def]) => def && typeof def === "object" ? [{ name, def }] : []
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
function assertObjectMetadataWritable(type, item, writer) {
|
|
56
|
+
if (String(type) !== OBJECT_METADATA_TYPE) return;
|
|
57
|
+
if (!item || typeof item !== "object") return;
|
|
58
|
+
const fields = item.fields;
|
|
59
|
+
for (const { name, def } of fieldEntries(fields)) {
|
|
60
|
+
if (CHOICE_TYPES_REQUIRING_OPTIONS.includes(String(def.type)) && !hasOptionSource(def)) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${writer} refused this object metadata write: the field \`${name}\` is a \`${String(def.type)}\` with no options: it carries ${describeOptions(def.options)}, so it offers nothing to choose and nothing to check a stored value against. A choice field needs at least one option or a shared \`picklist\` (objectstack#20827), so this draft stays here until it has one. Add an option, or change the field to a non-choice type.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!RELATIONSHIP_TYPES_REQUIRING_REFERENCE.includes(String(def.type))) continue;
|
|
66
|
+
if (isUsableTarget(def.reference)) continue;
|
|
67
|
+
throw new Error(
|
|
68
|
+
`${writer} refused this object metadata write: the field \`${name}\` is a \`${String(def.type)}\` and carries ${describeTarget(def.reference)}, so it names no object to link to. \`@objectstack/spec\` refuses the same document at the server with a 422 on \`fields.${name}.reference\`, and that refusal blocks every later save of this object for as long as the half-filled field rides along in the draft (objectui#7714). Pick the target object, or change the field to a non-relationship type.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
7
72
|
|
|
8
73
|
// src/metadata-client.ts
|
|
9
74
|
function readSaveAdvisories(body) {
|
|
@@ -16,6 +81,25 @@ function readSaveAdvisories(body) {
|
|
|
16
81
|
return typeof c.rule === "string" && typeof c.path === "string" && typeof c.where === "string" && typeof c.message === "string" && typeof c.hint === "string" && typeof c.severity === "string";
|
|
17
82
|
});
|
|
18
83
|
}
|
|
84
|
+
var PROTECTION_ENVELOPE_KEYS = [
|
|
85
|
+
"overlayScope",
|
|
86
|
+
"lock",
|
|
87
|
+
"lockReason",
|
|
88
|
+
"lockSource",
|
|
89
|
+
"lockDocsUrl",
|
|
90
|
+
"provenance",
|
|
91
|
+
"packageId",
|
|
92
|
+
"packageVersion",
|
|
93
|
+
"editable",
|
|
94
|
+
"deletable",
|
|
95
|
+
"resettable"
|
|
96
|
+
];
|
|
97
|
+
function unrecognizedEnvelopeKeys(body) {
|
|
98
|
+
const shape = GetMetaItemLayeredResponseSchema.shape;
|
|
99
|
+
return PROTECTION_ENVELOPE_KEYS.filter(
|
|
100
|
+
(key) => body[key] !== void 0 && !shape[key].safeParse(body[key]).success
|
|
101
|
+
);
|
|
102
|
+
}
|
|
19
103
|
var META_PREFIX = "/meta";
|
|
20
104
|
var API_PREFIX = "/api/v1";
|
|
21
105
|
function buildBase(config) {
|
|
@@ -28,6 +112,16 @@ function buildBase(config) {
|
|
|
28
112
|
}
|
|
29
113
|
return `${trimmed}${scoped}`;
|
|
30
114
|
}
|
|
115
|
+
function packagesBaseOf(metaBase) {
|
|
116
|
+
const origin = metaBase.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, "");
|
|
117
|
+
return `${origin}${API_PREFIX}/packages`;
|
|
118
|
+
}
|
|
119
|
+
function unwrapDispatcherEnvelope(body) {
|
|
120
|
+
if (!body || typeof body !== "object") return {};
|
|
121
|
+
const root = body;
|
|
122
|
+
const data = root.data;
|
|
123
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : root;
|
|
124
|
+
}
|
|
31
125
|
async function parseError(res) {
|
|
32
126
|
let body;
|
|
33
127
|
try {
|
|
@@ -60,7 +154,7 @@ var MetadataClient = class _MetadataClient {
|
|
|
60
154
|
__publicField(this, "headers");
|
|
61
155
|
/** ADR-0037: when true, reads render the draft-overlaid world. */
|
|
62
156
|
__publicField(this, "previewDrafts");
|
|
63
|
-
/** #4133 — sink for
|
|
157
|
+
/** #4133 / #5026 — sink for every write door's advisory findings; see the config field. */
|
|
64
158
|
__publicField(this, "onSaveAdvisory");
|
|
65
159
|
this.base = buildBase(config);
|
|
66
160
|
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
@@ -214,8 +308,20 @@ var MetadataClient = class _MetadataClient {
|
|
|
214
308
|
* {@link get} is deliberate and long-standing (the draft envelope's identity
|
|
215
309
|
* and protection carriers are part of what a draft reader inspects), so it
|
|
216
310
|
* is preserved by reading the transport directly instead of going through
|
|
217
|
-
* `get()`'s unwrap.
|
|
218
|
-
*
|
|
311
|
+
* `get()`'s unwrap.
|
|
312
|
+
*
|
|
313
|
+
* ⛔ Do NOT hand `.item` to a gate or a write yourself: the body this serves
|
|
314
|
+
* is DECORATED (`_draft`, then `_diagnostics` from `decorateMetadataItem`),
|
|
315
|
+
* and the spec says a served body is not a valid input to the schema that
|
|
316
|
+
* produced it until those come off. {@link extractDraftBody} is the one
|
|
317
|
+
* reader for this method's answer — it takes the presence verdict and then
|
|
318
|
+
* the strip, in that order. That function used to be copy-pasted into four
|
|
319
|
+
* views and only one copy knew the rule (objectui#8181).
|
|
320
|
+
*
|
|
321
|
+
* Its tolerant siblings exist for the OTHER client method: `unwrapDraftBody`
|
|
322
|
+
* (app-shell) and `unwrapViewDraft` (this package) read a draft that arrived
|
|
323
|
+
* through {@link get}, which already unwrapped the envelope. They split on
|
|
324
|
+
* which method feeds them, not on what they mean — all three strip.
|
|
219
325
|
*/
|
|
220
326
|
async getDraft(type, name, options = {}) {
|
|
221
327
|
return this.readItemResponse(type, name, {
|
|
@@ -223,6 +329,31 @@ var MetadataClient = class _MetadataClient {
|
|
|
223
329
|
...options.packageId ? { packageId: options.packageId } : {}
|
|
224
330
|
});
|
|
225
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Report the runtime authoring gate's advisory findings for a write the
|
|
334
|
+
* server has ALREADY committed (#4133 for the save door, #5026 for the
|
|
335
|
+
* publish door).
|
|
336
|
+
*
|
|
337
|
+
* Everything here is best-effort by construction, and that is the whole
|
|
338
|
+
* contract: the row is committed server-side before this runs, so neither a
|
|
339
|
+
* malformed body, nor a missing key, nor a throwing sink may change what the
|
|
340
|
+
* calling method returns or whether it throws. The server emits `advisories`
|
|
341
|
+
* ONLY when non-empty, so a clean write costs one absent-key check.
|
|
342
|
+
*
|
|
343
|
+
* One helper rather than a copy per door: every door's response declares the
|
|
344
|
+
* key identically, so a second inline copy could only ever drift. The batch
|
|
345
|
+
* door (`ce986aafc`) calls it once per `published[]` element, handing it
|
|
346
|
+
* the element — the object that carries the key there — and the same
|
|
347
|
+
* best-effort contract covers that loop unchanged.
|
|
348
|
+
*/
|
|
349
|
+
emitAdvisories(body, event) {
|
|
350
|
+
if (!this.onSaveAdvisory) return;
|
|
351
|
+
try {
|
|
352
|
+
const advisories = readSaveAdvisories(body);
|
|
353
|
+
if (advisories.length > 0) this.onSaveAdvisory({ ...event, advisories });
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
}
|
|
226
357
|
/**
|
|
227
358
|
* Save (PUT) a metadata item. The framework accepts both the bare
|
|
228
359
|
* item payload and the `{ item: ... }` / `{ metadata: ... }`
|
|
@@ -235,6 +366,7 @@ var MetadataClient = class _MetadataClient {
|
|
|
235
366
|
`MetadataClient.save: name must be non-empty (type="${type}"). The PUT /meta/:type/:name route requires a name segment.`
|
|
236
367
|
);
|
|
237
368
|
}
|
|
369
|
+
assertObjectMetadataWritable(type, item, "MetadataClient.save");
|
|
238
370
|
const params = [];
|
|
239
371
|
if (options.force) params.push("force=true");
|
|
240
372
|
if (options.mode === "draft") params.push("mode=draft");
|
|
@@ -246,7 +378,6 @@ var MetadataClient = class _MetadataClient {
|
|
|
246
378
|
"Content-Type": "application/json"
|
|
247
379
|
};
|
|
248
380
|
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
249
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
250
381
|
const res = await this.fetchImpl(url, {
|
|
251
382
|
method: "PUT",
|
|
252
383
|
headers,
|
|
@@ -254,20 +385,12 @@ var MetadataClient = class _MetadataClient {
|
|
|
254
385
|
});
|
|
255
386
|
if (!res.ok) throw await parseError(res);
|
|
256
387
|
const body = await res.json();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
name,
|
|
264
|
-
mode: options.mode === "draft" ? "draft" : "publish",
|
|
265
|
-
advisories
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
}
|
|
270
|
-
}
|
|
388
|
+
this.emitAdvisories(body, {
|
|
389
|
+
type,
|
|
390
|
+
name,
|
|
391
|
+
door: "save",
|
|
392
|
+
mode: options.mode === "draft" ? "draft" : "publish"
|
|
393
|
+
});
|
|
271
394
|
return body;
|
|
272
395
|
}
|
|
273
396
|
/**
|
|
@@ -281,6 +404,61 @@ var MetadataClient = class _MetadataClient {
|
|
|
281
404
|
* the rows and reports under `seedApplied` — a data problem never fails the
|
|
282
405
|
* publish, so callers should check `seedApplied?.success` and warn the user
|
|
283
406
|
* rather than assume the data went live.
|
|
407
|
+
*
|
|
408
|
+
* Same door as {@link publish} (`POST /meta/:type/:name/publish`), so it
|
|
409
|
+
* reports the gate's advisories the same way (#5026).
|
|
410
|
+
*
|
|
411
|
+
* The BATCH door (`POST /packages/:id/publish-drafts`) is a different route
|
|
412
|
+
* with a method of its own — {@link publishPackageDrafts} — and it reports
|
|
413
|
+
* too. It did not when this paragraph was first written: it discarded
|
|
414
|
+
* per-draft advisories server-side, which was objectstack#9343, and that card
|
|
415
|
+
* has since landed with a ruling that each `published[]` element carries
|
|
416
|
+
* them (`ce986aafc` is the client half).
|
|
417
|
+
*
|
|
418
|
+
* What survives that change is the READING RULE, which is about this method
|
|
419
|
+
* rather than about the other route: it reads the top level of the
|
|
420
|
+
* single-item body and nothing else. A batch-shaped body arriving HERE still
|
|
421
|
+
* reports nothing, because `PublishMetaItemResponse` declares no
|
|
422
|
+
* `published[]` and a client that went looking for findings wherever they
|
|
423
|
+
* might be would be inventing a contract instead of reading one.
|
|
424
|
+
*
|
|
425
|
+
* ## Why there is no `{ success, data }` unwrapping here (`7b433197d`)
|
|
426
|
+
*
|
|
427
|
+
* This method used to unwrap a dispatcher-shaped envelope before returning,
|
|
428
|
+
* while {@link publish} — the same route, a couple of hundred lines down —
|
|
429
|
+
* did not. Two spellings of one wire contract, and nothing said which was
|
|
430
|
+
* right. The tolerance is gone because the route was MEASURED, at the
|
|
431
|
+
* producer rather than inferred from the response schema:
|
|
432
|
+
*
|
|
433
|
+
* - The only mount of `POST /api/v1/meta/:type/:name/publish` is the REST
|
|
434
|
+
* server's own route-manager registration, which answers
|
|
435
|
+
* `res.json(await protocol.publishMetaItem(...))` — the protocol's object
|
|
436
|
+
* verbatim, with no envelope branch. The project-scoped mirror
|
|
437
|
+
* (`/api/v1/environments/:environmentId/...`) re-mounts that same handler.
|
|
438
|
+
* - The `{ success, data }` envelope has exactly one producer, the runtime
|
|
439
|
+
* `HttpDispatcher`, and it does not serve this route: its `/meta` domain
|
|
440
|
+
* has no publish branch, the dispatcher's own route ledger has no row for
|
|
441
|
+
* it, and a three-segment `/meta` path that is not `/published` terminates
|
|
442
|
+
* in a located `routeNotFound`. So no composition — REST server, the
|
|
443
|
+
* dispatcher-only Hono adapter, or both — puts an envelope on this body.
|
|
444
|
+
* - `packages/spec` states the same split in the two docstrings side by
|
|
445
|
+
* side: `PublishMetaItemResponseSchema` describes "the FULL body" this
|
|
446
|
+
* route returns and records that the REST route hands the producer's
|
|
447
|
+
* object to `res.json()` verbatim, while its batch sibling
|
|
448
|
+
* `PublishPackageDraftsResponseSchema` describes a body answered
|
|
449
|
+
* explicitly "inside the dispatcher's `{ success, data }` envelope".
|
|
450
|
+
*
|
|
451
|
+
* ⛔ So an enveloped body on THIS route is not a shape the platform emits,
|
|
452
|
+
* and unwrapping one is not a kindness: it is Commandment #0.1's lenient
|
|
453
|
+
* fallback, a second de-facto contract for a route that declares one. It
|
|
454
|
+
* also fails in the direction that hides the problem — a body that arrived
|
|
455
|
+
* enveloped is one this door did not actually serve, and unwrapping it
|
|
456
|
+
* would present that as a successful promotion.
|
|
457
|
+
*
|
|
458
|
+
* ⚠️ Sibling tolerances in this file are NOT this defect and must stay:
|
|
459
|
+
* {@link listDrafts} reads `data?.data?.drafts` because `GET /meta/_drafts`
|
|
460
|
+
* IS a dispatcher route, and `publishHealthFromResponse` unwraps because the
|
|
461
|
+
* batch door really is enveloped. The rule is per-route, not per-file.
|
|
284
462
|
*/
|
|
285
463
|
async publishDraft(type, name) {
|
|
286
464
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
@@ -291,24 +469,122 @@ var MetadataClient = class _MetadataClient {
|
|
|
291
469
|
});
|
|
292
470
|
if (!res.ok) throw await parseError(res);
|
|
293
471
|
const body = await res.json().catch(() => ({}));
|
|
294
|
-
|
|
295
|
-
return
|
|
472
|
+
this.emitAdvisories(body, { type, name, door: "publish", mode: "publish" });
|
|
473
|
+
return body;
|
|
296
474
|
}
|
|
297
475
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
476
|
+
* Publish EVERY pending draft bound to one package — Studio's "publish whole
|
|
477
|
+
* app" door, `POST /api/v1/packages/:id/publish-drafts` (ADR-0033; the route
|
|
478
|
+
* that orders structure-before-seeds server-side and runs the ADR-0038 L3
|
|
479
|
+
* runtime probes).
|
|
480
|
+
*
|
|
481
|
+
* ## Why it is expressed here at all (`ce986aafc`)
|
|
482
|
+
*
|
|
483
|
+
* It was not, and that was the defect. Two app-shell call sites fired this
|
|
484
|
+
* route with a bare `fetch` / a page-private `apiJson`, outside the seam that
|
|
485
|
+
* covers every other metadata write — so when objectstack#9343 landed and the
|
|
486
|
+
* server began sending per-draft advisories, the author publishing a whole
|
|
487
|
+
* app was told nothing, while the SAME button's client-side capability lint
|
|
488
|
+
* still raised a toast. A door that cannot report is not a door the gate can
|
|
489
|
+
* reach, however loudly the server speaks.
|
|
490
|
+
*
|
|
491
|
+
* ## What it reports, and what it refuses to invent
|
|
492
|
+
*
|
|
493
|
+
* `PublishPackageDraftsResponseSchema` declares `advisories` on EACH
|
|
494
|
+
* `published[]` element — objectstack#9343's ruled shape, the same element
|
|
495
|
+
* type and the same omitted-when-empty discipline as the single-item door,
|
|
496
|
+
* and explicitly not a parallel top-level map. So this method emits one
|
|
497
|
+
* {@link MetadataSaveAdvisoryEvent} per advised element, each naming that
|
|
498
|
+
* element's own `type` / `name`, through the same sink, event and renderer
|
|
499
|
+
* the save and single-item publish doors use.
|
|
500
|
+
*
|
|
501
|
+
* ⛔ It renders only what the server sent where the schema says it sits:
|
|
502
|
+
*
|
|
503
|
+
* - `advisories` is read off the element, by the one shared reader, which
|
|
504
|
+
* drops anything that is not a complete finding.
|
|
505
|
+
* - An element that does not carry the `type` and `name` the schema states as
|
|
506
|
+
* REQUIRED is skipped rather than reported under invented identity — the
|
|
507
|
+
* event names the item the author has to go fix, and an event that cannot
|
|
508
|
+
* name it truthfully is worse than silence.
|
|
509
|
+
* - Nothing is derived, counted or summarised from the batch: no advisory
|
|
510
|
+
* exists here that the server did not put in the body.
|
|
511
|
+
*
|
|
512
|
+
* ## The failure shapes stay the callers'
|
|
513
|
+
*
|
|
514
|
+
* Non-2xx throws {@link MetadataError}, like every other method. A 2xx is
|
|
515
|
+
* RETURNED unexamined — `success: false` is not a failure on this route
|
|
516
|
+
* (`outcome: 'nothing_to_publish'` answers it too, and the spec's own text
|
|
517
|
+
* says to read `outcome`, not the boolean), and the two callers have their
|
|
518
|
+
* own, different rules for the refusal and rolled-back cases. Deciding that
|
|
519
|
+
* here would change behaviour this card is not about.
|
|
520
|
+
*/
|
|
521
|
+
async publishPackageDrafts(packageId) {
|
|
522
|
+
if (!packageId || !String(packageId).trim()) {
|
|
523
|
+
throw new Error(
|
|
524
|
+
"MetadataClient.publishPackageDrafts: packageId must be non-empty. The POST /packages/:id/publish-drafts route requires an id segment."
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
const url = `${packagesBaseOf(this.base)}/${encodeURIComponent(packageId)}/publish-drafts`;
|
|
528
|
+
const res = await this.fetchImpl(url, {
|
|
529
|
+
method: "POST",
|
|
530
|
+
// Both call sites this replaces sent the session cookie, and the client's
|
|
531
|
+
// own fetch adds the console's Bearer token — so the routed call carries a
|
|
532
|
+
// superset of what the bare doors carried, never less.
|
|
533
|
+
credentials: "include",
|
|
534
|
+
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
535
|
+
body: "{}"
|
|
536
|
+
});
|
|
537
|
+
if (!res.ok) throw await parseError(res);
|
|
538
|
+
const body = unwrapDispatcherEnvelope(await res.json().catch(() => ({})));
|
|
539
|
+
for (const element of Array.isArray(body.published) ? body.published : []) {
|
|
540
|
+
if (!element || typeof element !== "object") continue;
|
|
541
|
+
const { type, name } = element;
|
|
542
|
+
if (typeof type !== "string" || typeof name !== "string") continue;
|
|
543
|
+
this.emitAdvisories(element, { type, name, door: "publish", mode: "publish" });
|
|
544
|
+
}
|
|
545
|
+
return body;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Get the 3-state layered view of a metadata item: `code` (the packaged
|
|
549
|
+
* artifact baseline), `overlay` (the tenant customisation row alone) and
|
|
550
|
+
* `effective` (the merged value the runtime sees).
|
|
551
|
+
*
|
|
552
|
+
* Reads **`GET /meta/:type/:name/layers`** — the path the framework declares
|
|
553
|
+
* for this projection, with a response schema of its own
|
|
554
|
+
* (`GetMetaItemLayeredResponseSchema`, objectstack#5882 ruling B). It used to
|
|
555
|
+
* be reached by hanging a `layers` flag on the ordinary item read, which made
|
|
556
|
+
* one route answer two unrelated representations while `packages/spec`
|
|
557
|
+
* declared only one of them. That spelling still answers this same body
|
|
558
|
+
* inside its deprecation window (the response carries RFC 9745
|
|
559
|
+
* `Deprecation: true` and an RFC 8288 `Link: rel="successor-version"` back to
|
|
560
|
+
* this path) and is scheduled for removal upstream, so nothing here may
|
|
561
|
+
* depend on it — the repo-wide ratchet is
|
|
562
|
+
* `scripts/__tests__/layered-read-declared-path-4016.test.ts`.
|
|
563
|
+
*
|
|
564
|
+
* The request is built here rather than delegated to `@objectstack/client`
|
|
565
|
+
* because the SDK expresses no layered read in EITHER spelling: the
|
|
566
|
+
* framework's REST route ledger records this route as `server-only`,
|
|
567
|
+
* "consumed by objectui over plain HTTP", and whether the SDK should express
|
|
568
|
+
* it is an open upstream product call.
|
|
569
|
+
*
|
|
570
|
+
* One behaviour delta rides along with the path, and it is the server's
|
|
571
|
+
* choice rather than ours: the retired flag FELL THROUGH to the plain item
|
|
572
|
+
* read on a backend whose protocol implementation had no layered support,
|
|
573
|
+
* answering the `{ type, name, item }` envelope. A dedicated path refuses to
|
|
574
|
+
* answer a different resource under this one's declared shape, so it returns
|
|
575
|
+
* 501 `NOT_IMPLEMENTED` instead — which surfaces here as a thrown error
|
|
576
|
+
* rather than a view with `code` and `overlay` silently blank.
|
|
301
577
|
*/
|
|
302
578
|
async layered(type, name, options = {}) {
|
|
303
|
-
const
|
|
304
|
-
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}
|
|
579
|
+
const qs = options.packageId ? `?package=${encodeURIComponent(options.packageId)}` : "";
|
|
580
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/layers${qs}`;
|
|
305
581
|
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
306
582
|
if (res.status === 404) {
|
|
307
583
|
return { code: null, overlay: null, overlayScope: null, effective: null };
|
|
308
584
|
}
|
|
309
585
|
if (!res.ok) throw await parseError(res);
|
|
310
586
|
const body = await res.json();
|
|
311
|
-
const hasEnvelope = body && ("code" in body || "overlay" in body || "effective" in body);
|
|
587
|
+
const hasEnvelope = typeof body === "object" && body !== null && ("code" in body || "overlay" in body || "effective" in body);
|
|
312
588
|
if (!hasEnvelope) {
|
|
313
589
|
return {
|
|
314
590
|
code: null,
|
|
@@ -317,14 +593,34 @@ var MetadataClient = class _MetadataClient {
|
|
|
317
593
|
effective: body
|
|
318
594
|
};
|
|
319
595
|
}
|
|
596
|
+
const parsed = GetMetaItemLayeredResponseSchema.safeParse(body);
|
|
597
|
+
if (parsed.success) {
|
|
598
|
+
const envelope = parsed.data;
|
|
599
|
+
return {
|
|
600
|
+
code: envelope.code ?? null,
|
|
601
|
+
overlay: envelope.overlay ?? null,
|
|
602
|
+
overlayScope: envelope.overlayScope,
|
|
603
|
+
effective: envelope.effective ?? null,
|
|
604
|
+
...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
|
|
605
|
+
lock: envelope.lock,
|
|
606
|
+
...envelope.lockReason !== void 0 ? { lockReason: envelope.lockReason } : {},
|
|
607
|
+
...envelope.lockSource !== void 0 ? { lockSource: envelope.lockSource } : {},
|
|
608
|
+
...envelope.lockDocsUrl !== void 0 ? { lockDocsUrl: envelope.lockDocsUrl } : {},
|
|
609
|
+
...envelope.provenance !== void 0 ? { provenance: envelope.provenance } : {},
|
|
610
|
+
...envelope.packageId !== void 0 ? { packageId: envelope.packageId } : {},
|
|
611
|
+
...envelope.packageVersion !== void 0 ? { packageVersion: envelope.packageVersion } : {},
|
|
612
|
+
editable: envelope.editable,
|
|
613
|
+
deletable: envelope.deletable,
|
|
614
|
+
resettable: envelope.resettable
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const unrecognized = unrecognizedEnvelopeKeys(body);
|
|
320
618
|
return {
|
|
321
619
|
code: body.code ?? null,
|
|
322
620
|
overlay: body.overlay ?? null,
|
|
323
621
|
overlayScope: body.overlayScope ?? null,
|
|
324
622
|
effective: body.effective ?? null,
|
|
325
623
|
...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
|
|
326
|
-
// ADR-0010 Phase 4 — pass through the metadata-protection envelope so
|
|
327
|
-
// the editor can render lock affordances without a second round trip.
|
|
328
624
|
...body.lock !== void 0 ? { lock: body.lock } : {},
|
|
329
625
|
...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
|
|
330
626
|
...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
|
|
@@ -334,7 +630,8 @@ var MetadataClient = class _MetadataClient {
|
|
|
334
630
|
...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
|
|
335
631
|
...body.editable !== void 0 ? { editable: body.editable } : {},
|
|
336
632
|
...body.deletable !== void 0 ? { deletable: body.deletable } : {},
|
|
337
|
-
...body.resettable !== void 0 ? { resettable: body.resettable } : {}
|
|
633
|
+
...body.resettable !== void 0 ? { resettable: body.resettable } : {},
|
|
634
|
+
...unrecognized.length > 0 ? { _unrecognized: unrecognized } : {}
|
|
338
635
|
};
|
|
339
636
|
}
|
|
340
637
|
/**
|
|
@@ -391,7 +688,6 @@ var MetadataClient = class _MetadataClient {
|
|
|
391
688
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
392
689
|
const headers = { ...this.headers };
|
|
393
690
|
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
394
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
395
691
|
const res = await this.fetchImpl(url, { method: "DELETE", headers });
|
|
396
692
|
if (!res.ok) throw await parseError(res);
|
|
397
693
|
return await res.json();
|
|
@@ -402,21 +698,29 @@ var MetadataClient = class _MetadataClient {
|
|
|
402
698
|
* `{ success, version, seq, message }`. Throws a `404 no_draft` if
|
|
403
699
|
* nothing is pending and `409 metadata_conflict` if the published
|
|
404
700
|
* overlay moved while the draft was sitting.
|
|
701
|
+
*
|
|
702
|
+
* Reports the runtime authoring gate's advisory findings through
|
|
703
|
+
* {@link MetadataClientConfig.onSaveAdvisory} when the response carries them
|
|
704
|
+
* (#5026 / objectstack#9176), with `door: 'publish'`. This is the door the
|
|
705
|
+
* Studio designer takes on every edit, so it is the one whose findings an
|
|
706
|
+
* author actually sees.
|
|
405
707
|
*/
|
|
406
708
|
async publish(type, name, options = {}) {
|
|
407
|
-
const
|
|
709
|
+
const qs = options.packageId ? `?package=${encodeURIComponent(options.packageId)}` : "";
|
|
710
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish${qs}`;
|
|
408
711
|
const headers = {
|
|
409
712
|
...this.headers,
|
|
410
713
|
"Content-Type": "application/json"
|
|
411
714
|
};
|
|
412
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
413
715
|
const res = await this.fetchImpl(url, {
|
|
414
716
|
method: "POST",
|
|
415
717
|
headers,
|
|
416
718
|
body: JSON.stringify(options.message ? { message: options.message } : {})
|
|
417
719
|
});
|
|
418
720
|
if (!res.ok) throw await parseError(res);
|
|
419
|
-
|
|
721
|
+
const body = await res.json();
|
|
722
|
+
this.emitAdvisories(body, { type, name, door: "publish", mode: "publish" });
|
|
723
|
+
return body;
|
|
420
724
|
}
|
|
421
725
|
/**
|
|
422
726
|
* Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
|
|
@@ -432,7 +736,6 @@ var MetadataClient = class _MetadataClient {
|
|
|
432
736
|
...this.headers,
|
|
433
737
|
"Content-Type": "application/json"
|
|
434
738
|
};
|
|
435
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
436
739
|
const res = await this.fetchImpl(url, {
|
|
437
740
|
method: "POST",
|
|
438
741
|
headers,
|
|
@@ -512,15 +815,38 @@ function errorCodeIsAnyOf(err, codes) {
|
|
|
512
815
|
}
|
|
513
816
|
|
|
514
817
|
// ../core/src/utils/filter-converter.ts
|
|
515
|
-
import {
|
|
818
|
+
import {
|
|
819
|
+
normalizeFilterOperator,
|
|
820
|
+
VIEW_FILTER_OPERATORS,
|
|
821
|
+
VIEW_FILTER_LIST_VALUE_OPERATORS,
|
|
822
|
+
VIEW_FILTER_PAIR_VALUE_OPERATORS
|
|
823
|
+
} from "@objectstack/spec/ui";
|
|
824
|
+
import { isAcceptedFilterComparand, ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE } from "@objectstack/spec/data";
|
|
825
|
+
|
|
826
|
+
// ../core/src/utils/text-comparand.ts
|
|
827
|
+
import { isRefusedTextComparand, textComparandRefusalReason } from "@objectstack/spec/data";
|
|
828
|
+
|
|
829
|
+
// ../core/src/utils/filter-converter.ts
|
|
516
830
|
var FilterOperatorError = class extends Error {
|
|
517
|
-
constructor(message) {
|
|
831
|
+
constructor(message, subject) {
|
|
518
832
|
super(message);
|
|
519
833
|
__publicField(this, "code", "INVALID_FILTER");
|
|
520
834
|
__publicField(this, "httpStatus", 400);
|
|
835
|
+
/** @see FilterRefusalSubject */
|
|
836
|
+
__publicField(this, "operator");
|
|
837
|
+
/** @see FilterRefusalSubject */
|
|
838
|
+
__publicField(this, "field");
|
|
521
839
|
this.name = "FilterOperatorError";
|
|
840
|
+
this.operator = subject?.operator;
|
|
841
|
+
this.field = subject?.field;
|
|
522
842
|
}
|
|
523
843
|
};
|
|
844
|
+
var RETIRED_OPERATOR_ALIASES = {
|
|
845
|
+
"$notin": "$nin",
|
|
846
|
+
"$notcontains": "$notContains",
|
|
847
|
+
"$startswith": "$startsWith",
|
|
848
|
+
"$endswith": "$endsWith"
|
|
849
|
+
};
|
|
524
850
|
function convertOperatorToAST(operator) {
|
|
525
851
|
const operatorMap = {
|
|
526
852
|
"$eq": "=",
|
|
@@ -531,27 +857,148 @@ function convertOperatorToAST(operator) {
|
|
|
531
857
|
"$lte": "<=",
|
|
532
858
|
"$in": "in",
|
|
533
859
|
"$nin": "nin",
|
|
534
|
-
"$notin": "nin",
|
|
535
860
|
"$between": "between",
|
|
536
861
|
"$contains": "contains",
|
|
537
862
|
"$notContains": "notcontains",
|
|
538
|
-
"$notcontains": "notcontains",
|
|
539
863
|
"$startsWith": "startswith",
|
|
540
|
-
"$startswith": "startswith",
|
|
541
864
|
"$endsWith": "endswith",
|
|
542
|
-
|
|
865
|
+
// Case-insensitive contains. A canonical `FILTER_OPERATORS` member that
|
|
866
|
+
// `ValueDataSource` executes and `FilterConditionField` emits (for its
|
|
867
|
+
// `icontains` builder row — spelled `containsCaseInsensitive` until
|
|
868
|
+
// objectui#9306), while this map refused it with the
|
|
869
|
+
// generic unknown-operator paragraph — so ONE authored filter selected rows
|
|
870
|
+
// through the in-memory matcher and 400'd on the ObjectStack lowering path
|
|
871
|
+
// (objectui#8976). The other direction of the same split objectui#8568 fixed:
|
|
872
|
+
// there the map was MORE tolerant than the matcher, here it was LESS tolerant
|
|
873
|
+
// than the contract. Restored by aligning the map with the spec, per AGENTS.md
|
|
874
|
+
// #0 — not by removing the operator from the builder, which the spec declares.
|
|
875
|
+
//
|
|
876
|
+
// The VALUE is an IDENTITY, and that is not a typo: `icontains` is itself a
|
|
877
|
+
// member of the spec's `VALID_AST_OPERATORS`, so unlike its camelCase siblings
|
|
878
|
+
// above there is no case to squash. Same identity row, for the same stated
|
|
879
|
+
// reason, that `FILTER_OPERATOR_ALIASES` carries in
|
|
880
|
+
// `packages/data-objectstack/src/index.ts`.
|
|
881
|
+
"$icontains": "icontains"
|
|
543
882
|
};
|
|
544
883
|
return operatorMap[operator] || null;
|
|
545
884
|
}
|
|
885
|
+
var AST_LOGIC_KEYWORD = {
|
|
886
|
+
$and: "and",
|
|
887
|
+
$or: "or"
|
|
888
|
+
};
|
|
889
|
+
function lowerLogicalGroup(field, keyword, value) {
|
|
890
|
+
if (!Array.isArray(value)) {
|
|
891
|
+
throw new FilterOperatorError(
|
|
892
|
+
`[ObjectUI] The '${field}' filter combinator takes an ARRAY of conditions. Received ${typeof value === "object" ? "an object" : typeof value}: ${JSON.stringify(value)}. Spec: FilterCondition declares '${field}?: FilterCondition[]' (data/filter.zod.ts).`,
|
|
893
|
+
{ operator: field }
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
if (value.length === 0) {
|
|
897
|
+
return keyword === "and" ? void 0 : falseIdentityLeaf(field, value);
|
|
898
|
+
}
|
|
899
|
+
const children = [];
|
|
900
|
+
let absorbed = false;
|
|
901
|
+
for (const child of value) {
|
|
902
|
+
if (child === null || typeof child !== "object" || Array.isArray(child)) {
|
|
903
|
+
throw new FilterOperatorError(
|
|
904
|
+
`[ObjectUI] Every member of '${field}' must be a filter condition OBJECT. Received ${JSON.stringify(child)}. Spec: FilterCondition declares '${field}?: FilterCondition[]' (data/filter.zod.ts).`,
|
|
905
|
+
{ operator: field }
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
const lowered = convertFiltersToAST(child);
|
|
909
|
+
if (!Array.isArray(lowered)) {
|
|
910
|
+
if (keyword === "or") absorbed = true;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
children.push(lowered);
|
|
914
|
+
}
|
|
915
|
+
if (absorbed) return void 0;
|
|
916
|
+
if (children.length === 0) return void 0;
|
|
917
|
+
return [keyword, ...children];
|
|
918
|
+
}
|
|
919
|
+
function falseIdentityLeaf(field, value) {
|
|
920
|
+
return [field, "=", value];
|
|
921
|
+
}
|
|
922
|
+
function isOperatorMap(value) {
|
|
923
|
+
const proto = Object.getPrototypeOf(value);
|
|
924
|
+
return proto === null || proto === Object.prototype || Object.getPrototypeOf(proto) === null;
|
|
925
|
+
}
|
|
926
|
+
function describeExoticComparand(value) {
|
|
927
|
+
let name;
|
|
928
|
+
let text2;
|
|
929
|
+
try {
|
|
930
|
+
const proto = Object.getPrototypeOf(value);
|
|
931
|
+
name = proto?.constructor?.name;
|
|
932
|
+
} catch {
|
|
933
|
+
name = void 0;
|
|
934
|
+
}
|
|
935
|
+
if (typeof name !== "string" || name === "") return "an object of an unrecognised type";
|
|
936
|
+
try {
|
|
937
|
+
text2 = String(value);
|
|
938
|
+
} catch {
|
|
939
|
+
text2 = void 0;
|
|
940
|
+
}
|
|
941
|
+
return text2 && text2 !== `[object ${name}]` ? `a ${name} instance (${text2})` : `a ${name} instance`;
|
|
942
|
+
}
|
|
943
|
+
function refuseTextComparand(field, operator, target) {
|
|
944
|
+
const ported = `It is refused here rather than lowered onto the wire (objectui#9001; ported from ValueDataSource's refuseTextComparand, objectui#8748).`;
|
|
945
|
+
throw new FilterOperatorError(
|
|
946
|
+
`[ObjectUI] The ${textComparandRefusalReason(field, operator, target)}. ${ported}`,
|
|
947
|
+
{ operator, field }
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
function refuseEmptyOperatorMap(field) {
|
|
951
|
+
throw new FilterOperatorError(
|
|
952
|
+
`[ObjectUI] The filter on field '${field}' is an EMPTY operator map \u2014 { ${field}: {} } names the field and no operator. It cannot be lowered: the ObjectQL filter AST has no node for a field with zero operators, and '@objectstack/spec' ruled the shape REJECTED wherever it appears (objectstack#5240, recorded on FilterConditionSchema). It is refused here rather than dropped: beside a key that lowers, the '${field}' condition used to vanish from the filter and silently WIDEN the result set (objectui#10788), and alone it was handed back as the caller's object, which reached the wire as ?${field}=[object Object] on one route and as a filter the server refuses on the other (objectui#9164). Choose an operator \u2014 { ${field}: { $eq: ... } }, { ${field}: { $in: [...] } }, { ${field}: { $null: true } } \u2014 or remove the key.`,
|
|
953
|
+
{ field }
|
|
954
|
+
);
|
|
955
|
+
}
|
|
546
956
|
function convertFiltersToAST(filter) {
|
|
547
957
|
const conditions = [];
|
|
958
|
+
let trueIdentityGroups = 0;
|
|
959
|
+
let skippedNullKeys = 0;
|
|
548
960
|
for (const [field, value] of Object.entries(filter)) {
|
|
549
|
-
if (value === null || value === void 0)
|
|
961
|
+
if (value === null || value === void 0) {
|
|
962
|
+
skippedNullKeys += 1;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
const logicKeyword = AST_LOGIC_KEYWORD[field];
|
|
966
|
+
if (logicKeyword) {
|
|
967
|
+
const group = lowerLogicalGroup(field, logicKeyword, value);
|
|
968
|
+
if (group !== void 0) conditions.push(group);
|
|
969
|
+
else trueIdentityGroups += 1;
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (field === "$not") {
|
|
973
|
+
throw new FilterOperatorError(
|
|
974
|
+
`[ObjectUI] The '$not' filter combinator cannot be lowered to the ObjectQL filter AST. '@objectstack/spec' declares it on FilterCondition, but the AST this layer emits has no negation keyword (FILTER_ARRAY_LOGIC_KEYWORDS is ['and', 'or']), and rewriting the negation inward is not available either \u2014 'startswith', 'endswith', 'between' and 'icontains' have no negated counterpart in VALID_AST_OPERATORS, so the rewrite would be silently partial. Express the negation with a negated operator instead ($ne, $nin, $notContains); note those follow each operator's own answer for a missing value rather than $not's NULL-safe rule (objectstack#5146). Value: ${JSON.stringify(value)}.`,
|
|
975
|
+
{ operator: "$not" }
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (Array.isArray(value)) {
|
|
979
|
+
throw new FilterOperatorError(
|
|
980
|
+
`[ObjectUI] The filter on field '${field}' carries a bare ARRAY as its equality comparand: ${JSON.stringify(value)}. It cannot be lowered: the ObjectQL filter AST has no array-equality node, so the lowered node [${field}, '=', [...]] is refused by @objectstack/driver-sql (400 INVALID_FILTER) and matches no row in the in-memory matchers \u2014 it can never select anything. It is deliberately NOT read as membership here: { ${field}: [...] } and { ${field}: { $in: [...] } } are different statements, and guessing the second from the first would silently change which rows a stored view returns (objectui#8530; the same ruling objectui#8514 applied one layer down). Spell membership as { ${field}: { $in: [...] } }, its negation as { ${field}: { $nin: [...] } }, or a range as { ${field}: { $between: [min, max] } }.`,
|
|
981
|
+
{ field }
|
|
982
|
+
);
|
|
983
|
+
}
|
|
550
984
|
if (typeof value === "object" && !Array.isArray(value)) {
|
|
551
|
-
|
|
985
|
+
if (isAcceptedFilterComparand(value)) {
|
|
986
|
+
conditions.push([field, "=", value]);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (!isOperatorMap(value)) {
|
|
990
|
+
throw new FilterOperatorError(
|
|
991
|
+
`[ObjectUI] The filter on field '${field}' carries ${describeExoticComparand(value)} in comparand position. '@objectstack/spec' does not accept it: a comparison value must be ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE} (ACCEPTED_FILTER_COMPARAND_TYPES), so isAcceptedFilterComparand is false for it and normalizeFilterComparandTypes answers 400 INVALID_FILTER \u2014 "which no driver can compare". It is not an operator map either: only a plain object can be one, so until objectui#8567 this value was read as ZERO operators and the '${field}' condition was dropped from the filter entirely, silently WIDENING the result set. Spell a text match as { ${field}: { $contains: '...' } } ($startsWith / $endsWith; a pattern itself has no operator here, see the $regex refusal), a membership test as { ${field}: { $in: [...] } }, and a date bound as { ${field}: { $gte: new Date(...) } }.`,
|
|
992
|
+
{ field }
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
const operatorEntries = Object.entries(value);
|
|
996
|
+
if (operatorEntries.length === 0) refuseEmptyOperatorMap(field);
|
|
997
|
+
for (const [operator, operatorValue] of operatorEntries) {
|
|
552
998
|
if (operator === "$regex") {
|
|
553
999
|
throw new FilterOperatorError(
|
|
554
|
-
`[ObjectUI] The '$regex' filter operator is not supported. It used to be converted to 'contains', which matches a literal substring rather than a pattern \u2014 a different result, not a degraded one. Field: '${field}', Value: ${JSON.stringify(operatorValue)}. Use $contains, $
|
|
1000
|
+
`[ObjectUI] The '$regex' filter operator is not supported. It used to be converted to 'contains', which matches a literal substring rather than a pattern \u2014 a different result, not a degraded one. Field: '${field}', Value: ${JSON.stringify(operatorValue)}. Use $contains for a case-sensitive substring, $icontains for a case-insensitive one, or $startsWith / $endsWith.`,
|
|
1001
|
+
{ operator: "$regex", field }
|
|
555
1002
|
);
|
|
556
1003
|
}
|
|
557
1004
|
if (operator === "$null") {
|
|
@@ -562,12 +1009,33 @@ function convertFiltersToAST(filter) {
|
|
|
562
1009
|
conditions.push([field, operatorValue ? "is_not_null" : "is_null", true]);
|
|
563
1010
|
continue;
|
|
564
1011
|
}
|
|
1012
|
+
if (operator === "$empty") {
|
|
1013
|
+
if (operatorValue !== true && operatorValue !== false) {
|
|
1014
|
+
throw new FilterOperatorError(
|
|
1015
|
+
`[ObjectUI] The '$empty' filter operator on field '${field}' takes a boolean flag (true or false); received ${JSON.stringify(operatorValue) ?? String(operatorValue)}. Write { ${field}: { $empty: true } } for an empty value, or { ${field}: { $empty: false } } for a non-empty one.`,
|
|
1016
|
+
{ operator: "$empty", field }
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
conditions.push([field, operatorValue ? "is_empty" : "is_not_empty", true]);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
565
1022
|
const astOperator = convertOperatorToAST(operator);
|
|
566
1023
|
if (astOperator) {
|
|
1024
|
+
if (astOperator === "icontains" && isRefusedTextComparand(operatorValue)) {
|
|
1025
|
+
refuseTextComparand(field, operator, operatorValue);
|
|
1026
|
+
}
|
|
567
1027
|
conditions.push([field, astOperator, operatorValue]);
|
|
568
1028
|
} else {
|
|
1029
|
+
const canonical = RETIRED_OPERATOR_ALIASES[operator];
|
|
1030
|
+
if (canonical) {
|
|
1031
|
+
throw new FilterOperatorError(
|
|
1032
|
+
`[ObjectUI] The '${operator}' filter operator is retired. Write '${canonical}' instead \u2014 the canonical spelling '@objectstack/spec' declares in FILTER_OPERATORS (data/filter.zod.ts). The operator itself is unchanged; only the key is renamed. Field: '${field}', Value: ${JSON.stringify(operatorValue)}. It used to be accepted here as a lowercase alias while the in-memory matcher refused it, so the same filter selected rows through one data source and none through the other (objectui#8568).`,
|
|
1033
|
+
{ operator, field }
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
569
1036
|
throw new FilterOperatorError(
|
|
570
|
-
`[ObjectUI] Unknown filter operator '${operator}' for field '${field}'. Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, $contains, $notContains, $startsWith, $endsWith, $null, $exists. If you need exact object matching, use the value directly without an operator
|
|
1037
|
+
`[ObjectUI] Unknown filter operator '${operator}' for field '${field}'. Supported operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $between, $contains, $notContains, $startsWith, $endsWith, $icontains, $null, $exists, $empty. If you need exact object matching, use the value directly without an operator.`,
|
|
1038
|
+
{ operator, field }
|
|
571
1039
|
);
|
|
572
1040
|
}
|
|
573
1041
|
}
|
|
@@ -576,6 +1044,13 @@ function convertFiltersToAST(filter) {
|
|
|
576
1044
|
}
|
|
577
1045
|
}
|
|
578
1046
|
if (conditions.length === 0) {
|
|
1047
|
+
const keysTheLoopProcessed = Object.keys(filter).length - skippedNullKeys;
|
|
1048
|
+
if (trueIdentityGroups > 0 && trueIdentityGroups === keysTheLoopProcessed) {
|
|
1049
|
+
return void 0;
|
|
1050
|
+
}
|
|
1051
|
+
if (skippedNullKeys > 0 && skippedNullKeys === Object.keys(filter).length) {
|
|
1052
|
+
return void 0;
|
|
1053
|
+
}
|
|
579
1054
|
return filter;
|
|
580
1055
|
}
|
|
581
1056
|
if (conditions.length === 1) {
|
|
@@ -583,6 +1058,52 @@ function convertFiltersToAST(filter) {
|
|
|
583
1058
|
}
|
|
584
1059
|
return ["and", ...conditions];
|
|
585
1060
|
}
|
|
1061
|
+
var ARRAY_VALUED_VIEW_OPERATORS = /* @__PURE__ */ new Set([
|
|
1062
|
+
...VIEW_FILTER_LIST_VALUE_OPERATORS,
|
|
1063
|
+
...VIEW_FILTER_PAIR_VALUE_OPERATORS
|
|
1064
|
+
]);
|
|
1065
|
+
var KNOWN_VIEW_OPERATORS = new Set(VIEW_FILTER_OPERATORS);
|
|
1066
|
+
|
|
1067
|
+
// ../core/src/utils/column-sortability.ts
|
|
1068
|
+
import {
|
|
1069
|
+
FIELD_UNSORTABLE_VIRTUAL_TYPE,
|
|
1070
|
+
FIELD_SORTABLE_UNPROVISIONED_ANCHOR
|
|
1071
|
+
} from "@objectstack/spec/api";
|
|
1072
|
+
var OBJECT_SORTABILITY_KEY = /* @__PURE__ */ Symbol.for("objectui.objectSortability");
|
|
1073
|
+
function asFieldSortability(value) {
|
|
1074
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1075
|
+
const sortable = value.sortable;
|
|
1076
|
+
if (typeof sortable !== "boolean") return void 0;
|
|
1077
|
+
const reason = value.reason;
|
|
1078
|
+
const caveat = value.caveat;
|
|
1079
|
+
return {
|
|
1080
|
+
sortable,
|
|
1081
|
+
...typeof reason === "string" ? { reason } : {},
|
|
1082
|
+
...typeof caveat === "string" ? { caveat } : {}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
function normalizeObjectSortability(served) {
|
|
1086
|
+
if (!served || typeof served !== "object" || Array.isArray(served)) return void 0;
|
|
1087
|
+
const rawFields = served.fields;
|
|
1088
|
+
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) return void 0;
|
|
1089
|
+
const fields = {};
|
|
1090
|
+
for (const [name, entry] of Object.entries(rawFields)) {
|
|
1091
|
+
const verdict = asFieldSortability(entry);
|
|
1092
|
+
if (verdict) fields[name] = verdict;
|
|
1093
|
+
}
|
|
1094
|
+
return { fields };
|
|
1095
|
+
}
|
|
1096
|
+
function attachObjectSortability(schema, served) {
|
|
1097
|
+
const projection = normalizeObjectSortability(served);
|
|
1098
|
+
if (!projection || !schema || typeof schema !== "object") return schema;
|
|
1099
|
+
Object.defineProperty(schema, OBJECT_SORTABILITY_KEY, {
|
|
1100
|
+
value: projection,
|
|
1101
|
+
enumerable: false,
|
|
1102
|
+
configurable: true,
|
|
1103
|
+
writable: true
|
|
1104
|
+
});
|
|
1105
|
+
return schema;
|
|
1106
|
+
}
|
|
586
1107
|
|
|
587
1108
|
// ../core/src/adapters/batchTransaction.ts
|
|
588
1109
|
function idOf(rec) {
|
|
@@ -651,20 +1172,143 @@ async function emulateBatchTransaction(dataSource, operations) {
|
|
|
651
1172
|
}
|
|
652
1173
|
|
|
653
1174
|
// ../core/src/utils/reference-keys.ts
|
|
654
|
-
|
|
1175
|
+
import { FIELD_KEY_GUIDANCE, FieldSchema } from "@objectstack/spec/data";
|
|
1176
|
+
var aliasProbe = (key) => key.toLowerCase().replace(/[_\-\s]/g, "");
|
|
1177
|
+
var declaredFieldKeys = null;
|
|
1178
|
+
var probeFolds = null;
|
|
1179
|
+
var collidingProbes = null;
|
|
1180
|
+
function fieldKeyFolds() {
|
|
1181
|
+
if (probeFolds) return probeFolds;
|
|
1182
|
+
const declared = Object.keys(FieldSchema.shape);
|
|
1183
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1184
|
+
for (const key of declared) {
|
|
1185
|
+
const probe = aliasProbe(key);
|
|
1186
|
+
seen.set(probe, [...seen.get(probe) ?? [], key]);
|
|
1187
|
+
}
|
|
1188
|
+
const folds = /* @__PURE__ */ new Map();
|
|
1189
|
+
const collisions = /* @__PURE__ */ new Map();
|
|
1190
|
+
for (const [probe, keys] of seen) {
|
|
1191
|
+
if (keys.length === 1) folds.set(probe, keys[0]);
|
|
1192
|
+
else collisions.set(probe, keys);
|
|
1193
|
+
}
|
|
1194
|
+
declaredFieldKeys = new Set(declared);
|
|
1195
|
+
collidingProbes = collisions;
|
|
1196
|
+
probeFolds = folds;
|
|
1197
|
+
return folds;
|
|
1198
|
+
}
|
|
1199
|
+
var warnedRetiredSpelling = /* @__PURE__ */ new Set();
|
|
1200
|
+
var warnedUnfoldableSpelling = /* @__PURE__ */ new Set();
|
|
1201
|
+
function warnUnfoldableRetiredKey(reason, key, detail, fieldName, objectName, f) {
|
|
1202
|
+
if (!isDev()) return;
|
|
1203
|
+
const named = fieldName ?? (typeof f.name === "string" ? f.name : "(unnamed field)");
|
|
1204
|
+
const owner = objectName ?? "(unknown object)";
|
|
1205
|
+
const memo = `${owner}:${named}:${key}:${reason}`;
|
|
1206
|
+
if (warnedUnfoldableSpelling.has(memo)) return;
|
|
1207
|
+
const published = reason === "no-declared-twin" && key === "id_field" ? ` \`@objectstack/spec\`'s \`FIELD_KEY_GUIDANCE\` gives the reason: ${FIELD_KEY_GUIDANCE.id_field.why}` : "";
|
|
1208
|
+
warnedUnfoldableSpelling.add(memo);
|
|
1209
|
+
console.warn(
|
|
1210
|
+
`[ObjectUI] Object \`${owner}\`, field \`${named}\`: the ingestion choke point CANNOT canonicalize the retired spelling \`${key}\`. ${detail}${published} The key and its value are LEFT on the def exactly as served \u2014 nothing is dropped here \u2014 but a consumer that reads only the spellings \`@objectstack/spec\`'s \`FieldSchema\` declares will not see this value. Fix the PRODUCER, or migrate the stored document. (maintainer ruling item 3 on objectui#7650; objectui#8938)`
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
function canonicalizeRetiredFieldKeys(f, fieldName, objectName) {
|
|
1214
|
+
const folds = fieldKeyFolds();
|
|
1215
|
+
for (const key of Object.keys(f)) {
|
|
1216
|
+
if (declaredFieldKeys.has(key)) continue;
|
|
1217
|
+
const value = f[key];
|
|
1218
|
+
if (value === void 0) continue;
|
|
1219
|
+
if (REFERENCE_ARM_KEYS.has(key)) continue;
|
|
1220
|
+
const probe = aliasProbe(key);
|
|
1221
|
+
const canonical = folds.get(probe);
|
|
1222
|
+
if (canonical === void 0) {
|
|
1223
|
+
const colliding = collidingProbes.get(probe);
|
|
1224
|
+
if (colliding) {
|
|
1225
|
+
warnUnfoldableRetiredKey(
|
|
1226
|
+
"ambiguous-probe",
|
|
1227
|
+
key,
|
|
1228
|
+
`Its alias spelling is shared by MORE THAN ONE key \`FieldSchema\` declares (${colliding.map((c) => `\`${c}\``).join(", ")}), so the fold refuses to choose between them rather than picking one.`,
|
|
1229
|
+
fieldName,
|
|
1230
|
+
objectName,
|
|
1231
|
+
f
|
|
1232
|
+
);
|
|
1233
|
+
} else {
|
|
1234
|
+
warnUnfoldableRetiredKey(
|
|
1235
|
+
"no-declared-twin",
|
|
1236
|
+
key,
|
|
1237
|
+
`\`FieldSchema\` declares neither \`${key}\` nor any key sharing its alias spelling (lowercased, with \`_\`, \`-\` and spaces removed), so there is no canonical key to fold it onto \u2014 and this path deliberately does not guess a near match.`,
|
|
1238
|
+
fieldName,
|
|
1239
|
+
objectName,
|
|
1240
|
+
f
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
if (f[canonical] !== void 0) {
|
|
1246
|
+
if (f[canonical] !== value) {
|
|
1247
|
+
warnUnfoldableRetiredKey(
|
|
1248
|
+
"canonical-occupied",
|
|
1249
|
+
key,
|
|
1250
|
+
`\`FieldSchema\` declares \`${canonical}\`, and this def already carries a DIFFERENT value under it; the producer's value stands, because this choke point never overwrites one.`,
|
|
1251
|
+
fieldName,
|
|
1252
|
+
objectName,
|
|
1253
|
+
f
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
f[canonical] = value;
|
|
1259
|
+
if (!isDev()) continue;
|
|
1260
|
+
const named = fieldName ?? (typeof f.name === "string" ? f.name : "(unnamed field)");
|
|
1261
|
+
const owner = objectName ?? "(unknown object)";
|
|
1262
|
+
const memo = `${owner}:${named}:${key}:${canonical}`;
|
|
1263
|
+
if (warnedRetiredSpelling.has(memo)) continue;
|
|
1264
|
+
warnedRetiredSpelling.add(memo);
|
|
1265
|
+
console.warn(
|
|
1266
|
+
`[ObjectUI] Object \`${owner}\`, field \`${named}\` carries the retired spelling \`${key}\`. \`@objectstack/spec\`'s \`FieldSchema\` declares \`${canonical}\` and does not declare \`${key}\`, so a producer emitting it would be refused at the metadata write door. ObjectUI stamps \`${canonical}\` here so this stored def still renders, but the consumers no longer carry a \`${key}\` fallback of their own \u2014 fix the PRODUCER, or migrate the stored document. (objectui#7650)`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
var warnedLegacyOnly = /* @__PURE__ */ new Set();
|
|
1271
|
+
var isDev = () => globalThis.process?.env?.NODE_ENV !== "production";
|
|
1272
|
+
var LEGACY_REFERENCE_KEYS = ["reference_to", "referenceTo"];
|
|
1273
|
+
var REFERENCE_ARM_KEYS = new Set(LEGACY_REFERENCE_KEYS);
|
|
1274
|
+
function warnOnLegacyOnlyReference(f, fieldName, objectName) {
|
|
1275
|
+
if (!isDev()) return;
|
|
1276
|
+
if (f.reference !== void 0) return;
|
|
1277
|
+
const spelled = LEGACY_REFERENCE_KEYS.filter((k) => f[k] !== void 0 && f[k] !== "");
|
|
1278
|
+
if (spelled.length === 0) return;
|
|
1279
|
+
const named = fieldName ?? (typeof f.name === "string" ? f.name : "(unnamed field)");
|
|
1280
|
+
const owner = objectName ?? "(unknown object)";
|
|
1281
|
+
for (const key of spelled) {
|
|
1282
|
+
const memo = `${owner}:${named}:${key}:${String(f[key])}`;
|
|
1283
|
+
if (warnedLegacyOnly.has(memo)) continue;
|
|
1284
|
+
warnedLegacyOnly.add(memo);
|
|
1285
|
+
console.warn(
|
|
1286
|
+
`[ObjectUI] Object \`${owner}\`, field \`${named}\` declares its relationship target as \`${key}: '${String(f[key])}'\` and does NOT carry \`reference\`. \`reference\` is the only spelling the protocol declares: \`@objectstack/spec\`'s \`FieldSchema\` refuses \`${key}\` by name with \`unrecognized_keys\` ("Did you mean \`${key}\` -> \`reference\`?"). ObjectUI stamps \`reference\` here so this def still renders, but the readers no longer carry a \`${key}\` fallback of their own \u2014 fix the PRODUCER to emit \`reference\`, or serve the def through a backend that normalizes it (objectstack#13847 does this on the serve path and in \`os migrate meta\`). Maintainer ruling 2026-08-31: protocol normalization belongs on the server, the front end just executes the protocol. (objectui#6837)`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
function normalizeFieldReferenceKeys(fieldDef, fieldName, objectName) {
|
|
655
1291
|
if (!fieldDef || typeof fieldDef !== "object") return fieldDef;
|
|
656
1292
|
const f = fieldDef;
|
|
657
|
-
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
if (
|
|
1293
|
+
canonicalizeRetiredFieldKeys(f, fieldName, objectName);
|
|
1294
|
+
if (f.reference !== void 0) return fieldDef;
|
|
1295
|
+
const legacy = f.reference_to ?? f.referenceTo;
|
|
1296
|
+
if (legacy == null || legacy === "") return fieldDef;
|
|
1297
|
+
warnOnLegacyOnlyReference(f, fieldName, objectName);
|
|
1298
|
+
f.reference = legacy;
|
|
661
1299
|
return fieldDef;
|
|
662
1300
|
}
|
|
663
1301
|
function normalizeSchemaReferenceKeys(schema) {
|
|
664
1302
|
const fields = schema && typeof schema === "object" ? schema.fields : null;
|
|
665
1303
|
if (!fields || typeof fields !== "object") return schema;
|
|
666
|
-
const
|
|
667
|
-
|
|
1304
|
+
const objectName = typeof schema.name === "string" ? schema.name : void 0;
|
|
1305
|
+
if (Array.isArray(fields)) {
|
|
1306
|
+
for (const def of fields) normalizeFieldReferenceKeys(def, void 0, objectName);
|
|
1307
|
+
} else {
|
|
1308
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
1309
|
+
normalizeFieldReferenceKeys(def, name, objectName);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
668
1312
|
return schema;
|
|
669
1313
|
}
|
|
670
1314
|
|
|
@@ -1037,50 +1681,23 @@ function isErrorType(error, errorClass) {
|
|
|
1037
1681
|
return error instanceof errorClass;
|
|
1038
1682
|
}
|
|
1039
1683
|
|
|
1040
|
-
// src/
|
|
1041
|
-
function
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
errors.push({ field: "exports", message: "At least one export is required", code: "NO_EXPORTS" });
|
|
1052
|
-
}
|
|
1053
|
-
if (contract.exports) {
|
|
1054
|
-
const validTypes = ["component", "hook", "utility", "provider"];
|
|
1055
|
-
for (const exp of contract.exports) {
|
|
1056
|
-
if (!exp.name) {
|
|
1057
|
-
errors.push({ field: "exports.name", message: "Export name is required", code: "MISSING_EXPORT_NAME" });
|
|
1058
|
-
}
|
|
1059
|
-
if (!validTypes.includes(exp.type)) {
|
|
1060
|
-
errors.push({ field: "exports.type", message: `Invalid export type: ${exp.type}`, code: "INVALID_EXPORT_TYPE" });
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (!contract.permissions || contract.permissions.length === 0) {
|
|
1065
|
-
warnings.push("No permissions declared \u2014 plugin will have minimal access");
|
|
1066
|
-
}
|
|
1067
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
1684
|
+
// src/error-envelope.ts
|
|
1685
|
+
function readErrorEnvelope(body) {
|
|
1686
|
+
if (!body || typeof body !== "object") return {};
|
|
1687
|
+
const env = body;
|
|
1688
|
+
const nested = env.error && typeof env.error === "object" ? env.error : void 0;
|
|
1689
|
+
const reading = {};
|
|
1690
|
+
const message = text(nested?.message) ?? text(env.message) ?? text(env.error);
|
|
1691
|
+
if (message !== void 0) reading.message = message;
|
|
1692
|
+
const code = present(env.code) ?? present(nested?.code);
|
|
1693
|
+
if (code !== void 0) reading.code = code;
|
|
1694
|
+
return reading;
|
|
1068
1695
|
}
|
|
1069
|
-
function
|
|
1070
|
-
return
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
peerDependencies: contract.peerDependencies ?? {},
|
|
1075
|
-
exports: contract.exports.map((exp) => ({
|
|
1076
|
-
name: exp.name,
|
|
1077
|
-
type: exp.type,
|
|
1078
|
-
description: exp.description ?? ""
|
|
1079
|
-
})),
|
|
1080
|
-
permissions: contract.permissions ?? [],
|
|
1081
|
-
api: contract.api ?? {},
|
|
1082
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1083
|
-
};
|
|
1696
|
+
function text(value) {
|
|
1697
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1698
|
+
}
|
|
1699
|
+
function present(value) {
|
|
1700
|
+
return typeof value === "string" ? value : void 0;
|
|
1084
1701
|
}
|
|
1085
1702
|
|
|
1086
1703
|
// src/userState.ts
|
|
@@ -1185,265 +1802,31 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1185
1802
|
};
|
|
1186
1803
|
}
|
|
1187
1804
|
|
|
1188
|
-
// src/
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Remove an integration.
|
|
1201
|
-
*/
|
|
1202
|
-
unregister(id) {
|
|
1203
|
-
this.integrations.delete(id);
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* Get all registered integrations.
|
|
1207
|
-
*/
|
|
1208
|
-
getAll() {
|
|
1209
|
-
return new Map(this.integrations);
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* Get integrations that match a specific event.
|
|
1213
|
-
*/
|
|
1214
|
-
getForEvent(event) {
|
|
1215
|
-
return Array.from(this.integrations.values()).filter(
|
|
1216
|
-
(integration) => integration.enabled && integration.triggers?.some((t) => t.event === event)
|
|
1217
|
-
);
|
|
1218
|
-
}
|
|
1219
|
-
/**
|
|
1220
|
-
* Dispatch an event to all matching integrations.
|
|
1221
|
-
* Returns results for each integration.
|
|
1222
|
-
*/
|
|
1223
|
-
async dispatch(event, payload) {
|
|
1224
|
-
const matching = this.getForEvent(event);
|
|
1225
|
-
const results = [];
|
|
1226
|
-
for (const [id, integration] of this.integrations) {
|
|
1227
|
-
if (!matching.includes(integration)) continue;
|
|
1228
|
-
try {
|
|
1229
|
-
await this.send(integration, payload);
|
|
1230
|
-
results.push({ id, success: true });
|
|
1231
|
-
} catch (err) {
|
|
1232
|
-
results.push({ id, success: false, error: err.message });
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return results;
|
|
1236
|
-
}
|
|
1237
|
-
/**
|
|
1238
|
-
* Send payload to a specific integration.
|
|
1239
|
-
*/
|
|
1240
|
-
async send(integration, payload) {
|
|
1241
|
-
switch (integration.provider) {
|
|
1242
|
-
case "webhook": {
|
|
1243
|
-
const cfg = integration.config;
|
|
1244
|
-
const url = cfg.url;
|
|
1245
|
-
try {
|
|
1246
|
-
const parsed = new URL(url);
|
|
1247
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1248
|
-
throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
|
|
1249
|
-
}
|
|
1250
|
-
} catch (e) {
|
|
1251
|
-
if (e instanceof TypeError) {
|
|
1252
|
-
throw new Error(`Invalid webhook URL: ${url}`);
|
|
1253
|
-
}
|
|
1254
|
-
throw e;
|
|
1255
|
-
}
|
|
1256
|
-
await fetch(url, {
|
|
1257
|
-
method: cfg.method,
|
|
1258
|
-
headers: {
|
|
1259
|
-
"Content-Type": "application/json",
|
|
1260
|
-
...cfg.headers
|
|
1261
|
-
},
|
|
1262
|
-
body: JSON.stringify(payload)
|
|
1263
|
-
});
|
|
1264
|
-
break;
|
|
1265
|
-
}
|
|
1266
|
-
case "slack": {
|
|
1267
|
-
const cfg = integration.config;
|
|
1268
|
-
const url = cfg.webhookUrl;
|
|
1269
|
-
try {
|
|
1270
|
-
const parsed = new URL(url);
|
|
1271
|
-
if (parsed.protocol !== "https:") {
|
|
1272
|
-
throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
|
|
1273
|
-
}
|
|
1274
|
-
} catch (e) {
|
|
1275
|
-
if (e instanceof TypeError) {
|
|
1276
|
-
throw new Error(`Invalid Slack webhook URL: ${url}`);
|
|
1277
|
-
}
|
|
1278
|
-
throw e;
|
|
1279
|
-
}
|
|
1280
|
-
await fetch(url, {
|
|
1281
|
-
method: "POST",
|
|
1282
|
-
headers: { "Content-Type": "application/json" },
|
|
1283
|
-
body: JSON.stringify({
|
|
1284
|
-
channel: cfg.channel,
|
|
1285
|
-
username: cfg.username,
|
|
1286
|
-
icon_emoji: cfg.iconEmoji,
|
|
1287
|
-
text: JSON.stringify(payload)
|
|
1288
|
-
})
|
|
1289
|
-
});
|
|
1290
|
-
break;
|
|
1291
|
-
}
|
|
1292
|
-
// Email and other providers would require server-side implementation
|
|
1293
|
-
default:
|
|
1294
|
-
break;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
};
|
|
1298
|
-
|
|
1299
|
-
// src/security.ts
|
|
1300
|
-
var SecurityManager = class {
|
|
1301
|
-
constructor(policy = {}) {
|
|
1302
|
-
__publicField(this, "policy");
|
|
1303
|
-
__publicField(this, "auditLog", []);
|
|
1304
|
-
this.policy = policy;
|
|
1305
|
-
}
|
|
1306
|
-
/**
|
|
1307
|
-
* Generate a CSP header string from the configuration.
|
|
1308
|
-
*/
|
|
1309
|
-
generateCSPHeader() {
|
|
1310
|
-
const csp = this.policy.csp;
|
|
1311
|
-
if (!csp) return "";
|
|
1312
|
-
const directives = [];
|
|
1313
|
-
if (csp.scriptSrc?.length) directives.push(`script-src ${csp.scriptSrc.join(" ")}`);
|
|
1314
|
-
if (csp.styleSrc?.length) directives.push(`style-src ${csp.styleSrc.join(" ")}`);
|
|
1315
|
-
if (csp.imgSrc?.length) directives.push(`img-src ${csp.imgSrc.join(" ")}`);
|
|
1316
|
-
if (csp.connectSrc?.length) directives.push(`connect-src ${csp.connectSrc.join(" ")}`);
|
|
1317
|
-
if (csp.fontSrc?.length) directives.push(`font-src ${csp.fontSrc.join(" ")}`);
|
|
1318
|
-
if (csp.frameSrc?.length) directives.push(`frame-src ${csp.frameSrc.join(" ")}`);
|
|
1319
|
-
if (csp.reportUri) directives.push(`report-uri ${csp.reportUri}`);
|
|
1320
|
-
return directives.join("; ");
|
|
1321
|
-
}
|
|
1322
|
-
/**
|
|
1323
|
-
* Record an audit log entry.
|
|
1324
|
-
*/
|
|
1325
|
-
recordAudit(entry) {
|
|
1326
|
-
if (!this.policy.auditLog?.enabled) return;
|
|
1327
|
-
if (this.policy.auditLog.events && !this.policy.auditLog.events.includes(entry.event)) return;
|
|
1328
|
-
const fullEntry = {
|
|
1329
|
-
...entry,
|
|
1330
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1331
|
-
};
|
|
1332
|
-
this.auditLog.push(fullEntry);
|
|
1333
|
-
const dest = this.policy.auditLog.destination ?? "console";
|
|
1334
|
-
if (dest === "console" || dest === "both") {
|
|
1335
|
-
console.info("[AUDIT]", fullEntry.event, fullEntry.userId, fullEntry.resource ?? "");
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Get audit log entries.
|
|
1340
|
-
*/
|
|
1341
|
-
getAuditLog(filter) {
|
|
1342
|
-
let entries = [...this.auditLog];
|
|
1343
|
-
if (filter?.event) entries = entries.filter((e) => e.event === filter.event);
|
|
1344
|
-
if (filter?.userId) entries = entries.filter((e) => e.userId === filter.userId);
|
|
1345
|
-
if (filter?.since) entries = entries.filter((e) => e.timestamp >= filter.since);
|
|
1346
|
-
return entries;
|
|
1347
|
-
}
|
|
1348
|
-
/**
|
|
1349
|
-
* Apply data masking to a record.
|
|
1350
|
-
*/
|
|
1351
|
-
maskRecord(record, userRoles = []) {
|
|
1352
|
-
if (!this.policy.dataMasking?.rules?.length) return record;
|
|
1353
|
-
const masked = { ...record };
|
|
1354
|
-
const maskChar = this.policy.dataMasking.maskChar ?? "*";
|
|
1355
|
-
for (const rule of this.policy.dataMasking.rules) {
|
|
1356
|
-
if (!(rule.field in masked) || masked[rule.field] == null) continue;
|
|
1357
|
-
if (rule.exemptRoles?.some((role) => userRoles.includes(role))) continue;
|
|
1358
|
-
const value = String(masked[rule.field]);
|
|
1359
|
-
switch (rule.strategy) {
|
|
1360
|
-
case "full":
|
|
1361
|
-
masked[rule.field] = maskChar.repeat(value.length);
|
|
1362
|
-
break;
|
|
1363
|
-
case "partial": {
|
|
1364
|
-
const visible = rule.visibleChars ?? 4;
|
|
1365
|
-
if (value.length <= visible) {
|
|
1366
|
-
masked[rule.field] = maskChar.repeat(value.length);
|
|
1367
|
-
} else {
|
|
1368
|
-
masked[rule.field] = value.slice(0, visible) + maskChar.repeat(value.length - visible);
|
|
1369
|
-
}
|
|
1370
|
-
break;
|
|
1371
|
-
}
|
|
1372
|
-
case "hash":
|
|
1373
|
-
masked[rule.field] = `[HASHED:${simpleHash(value)}]`;
|
|
1374
|
-
break;
|
|
1375
|
-
case "redact":
|
|
1376
|
-
masked[rule.field] = "[REDACTED]";
|
|
1377
|
-
break;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
return masked;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Update the security policy.
|
|
1384
|
-
*/
|
|
1385
|
-
updatePolicy(policy) {
|
|
1386
|
-
this.policy = { ...this.policy, ...policy };
|
|
1387
|
-
}
|
|
1388
|
-
/**
|
|
1389
|
-
* Get current security policy.
|
|
1390
|
-
*/
|
|
1391
|
-
getPolicy() {
|
|
1392
|
-
return { ...this.policy };
|
|
1393
|
-
}
|
|
1394
|
-
};
|
|
1395
|
-
function simpleHash(str) {
|
|
1396
|
-
let hash = 0;
|
|
1397
|
-
for (let i = 0; i < str.length; i++) {
|
|
1398
|
-
const char = str.charCodeAt(i);
|
|
1399
|
-
hash = (hash << 5) - hash + char;
|
|
1400
|
-
hash = hash & hash;
|
|
1401
|
-
}
|
|
1402
|
-
return Math.abs(hash).toString(36);
|
|
1805
|
+
// src/draft-envelope.ts
|
|
1806
|
+
import { stripReadDecorations } from "@objectstack/spec/kernel";
|
|
1807
|
+
function extractDraftBody(resp) {
|
|
1808
|
+
if (!resp || typeof resp !== "object") return null;
|
|
1809
|
+
const env = resp;
|
|
1810
|
+
if (!("item" in env)) return null;
|
|
1811
|
+
const body = env.item;
|
|
1812
|
+
if (!body || typeof body !== "object") return null;
|
|
1813
|
+
if (Object.keys(body).length === 0) return null;
|
|
1814
|
+
return stripReadDecorations(body);
|
|
1403
1815
|
}
|
|
1404
1816
|
|
|
1405
|
-
// src/
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
height: 800,
|
|
1410
|
-
background: "grid",
|
|
1411
|
-
gridSize: 8,
|
|
1412
|
-
snapToGrid: true,
|
|
1413
|
-
zoom: {
|
|
1414
|
-
min: 0.25,
|
|
1415
|
-
max: 3,
|
|
1416
|
-
step: 0.1,
|
|
1417
|
-
current: 1
|
|
1418
|
-
},
|
|
1419
|
-
panOffset: { x: 0, y: 0 },
|
|
1420
|
-
showMinimap: false,
|
|
1421
|
-
minimapPosition: "bottom-right",
|
|
1422
|
-
...overrides
|
|
1423
|
-
};
|
|
1817
|
+
// src/metadata-error.ts
|
|
1818
|
+
function extractIssues(e) {
|
|
1819
|
+
const issues = e?.issues;
|
|
1820
|
+
return Array.isArray(issues) ? issues : [];
|
|
1424
1821
|
}
|
|
1425
|
-
function
|
|
1426
|
-
return {
|
|
1427
|
-
x: Math.round(x / gridSize) * gridSize,
|
|
1428
|
-
y: Math.round(y / gridSize) * gridSize
|
|
1429
|
-
};
|
|
1822
|
+
function formatMetadataIssue(i) {
|
|
1823
|
+
return `\u2022 ${i.path && i.path.length > 0 ? i.path : "(root)"} \u2014 ${i.message}`;
|
|
1430
1824
|
}
|
|
1431
|
-
function
|
|
1432
|
-
const
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
for (const item of items) {
|
|
1437
|
-
if (currentX + item.width + padding > canvasWidth) {
|
|
1438
|
-
currentX = padding;
|
|
1439
|
-
currentY += rowMaxHeight + gap;
|
|
1440
|
-
rowMaxHeight = 0;
|
|
1441
|
-
}
|
|
1442
|
-
positions.push({ id: item.id, x: currentX, y: currentY });
|
|
1443
|
-
currentX += item.width + gap;
|
|
1444
|
-
rowMaxHeight = Math.max(rowMaxHeight, item.height);
|
|
1445
|
-
}
|
|
1446
|
-
return positions;
|
|
1825
|
+
function formatMetadataError(e) {
|
|
1826
|
+
const issues = extractIssues(e);
|
|
1827
|
+
if (issues.length > 0) return issues.map(formatMetadataIssue).join("\n");
|
|
1828
|
+
const err = e;
|
|
1829
|
+
return err?.message ?? String(e);
|
|
1447
1830
|
}
|
|
1448
1831
|
|
|
1449
1832
|
// src/index.ts
|
|
@@ -1473,6 +1856,15 @@ var FILTER_OPERATOR_ALIASES = {
|
|
|
1473
1856
|
notin: "nin",
|
|
1474
1857
|
nin: "nin",
|
|
1475
1858
|
contains: "contains",
|
|
1859
|
+
// Case-insensitive contains. A canonical `VIEW_FILTER_OPERATORS` member that
|
|
1860
|
+
// arrived with `@objectstack/spec` 17.1.0 (objectui#5328), and an IDENTITY row
|
|
1861
|
+
// like `contains` above: `icontains` is itself a member of
|
|
1862
|
+
// `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the
|
|
1863
|
+
// AST takes — nothing is translated and no case-sensitivity is lost.
|
|
1864
|
+
// Declared rather than left to the `?? op` fall-through on the rule this
|
|
1865
|
+
// table's test states: the AST gate accepting a spelling is not the driver
|
|
1866
|
+
// compiling it into a WHERE clause.
|
|
1867
|
+
icontains: "icontains",
|
|
1476
1868
|
not_contains: "notcontains",
|
|
1477
1869
|
notcontains: "notcontains",
|
|
1478
1870
|
starts_with: "startswith",
|
|
@@ -1494,14 +1886,21 @@ var FILTER_OPERATOR_ALIASES = {
|
|
|
1494
1886
|
before: "<",
|
|
1495
1887
|
after: ">"
|
|
1496
1888
|
};
|
|
1497
|
-
function
|
|
1889
|
+
function toAstFilterOperator(op) {
|
|
1498
1890
|
if (typeof op !== "string") return null;
|
|
1499
1891
|
const lower = op.toLowerCase();
|
|
1500
1892
|
return FILTER_OPERATOR_ALIASES[lower] ?? FILTER_OPERATOR_ALIASES[op] ?? op;
|
|
1501
1893
|
}
|
|
1894
|
+
function describeFilterEntry(entry) {
|
|
1895
|
+
try {
|
|
1896
|
+
return JSON.stringify(entry, (_key, value) => typeof value === "bigint" ? `${value}n` : value) ?? String(entry);
|
|
1897
|
+
} catch {
|
|
1898
|
+
return String(entry);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1502
1901
|
var MalformedFilterError = class extends Error {
|
|
1503
1902
|
constructor(entry, index) {
|
|
1504
|
-
const shown =
|
|
1903
|
+
const shown = describeFilterEntry(entry);
|
|
1505
1904
|
super(
|
|
1506
1905
|
`Filter entry ${index} is not a usable filter rule (${shown}). Expected { field, operator, value } with a non-empty field.`
|
|
1507
1906
|
);
|
|
@@ -1519,12 +1918,104 @@ function isMalformedFilterError(error) {
|
|
|
1519
1918
|
const e = error;
|
|
1520
1919
|
return e.code === "INVALID_FILTER" || e.name === "MalformedFilterError";
|
|
1521
1920
|
}
|
|
1522
|
-
|
|
1921
|
+
var UnloweredAggregateWhereError = class extends Error {
|
|
1922
|
+
constructor(where, resource) {
|
|
1923
|
+
const shown = JSON.stringify(where) ?? String(where);
|
|
1924
|
+
super(
|
|
1925
|
+
`aggregate('${resource}'): the spec-shape branch received a \`where\` array that is not a filter \u2014 ${shown}. This branch posts \`where\` to POST /data/:object/query verbatim, so it must ALREADY be lowered: either a FilterCondition object (\`QuerySchema.where\`, @objectstack/spec data/query.zod.ts), or a FilterArray the ingress can lower \u2014 a comparison tuple (['stage','=','won']), a logical node (['and',[..],[..]]), or an array of nodes (data/filter.zod.ts, gate \`isFilterAST\`). A ViewFilterRule[] ([{ field, operator, value }, ...]) is authoring sugar, not a filter: lower it in the producer that built these aggregate params, not here. Nothing was sent to the server, so no unfiltered numbers came back.`
|
|
1926
|
+
);
|
|
1927
|
+
__publicField(this, "code", "INVALID_FILTER");
|
|
1928
|
+
__publicField(this, "httpStatus", 400);
|
|
1929
|
+
/** The value as received, so a caller can log what its producer actually built. */
|
|
1930
|
+
__publicField(this, "where");
|
|
1931
|
+
/** The object `aggregate()` was called for. */
|
|
1932
|
+
__publicField(this, "resource");
|
|
1933
|
+
this.name = "UnloweredAggregateWhereError";
|
|
1934
|
+
this.where = where;
|
|
1935
|
+
this.resource = resource;
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
function assertSpecShapeWhereIsFilterAst(where, resource) {
|
|
1939
|
+
if (!Array.isArray(where)) return;
|
|
1940
|
+
if (where.length === 0) return;
|
|
1941
|
+
if (isFilterAST(where)) return;
|
|
1942
|
+
throw new UnloweredAggregateWhereError(where, resource);
|
|
1943
|
+
}
|
|
1944
|
+
var ANALYTICS_ONLY_AGGREGATE_KEYS = ["filter", "field", "function"];
|
|
1945
|
+
var ANALYTICS_ONLY_KEY_ADVICE = {
|
|
1946
|
+
filter: "`filter` is the ANALYTICS branch's filter and is lowered there through `parseFilterAST`; this branch's filter is `where` (`QuerySchema.where`, @objectstack/spec data/query.zod.ts) and is posted VERBATIM \u2014 rename it to `where` and lower it in the producer.",
|
|
1947
|
+
field: "`field` is half of the analytics branch's `${field}_${function}` measure; this branch's measure is `aggregations`, an AggregationNode[] such as [{ function: 'sum', field: 'amount', alias: 'amount_sum' }].",
|
|
1948
|
+
function: "`function` is the other half of that measure pair and belongs inside an `aggregations` node, not beside `groupBy`."
|
|
1949
|
+
};
|
|
1950
|
+
function specShapeSelectorReasons(params) {
|
|
1951
|
+
const reasons = [];
|
|
1952
|
+
if (Array.isArray(params?.groupBy)) reasons.push("`groupBy` is an array");
|
|
1953
|
+
if (Array.isArray(params?.aggregations)) reasons.push("`aggregations` is an array");
|
|
1954
|
+
if (params?.where !== void 0) reasons.push("a `where` key is present");
|
|
1955
|
+
return reasons;
|
|
1956
|
+
}
|
|
1957
|
+
var AnalyticsKeysOnSpecShapeError = class extends Error {
|
|
1958
|
+
constructor(keys, params, resource) {
|
|
1959
|
+
const received = {};
|
|
1960
|
+
for (const key of keys) received[key] = params?.[key];
|
|
1961
|
+
const named = keys.map((k) => `\`${k}\``).join(", ");
|
|
1962
|
+
const shown = keys.map((k) => `${k}=${JSON.stringify(params?.[k]) ?? String(params?.[k])}`).join(", ");
|
|
1963
|
+
const why = specShapeSelectorReasons(params).join(" and ");
|
|
1964
|
+
const noMeasure = !Array.isArray(params?.aggregations) ? " Because no `aggregations` was supplied either, the query this would have built is a grouping with NO MEASURE \u2014 the chart renders and its numbers mean nothing." : "";
|
|
1965
|
+
super(
|
|
1966
|
+
`aggregate('${resource}'): the spec-shape branch received the analytics branch's ${named} \u2014 ${shown} \u2014 and reads none of them. This call took the spec-shape branch because ${why}; the branch posts only \`groupBy\`, \`aggregations\`, \`where\` and \`limit\` to POST /data/:object/query, so those keys would have been dropped in silence.${noMeasure} ` + keys.map((k) => ANALYTICS_ONLY_KEY_ADVICE[k]).join(" ") + " The two shapes are alternatives, not a mixture: the analytics branch (`{ field, function, groupBy, filter }`, which lowers its filter for you) is reached only when NONE of an array `groupBy`, an array `aggregations` or a `where` key is present. Nothing was sent to the server, so no unfiltered numbers came back."
|
|
1967
|
+
);
|
|
1968
|
+
__publicField(this, "code", "INVALID_FILTER");
|
|
1969
|
+
__publicField(this, "httpStatus", 400);
|
|
1970
|
+
/** The offending keys, in the order {@link ANALYTICS_ONLY_AGGREGATE_KEYS} lists them. */
|
|
1971
|
+
__publicField(this, "keys");
|
|
1972
|
+
/** What each offending key carried, so a producer is identifiable from a log. */
|
|
1973
|
+
__publicField(this, "received");
|
|
1974
|
+
/** The object `aggregate()` was called for. */
|
|
1975
|
+
__publicField(this, "resource");
|
|
1976
|
+
this.name = "AnalyticsKeysOnSpecShapeError";
|
|
1977
|
+
this.keys = keys;
|
|
1978
|
+
this.received = received;
|
|
1979
|
+
this.resource = resource;
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
function assertNoAnalyticsKeysOnSpecShape(params, resource) {
|
|
1983
|
+
const present2 = ANALYTICS_ONLY_AGGREGATE_KEYS.filter((key) => params?.[key] != null);
|
|
1984
|
+
if (present2.length === 0) return;
|
|
1985
|
+
throw new AnalyticsKeysOnSpecShapeError(present2, params, resource);
|
|
1986
|
+
}
|
|
1987
|
+
var UnlowerableAnalyticsFilterError = class extends Error {
|
|
1988
|
+
constructor(filter, resource, detail) {
|
|
1989
|
+
const shown = JSON.stringify(filter) ?? String(filter);
|
|
1990
|
+
super(
|
|
1991
|
+
`aggregate('${resource}'): the analytics branch received a \`filter\` array that \`parseFilterAST\` cannot lower into a FilterCondition \u2014 ${shown}.` + (detail ? ` (${detail})` : "") + " `where` on the analytics wire is a FilterCondition (`AnalyticsQuerySchema`, @objectstack/spec data/analytics.zod.ts), and a FilterArray is input-only sugar lowered into one at the single sink `parseFilterAST` (data/filter.zod.ts, objectstack#5158 ruling C). Lowerable shapes are a comparison tuple (['stage','=','won']), a logical node (['and',[..],[..]]), an array of such nodes, and a ViewFilterRule[] this adapter translates first. An infix join ([condA, 'or', condB]) is none of them. Nothing was sent to the server, so no unfiltered numbers came back."
|
|
1992
|
+
);
|
|
1993
|
+
__publicField(this, "code", "INVALID_FILTER");
|
|
1994
|
+
__publicField(this, "httpStatus", 400);
|
|
1995
|
+
/** The filter as received, so a caller can log what its producer actually built. */
|
|
1996
|
+
__publicField(this, "filter");
|
|
1997
|
+
/** The object `aggregate()` was called for. */
|
|
1998
|
+
__publicField(this, "resource");
|
|
1999
|
+
this.name = "UnlowerableAnalyticsFilterError";
|
|
2000
|
+
this.filter = filter;
|
|
2001
|
+
this.resource = resource;
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
function refuseTextComparandEntry(entry, index, field, arrived) {
|
|
2005
|
+
const tail = `This is a { field, operator, value } rule entry, so it is refused as the filter is translated rather than sent: the spelling @objectstack/spec's FILTER_TEXT_CASES uses for this operator is the $-dialect '$icontains', which this form spells '${arrived}'. Remove the entry, or give it a non-empty string value (objectui#9048; the same refusal convertFiltersToAST and ValueDataSource already share).`;
|
|
2006
|
+
const error = new MalformedFilterError(entry, index);
|
|
2007
|
+
error.message = `Filter entry ${index} is refused. The ${textComparandRefusalReason2(field, arrived, entry.value)}. ${tail}`;
|
|
2008
|
+
throw error;
|
|
2009
|
+
}
|
|
2010
|
+
function objectFilterEntryToAST(entry, index) {
|
|
1523
2011
|
if (!entry || typeof entry !== "object") return null;
|
|
1524
2012
|
const field = entry.field;
|
|
1525
2013
|
const rawOp = entry.operator ?? entry.op ?? "=";
|
|
1526
|
-
const op =
|
|
2014
|
+
const op = toAstFilterOperator(rawOp);
|
|
1527
2015
|
if (!field || !op) return null;
|
|
2016
|
+
if (op === "icontains" && isRefusedTextComparand2(entry.value)) {
|
|
2017
|
+
refuseTextComparandEntry(entry, index, String(field), rawOp);
|
|
2018
|
+
}
|
|
1528
2019
|
return [String(field), op, entry.value];
|
|
1529
2020
|
}
|
|
1530
2021
|
function isObjectFilterEntryForm(filter) {
|
|
@@ -1533,8 +2024,8 @@ function isObjectFilterEntryForm(filter) {
|
|
|
1533
2024
|
}
|
|
1534
2025
|
function objectFilterEntriesToAST(entries) {
|
|
1535
2026
|
const nodes = entries.map((entry, i) => {
|
|
1536
|
-
if (Array.isArray(entry)) return translateFilterChild(entry);
|
|
1537
|
-
const tuple = objectFilterEntryToAST(entry);
|
|
2027
|
+
if (Array.isArray(entry)) return translateFilterChild(entry, i);
|
|
2028
|
+
const tuple = objectFilterEntryToAST(entry, i);
|
|
1538
2029
|
if (!tuple) throw new MalformedFilterError(entry, i);
|
|
1539
2030
|
return tuple;
|
|
1540
2031
|
});
|
|
@@ -1545,15 +2036,17 @@ function translateFilterArray(filter) {
|
|
|
1545
2036
|
if (isObjectFilterEntryForm(filter)) return objectFilterEntriesToAST(filter);
|
|
1546
2037
|
const head = filter[0];
|
|
1547
2038
|
if (typeof head === "string" && LOGICAL_AST_HEADS.has(head.toLowerCase())) {
|
|
1548
|
-
return [head, ...filter.slice(1).map(translateFilterChild)];
|
|
2039
|
+
return [head, ...filter.slice(1).map((child, i) => translateFilterChild(child, i + 1))];
|
|
2040
|
+
}
|
|
2041
|
+
if (filter.every((child) => Array.isArray(child))) {
|
|
2042
|
+
return filter.map((child, i) => translateFilterChild(child, i));
|
|
1549
2043
|
}
|
|
1550
|
-
if (filter.every((child) => Array.isArray(child))) return filter.map(translateFilterChild);
|
|
1551
2044
|
return filter;
|
|
1552
2045
|
}
|
|
1553
|
-
function translateFilterChild(child) {
|
|
2046
|
+
function translateFilterChild(child, index) {
|
|
1554
2047
|
if (Array.isArray(child)) return child.length > 0 ? translateFilterArray(child) : child;
|
|
1555
2048
|
if (child && typeof child === "object" && child.field !== void 0) {
|
|
1556
|
-
const tuple = objectFilterEntryToAST(child);
|
|
2049
|
+
const tuple = objectFilterEntryToAST(child, index);
|
|
1557
2050
|
if (tuple) return tuple;
|
|
1558
2051
|
}
|
|
1559
2052
|
return child;
|
|
@@ -1570,6 +2063,30 @@ function translateFilterToAST(filter) {
|
|
|
1570
2063
|
}
|
|
1571
2064
|
return void 0;
|
|
1572
2065
|
}
|
|
2066
|
+
function lowerAnalyticsFilterForWire(filter, resource) {
|
|
2067
|
+
if (!Array.isArray(filter)) return filter;
|
|
2068
|
+
if (filter.length === 0) return void 0;
|
|
2069
|
+
const ast = translateFilterArray(filter);
|
|
2070
|
+
if (!isFilterAST(ast)) throw new UnlowerableAnalyticsFilterError(filter, resource);
|
|
2071
|
+
let lowered;
|
|
2072
|
+
try {
|
|
2073
|
+
lowered = parseFilterAST(ast);
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
throw new UnlowerableAnalyticsFilterError(
|
|
2076
|
+
filter,
|
|
2077
|
+
resource,
|
|
2078
|
+
e instanceof Error ? e.message : String(e)
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
if (lowered === void 0) {
|
|
2082
|
+
throw new UnlowerableAnalyticsFilterError(
|
|
2083
|
+
filter,
|
|
2084
|
+
resource,
|
|
2085
|
+
"`parseFilterAST` answered `undefined` for a non-empty filter"
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
return lowered;
|
|
2089
|
+
}
|
|
1573
2090
|
function serializeOrderBy(orderby) {
|
|
1574
2091
|
if (orderby === void 0 || orderby === null) return void 0;
|
|
1575
2092
|
if (typeof orderby === "string") {
|
|
@@ -1632,22 +2149,134 @@ var APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
|
|
|
1632
2149
|
function isAppPermissionDeniedError(error) {
|
|
1633
2150
|
return errorCodeIs(error, APP_PERMISSION_DENIED_CODE);
|
|
1634
2151
|
}
|
|
2152
|
+
var META_ITEM_ABSENT_CODE = "RESOURCE_NOT_FOUND";
|
|
2153
|
+
function isMetaItemAbsentError(error) {
|
|
2154
|
+
return errorCodeIs(error, META_ITEM_ABSENT_CODE);
|
|
2155
|
+
}
|
|
2156
|
+
function metaEnvelopeCarriesItem(envelope) {
|
|
2157
|
+
if (envelope == null || typeof envelope !== "object") return false;
|
|
2158
|
+
return envelope.item != null;
|
|
2159
|
+
}
|
|
2160
|
+
var VIEW_CONFIG_CAPABILITY = "manage_metadata";
|
|
2161
|
+
var ViewConfigPermissionDeniedError = class extends Error {
|
|
2162
|
+
constructor(objectName, viewId) {
|
|
2163
|
+
super(
|
|
2164
|
+
`View configuration is shared: changing "${viewId}" changes it for everyone who uses this view. That requires the "${VIEW_CONFIG_CAPABILITY}" capability, which this session does not hold.`
|
|
2165
|
+
);
|
|
2166
|
+
__publicField(this, "code", "VIEW_CONFIG_PERMISSION_DENIED");
|
|
2167
|
+
/** The capability that was required and not held. */
|
|
2168
|
+
__publicField(this, "capability", VIEW_CONFIG_CAPABILITY);
|
|
2169
|
+
/** The object whose view configuration the refused write targeted. */
|
|
2170
|
+
__publicField(this, "objectName");
|
|
2171
|
+
/** The view id the refused write targeted. */
|
|
2172
|
+
__publicField(this, "viewId");
|
|
2173
|
+
this.name = "ViewConfigPermissionDeniedError";
|
|
2174
|
+
this.objectName = objectName;
|
|
2175
|
+
this.viewId = viewId;
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
function isViewConfigPermissionDeniedError(error) {
|
|
2179
|
+
if (!error || typeof error !== "object") return false;
|
|
2180
|
+
const e = error;
|
|
2181
|
+
return e.code === "VIEW_CONFIG_PERMISSION_DENIED" || e.name === "ViewConfigPermissionDeniedError";
|
|
2182
|
+
}
|
|
1635
2183
|
var AnalyticsNotInstalledError = class extends Error {
|
|
1636
|
-
constructor(surface, detail) {
|
|
2184
|
+
constructor(surface, detail, serverCode) {
|
|
1637
2185
|
super(
|
|
1638
2186
|
`Analytics capability is not installed on this deployment \u2014 ${surface} is unavailable. Install @objectstack/service-analytics and mount AnalyticsServicePlugin to enable it.` + (detail ? ` (server said: ${detail})` : "")
|
|
1639
2187
|
);
|
|
1640
2188
|
__publicField(this, "code", "ANALYTICS_NOT_INSTALLED");
|
|
1641
2189
|
/** The surface that was unavailable, for the message a host renders. */
|
|
1642
2190
|
__publicField(this, "surface");
|
|
2191
|
+
/**
|
|
2192
|
+
* The server's own ADR-0112 code — the field this branch was chosen BY, when
|
|
2193
|
+
* a producer named one (`NOT_IMPLEMENTED` from the mounted route with no
|
|
2194
|
+
* service behind it, `ROUTE_NOT_FOUND` from the dispatcher when the route is
|
|
2195
|
+
* not mounted at all). Absent for the no-code residual, where a bare
|
|
2196
|
+
* transport 404/501 is all there was to read.
|
|
2197
|
+
*
|
|
2198
|
+
* Additive (objectui#5663): carried so a reader can audit that the headline
|
|
2199
|
+
* and the quoted `detail` below came off the same answer.
|
|
2200
|
+
*/
|
|
2201
|
+
__publicField(this, "serverCode");
|
|
1643
2202
|
this.name = "AnalyticsNotInstalledError";
|
|
1644
2203
|
this.surface = surface;
|
|
2204
|
+
this.serverCode = serverCode;
|
|
1645
2205
|
}
|
|
1646
2206
|
};
|
|
1647
2207
|
function isAnalyticsNotInstalledError(error) {
|
|
1648
2208
|
if (!error || typeof error !== "object") return false;
|
|
1649
2209
|
return error.code === "ANALYTICS_NOT_INSTALLED";
|
|
1650
2210
|
}
|
|
2211
|
+
var AnalyticsDatasetNotFoundError = class extends Error {
|
|
2212
|
+
constructor(opts) {
|
|
2213
|
+
const subject = opts.datasetName ? `Dataset "${opts.datasetName}"` : "The requested dataset";
|
|
2214
|
+
super(
|
|
2215
|
+
`${subject} is not defined in this environment \u2014 the app that defines it is missing or out of date. Analytics itself is installed and answering, so the fix is to upgrade the installed app, not to install a server plugin.` + (opts.serverMessage ? ` (server said: ${opts.serverMessage})` : "")
|
|
2216
|
+
);
|
|
2217
|
+
__publicField(this, "code", "ANALYTICS_DATASET_NOT_FOUND");
|
|
2218
|
+
/**
|
|
2219
|
+
* The dataset this query asked for, taken from the REQUEST rather than parsed
|
|
2220
|
+
* back out of the server's prose: the request is what we know for certain,
|
|
2221
|
+
* and re-reading the name out of a message would make the headline depend on
|
|
2222
|
+
* that message's WORDING — the coupling framework#5367 spent a whole issue
|
|
2223
|
+
* removing from the producing route.
|
|
2224
|
+
*/
|
|
2225
|
+
__publicField(this, "datasetName");
|
|
2226
|
+
/** The server's own ADR-0112 code — the field this branch was chosen BY. */
|
|
2227
|
+
__publicField(this, "serverCode");
|
|
2228
|
+
/** The server's own message, quoted verbatim in the parenthetical. */
|
|
2229
|
+
__publicField(this, "serverMessage");
|
|
2230
|
+
this.name = "AnalyticsDatasetNotFoundError";
|
|
2231
|
+
this.datasetName = opts.datasetName;
|
|
2232
|
+
this.serverCode = opts.serverCode;
|
|
2233
|
+
this.serverMessage = opts.serverMessage;
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2236
|
+
var AnalyticsUnauthenticatedError = class extends Error {
|
|
2237
|
+
constructor(opts = {}) {
|
|
2238
|
+
super(
|
|
2239
|
+
"Analytics query refused: this session is not authenticated. Sign in again and retry \u2014 the request was refused before it ran, so it says nothing about whether the analytics capability is installed." + (opts.serverMessage ? ` (server said: ${opts.serverMessage})` : "")
|
|
2240
|
+
);
|
|
2241
|
+
__publicField(this, "code", "ANALYTICS_UNAUTHENTICATED");
|
|
2242
|
+
/** The server's own ADR-0112 code — the field this branch was chosen BY. */
|
|
2243
|
+
__publicField(this, "serverCode");
|
|
2244
|
+
/** The server's own message, quoted verbatim in the parenthetical. */
|
|
2245
|
+
__publicField(this, "serverMessage");
|
|
2246
|
+
this.name = "AnalyticsUnauthenticatedError";
|
|
2247
|
+
this.serverCode = opts.serverCode;
|
|
2248
|
+
this.serverMessage = opts.serverMessage;
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
var AnalyticsForbiddenError = class extends Error {
|
|
2252
|
+
constructor(opts = {}) {
|
|
2253
|
+
super(
|
|
2254
|
+
`Analytics query refused: this user may not read the data behind ${opts.datasetName ? `dataset "${opts.datasetName}"` : "this dataset"}.` + (opts.serverMessage ? ` (server said: ${opts.serverMessage})` : "")
|
|
2255
|
+
);
|
|
2256
|
+
__publicField(this, "code", "PERMISSION_DENIED");
|
|
2257
|
+
__publicField(this, "httpStatus", 403);
|
|
2258
|
+
/** The server's own ADR-0112 code — the field this branch was chosen BY. */
|
|
2259
|
+
__publicField(this, "serverCode");
|
|
2260
|
+
/** The server's own message, verbatim (diagnostics only). */
|
|
2261
|
+
__publicField(this, "serverMessage");
|
|
2262
|
+
/** The dataset the refused query asked for, when it named one. */
|
|
2263
|
+
__publicField(this, "datasetName");
|
|
2264
|
+
this.name = "AnalyticsForbiddenError";
|
|
2265
|
+
this.serverCode = opts.serverCode;
|
|
2266
|
+
this.serverMessage = opts.serverMessage;
|
|
2267
|
+
this.datasetName = opts.datasetName;
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
function readAnalyticsErrorEnvelope(body) {
|
|
2271
|
+
if (!body || typeof body !== "object") return {};
|
|
2272
|
+
const flat = body;
|
|
2273
|
+
const wrapped = flat.error && typeof flat.error === "object" ? flat.error : void 0;
|
|
2274
|
+
const source = wrapped ?? flat;
|
|
2275
|
+
const code = typeof source.code === "string" && source.code.length > 0 ? source.code : void 0;
|
|
2276
|
+
const message = typeof source.message === "string" && source.message.length > 0 ? source.message : typeof flat.error === "string" && flat.error.length > 0 ? flat.error : void 0;
|
|
2277
|
+
return { code, message };
|
|
2278
|
+
}
|
|
2279
|
+
var ANALYTICS_ABSENT_STATUSES = [404, 501];
|
|
1651
2280
|
var AnalyticsQueryRejectedError = class extends Error {
|
|
1652
2281
|
constructor(detail, serverCode) {
|
|
1653
2282
|
super(
|
|
@@ -1660,15 +2289,48 @@ var AnalyticsQueryRejectedError = class extends Error {
|
|
|
1660
2289
|
this.serverCode = serverCode;
|
|
1661
2290
|
}
|
|
1662
2291
|
};
|
|
2292
|
+
var AnalyticsResultShapeError = class extends Error {
|
|
2293
|
+
constructor(resource, result) {
|
|
2294
|
+
const value = result;
|
|
2295
|
+
const envelope = Array.isArray(value?.data?.rows);
|
|
2296
|
+
const received = result === null ? "null" : Array.isArray(result) ? "an array" : typeof result !== "object" ? typeof result : `an object with keys [${Object.keys(result).join(", ")}]`;
|
|
2297
|
+
super(
|
|
2298
|
+
envelope ? `aggregate('${resource}'): client.analytics.query resolved to the \`{ data: { rows } }\` envelope, not to an AnalyticsResult. @objectstack/client resolves this method to the payload itself since 17.3.0 (objectstack#13079), and this adapter reads \`rows\` only: an older client is installed beside it, or something between the client and POST /analytics/query wraps the payload a second time. Install @objectstack/client 17.3.0 or later.` : `aggregate('${resource}'): client.analytics.query resolved to ${received}, which has no \`rows\` array, so it is not an AnalyticsResult. This adapter reads \`rows\` only (objectui#7028).`
|
|
2299
|
+
);
|
|
2300
|
+
__publicField(this, "code", "ANALYTICS_RESULT_SHAPE_INVALID");
|
|
2301
|
+
/** The object `aggregate()` was called for. */
|
|
2302
|
+
__publicField(this, "resource");
|
|
2303
|
+
/**
|
|
2304
|
+
* True when the value is the envelope the client stopped handing back at
|
|
2305
|
+
* 17.3.0 — `{ success, data: { rows } }`, or its `{ data: { rows } }` core.
|
|
2306
|
+
*/
|
|
2307
|
+
__publicField(this, "envelope");
|
|
2308
|
+
/** What the value was, so the producer is identifiable from a log. */
|
|
2309
|
+
__publicField(this, "received");
|
|
2310
|
+
this.name = "AnalyticsResultShapeError";
|
|
2311
|
+
this.resource = resource;
|
|
2312
|
+
this.envelope = envelope;
|
|
2313
|
+
this.received = received;
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
1663
2316
|
function classifyAnalyticsFailure(error) {
|
|
1664
2317
|
const err = error ?? {};
|
|
1665
|
-
const code = typeof err.code === "string" ? err.code : void 0;
|
|
2318
|
+
const code = typeof err.code === "string" && err.code.length > 0 ? err.code : void 0;
|
|
1666
2319
|
const message = typeof err.message === "string" ? err.message : void 0;
|
|
1667
2320
|
const status = typeof err.httpStatus === "number" ? err.httpStatus : typeof err.status === "number" ? err.status : typeof err.statusCode === "number" ? err.statusCode : void 0;
|
|
1668
|
-
if (code
|
|
1669
|
-
if (status === 404 || status === 501 || code === "NOT_IMPLEMENTED" || code === "ROUTE_NOT_FOUND") {
|
|
2321
|
+
if (errorCodeIsAnyOf({ code }, ["NOT_IMPLEMENTED", "ROUTE_NOT_FOUND"])) {
|
|
1670
2322
|
return { kind: "not-installed", code, message };
|
|
1671
2323
|
}
|
|
2324
|
+
if (errorCodeIs({ code }, "VALIDATION_FAILED")) return { kind: "rejected", code, message };
|
|
2325
|
+
if (errorCodeIs({ code }, "UNAUTHENTICATED")) return { kind: "unauthenticated", code, message };
|
|
2326
|
+
if (errorCodeIs({ code }, "CUBE_NOT_FOUND")) return { kind: "cube-not-found", code, message };
|
|
2327
|
+
if (status === 400) return { kind: "rejected", code, message };
|
|
2328
|
+
if (code === void 0) {
|
|
2329
|
+
if (status !== void 0 && ANALYTICS_ABSENT_STATUSES.includes(status)) {
|
|
2330
|
+
return { kind: "not-installed", code, message };
|
|
2331
|
+
}
|
|
2332
|
+
if (status === 401) return { kind: "unauthenticated", code, message };
|
|
2333
|
+
}
|
|
1672
2334
|
return { kind: "unknown", code, message };
|
|
1673
2335
|
}
|
|
1674
2336
|
var ConcurrentUpdateError = class extends Error {
|
|
@@ -1678,9 +2340,22 @@ var ConcurrentUpdateError = class extends Error {
|
|
|
1678
2340
|
__publicField(this, "httpStatus", 409);
|
|
1679
2341
|
__publicField(this, "currentVersion");
|
|
1680
2342
|
__publicField(this, "currentRecord");
|
|
2343
|
+
/**
|
|
2344
|
+
* The refusal text the PRODUCER marked as addressed to the end user
|
|
2345
|
+
* (`ApiErrorSchema.userMessage`, objectstack `79c46da90`), or `null` when the
|
|
2346
|
+
* refusal carried no marking.
|
|
2347
|
+
*
|
|
2348
|
+
* Declared on the class because this error has no `details` bag: the shared
|
|
2349
|
+
* reader (`declaredUserMessage` in `@object-ui/react`) looks at the
|
|
2350
|
+
* top-level field first, so parking it here is what makes the marking
|
|
2351
|
+
* readable at the surface. The contract is status-agnostic — a 409 carries
|
|
2352
|
+
* it exactly like a 400 or a 403 does.
|
|
2353
|
+
*/
|
|
2354
|
+
__publicField(this, "userMessage");
|
|
1681
2355
|
this.name = "ConcurrentUpdateError";
|
|
1682
2356
|
this.currentVersion = opts.currentVersion;
|
|
1683
2357
|
this.currentRecord = opts.currentRecord;
|
|
2358
|
+
this.userMessage = opts.userMessage ?? null;
|
|
1684
2359
|
}
|
|
1685
2360
|
};
|
|
1686
2361
|
function isConcurrentUpdateError(error) {
|
|
@@ -1688,10 +2363,18 @@ function isConcurrentUpdateError(error) {
|
|
|
1688
2363
|
const e = error;
|
|
1689
2364
|
return e.code === "CONCURRENT_UPDATE" || e.name === "ConcurrentUpdateError";
|
|
1690
2365
|
}
|
|
2366
|
+
function liftUserMessage(e, details) {
|
|
2367
|
+
if (typeof e.userMessage === "string" && e.userMessage.trim()) return e.userMessage;
|
|
2368
|
+
if (typeof details.userMessage === "string" && details.userMessage.trim()) {
|
|
2369
|
+
return details.userMessage;
|
|
2370
|
+
}
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
1691
2373
|
function normaliseClientError(error) {
|
|
1692
2374
|
if (!error || typeof error !== "object") return error;
|
|
1693
2375
|
const e = error;
|
|
1694
2376
|
const details = e.details ?? {};
|
|
2377
|
+
const userMessage = liftUserMessage(e, details);
|
|
1695
2378
|
if (e.code === "VALIDATION_FAILED" || e.name === "ValidationError") {
|
|
1696
2379
|
const rawFields = Array.isArray(details.fields) ? details.fields : Array.isArray(e.fields) ? e.fields : [];
|
|
1697
2380
|
const validationErrors = rawFields.map((f) => {
|
|
@@ -1705,15 +2388,21 @@ function normaliseClientError(error) {
|
|
|
1705
2388
|
typeof e.message === "string" ? e.message : "Validation failed",
|
|
1706
2389
|
validationErrors[0]?.field,
|
|
1707
2390
|
validationErrors,
|
|
1708
|
-
|
|
2391
|
+
// The marking rides the details bag exactly the way `fields` already
|
|
2392
|
+
// does, landing on `err.details.userMessage` — the second of the two
|
|
2393
|
+
// places the shared reader looks. Purely additive: `field`,
|
|
2394
|
+
// `validationErrors` and `fields` are untouched, so the per-field
|
|
2395
|
+
// marking a form already draws keeps working. Omitted entirely when
|
|
2396
|
+
// unmarked, so an unmarked refusal carries no empty key.
|
|
2397
|
+
userMessage === null ? { fields: rawFields } : { fields: rawFields, userMessage }
|
|
1709
2398
|
);
|
|
1710
2399
|
}
|
|
1711
|
-
if (e.code !== "CONCURRENT_UPDATE" && e.httpStatus !== 409) return error;
|
|
1712
2400
|
if (e.code !== "CONCURRENT_UPDATE") return error;
|
|
1713
2401
|
return new ConcurrentUpdateError({
|
|
1714
2402
|
currentVersion: typeof details.currentVersion === "string" ? details.currentVersion : null,
|
|
1715
2403
|
currentRecord: details.currentRecord ?? null,
|
|
1716
|
-
message: typeof e.message === "string" ? e.message : void 0
|
|
2404
|
+
message: typeof e.message === "string" ? e.message : void 0,
|
|
2405
|
+
userMessage
|
|
1717
2406
|
});
|
|
1718
2407
|
}
|
|
1719
2408
|
function formatHttpFailureMessage(message, meta) {
|
|
@@ -1741,8 +2430,14 @@ function createQuietHttpLogger() {
|
|
|
1741
2430
|
return false;
|
|
1742
2431
|
};
|
|
1743
2432
|
const logger = {
|
|
1744
|
-
debug: (message, meta) =>
|
|
1745
|
-
|
|
2433
|
+
debug: (message, meta) => (
|
|
2434
|
+
// eslint-disable-next-line no-console -- objectui#4029, see comment above
|
|
2435
|
+
console.debug(message, meta ?? "")
|
|
2436
|
+
),
|
|
2437
|
+
info: (message, meta) => (
|
|
2438
|
+
// eslint-disable-next-line no-console -- objectui#4029, see comment above
|
|
2439
|
+
console.info(message, meta ?? "")
|
|
2440
|
+
),
|
|
1746
2441
|
warn: (message, meta) => console.warn(message, meta ?? ""),
|
|
1747
2442
|
error: (message, error, meta) => {
|
|
1748
2443
|
if (isExpected404(meta)) {
|
|
@@ -1755,12 +2450,87 @@ function createQuietHttpLogger() {
|
|
|
1755
2450
|
console.error(formatHttpFailureMessage(message, meta) ?? message, error ?? "", meta ?? "");
|
|
1756
2451
|
},
|
|
1757
2452
|
fatal: (message, error, meta) => console.error(formatHttpFailureMessage(message, meta) ?? message, error ?? "", meta ?? ""),
|
|
2453
|
+
// eslint-disable-next-line no-console -- objectui#4029, see comment above
|
|
1758
2454
|
log: (message, ...args) => console.log(message, ...args),
|
|
1759
2455
|
child: () => logger,
|
|
1760
2456
|
withTrace: () => logger
|
|
1761
2457
|
};
|
|
1762
2458
|
return logger;
|
|
1763
2459
|
}
|
|
2460
|
+
var RECOGNIZED_DROP_REASONS = new Set(
|
|
2461
|
+
DroppedFieldsEventSchema.shape.reason.options
|
|
2462
|
+
);
|
|
2463
|
+
var UNRECOGNIZED_DROP_REASON = "objectui:unrecognized-drop-reason";
|
|
2464
|
+
function isRecognizedDropReason(reason) {
|
|
2465
|
+
return RECOGNIZED_DROP_REASONS.has(reason);
|
|
2466
|
+
}
|
|
2467
|
+
function isWireDroppedFieldsEntry(e) {
|
|
2468
|
+
return !!e && typeof e === "object" && Array.isArray(e.fields) && e.fields.some((f) => typeof f === "string");
|
|
2469
|
+
}
|
|
2470
|
+
function asDroppedFieldsNotice(entry, fallbackObject) {
|
|
2471
|
+
const fields = entry.fields.filter((f) => typeof f === "string");
|
|
2472
|
+
const object = typeof entry.object === "string" ? entry.object : fallbackObject;
|
|
2473
|
+
if (isRecognizedDropReason(entry.reason)) {
|
|
2474
|
+
return { ...entry, object, fields, reason: entry.reason };
|
|
2475
|
+
}
|
|
2476
|
+
return {
|
|
2477
|
+
...entry,
|
|
2478
|
+
object,
|
|
2479
|
+
fields,
|
|
2480
|
+
reason: UNRECOGNIZED_DROP_REASON,
|
|
2481
|
+
unrecognizedReason: entry.reason
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
var UNATTRIBUTED_STRIP_OBJECT = "";
|
|
2485
|
+
var META_ROUTE_ABSENT_CODES = ["ROUTE_NOT_FOUND", "NOT_IMPLEMENTED"];
|
|
2486
|
+
var IMPORT_MAPPINGS_UNKNOWN_KIND_CODE = "INVALID_REQUEST";
|
|
2487
|
+
var IMPORT_MAPPINGS_UNKNOWN_KIND_STATUS = 400;
|
|
2488
|
+
var META_ABSENT_STATUSES = [404, 501];
|
|
2489
|
+
var META_REFUSAL_CODES = ["UNAUTHENTICATED", "PERMISSION_DENIED"];
|
|
2490
|
+
var META_REFUSAL_STATUSES = [401, 403, 405];
|
|
2491
|
+
function classifyImportMappingsFailure(error) {
|
|
2492
|
+
const err = error ?? {};
|
|
2493
|
+
const code = typeof err.code === "string" && err.code.length > 0 ? err.code : void 0;
|
|
2494
|
+
const message = typeof err.message === "string" ? err.message : void 0;
|
|
2495
|
+
const status = typeof err.httpStatus === "number" ? err.httpStatus : typeof err.status === "number" ? err.status : typeof err.statusCode === "number" ? err.statusCode : void 0;
|
|
2496
|
+
const found = { code, status, message };
|
|
2497
|
+
if (errorCodeIsAnyOf({ code }, META_ROUTE_ABSENT_CODES)) {
|
|
2498
|
+
return { kind: "not-served", ...found };
|
|
2499
|
+
}
|
|
2500
|
+
if (errorCodeIs({ code }, IMPORT_MAPPINGS_UNKNOWN_KIND_CODE) && status === IMPORT_MAPPINGS_UNKNOWN_KIND_STATUS) {
|
|
2501
|
+
return { kind: "not-served", ...found };
|
|
2502
|
+
}
|
|
2503
|
+
if (errorCodeIsAnyOf({ code }, META_REFUSAL_CODES)) {
|
|
2504
|
+
return { kind: "refused", ...found };
|
|
2505
|
+
}
|
|
2506
|
+
if (status !== void 0 && META_REFUSAL_STATUSES.includes(status)) {
|
|
2507
|
+
return { kind: "refused", ...found };
|
|
2508
|
+
}
|
|
2509
|
+
if (code === void 0 && status !== void 0 && META_ABSENT_STATUSES.includes(status)) {
|
|
2510
|
+
return { kind: "not-served", ...found };
|
|
2511
|
+
}
|
|
2512
|
+
return { kind: "unreadable", ...found };
|
|
2513
|
+
}
|
|
2514
|
+
function classifyViewsFailure(error) {
|
|
2515
|
+
const err = error ?? {};
|
|
2516
|
+
const code = typeof err.code === "string" && err.code.length > 0 ? err.code : void 0;
|
|
2517
|
+
const message = typeof err.message === "string" ? err.message : void 0;
|
|
2518
|
+
const status = typeof err.httpStatus === "number" ? err.httpStatus : typeof err.status === "number" ? err.status : typeof err.statusCode === "number" ? err.statusCode : void 0;
|
|
2519
|
+
const found = { code, status, message };
|
|
2520
|
+
if (errorCodeIsAnyOf({ code }, META_ROUTE_ABSENT_CODES)) {
|
|
2521
|
+
return { kind: "not-served", ...found };
|
|
2522
|
+
}
|
|
2523
|
+
if (errorCodeIsAnyOf({ code }, META_REFUSAL_CODES)) {
|
|
2524
|
+
return { kind: "refused", ...found };
|
|
2525
|
+
}
|
|
2526
|
+
if (status !== void 0 && META_REFUSAL_STATUSES.includes(status)) {
|
|
2527
|
+
return { kind: "refused", ...found };
|
|
2528
|
+
}
|
|
2529
|
+
if (code === void 0 && status !== void 0 && META_ABSENT_STATUSES.includes(status)) {
|
|
2530
|
+
return { kind: "not-served", ...found };
|
|
2531
|
+
}
|
|
2532
|
+
return { kind: "unreadable", ...found };
|
|
2533
|
+
}
|
|
1764
2534
|
function stableStringify(value) {
|
|
1765
2535
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1766
2536
|
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
@@ -1793,15 +2563,48 @@ function withoutNoOpDrops(droppedFields, sent, stored) {
|
|
|
1793
2563
|
}
|
|
1794
2564
|
return out;
|
|
1795
2565
|
}
|
|
2566
|
+
var VIEW_ITEM_ROUND_TRIP_KEYS = Object.keys(VIEW_CONSOLE_ROUND_TRIP_KEYS).filter((key) => VIEW_CONSOLE_ROUND_TRIP_KEYS[key].includes("viewItem"));
|
|
1796
2567
|
function viewItemObjectName(item) {
|
|
1797
2568
|
const spec = item?.list ?? item;
|
|
1798
|
-
return spec?.data?.object ?? spec?.object
|
|
2569
|
+
return spec?.data?.object ?? spec?.object;
|
|
2570
|
+
}
|
|
2571
|
+
var VIEW_OVERLAY_MARKER = "_isOverride";
|
|
2572
|
+
function isPersonalizationOverlayRow(item, spec) {
|
|
2573
|
+
return item?.[VIEW_OVERLAY_MARKER] === true || spec?.[VIEW_OVERLAY_MARKER] === true;
|
|
2574
|
+
}
|
|
2575
|
+
var VIEW_OVERLAY_OWNED_KEYS = Object.freeze([
|
|
2576
|
+
"rowHeight",
|
|
2577
|
+
"sort",
|
|
2578
|
+
"hiddenFields",
|
|
2579
|
+
"columnState",
|
|
2580
|
+
"inlineEdit"
|
|
2581
|
+
]);
|
|
2582
|
+
var VIEW_OVERLAY_IDENTITY_KEYS = Object.freeze([
|
|
2583
|
+
"name",
|
|
2584
|
+
"object",
|
|
2585
|
+
"viewKind",
|
|
2586
|
+
VIEW_OVERLAY_MARKER
|
|
2587
|
+
]);
|
|
2588
|
+
function narrowPersonalizationOverlay(row) {
|
|
2589
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return row;
|
|
2590
|
+
const item = row;
|
|
2591
|
+
const spec = item.list ?? item;
|
|
2592
|
+
if (!isPersonalizationOverlayRow(item, spec)) return row;
|
|
2593
|
+
const narrowed = {};
|
|
2594
|
+
for (const key of VIEW_OVERLAY_IDENTITY_KEYS) {
|
|
2595
|
+
if (item[key] !== void 0) narrowed[key] = item[key];
|
|
2596
|
+
}
|
|
2597
|
+
for (const key of VIEW_OVERLAY_OWNED_KEYS) {
|
|
2598
|
+
if (item[key] !== void 0) narrowed[key] = item[key];
|
|
2599
|
+
}
|
|
2600
|
+
return narrowed;
|
|
1799
2601
|
}
|
|
1800
2602
|
function unwrapViewDraft(resp) {
|
|
1801
2603
|
if (!resp || typeof resp !== "object") return null;
|
|
1802
2604
|
const env = resp;
|
|
1803
|
-
const
|
|
1804
|
-
if (!
|
|
2605
|
+
const raw = "item" in env ? env.item : env;
|
|
2606
|
+
if (!raw || typeof raw !== "object") return null;
|
|
2607
|
+
const body = stripReadDecorations2(raw);
|
|
1805
2608
|
const spec = body.list ?? body;
|
|
1806
2609
|
if (!spec || typeof spec !== "object") return null;
|
|
1807
2610
|
return Object.keys(spec).length > 0 ? spec : null;
|
|
@@ -1857,10 +2660,10 @@ var ObjectStackAdapter = class {
|
|
|
1857
2660
|
// one adapter that has to cope with a backend lacking server atomicity (#2679).
|
|
1858
2661
|
__publicField(this, "batchUnsupported", false);
|
|
1859
2662
|
// The server's declared cross-object atomic-batch capability, read from
|
|
1860
|
-
// discovery at connect() (
|
|
2663
|
+
// discovery at connect() (objectstack-ai/objectstack#3298 / objectui #2693). `true` → the
|
|
1861
2664
|
// backend GUARANTEES an atomic `/batch`, so batchTransaction trusts it and
|
|
1862
2665
|
// never degrades to the non-atomic emulation (any failure surfaces as a real
|
|
1863
|
-
// error). `false` or `undefined` (capability absent → backend predates #3298)
|
|
2666
|
+
// error). `false` or `undefined` (capability absent → backend predates objectstack-ai/objectstack#3298)
|
|
1864
2667
|
// → keep the legacy runtime-probe + emulation fallback so a save is still
|
|
1865
2668
|
// possible; dropping it there would turn "saves, less safe" into "no save
|
|
1866
2669
|
// path" on older backends (#2679 compatibility constraint).
|
|
@@ -1871,7 +2674,7 @@ var ObjectStackAdapter = class {
|
|
|
1871
2674
|
// inline-edit "Save All" writes without a manual reload.
|
|
1872
2675
|
__publicField(this, "mutationListeners", /* @__PURE__ */ new Set());
|
|
1873
2676
|
// Subscribers registered via onWriteWarning(). Emitted after a create/update
|
|
1874
|
-
// whose response carried `droppedFields` (
|
|
2677
|
+
// whose response carried `droppedFields` (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455) so the app
|
|
1875
2678
|
// shell can surface a toast instead of the strip passing silently.
|
|
1876
2679
|
__publicField(this, "writeWarningListeners", /* @__PURE__ */ new Set());
|
|
1877
2680
|
// Subscribers registered via onSaveAdvisory(). Emitted after a metadata save
|
|
@@ -1880,6 +2683,18 @@ var ObjectStackAdapter = class {
|
|
|
1880
2683
|
// above in every respect except which door produced the event: that one is
|
|
1881
2684
|
// record CRUD, this one is the metadata save door.
|
|
1882
2685
|
__publicField(this, "saveAdvisoryListeners", /* @__PURE__ */ new Set());
|
|
2686
|
+
// Subscribers registered via onMetadataReadWarning(). Emitted when a metadata
|
|
2687
|
+
// READ failed in a way that is NOT the supported "this deployment does not
|
|
2688
|
+
// serve that kind" shape, and was degraded to an empty result anyway
|
|
2689
|
+
// (objectui#7741). Third sibling of the two sets above: same seam shape, and
|
|
2690
|
+
// the opposite direction — those two describe a write that succeeded, this
|
|
2691
|
+
// one a read that did not happen.
|
|
2692
|
+
__publicField(this, "metadataReadWarningListeners", /* @__PURE__ */ new Set());
|
|
2693
|
+
// [ADR-0066] The session's REPORTED system capabilities, pushed in by the
|
|
2694
|
+
// host (see `setSystemCapabilities`). `undefined` means NO answer was ever
|
|
2695
|
+
// reported — which is NOT the same as a reported-empty grant, and the two
|
|
2696
|
+
// are treated differently by `maySetViewConfig` below.
|
|
2697
|
+
__publicField(this, "systemCapabilities");
|
|
1883
2698
|
this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
|
|
1884
2699
|
this.installSaveAdvisoryInterceptor();
|
|
1885
2700
|
this.metadataCache = new MetadataCache(config.cache);
|
|
@@ -1888,6 +2703,7 @@ var ObjectStackAdapter = class {
|
|
|
1888
2703
|
this.reconnectDelay = config.reconnectDelay ?? 1e3;
|
|
1889
2704
|
this.baseUrl = config.baseUrl;
|
|
1890
2705
|
this.token = config.token;
|
|
2706
|
+
this.systemCapabilities = config.systemCapabilities;
|
|
1891
2707
|
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
|
|
1892
2708
|
}
|
|
1893
2709
|
/**
|
|
@@ -2061,7 +2877,7 @@ var ObjectStackAdapter = class {
|
|
|
2061
2877
|
* — a per-object metadata-driven search (ADR-0061) — this consults the search
|
|
2062
2878
|
* index and ranks hits across objects, so it surfaces records the per-object
|
|
2063
2879
|
* fanout misses. Global affordances (⌘K command palette, search page) prefer
|
|
2064
|
-
* this path (
|
|
2880
|
+
* this path (objectstack-ai/objectstack#3371).
|
|
2065
2881
|
*
|
|
2066
2882
|
* Returns `{ query, hits }`. A backend without the search plugin installed
|
|
2067
2883
|
* answers `404`; we treat that as "no global search here" and return an empty
|
|
@@ -2094,10 +2910,10 @@ var ObjectStackAdapter = class {
|
|
|
2094
2910
|
if (!res.ok) {
|
|
2095
2911
|
if (res.status === 404) return { query: trimmed, hits: [] };
|
|
2096
2912
|
const errorBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
);
|
|
2913
|
+
const envelope = readErrorEnvelope(errorBody);
|
|
2914
|
+
const err = new Error(envelope.message ?? res.statusText);
|
|
2100
2915
|
err.status = res.status;
|
|
2916
|
+
err.code = envelope.code;
|
|
2101
2917
|
throw err;
|
|
2102
2918
|
}
|
|
2103
2919
|
const body = await res.json();
|
|
@@ -2200,24 +3016,28 @@ var ObjectStackAdapter = class {
|
|
|
2200
3016
|
}
|
|
2201
3017
|
}
|
|
2202
3018
|
/**
|
|
2203
|
-
* Read `droppedFields` off a create/update response (
|
|
3019
|
+
* Read `droppedFields` off a create/update response (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455)
|
|
2204
3020
|
* and, when present, notify write-warning subscribers. Tolerant of a client
|
|
2205
3021
|
* whose response type predates `droppedFields`: the field is read structurally
|
|
2206
3022
|
* and validated, so an older client (or a backend that never drops) is a no-op.
|
|
3023
|
+
*
|
|
3024
|
+
* SHAPE decides whether an entry is an event at all (it must name at least one
|
|
3025
|
+
* field); `object`, `fields` and `reason` are then PARSED — an unrecognized
|
|
3026
|
+
* reason routed to the skew arm, a non-string field element refused, a missing
|
|
3027
|
+
* `object` healed from `resource` — never asserted into the union, and the
|
|
3028
|
+
* entry itself never dropped for them (objectui#4934, `f75810e7c`).
|
|
2207
3029
|
*/
|
|
2208
3030
|
notifyDroppedFields(operation, resource, result, id, sent) {
|
|
2209
3031
|
const dropped = result?.droppedFields;
|
|
2210
3032
|
if (!Array.isArray(dropped) || dropped.length === 0) return;
|
|
2211
|
-
const valid = dropped.filter(
|
|
2212
|
-
(e) => !!e && typeof e === "object" && Array.isArray(e.fields) && e.fields.length > 0
|
|
2213
|
-
);
|
|
3033
|
+
const valid = dropped.filter(isWireDroppedFieldsEntry).map((e) => asDroppedFieldsNotice(e, resource));
|
|
2214
3034
|
const stored = result?.record;
|
|
2215
3035
|
const droppedFields = withoutNoOpDrops(valid, sent, stored);
|
|
2216
3036
|
if (droppedFields.length === 0) return;
|
|
2217
3037
|
this.emitWriteWarning({ operation, resource, ...id !== void 0 ? { id } : {}, droppedFields });
|
|
2218
3038
|
}
|
|
2219
3039
|
/**
|
|
2220
|
-
* Same, for the cross-object transactional batch (
|
|
3040
|
+
* Same, for the cross-object transactional batch (objectstack-ai/objectstack#3794). Its
|
|
2221
3041
|
* response hangs the events off a top-level `droppedFields` list, each tagged
|
|
2222
3042
|
* with the `index` of the operation it came from — `results` entries are bare
|
|
2223
3043
|
* record echoes with nowhere to hang a per-row list.
|
|
@@ -2233,13 +3053,13 @@ var ObjectStackAdapter = class {
|
|
|
2233
3053
|
if (!Array.isArray(dropped) || dropped.length === 0) return;
|
|
2234
3054
|
const results = payload?.results;
|
|
2235
3055
|
for (const entry of dropped) {
|
|
2236
|
-
if (!entry
|
|
3056
|
+
if (!isWireDroppedFieldsEntry(entry)) continue;
|
|
2237
3057
|
const e = entry;
|
|
2238
|
-
if (!Array.isArray(e.fields) || e.fields.length === 0) continue;
|
|
2239
3058
|
const op = typeof e.index === "number" ? operations[e.index] : void 0;
|
|
3059
|
+
const object = typeof e.object === "string" ? e.object : typeof op?.object === "string" ? op.object : UNATTRIBUTED_STRIP_OBJECT;
|
|
2240
3060
|
const stored = typeof e.index === "number" && Array.isArray(results) ? results[e.index] : void 0;
|
|
2241
3061
|
const [live] = withoutNoOpDrops(
|
|
2242
|
-
[{
|
|
3062
|
+
[asDroppedFieldsNotice({ fields: e.fields, reason: e.reason }, object)],
|
|
2243
3063
|
op?.data,
|
|
2244
3064
|
stored
|
|
2245
3065
|
);
|
|
@@ -2247,7 +3067,7 @@ var ObjectStackAdapter = class {
|
|
|
2247
3067
|
const operation = (op?.action ?? "create") === "create" ? "create" : "update";
|
|
2248
3068
|
this.emitWriteWarning({
|
|
2249
3069
|
operation,
|
|
2250
|
-
resource:
|
|
3070
|
+
resource: object,
|
|
2251
3071
|
...op?.id !== void 0 && op?.id !== null ? { id: op.id } : {},
|
|
2252
3072
|
droppedFields: [live]
|
|
2253
3073
|
});
|
|
@@ -2255,7 +3075,7 @@ var ObjectStackAdapter = class {
|
|
|
2255
3075
|
}
|
|
2256
3076
|
/**
|
|
2257
3077
|
* Subscribe to write-warning events (a create/update dropped caller-supplied
|
|
2258
|
-
* fields — #3431
|
|
3078
|
+
* fields — objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455). Returns an unsubscribe function. The app shell uses
|
|
2259
3079
|
* this to toast the user; the write itself already succeeded.
|
|
2260
3080
|
*/
|
|
2261
3081
|
onWriteWarning(callback) {
|
|
@@ -2269,7 +3089,7 @@ var ObjectStackAdapter = class {
|
|
|
2269
3089
|
* advisory findings on a save that SUCCEEDED (#4237; backend
|
|
2270
3090
|
* objectstack#7435). Returns an unsubscribe function.
|
|
2271
3091
|
*
|
|
2272
|
-
* Deliberately the same seam as {@link onWriteWarning} (#3431
|
|
3092
|
+
* Deliberately the same seam as {@link onWriteWarning} (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455), which
|
|
2273
3093
|
* is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
|
|
2274
3094
|
* was modelled on. It is a SIBLING of that channel rather than a second
|
|
2275
3095
|
* payload pushed down it: `WriteWarningEvent` is a closed shape whose
|
|
@@ -2306,6 +3126,45 @@ var ObjectStackAdapter = class {
|
|
|
2306
3126
|
}
|
|
2307
3127
|
}
|
|
2308
3128
|
}
|
|
3129
|
+
/**
|
|
3130
|
+
* Subscribe to metadata read-warning events — a metadata READ that could not
|
|
3131
|
+
* be answered and was degraded to an empty result anyway (objectui#7741).
|
|
3132
|
+
* Returns an unsubscribe function. The app shell uses this to toast the user;
|
|
3133
|
+
* the caller has already been handed the empty result.
|
|
3134
|
+
*
|
|
3135
|
+
* Deliberately the same seam as {@link onWriteWarning} and
|
|
3136
|
+
* {@link onSaveAdvisory}, and a SIBLING of them rather than a payload pushed
|
|
3137
|
+
* down either: both of those announce a write that SUCCEEDED, and an event
|
|
3138
|
+
* saying "this read did not happen" carried on one of them would make it lie
|
|
3139
|
+
* about what occurred. What is reused is the seam's SHAPE — one long-lived
|
|
3140
|
+
* instance with a `subscribe -> unsubscribe` registration that
|
|
3141
|
+
* `AdapterProvider` wires once — not its event type.
|
|
3142
|
+
*
|
|
3143
|
+
* ⛔ Nothing here changes what a caller RECEIVES. `listImportMappings` still
|
|
3144
|
+
* answers `[]` on every failure, including the loud ones; this channel is
|
|
3145
|
+
* added ALONGSIDE that return, which is why it is not a change to a surface
|
|
3146
|
+
* published since `@object-ui/data-objectstack@17.1.0`.
|
|
3147
|
+
*/
|
|
3148
|
+
onMetadataReadWarning(callback) {
|
|
3149
|
+
this.metadataReadWarningListeners.add(callback);
|
|
3150
|
+
return () => {
|
|
3151
|
+
this.metadataReadWarningListeners.delete(callback);
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
/**
|
|
3155
|
+
* Notify all metadata read-warning subscribers. Isolated exactly like
|
|
3156
|
+
* {@link emitWriteWarning}: a throwing listener must neither break the caller
|
|
3157
|
+
* nor starve the others.
|
|
3158
|
+
*/
|
|
3159
|
+
emitMetadataReadWarning(event) {
|
|
3160
|
+
for (const listener of this.metadataReadWarningListeners) {
|
|
3161
|
+
try {
|
|
3162
|
+
listener(event);
|
|
3163
|
+
} catch (err) {
|
|
3164
|
+
console.warn("ObjectStackAdapter: metadata read-warning listener error", err);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
2309
3168
|
/**
|
|
2310
3169
|
* Install the ONE emitter for the metadata save door (#4237).
|
|
2311
3170
|
*
|
|
@@ -2363,6 +3222,12 @@ var ObjectStackAdapter = class {
|
|
|
2363
3222
|
this.emitSaveAdvisory({
|
|
2364
3223
|
type,
|
|
2365
3224
|
name,
|
|
3225
|
+
// #5026 — this interceptor wraps `meta.saveItem`, the SAVE door
|
|
3226
|
+
// (`PUT /meta/:type/:name`) and only that one. The SDK's publish
|
|
3227
|
+
// door (`meta.publishItem`) has no caller in this repo, so wiring
|
|
3228
|
+
// it here would be a surface with no consumer; `MetadataClient` is
|
|
3229
|
+
// where the publish door is actually taken.
|
|
3230
|
+
door: "save",
|
|
2366
3231
|
mode: result?.state === "draft" ? "draft" : "publish",
|
|
2367
3232
|
advisories
|
|
2368
3233
|
});
|
|
@@ -2410,7 +3275,7 @@ var ObjectStackAdapter = class {
|
|
|
2410
3275
|
data,
|
|
2411
3276
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
2412
3277
|
);
|
|
2413
|
-
this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
|
|
3278
|
+
this.emitMutation({ type: "update", resource, id: String(id), record: { ...result.record } });
|
|
2414
3279
|
this.notifyDroppedFields("update", resource, result, id, data);
|
|
2415
3280
|
return result.record;
|
|
2416
3281
|
} catch (err) {
|
|
@@ -2433,7 +3298,7 @@ var ObjectStackAdapter = class {
|
|
|
2433
3298
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
2434
3299
|
);
|
|
2435
3300
|
if (result.success) {
|
|
2436
|
-
this.emitMutation({ type: "delete", resource, id });
|
|
3301
|
+
this.emitMutation({ type: "delete", resource, id: String(id) });
|
|
2437
3302
|
}
|
|
2438
3303
|
return result.success;
|
|
2439
3304
|
} catch (err) {
|
|
@@ -2534,25 +3399,25 @@ var ObjectStackAdapter = class {
|
|
|
2534
3399
|
* @returns Promise resolving to array of results
|
|
2535
3400
|
*/
|
|
2536
3401
|
/**
|
|
2537
|
-
* Cross-object transactional batch (
|
|
3402
|
+
* Cross-object transactional batch (objectstack-ai/objectstack#1604 / ADR-0034 item 4).
|
|
2538
3403
|
* Runs the operations in ONE server transaction — commit all or roll back
|
|
2539
3404
|
* all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
|
|
2540
3405
|
* created id, so a child can reference its parent created earlier in the same
|
|
2541
3406
|
* batch (master-detail).
|
|
2542
3407
|
*
|
|
2543
3408
|
* Transport: the published `@objectstack/client` SDK method
|
|
2544
|
-
* `data.batchTransaction` (
|
|
3409
|
+
* `data.batchTransaction` (objectstack-ai/objectstack#3271; shipped since client v16, our
|
|
2545
3410
|
* dependency floor). Per AGENTS.md §7 data always flows through the client —
|
|
2546
3411
|
* never a hand-rolled `fetch('/api/v1/batch')`.
|
|
2547
3412
|
*
|
|
2548
|
-
* Fallback decision — declarative capability negotiation (
|
|
3413
|
+
* Fallback decision — declarative capability negotiation (objectstack-ai/objectstack#3298 /
|
|
2549
3414
|
* objectui #2693). At connect() we read `capabilities.transactionalBatch`
|
|
2550
3415
|
* from discovery:
|
|
2551
3416
|
* - Declared `true` → the backend GUARANTEES atomicity (declared ===
|
|
2552
3417
|
* enforced). We TRUST it: any batch failure — including 404/405/501 —
|
|
2553
3418
|
* surfaces as a real error. No non-atomic client-side compensation. This
|
|
2554
3419
|
* is the path modern backends take.
|
|
2555
|
-
* - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
|
|
3420
|
+
* - Declared `false`, or ABSENT (backend predates objectstack-ai/objectstack#3298) → we can't rely on
|
|
2556
3421
|
* server atomicity, so we keep the legacy behaviour: on 404/405 (no
|
|
2557
3422
|
* endpoint) or 501 (runtime without transactions) degrade to the
|
|
2558
3423
|
* client-side, NON-atomic {@link emulateBatchTransaction} so a save is
|
|
@@ -2976,9 +3841,10 @@ var ObjectStackAdapter = class {
|
|
|
2976
3841
|
const res = await this.fetchImpl(url, { method: "GET", headers });
|
|
2977
3842
|
if (!res.ok) {
|
|
2978
3843
|
const errorBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
2979
|
-
const
|
|
3844
|
+
const envelope = readErrorEnvelope(errorBody);
|
|
3845
|
+
const err = new Error(envelope.message ?? res.statusText);
|
|
2980
3846
|
err.status = res.status;
|
|
2981
|
-
err.code =
|
|
3847
|
+
err.code = envelope.code;
|
|
2982
3848
|
err.httpStatus = res.status;
|
|
2983
3849
|
throw err;
|
|
2984
3850
|
}
|
|
@@ -3028,16 +3894,51 @@ var ObjectStackAdapter = class {
|
|
|
3028
3894
|
queryParams.set("searchFields", request.searchFields.join(","));
|
|
3029
3895
|
}
|
|
3030
3896
|
}
|
|
3897
|
+
return this.fetchExportBlob(resource, queryParams);
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Download the server's import template:
|
|
3901
|
+
* `GET /api/v1/data/:object/export?template=true` (objectui#9600).
|
|
3902
|
+
*
|
|
3903
|
+
* Same route as {@link exportDownload}, with the server's template switch. It
|
|
3904
|
+
* sends only `template=true`: the server refuses the export's row parameters
|
|
3905
|
+
* on a template request, and its answer is always an xlsx workbook.
|
|
3906
|
+
*
|
|
3907
|
+
* The server writes the template's labels in the request's locale, and the
|
|
3908
|
+
* import accepts a translated label only in its own request's locale. The
|
|
3909
|
+
* import request goes through `@objectstack/client`, which stamps
|
|
3910
|
+
* `Accept-Language` with the client's locale when one is set (and the host's
|
|
3911
|
+
* `fetch` stamps its own otherwise). This request bypasses the client, so it
|
|
3912
|
+
* stamps the client's locale the same way. Both requests then carry the same
|
|
3913
|
+
* locale.
|
|
3914
|
+
*/
|
|
3915
|
+
async downloadImportTemplate(resource) {
|
|
3916
|
+
const locale = this.client.getLocale();
|
|
3917
|
+
return this.fetchExportBlob(
|
|
3918
|
+
resource,
|
|
3919
|
+
new URLSearchParams({ template: "true" }),
|
|
3920
|
+
locale ? { "Accept-Language": locale } : void 0
|
|
3921
|
+
);
|
|
3922
|
+
}
|
|
3923
|
+
/**
|
|
3924
|
+
* GET `/api/v1/data/:object/export` with the given query and answer the body
|
|
3925
|
+
* as a Blob. A failure throws an `Error` that carries the server's `status`
|
|
3926
|
+
* and its ADR-0112 `code`. The one request path {@link exportDownload} and
|
|
3927
|
+
* {@link downloadImportTemplate} share.
|
|
3928
|
+
*/
|
|
3929
|
+
async fetchExportBlob(resource, queryParams, extraHeaders) {
|
|
3031
3930
|
const baseUrl = this.baseUrl.replace(/\/$/, "");
|
|
3032
3931
|
const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
|
|
3033
3932
|
const dataPath = hasApiVersionSuffix ? "/data" : "/api/v1/data";
|
|
3034
3933
|
const url = `${baseUrl}${dataPath}/${encodeURIComponent(resource)}/export?${queryParams.toString()}`;
|
|
3035
|
-
const headers = { ...this.getAuthHeaders() };
|
|
3934
|
+
const headers = { ...this.getAuthHeaders(), ...extraHeaders };
|
|
3036
3935
|
const res = await this.fetchImpl(url, { method: "GET", headers, credentials: "include" });
|
|
3037
3936
|
if (!res.ok) {
|
|
3038
3937
|
const errorBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
3039
|
-
const
|
|
3938
|
+
const envelope = readErrorEnvelope(errorBody);
|
|
3939
|
+
const err = new Error(envelope.message ?? res.statusText);
|
|
3040
3940
|
err.status = res.status;
|
|
3941
|
+
err.code = envelope.code;
|
|
3041
3942
|
throw err;
|
|
3042
3943
|
}
|
|
3043
3944
|
return await res.blob();
|
|
@@ -3058,7 +3959,8 @@ var ObjectStackAdapter = class {
|
|
|
3058
3959
|
if (Array.isArray(params.$filter)) {
|
|
3059
3960
|
options.filters = translateFilterArray(params.$filter);
|
|
3060
3961
|
} else {
|
|
3061
|
-
|
|
3962
|
+
const lowered = convertFiltersToAST(params.$filter);
|
|
3963
|
+
if (lowered !== void 0) options.filters = lowered;
|
|
3062
3964
|
}
|
|
3063
3965
|
}
|
|
3064
3966
|
}
|
|
@@ -3178,13 +4080,16 @@ var ObjectStackAdapter = class {
|
|
|
3178
4080
|
});
|
|
3179
4081
|
if (!res.ok) {
|
|
3180
4082
|
const errBody = await res.json().catch(() => ({ message: res.statusText }));
|
|
3181
|
-
const
|
|
4083
|
+
const envelope = readErrorEnvelope(errBody);
|
|
4084
|
+
const err = new Error(envelope.message ?? res.statusText);
|
|
3182
4085
|
err.status = res.status;
|
|
4086
|
+
err.code = envelope.code;
|
|
3183
4087
|
throw err;
|
|
3184
4088
|
}
|
|
3185
4089
|
const body = await res.json();
|
|
3186
4090
|
const data = body && typeof body === "object" && "success" in body && "data" in body ? body.data : body;
|
|
3187
|
-
|
|
4091
|
+
const item = data && typeof data === "object" && "item" in data ? data.item : data;
|
|
4092
|
+
return attachObjectSortability(item, data && typeof data === "object" ? data.sortability : void 0);
|
|
3188
4093
|
}
|
|
3189
4094
|
/**
|
|
3190
4095
|
* List every registered object (code- and DB-defined) from the metadata
|
|
@@ -3366,25 +4271,94 @@ var ObjectStackAdapter = class {
|
|
|
3366
4271
|
}
|
|
3367
4272
|
}
|
|
3368
4273
|
/**
|
|
3369
|
-
* Persist a view
|
|
4274
|
+
* Persist a toolbar-driven view config patch — density, column widths,
|
|
4275
|
+
* sort, hidden columns, inline edit. Symmetric counterpart to
|
|
4276
|
+
* {@link getView}: writes the row to the server metadata store via
|
|
4277
|
+
* `client.meta.saveItem`, then invalidates the matching cache entry so the
|
|
4278
|
+
* next {@link getView} reflects the new payload. Returns the persisted item
|
|
4279
|
+
* when the server echoes it, otherwise undefined.
|
|
4280
|
+
*
|
|
4281
|
+
* Called from exactly ONE production site — `ObjectView`'s
|
|
4282
|
+
* `persistViewPatch`, for the toolbar toggle — but that ONE call site
|
|
4283
|
+
* fires for BOTH kinds of active tab: a code-defined **system** view (no
|
|
4284
|
+
* row of its own yet) and a genuinely user-created **saved** view (already
|
|
4285
|
+
* has a row — the toggle is editing ITS OWN definition, not laying an
|
|
4286
|
+
* overlay on top of it). Which one a given call means is NOT re-derived
|
|
4287
|
+
* here from the write's shape (objectui#4227's own lesson: shape inference
|
|
4288
|
+
* on this namespace is exactly what let a system view masquerade as
|
|
4289
|
+
* saved) — the caller already knows, via the same `isSavedViewId`
|
|
4290
|
+
* classification that gates the switcher's readonly flag and its five
|
|
4291
|
+
* mutating handlers, and passes it as {@link opts.isSavedView}.
|
|
3370
4292
|
*
|
|
3371
|
-
*
|
|
3372
|
-
*
|
|
3373
|
-
*
|
|
3374
|
-
*
|
|
3375
|
-
*
|
|
4293
|
+
* - `isSavedView` false/omitted (system-view target, the common case and
|
|
4294
|
+
* the default for backward compatibility): stamps
|
|
4295
|
+
* {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
|
|
4296
|
+
* the original objectui#4227 fix.
|
|
4297
|
+
* - `isSavedView` true: the marker is withheld. Stamping it here would
|
|
4298
|
+
* flag the saved view's OWN row as a personalization overlay, and
|
|
4299
|
+
* `listViews()` would exclude it on the very next read — the user's own
|
|
4300
|
+
* view would vanish from the switcher the moment they toggled its
|
|
4301
|
+
* density (objectui#4227 follow-up, PM review on PR #4713, measured:
|
|
4302
|
+
* `persistViewPatch` has no gate on which kind of tab is active, and
|
|
4303
|
+
* this method writes to the exact same `(type='view', name=viewId)` key
|
|
4304
|
+
* {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
|
|
4305
|
+
* that view, so the write is an upsert onto the saved view's row, not a
|
|
4306
|
+
* new one).
|
|
3376
4307
|
*
|
|
3377
|
-
*
|
|
3378
|
-
*
|
|
3379
|
-
*
|
|
4308
|
+
* Per objectstack#7494's ruling, the overlay this writes (system-view
|
|
4309
|
+
* case) is ORG-WIDE shared view settings, not a per-user preference — a
|
|
4310
|
+
* true per-user scope is a parked v18 direction on the platform side.
|
|
3380
4311
|
*
|
|
3381
4312
|
* @param objectName - Object name (e.g. 'lead')
|
|
3382
4313
|
* @param viewId - View identifier (e.g. 'all_leads')
|
|
3383
4314
|
* @param config - Full view definition to persist
|
|
4315
|
+
* @param opts.isSavedView - Whether `viewId` already names a saved view
|
|
4316
|
+
* (vs. a system view being personalized for the first time). Omit /
|
|
4317
|
+
* `false` for the default overlay-marking behavior.
|
|
3384
4318
|
*/
|
|
3385
|
-
|
|
4319
|
+
/**
|
|
4320
|
+
* [ADR-0066] Tell this adapter which system capabilities the session holds.
|
|
4321
|
+
*
|
|
4322
|
+
* Pass `undefined` for "no answer was reported" (a backend predating
|
|
4323
|
+
* ADR-0066, or no permission provider mounted). Pass `[]` for a real,
|
|
4324
|
+
* reported, empty grant — {@link maySetViewConfig} treats those two
|
|
4325
|
+
* differently and callers must not collapse them, which is the same
|
|
4326
|
+
* distinction `MePermissionsProvider` preserves natively (objectui#4656).
|
|
4327
|
+
*/
|
|
4328
|
+
setSystemCapabilities(capabilities) {
|
|
4329
|
+
this.systemCapabilities = capabilities ? [...capabilities] : void 0;
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* May this session write ORG-WIDE view configuration?
|
|
4333
|
+
*
|
|
4334
|
+
* **Unknown fails OPEN**, deliberately, and this is the one judgement in the
|
|
4335
|
+
* gate worth reading twice. It is the doctrine `PermissionContextValue`
|
|
4336
|
+
* .`hasCapabilities` states for every ADR-0066 gate in this repo
|
|
4337
|
+
* (objectui#4656, framework#3923): the server enforces `manage_metadata` on
|
|
4338
|
+
* the metadata door regardless of what the client believes, so a client-side
|
|
4339
|
+
* denial on MISSING DATA cannot protect anything the server was not already
|
|
4340
|
+
* protecting — it can only break a permitted user. Failing closed here would
|
|
4341
|
+
* have refused the write on every deployment predating ADR-0066 and every
|
|
4342
|
+
* host with no permission provider, which is "break the write for everyone"
|
|
4343
|
+
* wearing the costume of a security fix.
|
|
4344
|
+
*
|
|
4345
|
+
* A REPORTED empty array gates strictly: "holds nothing" is a real answer.
|
|
4346
|
+
*/
|
|
4347
|
+
maySetViewConfig() {
|
|
4348
|
+
if (this.systemCapabilities === void 0) return true;
|
|
4349
|
+
return this.systemCapabilities.includes(VIEW_CONFIG_CAPABILITY);
|
|
4350
|
+
}
|
|
4351
|
+
async updateViewConfig(objectName, viewId, config, opts) {
|
|
4352
|
+
if (!this.maySetViewConfig()) {
|
|
4353
|
+
throw new ViewConfigPermissionDeniedError(objectName, viewId);
|
|
4354
|
+
}
|
|
3386
4355
|
await this.connect();
|
|
3387
|
-
const merged = {
|
|
4356
|
+
const merged = {
|
|
4357
|
+
...config || {},
|
|
4358
|
+
object: config?.object || objectName,
|
|
4359
|
+
name: viewId,
|
|
4360
|
+
...opts?.isSavedView ? {} : { [VIEW_OVERLAY_MARKER]: true }
|
|
4361
|
+
};
|
|
3388
4362
|
const result = await this.client.meta.saveItem(
|
|
3389
4363
|
"view",
|
|
3390
4364
|
viewId,
|
|
@@ -3405,6 +4379,42 @@ var ObjectStackAdapter = class {
|
|
|
3405
4379
|
* the metadata index is name-only, not field-typed, so the route has no
|
|
3406
4380
|
* `?object=` to push the filter down into. {@link listViewOverrides}
|
|
3407
4381
|
* reads the same rows through the same accessor.
|
|
4382
|
+
*
|
|
4383
|
+
* ## Every failure still degrades to an empty list — and now says which kind
|
|
4384
|
+
* ## of failure it was (objectui#8151)
|
|
4385
|
+
*
|
|
4386
|
+
* The degrade is unchanged and deliberate, exactly as it is one method over
|
|
4387
|
+
* in {@link listImportMappings}: a host with no metadata door keeps answering
|
|
4388
|
+
* `[]` rather than breaking the page. What changed is that the OTHER failures
|
|
4389
|
+
* no longer render as that one.
|
|
4390
|
+
*
|
|
4391
|
+
* This method's empty list is read as *this object has no saved views*, and
|
|
4392
|
+
* that reading reaches the user in two places measured on this tree: the list
|
|
4393
|
+
* view switcher simply shows fewer tabs, and `@object-ui/core`'s
|
|
4394
|
+
* `elementDataSourceViewNotFoundMessage` states it outright. So a session
|
|
4395
|
+
* whose token lapsed mid-read was shown an object that appears to have no
|
|
4396
|
+
* saved views AT ALL — including ones the user created — with a
|
|
4397
|
+
* `console.warn` as the only discriminator, in a console nothing in the UI
|
|
4398
|
+
* points at. That is the silence objectui#7741 removed from the sibling after
|
|
4399
|
+
* it produced a confident wrong diagnosis in objectstack#14026.
|
|
4400
|
+
*
|
|
4401
|
+
* The `catch` now asks {@link classifyViewsFailure} what the failure WAS,
|
|
4402
|
+
* reading the error's own ADR-0112 `code` and status — never "is the result
|
|
4403
|
+
* empty", which is what both conditions produce and so can never tell them
|
|
4404
|
+
* apart — and anything that is not the doorless shape is announced on
|
|
4405
|
+
* {@link onMetadataReadWarning}.
|
|
4406
|
+
*
|
|
4407
|
+
* ⛔ That classifier is a SEPARATE reading, not a second caller of the
|
|
4408
|
+
* sibling's: `view`'s quiet set is strictly smaller, because the arm
|
|
4409
|
+
* `listImportMappings` is built around (*an older server without this kind*)
|
|
4410
|
+
* has no members here. See {@link classifyViewsFailure} for the measurement.
|
|
4411
|
+
*
|
|
4412
|
+
* ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
|
|
4413
|
+
* throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
|
|
4414
|
+
* nothing new sees exactly what it saw before, including on the loud arms.
|
|
4415
|
+
* Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
|
|
4416
|
+
* READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
|
|
4417
|
+
* moving that contract.
|
|
3408
4418
|
*/
|
|
3409
4419
|
async listViews(objectName, options) {
|
|
3410
4420
|
await this.connect();
|
|
@@ -3423,23 +4433,42 @@ var ObjectStackAdapter = class {
|
|
|
3423
4433
|
const spec = v.list ?? v;
|
|
3424
4434
|
if (viewItemObjectName(v) !== objectName) return false;
|
|
3425
4435
|
const viewKind = v.viewKind ?? spec?.viewKind;
|
|
3426
|
-
|
|
4436
|
+
if (viewKind && FORM_FAMILY.has(viewKind)) return false;
|
|
4437
|
+
if (isPersonalizationOverlayRow(v, spec)) return false;
|
|
4438
|
+
return true;
|
|
3427
4439
|
}).map((v) => {
|
|
3428
4440
|
const spec = v.list ?? v;
|
|
3429
4441
|
const isDraft = v._draft === true || spec?._draft === true;
|
|
3430
4442
|
if (spec && spec.config && typeof spec.config === "object") {
|
|
3431
|
-
|
|
4443
|
+
const row = {
|
|
3432
4444
|
...spec.config,
|
|
3433
4445
|
name: spec.name ?? spec.config.name,
|
|
3434
|
-
label: spec.label ?? spec.config.label
|
|
3435
|
-
isDefault: !!spec.isDefault,
|
|
3436
|
-
...isDraft ? { _draft: true } : {}
|
|
4446
|
+
label: spec.label ?? spec.config.label
|
|
3437
4447
|
};
|
|
4448
|
+
if (spec.object !== void 0) row.object = spec.object;
|
|
4449
|
+
for (const key of VIEW_ITEM_ROUND_TRIP_KEYS) {
|
|
4450
|
+
if (spec[key] !== void 0) row[key] = spec[key];
|
|
4451
|
+
}
|
|
4452
|
+
row.isDefault = !!spec.isDefault;
|
|
4453
|
+
if (isDraft) row._draft = true;
|
|
4454
|
+
return row;
|
|
3438
4455
|
}
|
|
3439
4456
|
return isDraft ? { ...spec, _draft: true } : spec;
|
|
3440
4457
|
});
|
|
3441
4458
|
} catch (err) {
|
|
3442
4459
|
console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
|
|
4460
|
+
const failure = classifyViewsFailure(err);
|
|
4461
|
+
if (failure.kind !== "not-served") {
|
|
4462
|
+
this.emitMetadataReadWarning({
|
|
4463
|
+
operation: "listViews",
|
|
4464
|
+
kind: "view",
|
|
4465
|
+
objectName,
|
|
4466
|
+
reason: failure.kind,
|
|
4467
|
+
...failure.code !== void 0 ? { code: failure.code } : {},
|
|
4468
|
+
...failure.status !== void 0 ? { status: failure.status } : {},
|
|
4469
|
+
...failure.message !== void 0 ? { message: failure.message } : {}
|
|
4470
|
+
});
|
|
4471
|
+
}
|
|
3443
4472
|
return [];
|
|
3444
4473
|
}
|
|
3445
4474
|
}
|
|
@@ -3458,11 +4487,36 @@ var ObjectStackAdapter = class {
|
|
|
3458
4487
|
}
|
|
3459
4488
|
/**
|
|
3460
4489
|
* List registered import `mapping` artifacts targeting a given object
|
|
3461
|
-
* (
|
|
4490
|
+
* (objectstack-ai/objectstack#2611). Reads the `mapping` metadata kind via the overlay API
|
|
3462
4491
|
* and filters by `targetObject` client-side (the metadata index is
|
|
3463
|
-
* name-only). Feeds the import wizard's "saved mapping" selector
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
4492
|
+
* name-only). Feeds the import wizard's "saved mapping" selector.
|
|
4493
|
+
*
|
|
4494
|
+
* ## Every failure still degrades to an empty list — and now says which kind
|
|
4495
|
+
* ## of failure it was (objectui#7741)
|
|
4496
|
+
*
|
|
4497
|
+
* The degrade is unchanged and deliberate: an older server without the
|
|
4498
|
+
* `mapping` kind must not break the wizard, so it keeps answering `[]` and
|
|
4499
|
+
* the selector simply doesn't appear. That is a real, supported deployment
|
|
4500
|
+
* shape and it stays quiet.
|
|
4501
|
+
*
|
|
4502
|
+
* What changed is that the OTHER failures no longer render as that one. A
|
|
4503
|
+
* refused door and a broken one used to be indistinguishable, in the UI, from
|
|
4504
|
+
* "no mapping is registered" — both were an empty list behind a
|
|
4505
|
+
* `console.warn` nothing pointed at — and that silence produced a confident
|
|
4506
|
+
* wrong diagnosis in a careful reporter (objectstack#14026 was filed, routed
|
|
4507
|
+
* and worked by two seats against a wizard that had been correct since
|
|
4508
|
+
* 17.1.0). So the `catch` now asks {@link classifyImportMappingsFailure} what
|
|
4509
|
+
* the failure WAS, reading the error's own ADR-0112 `code` and status —
|
|
4510
|
+
* never "is the result empty", which is what both conditions produce and so
|
|
4511
|
+
* can never tell them apart — and anything that is not the supported
|
|
4512
|
+
* kind-absent shape is announced on {@link onMetadataReadWarning}.
|
|
4513
|
+
*
|
|
4514
|
+
* ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
|
|
4515
|
+
* throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
|
|
4516
|
+
* nothing new sees exactly what it saw before, including on the loud arms.
|
|
4517
|
+
* Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
|
|
4518
|
+
* READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
|
|
4519
|
+
* moving that contract.
|
|
3466
4520
|
*/
|
|
3467
4521
|
async listImportMappings(objectName) {
|
|
3468
4522
|
await this.connect();
|
|
@@ -3472,6 +4526,18 @@ var ObjectStackAdapter = class {
|
|
|
3472
4526
|
return items.filter((m) => m && m.targetObject === objectName);
|
|
3473
4527
|
} catch (err) {
|
|
3474
4528
|
console.warn("[OBJECTSTACKDataSource] listImportMappings failed:", err);
|
|
4529
|
+
const failure = classifyImportMappingsFailure(err);
|
|
4530
|
+
if (failure.kind !== "not-served") {
|
|
4531
|
+
this.emitMetadataReadWarning({
|
|
4532
|
+
operation: "listImportMappings",
|
|
4533
|
+
kind: "mapping",
|
|
4534
|
+
objectName,
|
|
4535
|
+
reason: failure.kind,
|
|
4536
|
+
...failure.code !== void 0 ? { code: failure.code } : {},
|
|
4537
|
+
...failure.status !== void 0 ? { status: failure.status } : {},
|
|
4538
|
+
...failure.message !== void 0 ? { message: failure.message } : {}
|
|
4539
|
+
});
|
|
4540
|
+
}
|
|
3475
4541
|
return [];
|
|
3476
4542
|
}
|
|
3477
4543
|
}
|
|
@@ -3698,7 +4764,7 @@ var ObjectStackAdapter = class {
|
|
|
3698
4764
|
try {
|
|
3699
4765
|
const cacheKey = `app:${appId}`;
|
|
3700
4766
|
return await this.metadataCache.get(cacheKey, async () => {
|
|
3701
|
-
const result = await this.client.meta.getItem("
|
|
4767
|
+
const result = await this.client.meta.getItem("app", appId);
|
|
3702
4768
|
if (result && result.item) return result.item;
|
|
3703
4769
|
return result ?? null;
|
|
3704
4770
|
});
|
|
@@ -3710,8 +4776,13 @@ var ObjectStackAdapter = class {
|
|
|
3710
4776
|
* Ask the by-name meta app route WHY an app is not in this session's app list
|
|
3711
4777
|
* (objectui#4252).
|
|
3712
4778
|
*
|
|
3713
|
-
* `GET /api/v1/meta
|
|
3714
|
-
*
|
|
4779
|
+
* The app LIST is the generic metadata list route `GET /api/v1/meta/:type`,
|
|
4780
|
+
* requested with the singular type segment `app` — the same address this
|
|
4781
|
+
* method appends a name to below, and the one `MetadataProvider` reads its
|
|
4782
|
+
* items from. The server filters that list per session in `filterAppForUser`
|
|
4783
|
+
* (`packages/rest/src/rest-server.ts`, applied inside the `:type` list handler
|
|
4784
|
+
* once the type segment resolves to `app`), so an app withheld by
|
|
4785
|
+
* `requiredPermissions` and an app
|
|
3715
4786
|
* that does not exist are byte-identical there: both are simply absent. A
|
|
3716
4787
|
* console reading only that list has one fact and two conditions, and it
|
|
3717
4788
|
* renders its copy for the wrong one — "it may still be publishing" over a
|
|
@@ -3721,11 +4792,17 @@ var ObjectStackAdapter = class {
|
|
|
3721
4792
|
*
|
|
3722
4793
|
* The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
|
|
3723
4794
|
* rather than in the list, so the enumeration surface is not widened past what
|
|
3724
|
-
* a by-name probe already implies (objectstack#8013 / PR #8135): an app that
|
|
4795
|
+
* a by-name probe already implies (objectstack#8013 / PR objectstack-ai/objectstack#8135): an app that
|
|
3725
4796
|
* exists and whose `requiredPermissions` the session lacks answers `403` with
|
|
3726
4797
|
* `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
|
|
3727
|
-
* name, an unpublished app, an app gated by an absent optional service —
|
|
3728
|
-
*
|
|
4798
|
+
* name, an unpublished app, an app gated by an absent optional service — is
|
|
4799
|
+
* one undifferentiated answer.
|
|
4800
|
+
*
|
|
4801
|
+
* ⚠️ That absence is NOT uniformly a `404`, which this docstring asserted
|
|
4802
|
+
* until objectui#9262 measured it: the 404 covers an app that EXISTS and is
|
|
4803
|
+
* withheld for a non-permission reason, while a name that resolves to nothing
|
|
4804
|
+
* answers `200` with an envelope carrying no `item`. Both are absence and both
|
|
4805
|
+
* read as `not_found`; see {@link metaEnvelopeCarriesItem} for the measurement.
|
|
3729
4806
|
*
|
|
3730
4807
|
* ## Why this is a separate method and not a flavour of {@link getApp}
|
|
3731
4808
|
*
|
|
@@ -3736,19 +4813,41 @@ var ObjectStackAdapter = class {
|
|
|
3736
4813
|
* be cached beside a document about the APP — one grant, and a cached
|
|
3737
4814
|
* denial outlives the session it described.
|
|
3738
4815
|
*
|
|
3739
|
-
* Nothing here throws:
|
|
3740
|
-
*
|
|
3741
|
-
*
|
|
4816
|
+
* Nothing here throws: every outcome is a verdict the caller renders a screen
|
|
4817
|
+
* from, never a catch block. Only the measured `code` produces `denied` —
|
|
4818
|
+
* never a status, never a message (objectui#4408).
|
|
4819
|
+
*
|
|
4820
|
+
* ## objectui#9262 — what this method stopped folding away
|
|
4821
|
+
*
|
|
4822
|
+
* It used to answer `granted` whenever the call did not throw, and `unknown`
|
|
4823
|
+
* for everything that did. Both halves were wrong in the same direction, and
|
|
4824
|
+
* the first was measured on a real server before this change landed:
|
|
4825
|
+
*
|
|
4826
|
+
* - **an app that does not exist answers `200` with the envelope minus its
|
|
4827
|
+
* `item`**, so "did not throw" reported `granted` for a name with nothing
|
|
4828
|
+
* behind it — the shape issue objectui#9262 listed as cause 8 and expected
|
|
4829
|
+
* to be structural. It is the ordinary case: every typo, every never-created
|
|
4830
|
+
* app and every unpublished draft took it. See
|
|
4831
|
+
* {@link metaEnvelopeCarriesItem} for the measurement and the server-side
|
|
4832
|
+
* mechanism.
|
|
4833
|
+
* - a 404 absence and an unreachable server were one `unknown`, though the
|
|
4834
|
+
* transport had already told them apart.
|
|
4835
|
+
*
|
|
4836
|
+
* The verdicts now say only what was obtained: a served document, a refusal
|
|
4837
|
+
* naming permission, an answer that there is no such app here, or no answer
|
|
4838
|
+
* at all. `unknown` is left for the one case where nothing was asked.
|
|
3742
4839
|
*
|
|
3743
4840
|
* @param appName - the app name as it appears in the URL segment
|
|
3744
4841
|
*/
|
|
3745
4842
|
async probeAppAccess(appName) {
|
|
3746
4843
|
if (!appName) return "unknown";
|
|
3747
4844
|
try {
|
|
3748
|
-
await this.client.meta.getItem("app", appName);
|
|
3749
|
-
return "granted";
|
|
4845
|
+
const envelope = await this.client.meta.getItem("app", appName);
|
|
4846
|
+
return metaEnvelopeCarriesItem(envelope) ? "granted" : "not_found";
|
|
3750
4847
|
} catch (err) {
|
|
3751
|
-
|
|
4848
|
+
if (isAppPermissionDeniedError(err)) return "denied";
|
|
4849
|
+
if (isMetaItemAbsentError(err)) return "not_found";
|
|
4850
|
+
return "unreachable";
|
|
3752
4851
|
}
|
|
3753
4852
|
}
|
|
3754
4853
|
/**
|
|
@@ -3761,7 +4860,7 @@ var ObjectStackAdapter = class {
|
|
|
3761
4860
|
try {
|
|
3762
4861
|
const cacheKey = `page:${pageId}`;
|
|
3763
4862
|
return await this.metadataCache.get(cacheKey, async () => {
|
|
3764
|
-
const result = await this.client.meta.getItem("
|
|
4863
|
+
const result = await this.client.meta.getItem("page", pageId);
|
|
3765
4864
|
if (result && result.item) return result.item;
|
|
3766
4865
|
return result ?? null;
|
|
3767
4866
|
});
|
|
@@ -3798,7 +4897,8 @@ var ObjectStackAdapter = class {
|
|
|
3798
4897
|
* Uses `this.client.analytics.query()` from @objectstack/client to leverage
|
|
3799
4898
|
* the SDK's built-in auth, headers, and fetch configuration.
|
|
3800
4899
|
* Falls back to client-side aggregation via find() if the analytics endpoint
|
|
3801
|
-
* is not available.
|
|
4900
|
+
* is not available. Throws {@link AnalyticsResultShapeError} when it answers
|
|
4901
|
+
* with anything but an `AnalyticsResult` carrying `rows`.
|
|
3802
4902
|
*/
|
|
3803
4903
|
async aggregate(resource, params) {
|
|
3804
4904
|
await this.connect();
|
|
@@ -3807,7 +4907,11 @@ var ObjectStackAdapter = class {
|
|
|
3807
4907
|
const queryAst = {};
|
|
3808
4908
|
if (Array.isArray(params.groupBy)) queryAst.groupBy = params.groupBy;
|
|
3809
4909
|
if (Array.isArray(params.aggregations)) queryAst.aggregations = params.aggregations;
|
|
3810
|
-
if (params.where !== void 0)
|
|
4910
|
+
if (params.where !== void 0) {
|
|
4911
|
+
assertSpecShapeWhereIsFilterAst(params.where, resource);
|
|
4912
|
+
queryAst.where = params.where;
|
|
4913
|
+
}
|
|
4914
|
+
assertNoAnalyticsKeysOnSpecShape(params, resource);
|
|
3811
4915
|
if (typeof params.limit === "number") queryAst.limit = params.limit;
|
|
3812
4916
|
const result = await this.client.data.query(resource, queryAst);
|
|
3813
4917
|
if (Array.isArray(result)) return result;
|
|
@@ -3815,6 +4919,7 @@ var ObjectStackAdapter = class {
|
|
|
3815
4919
|
if (Array.isArray(result?.data)) return result.data;
|
|
3816
4920
|
return [];
|
|
3817
4921
|
}
|
|
4922
|
+
const analyticsWhere = params.filter ? lowerAnalyticsFilterForWire(params.filter, resource) : void 0;
|
|
3818
4923
|
try {
|
|
3819
4924
|
const measureName = params.function === "count" ? "count" : `${params.field}_${params.function}`;
|
|
3820
4925
|
const valueKey = this.aggregateValueKey(params);
|
|
@@ -3824,11 +4929,12 @@ var ObjectStackAdapter = class {
|
|
|
3824
4929
|
// When groupBy is '_all' no dimensions are needed (single-bucket).
|
|
3825
4930
|
dimensions: params.groupBy && params.groupBy !== "_all" ? [params.groupBy] : []
|
|
3826
4931
|
};
|
|
3827
|
-
if (
|
|
3828
|
-
|
|
4932
|
+
if (analyticsWhere !== void 0) payload.where = analyticsWhere;
|
|
4933
|
+
const contractResult = await this.client.analytics.query(payload);
|
|
4934
|
+
if (!Array.isArray(contractResult?.rows)) {
|
|
4935
|
+
throw new AnalyticsResultShapeError(resource, contractResult);
|
|
3829
4936
|
}
|
|
3830
|
-
const
|
|
3831
|
-
const rawRows = Array.isArray(data) ? data : data?.rows && Array.isArray(data.rows) ? data.rows : data?.data && Array.isArray(data.data) ? data.data : data?.data?.rows && Array.isArray(data.data.rows) ? data.data.rows : data?.results && Array.isArray(data.results) ? data.results : [];
|
|
4937
|
+
const rawRows = contractResult.rows;
|
|
3832
4938
|
const measureMissing = rawRows.length > 0 && rawRows.every((row) => {
|
|
3833
4939
|
if (row == null) return true;
|
|
3834
4940
|
if (measureName in row && row[measureName] != null) return false;
|
|
@@ -3847,16 +4953,54 @@ var ObjectStackAdapter = class {
|
|
|
3847
4953
|
return mapped;
|
|
3848
4954
|
});
|
|
3849
4955
|
} catch (e) {
|
|
4956
|
+
if (e instanceof AnalyticsResultShapeError) throw e;
|
|
3850
4957
|
const failure = classifyAnalyticsFailure(e);
|
|
3851
4958
|
if (failure.kind === "rejected") {
|
|
3852
4959
|
throw new AnalyticsQueryRejectedError(failure.message, failure.code);
|
|
3853
4960
|
}
|
|
4961
|
+
if (failure.kind === "unauthenticated") {
|
|
4962
|
+
throw new AnalyticsUnauthenticatedError({
|
|
4963
|
+
serverCode: failure.code,
|
|
4964
|
+
serverMessage: failure.message
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
if (failure.kind === "cube-not-found") {
|
|
4968
|
+
throw e;
|
|
4969
|
+
}
|
|
3854
4970
|
if (failure.kind === "not-installed") {
|
|
3855
4971
|
this.warnAnalyticsCapabilityOnce(failure.message);
|
|
3856
4972
|
}
|
|
3857
4973
|
return await this.aggregateViaFind(resource, params);
|
|
3858
4974
|
}
|
|
3859
4975
|
}
|
|
4976
|
+
/**
|
|
4977
|
+
* Answer a grouped list view's GROUP HEADER query (objectui#7189) — see
|
|
4978
|
+
* `DataSource.queryGroupHeaders` in `@object-ui/types` for the contract.
|
|
4979
|
+
*
|
|
4980
|
+
* The compiled query rides the platform's EXISTING data door verbatim:
|
|
4981
|
+
* `client.data.query()` posts it to `POST /data/:object/query`, whose
|
|
4982
|
+
* `findData` routes a body carrying `groupBy` / `aggregations` to
|
|
4983
|
+
* `engine.aggregate` and answers the header rows as `records` (the door the
|
|
4984
|
+
* spec half of objectstack#14556 names, pinned platform-side by
|
|
4985
|
+
* objectstack#15330). No new route, no new wire shape.
|
|
4986
|
+
*
|
|
4987
|
+
* ⛔ No fallback, deliberately — unlike {@link aggregate}. A header query that
|
|
4988
|
+
* degraded to bucketing a fetched page would answer page slices under a name
|
|
4989
|
+
* that promises the query's own numbers, which is the defect this member
|
|
4990
|
+
* exists to end. A failure is thrown to the caller, and a body without a
|
|
4991
|
+
* `records` array is refused rather than read as "no groups".
|
|
4992
|
+
*/
|
|
4993
|
+
async queryGroupHeaders(resource, query) {
|
|
4994
|
+
await this.connect();
|
|
4995
|
+
const result = await this.client.data.query(resource, query);
|
|
4996
|
+
const records = result?.records;
|
|
4997
|
+
if (!Array.isArray(records)) {
|
|
4998
|
+
throw new Error(
|
|
4999
|
+
`queryGroupHeaders('${resource}'): POST /data/${resource}/query answered without a \`records\` array, so no group header can be read from it. The header query was sent verbatim; nothing was approximated from a page of rows.`
|
|
5000
|
+
);
|
|
5001
|
+
}
|
|
5002
|
+
return records;
|
|
5003
|
+
}
|
|
3860
5004
|
/**
|
|
3861
5005
|
* Client-side aggregation over a server-scoped `find()` — the fallback used
|
|
3862
5006
|
* whenever the analytics endpoint cannot answer (capability absent, network
|
|
@@ -3930,14 +5074,42 @@ var ObjectStackAdapter = class {
|
|
|
3930
5074
|
body: JSON.stringify(requestBody)
|
|
3931
5075
|
});
|
|
3932
5076
|
if (!res.ok) {
|
|
3933
|
-
let
|
|
5077
|
+
let errBody;
|
|
3934
5078
|
try {
|
|
3935
|
-
|
|
3936
|
-
detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
|
|
5079
|
+
errBody = await res.json();
|
|
3937
5080
|
} catch {
|
|
3938
5081
|
}
|
|
3939
|
-
|
|
3940
|
-
|
|
5082
|
+
const { code: serverCode, message: serverMessage } = readAnalyticsErrorEnvelope(errBody);
|
|
5083
|
+
const detail = serverMessage || (errBody === void 0 ? "" : JSON.stringify(errBody));
|
|
5084
|
+
const surface = "POST /analytics/dataset/query";
|
|
5085
|
+
const requestedDatasetName = typeof dataset === "string" ? dataset : typeof dataset?.name === "string" ? String(dataset.name) : void 0;
|
|
5086
|
+
if (errorCodeIsAnyOf({ code: serverCode }, ["NOT_IMPLEMENTED", "ROUTE_NOT_FOUND"])) {
|
|
5087
|
+
throw new AnalyticsNotInstalledError(surface, serverMessage, serverCode);
|
|
5088
|
+
}
|
|
5089
|
+
if (errorCodeIs({ code: serverCode }, "NOT_FOUND")) {
|
|
5090
|
+
throw new AnalyticsDatasetNotFoundError({
|
|
5091
|
+
datasetName: requestedDatasetName,
|
|
5092
|
+
serverCode,
|
|
5093
|
+
serverMessage
|
|
5094
|
+
});
|
|
5095
|
+
}
|
|
5096
|
+
if (errorCodeIs({ code: serverCode }, "UNAUTHENTICATED")) {
|
|
5097
|
+
throw new AnalyticsUnauthenticatedError({ serverCode, serverMessage });
|
|
5098
|
+
}
|
|
5099
|
+
if (errorCodeIs({ code: serverCode }, "PERMISSION_DENIED")) {
|
|
5100
|
+
throw new AnalyticsForbiddenError({
|
|
5101
|
+
serverCode,
|
|
5102
|
+
serverMessage,
|
|
5103
|
+
datasetName: requestedDatasetName
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
if (serverCode === void 0) {
|
|
5107
|
+
if (ANALYTICS_ABSENT_STATUSES.includes(res.status)) {
|
|
5108
|
+
throw new AnalyticsNotInstalledError(surface, detail || void 0);
|
|
5109
|
+
}
|
|
5110
|
+
if (res.status === 401) {
|
|
5111
|
+
throw new AnalyticsUnauthenticatedError({ serverMessage });
|
|
5112
|
+
}
|
|
3941
5113
|
}
|
|
3942
5114
|
throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
3943
5115
|
}
|
|
@@ -4071,9 +5243,10 @@ var ObjectStackAdapter = class {
|
|
|
4071
5243
|
}
|
|
4072
5244
|
});
|
|
4073
5245
|
if (!response.ok) {
|
|
4074
|
-
const
|
|
5246
|
+
const errorBody = await response.json().catch(() => ({ message: response.statusText }));
|
|
5247
|
+
const envelope = readErrorEnvelope(errorBody);
|
|
4075
5248
|
throw new ObjectStackError(
|
|
4076
|
-
|
|
5249
|
+
envelope.message ?? `Upload failed with status ${response.status}`,
|
|
4077
5250
|
"UPLOAD_ERROR",
|
|
4078
5251
|
response.status
|
|
4079
5252
|
);
|
|
@@ -4113,9 +5286,10 @@ var ObjectStackAdapter = class {
|
|
|
4113
5286
|
}
|
|
4114
5287
|
});
|
|
4115
5288
|
if (!response.ok) {
|
|
4116
|
-
const
|
|
5289
|
+
const errorBody = await response.json().catch(() => ({ message: response.statusText }));
|
|
5290
|
+
const envelope = readErrorEnvelope(errorBody);
|
|
4117
5291
|
throw new ObjectStackError(
|
|
4118
|
-
|
|
5292
|
+
envelope.message ?? `Upload failed with status ${response.status}`,
|
|
4119
5293
|
"UPLOAD_ERROR",
|
|
4120
5294
|
response.status
|
|
4121
5295
|
);
|
|
@@ -4196,31 +5370,47 @@ function createObjectStackAdapter(config) {
|
|
|
4196
5370
|
export {
|
|
4197
5371
|
API_ACCESS_DENIED_CODES,
|
|
4198
5372
|
APP_PERMISSION_DENIED_CODE,
|
|
5373
|
+
AnalyticsDatasetNotFoundError,
|
|
5374
|
+
AnalyticsForbiddenError,
|
|
5375
|
+
AnalyticsKeysOnSpecShapeError,
|
|
4199
5376
|
AnalyticsNotInstalledError,
|
|
4200
5377
|
AnalyticsQueryRejectedError,
|
|
5378
|
+
AnalyticsResultShapeError,
|
|
5379
|
+
AnalyticsUnauthenticatedError,
|
|
4201
5380
|
AuthenticationError,
|
|
4202
5381
|
BulkOperationError,
|
|
5382
|
+
CHOICE_TYPES_REQUIRING_OPTIONS,
|
|
4203
5383
|
ConcurrentUpdateError,
|
|
4204
5384
|
ConnectionError,
|
|
4205
5385
|
DataApiValidationError,
|
|
4206
5386
|
FILTER_OPERATOR_ALIASES,
|
|
4207
|
-
|
|
5387
|
+
META_ITEM_ABSENT_CODE,
|
|
4208
5388
|
MalformedFilterError,
|
|
4209
5389
|
MetadataClient,
|
|
4210
5390
|
MetadataNotFoundError,
|
|
5391
|
+
OBJECT_METADATA_TYPE,
|
|
4211
5392
|
ObjectStackAdapter,
|
|
4212
5393
|
ObjectStackError,
|
|
4213
|
-
|
|
4214
|
-
|
|
5394
|
+
RELATIONSHIP_TYPES_REQUIRING_REFERENCE,
|
|
5395
|
+
UNRECOGNIZED_DROP_REASON,
|
|
5396
|
+
UnlowerableAnalyticsFilterError,
|
|
5397
|
+
UnloweredAggregateWhereError,
|
|
5398
|
+
VIEW_CONFIG_CAPABILITY,
|
|
5399
|
+
VIEW_OVERLAY_OWNED_KEYS,
|
|
5400
|
+
ViewConfigPermissionDeniedError,
|
|
5401
|
+
assertObjectMetadataWritable,
|
|
4215
5402
|
classifyAnalyticsFailure,
|
|
5403
|
+
classifyImportMappingsFailure,
|
|
5404
|
+
classifyViewsFailure,
|
|
4216
5405
|
clearSharedDiscoveryCache,
|
|
4217
|
-
createDefaultCanvasConfig,
|
|
4218
5406
|
createErrorFromResponse,
|
|
4219
5407
|
createObjectStackAdapter,
|
|
4220
5408
|
createObjectStackUserStateAdapter,
|
|
4221
5409
|
createQuietHttpLogger,
|
|
5410
|
+
extractDraftBody,
|
|
4222
5411
|
formatHttpFailureMessage,
|
|
4223
|
-
|
|
5412
|
+
formatMetadataError,
|
|
5413
|
+
formatMetadataIssue,
|
|
4224
5414
|
getSharedDiscovery,
|
|
4225
5415
|
is404Error,
|
|
4226
5416
|
isAnalyticsNotInstalledError,
|
|
@@ -4229,12 +5419,13 @@ export {
|
|
|
4229
5419
|
isConcurrentUpdateError,
|
|
4230
5420
|
isErrorType,
|
|
4231
5421
|
isMalformedFilterError,
|
|
5422
|
+
isMetaItemAbsentError,
|
|
4232
5423
|
isObjectStackError,
|
|
5424
|
+
isViewConfigPermissionDeniedError,
|
|
5425
|
+
narrowPersonalizationOverlay,
|
|
4233
5426
|
normaliseClientError,
|
|
4234
5427
|
readSaveAdvisories,
|
|
4235
5428
|
readTransactionalBatchCapability,
|
|
4236
5429
|
serializeOrderBy,
|
|
4237
|
-
snapToGrid,
|
|
4238
|
-
validatePluginContract,
|
|
4239
5430
|
viewItemObjectName
|
|
4240
5431
|
};
|