@object-ui/data-objectstack 17.7.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +368 -0
- package/README.md +172 -2
- package/dist/index.cjs +875 -362
- package/dist/index.d.cts +742 -307
- package/dist/index.d.ts +742 -307
- package/dist/index.js +870 -360
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -45,13 +45,12 @@ __export(index_exports, {
|
|
|
45
45
|
OBJECT_METADATA_TYPE: () => OBJECT_METADATA_TYPE,
|
|
46
46
|
ObjectStackAdapter: () => ObjectStackAdapter,
|
|
47
47
|
ObjectStackError: () => ObjectStackError,
|
|
48
|
+
PREFERENCE_FAMILY_ROW_LIMIT: () => PREFERENCE_FAMILY_ROW_LIMIT,
|
|
48
49
|
RELATIONSHIP_TYPES_REQUIRING_REFERENCE: () => RELATIONSHIP_TYPES_REQUIRING_REFERENCE,
|
|
49
50
|
UNRECOGNIZED_DROP_REASON: () => UNRECOGNIZED_DROP_REASON,
|
|
50
51
|
UnlowerableAnalyticsFilterError: () => UnlowerableAnalyticsFilterError,
|
|
51
52
|
UnloweredAggregateWhereError: () => UnloweredAggregateWhereError,
|
|
52
|
-
VIEW_CONFIG_CAPABILITY: () => VIEW_CONFIG_CAPABILITY,
|
|
53
53
|
VIEW_OVERLAY_OWNED_KEYS: () => VIEW_OVERLAY_OWNED_KEYS,
|
|
54
|
-
ViewConfigPermissionDeniedError: () => ViewConfigPermissionDeniedError,
|
|
55
54
|
assertObjectMetadataWritable: () => assertObjectMetadataWritable,
|
|
56
55
|
classifyAnalyticsFailure: () => classifyAnalyticsFailure,
|
|
57
56
|
classifyImportMappingsFailure: () => classifyImportMappingsFailure,
|
|
@@ -59,13 +58,17 @@ __export(index_exports, {
|
|
|
59
58
|
clearSharedDiscoveryCache: () => clearSharedDiscoveryCache,
|
|
60
59
|
createErrorFromResponse: () => createErrorFromResponse,
|
|
61
60
|
createObjectStackAdapter: () => createObjectStackAdapter,
|
|
61
|
+
createObjectStackUserPreferenceFamily: () => createObjectStackUserPreferenceFamily,
|
|
62
62
|
createObjectStackUserStateAdapter: () => createObjectStackUserStateAdapter,
|
|
63
63
|
createQuietHttpLogger: () => createQuietHttpLogger,
|
|
64
|
+
draftChangesUnder: () => draftChangesUnder,
|
|
65
|
+
dropServedPicklistOptions: () => dropServedPicklistOptions,
|
|
64
66
|
extractDraftBody: () => extractDraftBody,
|
|
65
67
|
formatHttpFailureMessage: () => formatHttpFailureMessage,
|
|
66
68
|
formatMetadataError: () => formatMetadataError,
|
|
67
69
|
formatMetadataIssue: () => formatMetadataIssue,
|
|
68
70
|
getSharedDiscovery: () => getSharedDiscovery,
|
|
71
|
+
governingDraftChange: () => governingDraftChange,
|
|
69
72
|
is404Error: () => is404Error,
|
|
70
73
|
isAnalyticsNotInstalledError: () => isAnalyticsNotInstalledError,
|
|
71
74
|
isApiAccessDeniedError: () => isApiAccessDeniedError,
|
|
@@ -75,9 +78,9 @@ __export(index_exports, {
|
|
|
75
78
|
isMalformedFilterError: () => isMalformedFilterError,
|
|
76
79
|
isMetaItemAbsentError: () => isMetaItemAbsentError,
|
|
77
80
|
isObjectStackError: () => isObjectStackError,
|
|
78
|
-
isViewConfigPermissionDeniedError: () => isViewConfigPermissionDeniedError,
|
|
79
81
|
narrowPersonalizationOverlay: () => narrowPersonalizationOverlay,
|
|
80
82
|
normaliseClientError: () => normaliseClientError,
|
|
83
|
+
readDraftChanges: () => readDraftChanges,
|
|
81
84
|
readSaveAdvisories: () => readSaveAdvisories,
|
|
82
85
|
readTransactionalBatchCapability: () => readTransactionalBatchCapability,
|
|
83
86
|
serializeOrderBy: () => serializeOrderBy,
|
|
@@ -89,6 +92,7 @@ var import_ui2 = require("@objectstack/spec/ui");
|
|
|
89
92
|
var import_data4 = require("@objectstack/spec/data");
|
|
90
93
|
var import_data5 = require("@objectstack/spec/data");
|
|
91
94
|
var import_data6 = require("@objectstack/spec/data");
|
|
95
|
+
var import_api3 = require("@objectstack/spec/api");
|
|
92
96
|
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
93
97
|
|
|
94
98
|
// src/metadata-client.ts
|
|
@@ -133,20 +137,20 @@ function fieldEntries(fields) {
|
|
|
133
137
|
}
|
|
134
138
|
return [];
|
|
135
139
|
}
|
|
136
|
-
function assertObjectMetadataWritable(type, item,
|
|
140
|
+
function assertObjectMetadataWritable(type, item, _writer) {
|
|
137
141
|
if (String(type) !== OBJECT_METADATA_TYPE) return;
|
|
138
142
|
if (!item || typeof item !== "object") return;
|
|
139
143
|
const fields = item.fields;
|
|
140
144
|
for (const { name, def } of fieldEntries(fields)) {
|
|
141
145
|
if (CHOICE_TYPES_REQUIRING_OPTIONS.includes(String(def.type)) && !hasOptionSource(def)) {
|
|
142
146
|
throw new Error(
|
|
143
|
-
|
|
147
|
+
`The object was not saved: 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\`, so this draft stays here until it has one. Add an option, or change the field to a non-choice type.`
|
|
144
148
|
);
|
|
145
149
|
}
|
|
146
150
|
if (!RELATIONSHIP_TYPES_REQUIRING_REFERENCE.includes(String(def.type))) continue;
|
|
147
151
|
if (isUsableTarget(def.reference)) continue;
|
|
148
152
|
throw new Error(
|
|
149
|
-
|
|
153
|
+
`The object was not saved: the field \`${name}\` is a \`${String(def.type)}\` and carries ${describeTarget(def.reference)}, so it names no object to link to. The server refuses the same document 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. Pick the target object, or change the field to a non-relationship type.`
|
|
150
154
|
);
|
|
151
155
|
}
|
|
152
156
|
}
|
|
@@ -162,6 +166,15 @@ function readSaveAdvisories(body) {
|
|
|
162
166
|
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";
|
|
163
167
|
});
|
|
164
168
|
}
|
|
169
|
+
function readRegistrations(raw) {
|
|
170
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
171
|
+
const out = {};
|
|
172
|
+
for (const [name, verdict] of Object.entries(raw)) {
|
|
173
|
+
const parsed = import_api.FlowRegistrationSchema.safeParse(verdict);
|
|
174
|
+
if (parsed.success) out[name] = parsed.data;
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
165
178
|
var PROTECTION_ENVELOPE_KEYS = [
|
|
166
179
|
"overlayScope",
|
|
167
180
|
"lock",
|
|
@@ -228,6 +241,26 @@ function isMetaItemEnvelope(value) {
|
|
|
228
241
|
const env = value;
|
|
229
242
|
return typeof env.type === "string" && typeof env.name === "string" && "item" in env;
|
|
230
243
|
}
|
|
244
|
+
var pendingReads = /* @__PURE__ */ new WeakMap();
|
|
245
|
+
function pendingReadsOn(transport) {
|
|
246
|
+
let reads = pendingReads.get(transport);
|
|
247
|
+
if (!reads) {
|
|
248
|
+
reads = /* @__PURE__ */ new Map();
|
|
249
|
+
pendingReads.set(transport, reads);
|
|
250
|
+
}
|
|
251
|
+
return reads;
|
|
252
|
+
}
|
|
253
|
+
function handOut(entry) {
|
|
254
|
+
entry.waiting += 1;
|
|
255
|
+
return entry.read.then((value) => {
|
|
256
|
+
entry.waiting -= 1;
|
|
257
|
+
return entry.waiting === 0 ? value : ownCopy(value);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
function ownCopy(value) {
|
|
261
|
+
if (value === null || typeof value !== "object") return value;
|
|
262
|
+
return typeof structuredClone === "function" ? structuredClone(value) : JSON.parse(JSON.stringify(value));
|
|
263
|
+
}
|
|
231
264
|
var MetadataClient = class _MetadataClient {
|
|
232
265
|
constructor(config) {
|
|
233
266
|
__publicField(this, "base");
|
|
@@ -278,34 +311,102 @@ var MetadataClient = class _MetadataClient {
|
|
|
278
311
|
...this.onSaveAdvisory ? { onSaveAdvisory: this.onSaveAdvisory } : {}
|
|
279
312
|
});
|
|
280
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Share one in-flight GET among the callers asking the same method for the
|
|
316
|
+
* same URL with the same headers through the same transport (objectui#11797).
|
|
317
|
+
*
|
|
318
|
+
* Opening a Studio package mounts several readers that ask the same
|
|
319
|
+
* question at once, and each one sent its own request. The first call for a
|
|
320
|
+
* key sends `read()`; a call that arrives while it is pending waits for that
|
|
321
|
+
* request instead of sending another.
|
|
322
|
+
*
|
|
323
|
+
* ⛔ Not a response cache, deliberately — the rules `findOne` keeps on the
|
|
324
|
+
* data adapter (objectui#11699):
|
|
325
|
+
* - the entry goes the moment the read settles, so a call made after an
|
|
326
|
+
* answer always reaches the network (there is no reuse window: a client
|
|
327
|
+
* cannot see every write that changes metadata, an agent's server-side
|
|
328
|
+
* edits among them, and a window would answer the read after such a write
|
|
329
|
+
* with the state from before it);
|
|
330
|
+
* - a rejection reaches every caller already waiting and is not remembered;
|
|
331
|
+
* - a write sent through the same transport drops every pending read when
|
|
332
|
+
* it lands (see {@link sendWrite}), so a read asked after a save or a
|
|
333
|
+
* publish is never answered by a request sent before it;
|
|
334
|
+
* - every caller, the first one included, gets an answer no other caller
|
|
335
|
+
* holds (see {@link handOut}).
|
|
336
|
+
*/
|
|
337
|
+
shareRead(reader, url, read) {
|
|
338
|
+
const reads = pendingReadsOn(this.fetchImpl);
|
|
339
|
+
const key = JSON.stringify([reader, url, Object.entries(this.headers).sort(([a], [b]) => a.localeCompare(b))]);
|
|
340
|
+
let entry = reads.get(key);
|
|
341
|
+
if (!entry) {
|
|
342
|
+
const created = { read: read(), waiting: 0 };
|
|
343
|
+
reads.set(key, created);
|
|
344
|
+
const settle = () => {
|
|
345
|
+
if (reads.get(key) === created) reads.delete(key);
|
|
346
|
+
};
|
|
347
|
+
created.read.then(settle, settle);
|
|
348
|
+
entry = created;
|
|
349
|
+
}
|
|
350
|
+
return handOut(entry);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Send a write, then drop every read still pending on this transport
|
|
354
|
+
* (objectui#11797): a read on the wire when the write lands was asked before
|
|
355
|
+
* it, so a caller asking after the write must not join it. Dropped however
|
|
356
|
+
* the write ends — a refused or interrupted write may still have changed
|
|
357
|
+
* something. The callers already holding a pending read keep its answer.
|
|
358
|
+
*/
|
|
359
|
+
async sendWrite(url, init) {
|
|
360
|
+
try {
|
|
361
|
+
return await this.fetchImpl(url, init);
|
|
362
|
+
} finally {
|
|
363
|
+
pendingReads.get(this.fetchImpl)?.clear();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
281
366
|
/** List all registered metadata types (returns the registry rows). */
|
|
282
367
|
async listTypes() {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
368
|
+
return this.shareRead("listTypes", this.base, async () => {
|
|
369
|
+
const res = await this.fetchImpl(this.base, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
370
|
+
if (!res.ok) throw await parseError(res);
|
|
371
|
+
const data = await res.json();
|
|
372
|
+
if (Array.isArray(data)) return data;
|
|
373
|
+
if (data && Array.isArray(data.entries)) return data.entries;
|
|
374
|
+
if (data && Array.isArray(data.items)) return data.items;
|
|
375
|
+
if (data && Array.isArray(data.types)) {
|
|
376
|
+
return data.types.map((t) => ({ type: t }));
|
|
377
|
+
}
|
|
378
|
+
return [];
|
|
379
|
+
});
|
|
293
380
|
}
|
|
294
381
|
/** List items of a metadata type (e.g. `object`, `field`, `view`). */
|
|
295
382
|
async list(type, options = {}) {
|
|
383
|
+
return (await this.listResponse(type, options)).items;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* The same list read as {@link list}, with what the server serves beside
|
|
387
|
+
* the items (objectui#12349): today that is the automation engine's
|
|
388
|
+
* `registrations` on a `flow` list — see {@link MetadataListAnswer}.
|
|
389
|
+
*
|
|
390
|
+
* One request for both methods: `list` is this read with the items picked
|
|
391
|
+
* out, so a caller asking both at once shares one in-flight GET.
|
|
392
|
+
*/
|
|
393
|
+
async listResponse(type, options = {}) {
|
|
296
394
|
const params = [];
|
|
297
395
|
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
298
396
|
if (this.previewDrafts) params.push("preview=draft");
|
|
299
397
|
const qs = params.length ? `?${params.join("&")}` : "";
|
|
300
398
|
const url = `${this.base}/${encodeURIComponent(type)}${qs}`;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
399
|
+
return this.shareRead("list", url, async () => {
|
|
400
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
401
|
+
if (!res.ok) throw await parseError(res);
|
|
402
|
+
const data = await res.json();
|
|
403
|
+
if (Array.isArray(data)) return { items: data };
|
|
404
|
+
if (data && Array.isArray(data.items)) {
|
|
405
|
+
const registrations = readRegistrations(data.registrations);
|
|
406
|
+
return { items: data.items, ...registrations ? { registrations } : {} };
|
|
407
|
+
}
|
|
408
|
+
return { items: [] };
|
|
409
|
+
});
|
|
309
410
|
}
|
|
310
411
|
/**
|
|
311
412
|
* List pending DRAFT items (ADR-0033) — what an AI authored but nobody
|
|
@@ -321,11 +422,13 @@ var MetadataClient = class _MetadataClient {
|
|
|
321
422
|
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
322
423
|
const qs = params.length ? `?${params.join("&")}` : "";
|
|
323
424
|
const url = `${this.base}/_drafts${qs}`;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
425
|
+
return this.shareRead("listDrafts", url, async () => {
|
|
426
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
427
|
+
if (!res.ok) throw await parseError(res);
|
|
428
|
+
const data = await res.json();
|
|
429
|
+
if (Array.isArray(data)) return data;
|
|
430
|
+
return data?.drafts ?? data?.data?.drafts ?? [];
|
|
431
|
+
});
|
|
329
432
|
}
|
|
330
433
|
/**
|
|
331
434
|
* Fetch a single metadata item and return the response body EXACTLY as the
|
|
@@ -344,10 +447,12 @@ var MetadataClient = class _MetadataClient {
|
|
|
344
447
|
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
345
448
|
const qs = params.length ? `?${params.join("&")}` : "";
|
|
346
449
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
450
|
+
return this.shareRead("readItemResponse", url, async () => {
|
|
451
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
452
|
+
if (res.status === 404) return null;
|
|
453
|
+
if (!res.ok) throw await parseError(res);
|
|
454
|
+
return await res.json();
|
|
455
|
+
});
|
|
351
456
|
}
|
|
352
457
|
/**
|
|
353
458
|
* Get a single metadata item. Returns the unwrapped item CONTENT — the
|
|
@@ -403,13 +508,51 @@ var MetadataClient = class _MetadataClient {
|
|
|
403
508
|
* (app-shell) and `unwrapViewDraft` (this package) read a draft that arrived
|
|
404
509
|
* through {@link get}, which already unwrapped the envelope. They split on
|
|
405
510
|
* which method feeds them, not on what they mean — all three strip.
|
|
511
|
+
*
|
|
512
|
+
* objectui#11799 — whether a draft exists is answered by the pending-drafts
|
|
513
|
+
* ledger first (see {@link draftLedgerMayHold}), and `?state=draft` is sent
|
|
514
|
+
* only when the ledger lists the name. The item read answers 404 when there
|
|
515
|
+
* is no draft, so opening every item in Studio used to log one error per
|
|
516
|
+
* item for an answer the ledger gives with a 200.
|
|
406
517
|
*/
|
|
407
518
|
async getDraft(type, name, options = {}) {
|
|
519
|
+
if (!await this.draftLedgerMayHold(name)) return null;
|
|
408
520
|
return this.readItemResponse(type, name, {
|
|
409
521
|
state: "draft",
|
|
410
522
|
...options.packageId ? { packageId: options.packageId } : {}
|
|
411
523
|
});
|
|
412
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Whether the pending-drafts ledger (`GET /meta/_drafts`) may hold a draft
|
|
527
|
+
* named `name` (objectui#11799).
|
|
528
|
+
*
|
|
529
|
+
* Read through {@link listDrafts}, the shared read objectui#11797 made: a
|
|
530
|
+
* question asked while a ledger read is pending waits for it, and a write
|
|
531
|
+
* through this transport drops it when the write lands ({@link sendWrite}),
|
|
532
|
+
* so a draft saved a moment ago is never reported absent by a read sent
|
|
533
|
+
* before the save.
|
|
534
|
+
*
|
|
535
|
+
* The WHOLE ledger, matched by name. The `?state=draft` read finds a row
|
|
536
|
+
* under either spelling of the type, and, when it names a package, that
|
|
537
|
+
* package's row or the package-less one. The ledger's `type` and `packageId`
|
|
538
|
+
* filters are exact, so a filtered ledger could miss a draft the item read
|
|
539
|
+
* would serve, and the editor would open the published body over a pending
|
|
540
|
+
* draft. Unfiltered, every miss here is a real absence; a same-name row of
|
|
541
|
+
* another type or package only costs the item read that was always sent.
|
|
542
|
+
*
|
|
543
|
+
* A ledger that cannot be read (403 without an authoring capability, 501 on
|
|
544
|
+
* a kernel without drafts, a network fault) is UNKNOWN, not empty: the item
|
|
545
|
+
* read is sent, as before.
|
|
546
|
+
*/
|
|
547
|
+
async draftLedgerMayHold(name) {
|
|
548
|
+
let drafts;
|
|
549
|
+
try {
|
|
550
|
+
drafts = await this.listDrafts();
|
|
551
|
+
} catch {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
return drafts.some((d) => d?.name === name);
|
|
555
|
+
}
|
|
413
556
|
/**
|
|
414
557
|
* Report the runtime authoring gate's advisory findings for a write the
|
|
415
558
|
* server has ALREADY committed (#4133 for the save door, #5026 for the
|
|
@@ -447,11 +590,17 @@ var MetadataClient = class _MetadataClient {
|
|
|
447
590
|
`MetadataClient.save: name must be non-empty (type="${type}"). The PUT /meta/:type/:name route requires a name segment.`
|
|
448
591
|
);
|
|
449
592
|
}
|
|
593
|
+
if (options.ifMatch && options.ifNoneMatch) {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`MetadataClient.save: ifMatch and ifNoneMatch are mutually exclusive (type="${type}", name="${name}"). Send ifMatch with the version you read, or ifNoneMatch: "*" when no row is there.`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
450
598
|
assertObjectMetadataWritable(type, item, "MetadataClient.save");
|
|
451
599
|
const params = [];
|
|
452
600
|
if (options.force) params.push("force=true");
|
|
453
601
|
if (options.mode === "draft") params.push("mode=draft");
|
|
454
602
|
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
603
|
+
if (options.surface) params.push(`surface=${encodeURIComponent(options.surface)}`);
|
|
455
604
|
const qs = params.length ? `?${params.join("&")}` : "";
|
|
456
605
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
457
606
|
const headers = {
|
|
@@ -459,7 +608,8 @@ var MetadataClient = class _MetadataClient {
|
|
|
459
608
|
"Content-Type": "application/json"
|
|
460
609
|
};
|
|
461
610
|
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
462
|
-
|
|
611
|
+
if (options.ifNoneMatch) headers["If-None-Match"] = options.ifNoneMatch;
|
|
612
|
+
const res = await this.sendWrite(url, {
|
|
463
613
|
method: "PUT",
|
|
464
614
|
headers,
|
|
465
615
|
body: JSON.stringify(item)
|
|
@@ -540,17 +690,40 @@ var MetadataClient = class _MetadataClient {
|
|
|
540
690
|
* {@link listDrafts} reads `data?.data?.drafts` because `GET /meta/_drafts`
|
|
541
691
|
* IS a dispatcher route, and `publishHealthFromResponse` unwraps because the
|
|
542
692
|
* batch door really is enveloped. The rule is per-route, not per-file.
|
|
693
|
+
*
|
|
694
|
+
* ## The bundle member (objectui#12402)
|
|
695
|
+
*
|
|
696
|
+
* Pass `member` to publish one member of a type whose identity is a pair
|
|
697
|
+
* (today only `email_template`, `(name, locale)`), sent as `?member=` — the
|
|
698
|
+
* selector the by-name doors read since objectstack `038d4c958`, and the
|
|
699
|
+
* value a `GET /meta/_drafts` row carries as `member`. A publish that names
|
|
700
|
+
* none promotes the CANONICAL member's draft, so two languages of one
|
|
701
|
+
* template drafted outside a package cannot both be published by name
|
|
702
|
+
* without it. The parameter is OMITTED, never sent empty, when the caller
|
|
703
|
+
* has no member: the door refuses a blank selector, and refuses any
|
|
704
|
+
* selector on a type with no members.
|
|
543
705
|
*/
|
|
544
|
-
async publishDraft(type, name) {
|
|
545
|
-
const
|
|
546
|
-
|
|
706
|
+
async publishDraft(type, name, options = {}) {
|
|
707
|
+
const params = [];
|
|
708
|
+
if (options.member) params.push(`member=${encodeURIComponent(options.member)}`);
|
|
709
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
710
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish${qs}`;
|
|
711
|
+
const res = await this.sendWrite(url, {
|
|
547
712
|
method: "POST",
|
|
548
713
|
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
549
714
|
body: "{}"
|
|
550
715
|
});
|
|
551
716
|
if (!res.ok) throw await parseError(res);
|
|
552
717
|
const body = await res.json().catch(() => ({}));
|
|
553
|
-
this.emitAdvisories(body, {
|
|
718
|
+
this.emitAdvisories(body, {
|
|
719
|
+
type,
|
|
720
|
+
name,
|
|
721
|
+
// objectui#12413 — the member this publish named, under the same rule as
|
|
722
|
+
// the query parameter above: present only when the caller had one.
|
|
723
|
+
...options.member ? { member: options.member } : {},
|
|
724
|
+
door: "publish",
|
|
725
|
+
mode: "publish"
|
|
726
|
+
});
|
|
554
727
|
return body;
|
|
555
728
|
}
|
|
556
729
|
/**
|
|
@@ -598,22 +771,32 @@ var MetadataClient = class _MetadataClient {
|
|
|
598
771
|
* says to read `outcome`, not the boolean), and the two callers have their
|
|
599
772
|
* own, different rules for the refusal and rolled-back cases. Deciding that
|
|
600
773
|
* here would change behaviour this card is not about.
|
|
774
|
+
*
|
|
775
|
+
* ## The opt-in backfill (objectui#12929)
|
|
776
|
+
*
|
|
777
|
+
* `options.backfill` is sent as the body's `backfill` key, as given, and only
|
|
778
|
+
* when the caller passes one ({@link MetadataPublishPackageDraftsOptions}).
|
|
779
|
+
* Without it the body is `{}`, byte for byte what every caller sent before,
|
|
780
|
+
* so a caller that names nothing writes nothing into existing rows. The
|
|
781
|
+
* receipt is `backfillApplied` on the returned body, unexamined like the rest
|
|
782
|
+
* of a 2xx: one element per named field with the rows written, and `error`
|
|
783
|
+
* on a field that stopped short (the publish itself stands).
|
|
601
784
|
*/
|
|
602
|
-
async publishPackageDrafts(packageId) {
|
|
785
|
+
async publishPackageDrafts(packageId, options = {}) {
|
|
603
786
|
if (!packageId || !String(packageId).trim()) {
|
|
604
787
|
throw new Error(
|
|
605
788
|
"MetadataClient.publishPackageDrafts: packageId must be non-empty. The POST /packages/:id/publish-drafts route requires an id segment."
|
|
606
789
|
);
|
|
607
790
|
}
|
|
608
791
|
const url = `${packagesBaseOf(this.base)}/${encodeURIComponent(packageId)}/publish-drafts`;
|
|
609
|
-
const res = await this.
|
|
792
|
+
const res = await this.sendWrite(url, {
|
|
610
793
|
method: "POST",
|
|
611
794
|
// Both call sites this replaces sent the session cookie, and the client's
|
|
612
795
|
// own fetch adds the console's Bearer token — so the routed call carries a
|
|
613
796
|
// superset of what the bare doors carried, never less.
|
|
614
797
|
credentials: "include",
|
|
615
798
|
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
616
|
-
body:
|
|
799
|
+
body: JSON.stringify(options.backfill !== void 0 ? { backfill: options.backfill } : {})
|
|
617
800
|
});
|
|
618
801
|
if (!res.ok) throw await parseError(res);
|
|
619
802
|
const body = unwrapDispatcherEnvelope(await res.json().catch(() => ({})));
|
|
@@ -763,13 +946,22 @@ var MetadataClient = class _MetadataClient {
|
|
|
763
946
|
* Idempotent: returns the result even when no overlay row existed.
|
|
764
947
|
* Pass `state: 'draft'` to discard the pending draft only (keeps the
|
|
765
948
|
* published overlay intact) — useful for a Studio "Discard draft" button.
|
|
949
|
+
*
|
|
950
|
+
* Pass `packageId` to name the package whose row the delete removes, sent
|
|
951
|
+
* as `?package=`. A delete that names none addresses the one package that
|
|
952
|
+
* holds the item; when two or more packages hold it the door refuses it
|
|
953
|
+
* `409 RESOURCE_CONFLICT`, deletes nothing, and names `?package=` as the
|
|
954
|
+
* remedy. That refusal is thrown like any other {@link MetadataError}.
|
|
766
955
|
*/
|
|
767
956
|
async reset(type, name, options = {}) {
|
|
768
|
-
const
|
|
957
|
+
const params = [];
|
|
958
|
+
if (options.state === "draft") params.push("state=draft");
|
|
959
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
960
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
769
961
|
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
770
962
|
const headers = { ...this.headers };
|
|
771
963
|
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
772
|
-
const res = await this.
|
|
964
|
+
const res = await this.sendWrite(url, { method: "DELETE", headers });
|
|
773
965
|
if (!res.ok) throw await parseError(res);
|
|
774
966
|
return await res.json();
|
|
775
967
|
}
|
|
@@ -793,7 +985,7 @@ var MetadataClient = class _MetadataClient {
|
|
|
793
985
|
...this.headers,
|
|
794
986
|
"Content-Type": "application/json"
|
|
795
987
|
};
|
|
796
|
-
const res = await this.
|
|
988
|
+
const res = await this.sendWrite(url, {
|
|
797
989
|
method: "POST",
|
|
798
990
|
headers,
|
|
799
991
|
body: JSON.stringify(options.message ? { message: options.message } : {})
|
|
@@ -817,7 +1009,7 @@ var MetadataClient = class _MetadataClient {
|
|
|
817
1009
|
...this.headers,
|
|
818
1010
|
"Content-Type": "application/json"
|
|
819
1011
|
};
|
|
820
|
-
const res = await this.
|
|
1012
|
+
const res = await this.sendWrite(url, {
|
|
821
1013
|
method: "POST",
|
|
822
1014
|
headers,
|
|
823
1015
|
body: JSON.stringify({
|
|
@@ -1430,21 +1622,54 @@ var MetadataCache = class {
|
|
|
1430
1622
|
this.cache.delete(key);
|
|
1431
1623
|
}
|
|
1432
1624
|
}
|
|
1625
|
+
return this.load(key, fetcher);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Join the read in flight for `key`, or start one, and never answer from the
|
|
1629
|
+
* settled entry: a caller of this always gets an answer from a request that
|
|
1630
|
+
* was on the wire when it asked.
|
|
1631
|
+
*
|
|
1632
|
+
* objectui#13044. This is the door for a caller whose own contract is "ask
|
|
1633
|
+
* the server", which should still not put a second identical request on the
|
|
1634
|
+
* wire while one is already there.
|
|
1635
|
+
*
|
|
1636
|
+
* objectui#13057. The landing is stored, exactly as one {@link get} started
|
|
1637
|
+
* would store it, so a later {@link get} for `key` may be answered from it.
|
|
1638
|
+
* The caller asked the server; a reader that may be served what the cache
|
|
1639
|
+
* already holds is served the answer that caller just received.
|
|
1640
|
+
*/
|
|
1641
|
+
share(key, fetcher) {
|
|
1642
|
+
return this.load(key, fetcher);
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* The in-flight half of {@link get} and {@link share}: join the request in
|
|
1646
|
+
* flight for `key`, or start one.
|
|
1647
|
+
*
|
|
1648
|
+
* A landing is stored, and its in-flight entry cleared, only
|
|
1649
|
+
* while it is still the request registered for `key` (objectui#13044). {@link invalidate}
|
|
1650
|
+
* and {@link clear} detach the request in flight, so a request sent before an
|
|
1651
|
+
* invalidation can neither answer a caller who asks after it nor write its
|
|
1652
|
+
* answer over a newer one.
|
|
1653
|
+
*/
|
|
1654
|
+
load(key, fetcher) {
|
|
1433
1655
|
const existing = this.inflight.get(key);
|
|
1434
1656
|
if (existing) {
|
|
1435
1657
|
this.stats.coalesced++;
|
|
1436
1658
|
return existing;
|
|
1437
1659
|
}
|
|
1438
1660
|
this.stats.misses++;
|
|
1439
|
-
const
|
|
1661
|
+
const self = {};
|
|
1662
|
+
const run = async () => {
|
|
1440
1663
|
try {
|
|
1441
1664
|
const data = await fetcher();
|
|
1442
|
-
this.set(key, data);
|
|
1665
|
+
if (this.inflight.get(key) === self.promise) this.set(key, data);
|
|
1443
1666
|
return data;
|
|
1444
1667
|
} finally {
|
|
1445
|
-
this.inflight.delete(key);
|
|
1668
|
+
if (this.inflight.get(key) === self.promise) this.inflight.delete(key);
|
|
1446
1669
|
}
|
|
1447
|
-
}
|
|
1670
|
+
};
|
|
1671
|
+
const promise = run();
|
|
1672
|
+
self.promise = promise;
|
|
1448
1673
|
this.inflight.set(key, promise);
|
|
1449
1674
|
return promise;
|
|
1450
1675
|
}
|
|
@@ -1492,8 +1717,10 @@ var MetadataCache = class {
|
|
|
1492
1717
|
invalidate(key) {
|
|
1493
1718
|
if (key) {
|
|
1494
1719
|
this.cache.delete(key);
|
|
1720
|
+
this.inflight.delete(key);
|
|
1495
1721
|
} else {
|
|
1496
1722
|
this.cache.clear();
|
|
1723
|
+
this.inflight.clear();
|
|
1497
1724
|
}
|
|
1498
1725
|
}
|
|
1499
1726
|
/**
|
|
@@ -1563,6 +1790,21 @@ var MetadataCache = class {
|
|
|
1563
1790
|
}
|
|
1564
1791
|
};
|
|
1565
1792
|
|
|
1793
|
+
// src/metadata-error.ts
|
|
1794
|
+
function extractIssues(e) {
|
|
1795
|
+
const issues = e?.issues;
|
|
1796
|
+
return Array.isArray(issues) ? issues : [];
|
|
1797
|
+
}
|
|
1798
|
+
function formatMetadataIssue(i) {
|
|
1799
|
+
return `\u2022 ${i.path && i.path.length > 0 ? i.path : "(root)"} \u2014 ${i.message}`;
|
|
1800
|
+
}
|
|
1801
|
+
function formatMetadataError(e) {
|
|
1802
|
+
const issues = extractIssues(e);
|
|
1803
|
+
if (issues.length > 0) return issues.map(formatMetadataIssue).join("\n");
|
|
1804
|
+
const err = e;
|
|
1805
|
+
return err?.message ?? String(e);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1566
1808
|
// src/errors.ts
|
|
1567
1809
|
var ObjectStackError = class extends Error {
|
|
1568
1810
|
/**
|
|
@@ -1771,51 +2013,67 @@ function present(value) {
|
|
|
1771
2013
|
|
|
1772
2014
|
// src/userState.ts
|
|
1773
2015
|
var DEFAULT_RESOURCE = "sys_user_preference";
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
2016
|
+
var PREFERENCE_FAMILY_ROW_LIMIT = 200;
|
|
2017
|
+
function preferenceQuery(userId, keys, prefixes = []) {
|
|
2018
|
+
if (prefixes.length === 0) {
|
|
2019
|
+
if (keys.length === 1) {
|
|
2020
|
+
return { $filter: { user_id: userId, key: keys[0] }, $top: 1 };
|
|
2021
|
+
}
|
|
2022
|
+
return { $filter: { user_id: userId, key: { $in: [...keys] } }, $top: keys.length };
|
|
2023
|
+
}
|
|
2024
|
+
const arms = [];
|
|
2025
|
+
if (keys.length === 1) arms.push({ key: keys[0] });
|
|
2026
|
+
else if (keys.length > 1) arms.push({ key: { $in: [...keys] } });
|
|
2027
|
+
for (const prefix of prefixes) arms.push({ key: { $startsWith: prefix } });
|
|
2028
|
+
return {
|
|
2029
|
+
$filter: { user_id: userId, ...arms.length === 1 ? arms[0] : { $or: arms } },
|
|
2030
|
+
$top: keys.length + prefixes.length * PREFERENCE_FAMILY_ROW_LIMIT
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
var openReads = /* @__PURE__ */ new WeakMap();
|
|
2034
|
+
function joinSharedRead(dataSource, resource, userId, want) {
|
|
2035
|
+
let scopes = openReads.get(dataSource);
|
|
2036
|
+
if (!scopes) {
|
|
2037
|
+
scopes = /* @__PURE__ */ new Map();
|
|
2038
|
+
openReads.set(dataSource, scopes);
|
|
2039
|
+
}
|
|
2040
|
+
const scope = JSON.stringify([resource, userId]);
|
|
2041
|
+
const open = scopes.get(scope);
|
|
2042
|
+
if (open) {
|
|
2043
|
+
if ("key" in want) open.keys.add(want.key);
|
|
2044
|
+
else open.prefixes.add(want.prefix);
|
|
2045
|
+
return open;
|
|
2046
|
+
}
|
|
2047
|
+
const keys = new Set("key" in want ? [want.key] : []);
|
|
2048
|
+
const prefixes = new Set("prefix" in want ? [want.prefix] : []);
|
|
2049
|
+
const opened = scopes;
|
|
2050
|
+
const read = {
|
|
2051
|
+
keys,
|
|
2052
|
+
prefixes,
|
|
2053
|
+
answer: Promise.resolve().then(async () => {
|
|
2054
|
+
if (opened.get(scope) === read) opened.delete(scope);
|
|
2055
|
+
const query = preferenceQuery(userId, [...keys], [...prefixes]);
|
|
2056
|
+
const result = await dataSource.find(resource, query);
|
|
2057
|
+
const rows = result?.data ?? [];
|
|
2058
|
+
return {
|
|
2059
|
+
rows,
|
|
2060
|
+
saturated: prefixes.size > 0 && rows.length >= (query.$top ?? Infinity)
|
|
2061
|
+
};
|
|
2062
|
+
})
|
|
2063
|
+
};
|
|
2064
|
+
scopes.set(scope, read);
|
|
2065
|
+
return read;
|
|
2066
|
+
}
|
|
2067
|
+
function createPreferenceRowWriter(dataSource, resource, userId, key, onError) {
|
|
1783
2068
|
let cachedRowId = null;
|
|
1784
|
-
let saveChain = Promise.resolve();
|
|
1785
2069
|
const findExisting = async () => {
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
user_id: userId,
|
|
1789
|
-
key
|
|
1790
|
-
},
|
|
1791
|
-
$top: 1
|
|
1792
|
-
};
|
|
1793
|
-
const result = await dataSource.find(resource, params);
|
|
1794
|
-
const rows = result?.data ?? [];
|
|
1795
|
-
const match = rows.find((r) => {
|
|
1796
|
-
if (!r) return false;
|
|
1797
|
-
const userMismatch = r.user_id !== void 0 && r.user_id !== userId;
|
|
1798
|
-
const keyMismatch = r.key !== void 0 && r.key !== key;
|
|
1799
|
-
return !userMismatch && !keyMismatch;
|
|
1800
|
-
});
|
|
1801
|
-
return match ?? null;
|
|
2070
|
+
const result = await dataSource.find(resource, preferenceQuery(userId, [key]));
|
|
2071
|
+
return matchPreferenceRow(result?.data ?? [], userId, key);
|
|
1802
2072
|
};
|
|
1803
|
-
const
|
|
1804
|
-
if (Array.isArray(value)) return value;
|
|
1805
|
-
if (typeof value === "string") {
|
|
1806
|
-
try {
|
|
1807
|
-
const parsed = JSON.parse(value);
|
|
1808
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1809
|
-
} catch {
|
|
1810
|
-
return [];
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
return [];
|
|
1814
|
-
};
|
|
1815
|
-
const upsert = async (items) => {
|
|
2073
|
+
const upsert = async (value) => {
|
|
1816
2074
|
if (cachedRowId !== null) {
|
|
1817
2075
|
try {
|
|
1818
|
-
await dataSource.update(resource, cachedRowId, { value
|
|
2076
|
+
await dataSource.update(resource, cachedRowId, { value });
|
|
1819
2077
|
return;
|
|
1820
2078
|
} catch (updateError) {
|
|
1821
2079
|
cachedRowId = null;
|
|
@@ -1825,14 +2083,14 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1825
2083
|
const existing = await findExisting();
|
|
1826
2084
|
if (existing && existing.id !== void 0 && existing.id !== null) {
|
|
1827
2085
|
cachedRowId = existing.id;
|
|
1828
|
-
await dataSource.update(resource, existing.id, { value
|
|
2086
|
+
await dataSource.update(resource, existing.id, { value });
|
|
1829
2087
|
return;
|
|
1830
2088
|
}
|
|
1831
2089
|
try {
|
|
1832
2090
|
const created = await dataSource.create(resource, {
|
|
1833
2091
|
user_id: userId,
|
|
1834
2092
|
key,
|
|
1835
|
-
value
|
|
2093
|
+
value
|
|
1836
2094
|
});
|
|
1837
2095
|
const newId = created?.id;
|
|
1838
2096
|
if (newId !== void 0 && newId !== null) cachedRowId = newId;
|
|
@@ -1840,21 +2098,74 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1840
2098
|
const recovered = await findExisting();
|
|
1841
2099
|
if (recovered && recovered.id !== void 0 && recovered.id !== null) {
|
|
1842
2100
|
cachedRowId = recovered.id;
|
|
1843
|
-
await dataSource.update(resource, recovered.id, { value
|
|
2101
|
+
await dataSource.update(resource, recovered.id, { value });
|
|
1844
2102
|
return;
|
|
1845
2103
|
}
|
|
1846
2104
|
throw createError;
|
|
1847
2105
|
}
|
|
1848
2106
|
};
|
|
2107
|
+
return {
|
|
2108
|
+
remember(id) {
|
|
2109
|
+
cachedRowId = id;
|
|
2110
|
+
},
|
|
2111
|
+
upsert,
|
|
2112
|
+
find: findExisting
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
function matchPreferenceRow(rows, userId, key) {
|
|
2116
|
+
const match = rows.find((r) => {
|
|
2117
|
+
if (!r) return false;
|
|
2118
|
+
const userMismatch = r.user_id !== void 0 && r.user_id !== userId;
|
|
2119
|
+
const keyMismatch = r.key !== void 0 && r.key !== key;
|
|
2120
|
+
return !userMismatch && !keyMismatch;
|
|
2121
|
+
});
|
|
2122
|
+
return match ?? null;
|
|
2123
|
+
}
|
|
2124
|
+
function createObjectStackUserStateAdapter(options) {
|
|
2125
|
+
const {
|
|
2126
|
+
dataSource,
|
|
2127
|
+
userId,
|
|
2128
|
+
key,
|
|
2129
|
+
resource = DEFAULT_RESOURCE,
|
|
2130
|
+
onError = () => {
|
|
2131
|
+
}
|
|
2132
|
+
} = options;
|
|
2133
|
+
const writer = createPreferenceRowWriter(dataSource, resource, userId, key, onError);
|
|
2134
|
+
let saveChain = Promise.resolve();
|
|
2135
|
+
const loadExisting = async () => {
|
|
2136
|
+
const read = joinSharedRead(dataSource, resource, userId, { key });
|
|
2137
|
+
const { rows, saturated } = await read.answer;
|
|
2138
|
+
if (read.keys.size === 1 && read.prefixes.size === 0) {
|
|
2139
|
+
return matchPreferenceRow(rows, userId, key);
|
|
2140
|
+
}
|
|
2141
|
+
const own = rows.find(
|
|
2142
|
+
(r) => r && r.key === key && (r.user_id === void 0 || r.user_id === userId)
|
|
2143
|
+
);
|
|
2144
|
+
if (own) return own;
|
|
2145
|
+
if (saturated || rows.some((r) => r && r.key === void 0)) return writer.find();
|
|
2146
|
+
return null;
|
|
2147
|
+
};
|
|
2148
|
+
const decodeValue = (value) => {
|
|
2149
|
+
if (Array.isArray(value)) return value;
|
|
2150
|
+
if (typeof value === "string") {
|
|
2151
|
+
try {
|
|
2152
|
+
const parsed = JSON.parse(value);
|
|
2153
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2154
|
+
} catch {
|
|
2155
|
+
return [];
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return [];
|
|
2159
|
+
};
|
|
1849
2160
|
return {
|
|
1850
2161
|
async load() {
|
|
1851
2162
|
try {
|
|
1852
|
-
const row = await
|
|
2163
|
+
const row = await loadExisting();
|
|
1853
2164
|
if (!row) {
|
|
1854
|
-
|
|
2165
|
+
writer.remember(null);
|
|
1855
2166
|
return [];
|
|
1856
2167
|
}
|
|
1857
|
-
if (row.id !== void 0 && row.id !== null)
|
|
2168
|
+
if (row.id !== void 0 && row.id !== null) writer.remember(row.id);
|
|
1858
2169
|
return decodeValue(row.value);
|
|
1859
2170
|
} catch (error) {
|
|
1860
2171
|
onError("load", error);
|
|
@@ -1862,7 +2173,7 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1862
2173
|
}
|
|
1863
2174
|
},
|
|
1864
2175
|
async save(items) {
|
|
1865
|
-
const run = saveChain.then(() => upsert(items)).catch((error) => {
|
|
2176
|
+
const run = saveChain.then(() => writer.upsert(items)).catch((error) => {
|
|
1866
2177
|
onError("save", error);
|
|
1867
2178
|
});
|
|
1868
2179
|
saveChain = run;
|
|
@@ -1870,6 +2181,69 @@ function createObjectStackUserStateAdapter(options) {
|
|
|
1870
2181
|
}
|
|
1871
2182
|
};
|
|
1872
2183
|
}
|
|
2184
|
+
function createObjectStackUserPreferenceFamily(options) {
|
|
2185
|
+
const {
|
|
2186
|
+
dataSource,
|
|
2187
|
+
userId,
|
|
2188
|
+
prefix,
|
|
2189
|
+
resource = DEFAULT_RESOURCE,
|
|
2190
|
+
onError = () => {
|
|
2191
|
+
}
|
|
2192
|
+
} = options;
|
|
2193
|
+
const writers = /* @__PURE__ */ new Map();
|
|
2194
|
+
const chains = /* @__PURE__ */ new Map();
|
|
2195
|
+
const writerFor = (key) => {
|
|
2196
|
+
let writer = writers.get(key);
|
|
2197
|
+
if (!writer) {
|
|
2198
|
+
writer = createPreferenceRowWriter(dataSource, resource, userId, key, onError);
|
|
2199
|
+
writers.set(key, writer);
|
|
2200
|
+
}
|
|
2201
|
+
return writer;
|
|
2202
|
+
};
|
|
2203
|
+
const decodeValue = (value) => {
|
|
2204
|
+
let parsed = value;
|
|
2205
|
+
if (typeof value === "string") {
|
|
2206
|
+
try {
|
|
2207
|
+
parsed = JSON.parse(value);
|
|
2208
|
+
} catch {
|
|
2209
|
+
return void 0;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
2213
|
+
};
|
|
2214
|
+
return {
|
|
2215
|
+
prefix,
|
|
2216
|
+
async load() {
|
|
2217
|
+
try {
|
|
2218
|
+
const read = joinSharedRead(dataSource, resource, userId, { prefix });
|
|
2219
|
+
const { rows } = await read.answer;
|
|
2220
|
+
const out = {};
|
|
2221
|
+
for (const row of rows) {
|
|
2222
|
+
if (!row || typeof row.key !== "string" || !row.key.startsWith(prefix)) continue;
|
|
2223
|
+
if (row.user_id !== void 0 && row.user_id !== userId) continue;
|
|
2224
|
+
if (row.id !== void 0 && row.id !== null) writerFor(row.key).remember(row.id);
|
|
2225
|
+
const value = decodeValue(row.value);
|
|
2226
|
+
if (value !== void 0) out[row.key] = value;
|
|
2227
|
+
}
|
|
2228
|
+
return out;
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
onError("load", error);
|
|
2231
|
+
return {};
|
|
2232
|
+
}
|
|
2233
|
+
},
|
|
2234
|
+
async save(key, value) {
|
|
2235
|
+
if (!key.startsWith(prefix) || key.length === prefix.length) {
|
|
2236
|
+
onError("save", new Error(`key "${key}" is not in the family "${prefix}"`));
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const run = (chains.get(key) ?? Promise.resolve()).then(() => writerFor(key).upsert(value)).catch((error) => {
|
|
2240
|
+
onError("save", error);
|
|
2241
|
+
});
|
|
2242
|
+
chains.set(key, run);
|
|
2243
|
+
return run;
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
1873
2247
|
|
|
1874
2248
|
// src/draft-envelope.ts
|
|
1875
2249
|
var import_kernel = require("@objectstack/spec/kernel");
|
|
@@ -1882,20 +2256,61 @@ function extractDraftBody(resp) {
|
|
|
1882
2256
|
if (Object.keys(body).length === 0) return null;
|
|
1883
2257
|
return (0, import_kernel.stripReadDecorations)(body);
|
|
1884
2258
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
return Array.isArray(issues) ? issues : [];
|
|
2259
|
+
function readDraftChanges(resp) {
|
|
2260
|
+
if (!resp || typeof resp !== "object") return null;
|
|
2261
|
+
const changes = resp.draftChanges;
|
|
2262
|
+
return Array.isArray(changes) ? changes : null;
|
|
1890
2263
|
}
|
|
1891
|
-
function
|
|
1892
|
-
|
|
2264
|
+
function isPathPrefix(prefix, path) {
|
|
2265
|
+
if (prefix.length > path.length) return false;
|
|
2266
|
+
for (let i = 0; i < prefix.length; i++) if (prefix[i] !== path[i]) return false;
|
|
2267
|
+
return true;
|
|
1893
2268
|
}
|
|
1894
|
-
function
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2269
|
+
function governingDraftChange(changes, path) {
|
|
2270
|
+
let best = null;
|
|
2271
|
+
for (const change of changes ?? []) {
|
|
2272
|
+
if (!isPathPrefix(change.path, path)) continue;
|
|
2273
|
+
if (!best || change.path.length > best.path.length) best = change;
|
|
2274
|
+
}
|
|
2275
|
+
return best;
|
|
2276
|
+
}
|
|
2277
|
+
function draftChangesUnder(changes, path) {
|
|
2278
|
+
return (changes ?? []).filter((change) => change.path.length > path.length && isPathPrefix(path, change.path));
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// src/picklist-binding.ts
|
|
2282
|
+
function dropServedPicklistOptions(body) {
|
|
2283
|
+
if (!body || typeof body !== "object") return body;
|
|
2284
|
+
const fields = body.fields;
|
|
2285
|
+
if (Array.isArray(fields)) {
|
|
2286
|
+
let changed = false;
|
|
2287
|
+
const next = fields.map((def) => {
|
|
2288
|
+
const stripped = withoutServedOptions(def);
|
|
2289
|
+
if (stripped !== def) changed = true;
|
|
2290
|
+
return stripped;
|
|
2291
|
+
});
|
|
2292
|
+
return changed ? { ...body, fields: next } : body;
|
|
2293
|
+
}
|
|
2294
|
+
if (fields && typeof fields === "object") {
|
|
2295
|
+
let changed = false;
|
|
2296
|
+
const next = Object.fromEntries(
|
|
2297
|
+
Object.entries(fields).map(([name, def]) => {
|
|
2298
|
+
const stripped = withoutServedOptions(def);
|
|
2299
|
+
if (stripped !== def) changed = true;
|
|
2300
|
+
return [name, stripped];
|
|
2301
|
+
})
|
|
2302
|
+
);
|
|
2303
|
+
return changed ? { ...body, fields: next } : body;
|
|
2304
|
+
}
|
|
2305
|
+
return body;
|
|
2306
|
+
}
|
|
2307
|
+
function withoutServedOptions(def) {
|
|
2308
|
+
if (!def || typeof def !== "object" || Array.isArray(def)) return def;
|
|
2309
|
+
const record = def;
|
|
2310
|
+
if (record.picklist === void 0) return def;
|
|
2311
|
+
if (!Object.prototype.hasOwnProperty.call(record, "options")) return def;
|
|
2312
|
+
const { options: _served, ...authored } = record;
|
|
2313
|
+
return authored;
|
|
1899
2314
|
}
|
|
1900
2315
|
|
|
1901
2316
|
// src/index.ts
|
|
@@ -2173,6 +2588,7 @@ function serializeOrderBy(orderby) {
|
|
|
2173
2588
|
}
|
|
2174
2589
|
return void 0;
|
|
2175
2590
|
}
|
|
2591
|
+
var VIEW_LIST_KEY = "list:view";
|
|
2176
2592
|
var discoveryCache = /* @__PURE__ */ new Map();
|
|
2177
2593
|
async function getSharedDiscovery(baseUrl, fetcher) {
|
|
2178
2594
|
const key = baseUrl || "<default>";
|
|
@@ -2226,29 +2642,6 @@ function metaEnvelopeCarriesItem(envelope) {
|
|
|
2226
2642
|
if (envelope == null || typeof envelope !== "object") return false;
|
|
2227
2643
|
return envelope.item != null;
|
|
2228
2644
|
}
|
|
2229
|
-
var VIEW_CONFIG_CAPABILITY = "manage_metadata";
|
|
2230
|
-
var ViewConfigPermissionDeniedError = class extends Error {
|
|
2231
|
-
constructor(objectName, viewId) {
|
|
2232
|
-
super(
|
|
2233
|
-
`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.`
|
|
2234
|
-
);
|
|
2235
|
-
__publicField(this, "code", "VIEW_CONFIG_PERMISSION_DENIED");
|
|
2236
|
-
/** The capability that was required and not held. */
|
|
2237
|
-
__publicField(this, "capability", VIEW_CONFIG_CAPABILITY);
|
|
2238
|
-
/** The object whose view configuration the refused write targeted. */
|
|
2239
|
-
__publicField(this, "objectName");
|
|
2240
|
-
/** The view id the refused write targeted. */
|
|
2241
|
-
__publicField(this, "viewId");
|
|
2242
|
-
this.name = "ViewConfigPermissionDeniedError";
|
|
2243
|
-
this.objectName = objectName;
|
|
2244
|
-
this.viewId = viewId;
|
|
2245
|
-
}
|
|
2246
|
-
};
|
|
2247
|
-
function isViewConfigPermissionDeniedError(error) {
|
|
2248
|
-
if (!error || typeof error !== "object") return false;
|
|
2249
|
-
const e = error;
|
|
2250
|
-
return e.code === "VIEW_CONFIG_PERMISSION_DENIED" || e.name === "ViewConfigPermissionDeniedError";
|
|
2251
|
-
}
|
|
2252
2645
|
var AnalyticsNotInstalledError = class extends Error {
|
|
2253
2646
|
constructor(surface, detail, serverCode) {
|
|
2254
2647
|
super(
|
|
@@ -2614,6 +3007,16 @@ function sameWireValue(a, b) {
|
|
|
2614
3007
|
if (aEmpty || bEmpty) return aEmpty && bEmpty;
|
|
2615
3008
|
return stableStringify(a) === stableStringify(b);
|
|
2616
3009
|
}
|
|
3010
|
+
function readWriteWarnings(result) {
|
|
3011
|
+
const raw = result?.warnings;
|
|
3012
|
+
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
3013
|
+
const out = [];
|
|
3014
|
+
for (const entry of raw) {
|
|
3015
|
+
const parsed = import_api3.ValidateDataIssueSchema.safeParse(entry);
|
|
3016
|
+
if (parsed.success) out.push(parsed.data);
|
|
3017
|
+
}
|
|
3018
|
+
return out;
|
|
3019
|
+
}
|
|
2617
3020
|
function withoutNoOpDrops(droppedFields, sent, stored) {
|
|
2618
3021
|
if (!sent || !stored || typeof sent !== "object" || typeof stored !== "object") {
|
|
2619
3022
|
return droppedFields;
|
|
@@ -2694,6 +3097,10 @@ function mergeViewPatch(current, partial, viewName, objectName) {
|
|
|
2694
3097
|
object: current?.object || current?.data?.object || objectName
|
|
2695
3098
|
};
|
|
2696
3099
|
}
|
|
3100
|
+
function isCallerAbort(error, init) {
|
|
3101
|
+
if (init?.signal?.aborted) return true;
|
|
3102
|
+
return error?.name === "AbortError";
|
|
3103
|
+
}
|
|
2697
3104
|
var ObjectStackAdapter = class {
|
|
2698
3105
|
constructor(config) {
|
|
2699
3106
|
__publicField(this, "client");
|
|
@@ -2717,6 +3124,13 @@ var ObjectStackAdapter = class {
|
|
|
2717
3124
|
// multiple sibling components requesting the same dataset on first paint)
|
|
2718
3125
|
// into a single network round trip.
|
|
2719
3126
|
__publicField(this, "inflightFinds", /* @__PURE__ */ new Map());
|
|
3127
|
+
// In-flight findOne() reads keyed by resource + record id + serialized
|
|
3128
|
+
// params, shared the same way (objectui#11699). Opening one record page
|
|
3129
|
+
// asked for the same record twice at once: `record:details`' DetailView
|
|
3130
|
+
// re-runs its load effect while its first read is still on the wire. A
|
|
3131
|
+
// write to a resource drops that resource's entries (see `emitMutation`),
|
|
3132
|
+
// so a read asked after a save is never answered by one sent before it.
|
|
3133
|
+
__publicField(this, "inflightFindOnes", /* @__PURE__ */ new Map());
|
|
2720
3134
|
// Resources that have responded 404 at least once (collection not installed
|
|
2721
3135
|
// on this backend). Subsequent find() calls short-circuit to an empty result
|
|
2722
3136
|
// so optional collections like sys_presence don't hammer the server with
|
|
@@ -2759,68 +3173,78 @@ var ObjectStackAdapter = class {
|
|
|
2759
3173
|
// the opposite direction — those two describe a write that succeeded, this
|
|
2760
3174
|
// one a read that did not happen.
|
|
2761
3175
|
__publicField(this, "metadataReadWarningListeners", /* @__PURE__ */ new Set());
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
3176
|
+
const transport = config.fetch || globalThis.fetch.bind(globalThis);
|
|
3177
|
+
this.fetchImpl = (input, init) => this.observeServerAnswer(() => transport(input, init), init);
|
|
3178
|
+
this.client = new import_client.ObjectStackClient({
|
|
3179
|
+
...config,
|
|
3180
|
+
fetch: this.fetchImpl,
|
|
3181
|
+
logger: createQuietHttpLogger()
|
|
3182
|
+
});
|
|
2768
3183
|
this.installSaveAdvisoryInterceptor();
|
|
2769
3184
|
this.metadataCache = new MetadataCache(config.cache);
|
|
3185
|
+
this.installViewListShare();
|
|
2770
3186
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
2771
3187
|
this.maxReconnectAttempts = config.maxReconnectAttempts ?? 3;
|
|
2772
3188
|
this.reconnectDelay = config.reconnectDelay ?? 1e3;
|
|
2773
3189
|
this.baseUrl = config.baseUrl;
|
|
2774
3190
|
this.token = config.token;
|
|
2775
|
-
this.systemCapabilities = config.systemCapabilities;
|
|
2776
|
-
this.fetchImpl = config.fetch || globalThis.fetch.bind(globalThis);
|
|
2777
3191
|
}
|
|
2778
3192
|
/**
|
|
2779
3193
|
* Ensure the client is connected to the server.
|
|
2780
3194
|
* Call this before making requests or it will auto-connect on first request.
|
|
3195
|
+
*
|
|
3196
|
+
* With `autoReconnect`, a failed discovery read is retried up to
|
|
3197
|
+
* `maxReconnectAttempts` times with exponential backoff. The promise resolves
|
|
3198
|
+
* once discovery answers and rejects with the last `ConnectionError` once the
|
|
3199
|
+
* retries are spent (objectui#12318).
|
|
2781
3200
|
*/
|
|
2782
3201
|
async connect() {
|
|
2783
3202
|
if (this.connected) return;
|
|
2784
3203
|
if (this.connectPromise) return this.connectPromise;
|
|
2785
3204
|
this.setConnectionState("connecting");
|
|
2786
|
-
this.connectPromise = (
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3205
|
+
this.connectPromise = this.attemptConnect().finally(() => {
|
|
3206
|
+
this.connectPromise = null;
|
|
3207
|
+
});
|
|
3208
|
+
return this.connectPromise;
|
|
3209
|
+
}
|
|
3210
|
+
/**
|
|
3211
|
+
* One discovery attempt, and the retry that follows it when it fails with
|
|
3212
|
+
* budget left. Runs inside the promise {@link connect} hands every caller.
|
|
3213
|
+
*/
|
|
3214
|
+
async attemptConnect() {
|
|
3215
|
+
try {
|
|
3216
|
+
const baseUrl = this.baseUrl || "";
|
|
3217
|
+
const discoveryUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}/api/v1/discovery` : "/api/v1/discovery";
|
|
3218
|
+
const data = await getSharedDiscovery(baseUrl, async () => {
|
|
3219
|
+
const res = await this.fetchImpl(discoveryUrl, {
|
|
3220
|
+
method: "GET",
|
|
3221
|
+
headers: this.token ? { Authorization: `Bearer ${this.token}` } : void 0
|
|
2800
3222
|
});
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
this.connected = true;
|
|
2804
|
-
this.reconnectAttempts = 0;
|
|
2805
|
-
this.setConnectionState("connected");
|
|
2806
|
-
} catch (error) {
|
|
2807
|
-
const errorMessage = error instanceof Error ? error.message : "Failed to connect to ObjectStack server";
|
|
2808
|
-
const connectionError = new ConnectionError(
|
|
2809
|
-
errorMessage,
|
|
2810
|
-
void 0,
|
|
2811
|
-
{ originalError: error }
|
|
2812
|
-
);
|
|
2813
|
-
this.setConnectionState("error", connectionError);
|
|
2814
|
-
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
2815
|
-
await this.attemptReconnect();
|
|
2816
|
-
} else {
|
|
2817
|
-
throw connectionError;
|
|
3223
|
+
if (!res.ok) {
|
|
3224
|
+
throw new Error(`discovery ${res.status} ${res.statusText}`);
|
|
2818
3225
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
3226
|
+
const body = await res.json();
|
|
3227
|
+
return body && typeof body.success === "boolean" && "data" in body ? body.data : body;
|
|
3228
|
+
});
|
|
3229
|
+
this.client.discoveryInfo = data;
|
|
3230
|
+
this.atomicBatchCapability = readTransactionalBatchCapability(data);
|
|
3231
|
+
this.connected = true;
|
|
3232
|
+
this.reconnectAttempts = 0;
|
|
3233
|
+
this.setConnectionState("connected");
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to connect to ObjectStack server";
|
|
3236
|
+
const connectionError = new ConnectionError(
|
|
3237
|
+
errorMessage,
|
|
3238
|
+
void 0,
|
|
3239
|
+
{ originalError: error }
|
|
3240
|
+
);
|
|
3241
|
+
this.setConnectionState("error", connectionError);
|
|
3242
|
+
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
3243
|
+
await this.attemptReconnect();
|
|
3244
|
+
} else {
|
|
3245
|
+
throw connectionError;
|
|
2821
3246
|
}
|
|
2822
|
-
}
|
|
2823
|
-
return this.connectPromise;
|
|
3247
|
+
}
|
|
2824
3248
|
}
|
|
2825
3249
|
/**
|
|
2826
3250
|
* Attempt to reconnect to the server with exponential backoff
|
|
@@ -2830,8 +3254,7 @@ var ObjectStackAdapter = class {
|
|
|
2830
3254
|
this.setConnectionState("reconnecting");
|
|
2831
3255
|
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
2832
3256
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2833
|
-
this.
|
|
2834
|
-
await this.connect();
|
|
3257
|
+
await this.attemptConnect();
|
|
2835
3258
|
}
|
|
2836
3259
|
/**
|
|
2837
3260
|
* Get the current connection state
|
|
@@ -2887,6 +3310,63 @@ var ObjectStackAdapter = class {
|
|
|
2887
3310
|
}
|
|
2888
3311
|
});
|
|
2889
3312
|
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Run one request through the adapter's fetch and let its outcome move the
|
|
3315
|
+
* connection state (objectui#12295). Before the first successful
|
|
3316
|
+
* {@link connect} this records nothing: `connect()` owns the state until
|
|
3317
|
+
* then, and its own discovery read passes through here too.
|
|
3318
|
+
*
|
|
3319
|
+
* - **Answered** — the fetch resolved with a Response, whatever its status.
|
|
3320
|
+
* A 4xx or 5xx is an answer: the server is reachable, so it counts the
|
|
3321
|
+
* same as a 200 and ends any outage.
|
|
3322
|
+
* - **Not answered** — the fetch rejected: no HTTP answer at all (refused,
|
|
3323
|
+
* reset, DNS, an offline browser). A rejection the caller asked for by
|
|
3324
|
+
* aborting is not evidence about the server and records nothing.
|
|
3325
|
+
*
|
|
3326
|
+
* The rejection or the Response reaches the caller unchanged.
|
|
3327
|
+
*/
|
|
3328
|
+
async observeServerAnswer(send, init) {
|
|
3329
|
+
let response;
|
|
3330
|
+
try {
|
|
3331
|
+
response = await send();
|
|
3332
|
+
} catch (error) {
|
|
3333
|
+
if (!isCallerAbort(error, init)) this.noteServerUnanswered(error);
|
|
3334
|
+
throw error;
|
|
3335
|
+
}
|
|
3336
|
+
this.noteServerAnswered();
|
|
3337
|
+
return response;
|
|
3338
|
+
}
|
|
3339
|
+
/** An answered request after the first connect: the server is reachable. */
|
|
3340
|
+
noteServerAnswered() {
|
|
3341
|
+
if (!this.connected) return;
|
|
3342
|
+
this.reconnectAttempts = 0;
|
|
3343
|
+
if (this.connectionState !== "connected") this.setConnectionState("connected");
|
|
3344
|
+
}
|
|
3345
|
+
/**
|
|
3346
|
+
* An unanswered request after the first connect. It spends the adapter's own
|
|
3347
|
+
* retry window — the `autoReconnect` / `maxReconnectAttempts` budget that
|
|
3348
|
+
* {@link connect} grants a failed discovery — counted in the requests the
|
|
3349
|
+
* host already makes rather than in timed probes: while the budget lasts the
|
|
3350
|
+
* state is `reconnecting`, and the first unanswered request past it moves the
|
|
3351
|
+
* state to `error`. No timer and no request of the adapter's own is involved;
|
|
3352
|
+
* the next answered request, whenever it comes, restores `connected`.
|
|
3353
|
+
*/
|
|
3354
|
+
noteServerUnanswered(error) {
|
|
3355
|
+
if (!this.connected || this.connectionState === "error") return;
|
|
3356
|
+
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
3357
|
+
this.reconnectAttempts++;
|
|
3358
|
+
if (this.connectionState !== "reconnecting") this.setConnectionState("reconnecting");
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
this.setConnectionState(
|
|
3362
|
+
"error",
|
|
3363
|
+
new ConnectionError(
|
|
3364
|
+
error instanceof Error ? error.message : "ObjectStack server did not answer",
|
|
3365
|
+
void 0,
|
|
3366
|
+
{ originalError: error }
|
|
3367
|
+
)
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
2890
3370
|
/**
|
|
2891
3371
|
* Emit batch progress event to listeners
|
|
2892
3372
|
*/
|
|
@@ -2908,9 +3388,7 @@ var ObjectStackAdapter = class {
|
|
|
2908
3388
|
return { data: [], total: 0 };
|
|
2909
3389
|
}
|
|
2910
3390
|
const key = `${resource}::${stableStringify(params)}`;
|
|
2911
|
-
|
|
2912
|
-
if (existing) return existing;
|
|
2913
|
-
const promise = (async () => {
|
|
3391
|
+
return this.shareInFlight(this.inflightFinds, key, async () => {
|
|
2914
3392
|
await this.connect();
|
|
2915
3393
|
if (params?.$expand && params.$expand.length > 0 || params?.$search != null && String(params.$search).trim() !== "") {
|
|
2916
3394
|
const result = await this.rawFindWithPopulate(resource, params);
|
|
@@ -2927,11 +3405,25 @@ var ObjectStackAdapter = class {
|
|
|
2927
3405
|
}
|
|
2928
3406
|
throw err;
|
|
2929
3407
|
}
|
|
2930
|
-
})
|
|
2931
|
-
|
|
3408
|
+
});
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Share one in-flight read among concurrent callers asking the same
|
|
3412
|
+
* question: the first call for `key` issues `read()`, every call that
|
|
3413
|
+
* arrives while it is still pending gets that same promise, and the entry is
|
|
3414
|
+
* dropped the moment it settles — so nothing is cached past the round trip,
|
|
3415
|
+
* a rejection reaches every caller that shared it and is not remembered, and
|
|
3416
|
+
* the next call after settle issues a fresh read. Used by `find` and
|
|
3417
|
+
* `findOne` (objectui#11699).
|
|
3418
|
+
*/
|
|
3419
|
+
shareInFlight(inflight, key, read) {
|
|
3420
|
+
const existing = inflight.get(key);
|
|
3421
|
+
if (existing) return existing;
|
|
3422
|
+
const promise = read();
|
|
3423
|
+
inflight.set(key, promise);
|
|
2932
3424
|
const cleanup = () => {
|
|
2933
|
-
if (
|
|
2934
|
-
|
|
3425
|
+
if (inflight.get(key) === promise) {
|
|
3426
|
+
inflight.delete(key);
|
|
2935
3427
|
}
|
|
2936
3428
|
};
|
|
2937
3429
|
promise.then(cleanup, cleanup);
|
|
@@ -3010,8 +3502,16 @@ var ObjectStackAdapter = class {
|
|
|
3010
3502
|
}
|
|
3011
3503
|
/**
|
|
3012
3504
|
* Find a single record by ID.
|
|
3505
|
+
*
|
|
3506
|
+
* Concurrent calls for the same resource, id and params share one request,
|
|
3507
|
+
* exactly as `find` does (objectui#11699); see {@link shareInFlight}.
|
|
3013
3508
|
*/
|
|
3014
3509
|
async findOne(resource, id, params) {
|
|
3510
|
+
const key = `${resource}::${JSON.stringify(String(id))}::${stableStringify(params)}`;
|
|
3511
|
+
return this.shareInFlight(this.inflightFindOnes, key, () => this.readOne(resource, id, params));
|
|
3512
|
+
}
|
|
3513
|
+
/** The one round trip behind {@link findOne}, unshared. */
|
|
3514
|
+
async readOne(resource, id, params) {
|
|
3015
3515
|
await this.connect();
|
|
3016
3516
|
if (params?.$expand && params.$expand.length > 0) {
|
|
3017
3517
|
try {
|
|
@@ -3051,6 +3551,10 @@ var ObjectStackAdapter = class {
|
|
|
3051
3551
|
* mutation or starve the other subscribers, so each is isolated.
|
|
3052
3552
|
*/
|
|
3053
3553
|
emitMutation(event) {
|
|
3554
|
+
const prefix = `${event.resource}::`;
|
|
3555
|
+
for (const key of Array.from(this.inflightFindOnes.keys())) {
|
|
3556
|
+
if (key.startsWith(prefix)) this.inflightFindOnes.delete(key);
|
|
3557
|
+
}
|
|
3054
3558
|
for (const listener of this.mutationListeners) {
|
|
3055
3559
|
try {
|
|
3056
3560
|
listener(event);
|
|
@@ -3095,15 +3599,28 @@ var ObjectStackAdapter = class {
|
|
|
3095
3599
|
* reason routed to the skew arm, a non-string field element refused, a missing
|
|
3096
3600
|
* `object` healed from `resource` — never asserted into the union, and the
|
|
3097
3601
|
* entry itself never dropped for them (objectui#4934, `f75810e7c`).
|
|
3602
|
+
*
|
|
3603
|
+
* The same answer's `warnings` (objectui#12107) rides this one event: it is
|
|
3604
|
+
* emitted when either list is non-empty, so a write whose only news is an
|
|
3605
|
+
* advisory rule hit still reaches the subscriber, with `droppedFields: []`.
|
|
3098
3606
|
*/
|
|
3099
3607
|
notifyDroppedFields(operation, resource, result, id, sent) {
|
|
3100
3608
|
const dropped = result?.droppedFields;
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3609
|
+
let droppedFields = [];
|
|
3610
|
+
if (Array.isArray(dropped) && dropped.length > 0) {
|
|
3611
|
+
const valid = dropped.filter(isWireDroppedFieldsEntry).map((e) => asDroppedFieldsNotice(e, resource));
|
|
3612
|
+
const stored = result?.record;
|
|
3613
|
+
droppedFields = withoutNoOpDrops(valid, sent, stored);
|
|
3614
|
+
}
|
|
3615
|
+
const warnings = readWriteWarnings(result);
|
|
3616
|
+
if (droppedFields.length === 0 && warnings.length === 0) return;
|
|
3617
|
+
this.emitWriteWarning({
|
|
3618
|
+
operation,
|
|
3619
|
+
resource,
|
|
3620
|
+
...id !== void 0 ? { id } : {},
|
|
3621
|
+
droppedFields,
|
|
3622
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
3623
|
+
});
|
|
3107
3624
|
}
|
|
3108
3625
|
/**
|
|
3109
3626
|
* Same, for the cross-object transactional batch (objectstack-ai/objectstack#3794). Its
|
|
@@ -3241,8 +3758,8 @@ var ObjectStackAdapter = class {
|
|
|
3241
3758
|
*
|
|
3242
3759
|
* `ObjectStackClient.meta.saveItem` is the second client class that writes
|
|
3243
3760
|
* through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches
|
|
3244
|
-
* it through an adapter this class constructed — the
|
|
3245
|
-
* (
|
|
3761
|
+
* it through an adapter this class constructed — the three inside this file
|
|
3762
|
+
* (the two view paths, `updateDashboard`) via
|
|
3246
3763
|
* `this.client`, and every caller outside it via {@link getClient}, which
|
|
3247
3764
|
* hands back this same instance: `MetadataService` (app-shell, five saves),
|
|
3248
3765
|
* `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the
|
|
@@ -3271,20 +3788,30 @@ var ObjectStackAdapter = class {
|
|
|
3271
3788
|
*
|
|
3272
3789
|
* Drafts are NEVER gated: the framework returns at its D1 early-return
|
|
3273
3790
|
* (`if (args.state !== 'active') return null`) before running a rule, so a
|
|
3274
|
-
* draft save produces no findings to withhold.
|
|
3275
|
-
*
|
|
3276
|
-
*
|
|
3277
|
-
*
|
|
3278
|
-
* the
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3791
|
+
* draft save produces no findings to withhold. `mode` on the emitted event is
|
|
3792
|
+
* derived from the response's own `state`, not from the caller's options:
|
|
3793
|
+
* `'draft'` when the server says the row landed as a draft, `'publish'`
|
|
3794
|
+
* otherwise. The server's answer is the one that says which door the write
|
|
3795
|
+
* took, so the event cannot claim a door the request only asked for.
|
|
3796
|
+
*
|
|
3797
|
+
* ## Every argument is forwarded (objectui#12300)
|
|
3798
|
+
*
|
|
3799
|
+
* The SDK's `saveItem(type, name, item, options?)` carries the write's scope
|
|
3800
|
+
* in its fourth argument, `SaveMetaItemOptions`: `packageId` (`?package=`),
|
|
3801
|
+
* `force`, `mode` and `ifMatch`. This wrapper used to name three parameters
|
|
3802
|
+
* and call the original with three, so that bag was dropped before the SDK
|
|
3803
|
+
* saw it. Every caller reaching the SDK through {@link getClient} got a bare
|
|
3804
|
+
* `PUT` whatever it passed, while the declared signature still accepted the
|
|
3805
|
+
* bag, so nothing at the call site could tell. The wrapper now takes and
|
|
3806
|
+
* passes the original's own parameter list, so it forwards whatever the SDK
|
|
3807
|
+
* declares, today's bag and any later member, without naming one.
|
|
3282
3808
|
*/
|
|
3283
3809
|
installSaveAdvisoryInterceptor() {
|
|
3284
3810
|
const meta = this.client.meta;
|
|
3285
3811
|
const original = meta.saveItem.bind(meta);
|
|
3286
|
-
meta.saveItem = async (
|
|
3287
|
-
const
|
|
3812
|
+
meta.saveItem = async (...args) => {
|
|
3813
|
+
const [type, name] = args;
|
|
3814
|
+
const result = await original(...args);
|
|
3288
3815
|
try {
|
|
3289
3816
|
const advisories = readSaveAdvisories(result);
|
|
3290
3817
|
if (advisories.length > 0) {
|
|
@@ -3307,6 +3834,61 @@ var ObjectStackAdapter = class {
|
|
|
3307
3834
|
return result;
|
|
3308
3835
|
};
|
|
3309
3836
|
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Route this adapter's client's published view-list read through the one
|
|
3839
|
+
* {@link VIEW_LIST_KEY} (objectui#13044).
|
|
3840
|
+
*
|
|
3841
|
+
* The app-shell `MetadataProvider` reads `client.meta.getItems('view')`
|
|
3842
|
+
* through {@link getClient}; {@link listViews} and {@link listViewOverrides}
|
|
3843
|
+
* read the same `GET /api/v1/meta/view`. Each used to put its own request on
|
|
3844
|
+
* the wire, so one list-page open asked the server three times for one URL.
|
|
3845
|
+
*
|
|
3846
|
+
* A caller of the client keeps its contract, "ask the server": it is never
|
|
3847
|
+
* answered from a settled entry. It joins a request already in flight for
|
|
3848
|
+
* the same URL instead of sending a second one, and its answer is stored
|
|
3849
|
+
* under {@link VIEW_LIST_KEY} (see {@link MetadataCache.share}), which is
|
|
3850
|
+
* what {@link readViewList} answers this adapter's own readers from
|
|
3851
|
+
* (objectui#13057). Only the bare call is routed: any argument after the
|
|
3852
|
+
* type changes the URL, so such a call goes straight to the SDK.
|
|
3853
|
+
*
|
|
3854
|
+
* Every caller gets its own copy of the answer. Before this, every reader
|
|
3855
|
+
* got its own parsed response, so a reader that changed what it was handed
|
|
3856
|
+
* changed nothing anyone else read; a shared answer must not change that.
|
|
3857
|
+
*
|
|
3858
|
+
* Same wrap shape and the same bound as `installSaveAdvisoryInterceptor`:
|
|
3859
|
+
* `meta` is an own property of a client this adapter built and never shares.
|
|
3860
|
+
*/
|
|
3861
|
+
installViewListShare() {
|
|
3862
|
+
const meta = this.client.meta;
|
|
3863
|
+
const original = meta.getItems.bind(meta);
|
|
3864
|
+
meta.getItems = ((...args) => {
|
|
3865
|
+
if (args[0] !== "view" || args.length > 1) return original(...args);
|
|
3866
|
+
return this.metadataCache.share(VIEW_LIST_KEY, () => original(...args)).then((answer) => structuredClone(answer));
|
|
3867
|
+
});
|
|
3868
|
+
for (const door of ["saveItem", "deleteItem", "publishItem", "rollbackItem"]) {
|
|
3869
|
+
const write = meta[door];
|
|
3870
|
+
if (typeof write !== "function") continue;
|
|
3871
|
+
const bound = write.bind(meta);
|
|
3872
|
+
meta[door] = (...args) => {
|
|
3873
|
+
if (args[0] !== "view") return bound(...args);
|
|
3874
|
+
return bound(...args).finally(() => this.metadataCache.invalidate?.(VIEW_LIST_KEY));
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* The published view list for this adapter's own readers
|
|
3880
|
+
* ({@link listViews}, {@link listViewOverrides}): the answer stored under
|
|
3881
|
+
* {@link VIEW_LIST_KEY} while it holds one (objectui#13057), else the
|
|
3882
|
+
* client's read, which joins the request in flight for that key or starts
|
|
3883
|
+
* one, and stores its answer (objectui#13044). Always a copy the caller may
|
|
3884
|
+
* keep. Every path that should see a fresh list drops the key first; the
|
|
3885
|
+
* list is in {@link VIEW_LIST_KEY}'s doc.
|
|
3886
|
+
*/
|
|
3887
|
+
readViewList() {
|
|
3888
|
+
const settled = this.metadataCache.getCachedSync(VIEW_LIST_KEY);
|
|
3889
|
+
if (settled !== void 0) return Promise.resolve(structuredClone(settled));
|
|
3890
|
+
return this.client.meta.getItems("view");
|
|
3891
|
+
}
|
|
3310
3892
|
async create(resource, data) {
|
|
3311
3893
|
await this.connect();
|
|
3312
3894
|
try {
|
|
@@ -4064,7 +4646,6 @@ var ObjectStackAdapter = class {
|
|
|
4064
4646
|
() => this.fetchObjectSchemaFresh(objectName)
|
|
4065
4647
|
);
|
|
4066
4648
|
normalizeSchemaReferenceKeys(schema);
|
|
4067
|
-
this.applyFieldWidgetOverrides(objectName, schema);
|
|
4068
4649
|
return schema;
|
|
4069
4650
|
} catch (error) {
|
|
4070
4651
|
const errorObj = error;
|
|
@@ -4077,44 +4658,6 @@ var ObjectStackAdapter = class {
|
|
|
4077
4658
|
throw createErrorFromResponse(errorObj, `getObjectSchema(${objectName})`);
|
|
4078
4659
|
}
|
|
4079
4660
|
}
|
|
4080
|
-
/**
|
|
4081
|
-
* ADR-0056 P2 (epic #2398) — stamp structured-widget hints onto platform
|
|
4082
|
-
* fields whose framework type is a storage primitive (e.g. `textarea`) but
|
|
4083
|
-
* whose authoring UX should be a structured editor. Only the render `widget`
|
|
4084
|
-
* is added; the field's `type` (the storage contract) is untouched. Applied
|
|
4085
|
-
* idempotently to the cached schema so form + detail both honor it. Widget
|
|
4086
|
-
* components are registered as `field:<widget>` in `@object-ui/fields`.
|
|
4087
|
-
*/
|
|
4088
|
-
applyFieldWidgetOverrides(objectName, schema) {
|
|
4089
|
-
const OVERRIDES = {
|
|
4090
|
-
// ADR-0056 pure model — a permission set's six authorization facets are
|
|
4091
|
-
// *designed* in Studio's structured editors and only *assigned* (to
|
|
4092
|
-
// users) in Setup. In Setup they render read-only as a summary + a
|
|
4093
|
-
// "Design in Studio →" deep-link (the `permission-facet-link` widget),
|
|
4094
|
-
// never as raw [Object]/JSON. The capability *editor* itself lives in
|
|
4095
|
-
// Studio (epic #2398 P2).
|
|
4096
|
-
sys_permission_set: {
|
|
4097
|
-
object_permissions: "permission-facet-link",
|
|
4098
|
-
field_permissions: "permission-facet-link",
|
|
4099
|
-
system_permissions: "permission-facet-link",
|
|
4100
|
-
row_level_security: "permission-facet-link",
|
|
4101
|
-
tab_permissions: "permission-facet-link",
|
|
4102
|
-
admin_scope: "permission-facet-link"
|
|
4103
|
-
}
|
|
4104
|
-
};
|
|
4105
|
-
const overrides = OVERRIDES[objectName];
|
|
4106
|
-
const fields = schema && typeof schema === "object" ? schema.fields : null;
|
|
4107
|
-
if (!overrides || !fields) return;
|
|
4108
|
-
for (const [fname, widget] of Object.entries(overrides)) {
|
|
4109
|
-
if (Array.isArray(fields)) {
|
|
4110
|
-
const f = fields.find((x) => x?.name === fname);
|
|
4111
|
-
if (f && !f.widget) f.widget = widget;
|
|
4112
|
-
} else {
|
|
4113
|
-
const f = fields[fname];
|
|
4114
|
-
if (f && !f.widget) f.widget = widget;
|
|
4115
|
-
}
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
4661
|
/**
|
|
4119
4662
|
* Fetch a single object's schema while always revalidating the browser cache.
|
|
4120
4663
|
*
|
|
@@ -4189,6 +4732,38 @@ var ObjectStackAdapter = class {
|
|
|
4189
4732
|
return [];
|
|
4190
4733
|
}
|
|
4191
4734
|
}
|
|
4735
|
+
/**
|
|
4736
|
+
* List the items one metadata registry type declares —
|
|
4737
|
+
* `GET /api/v1/meta/:type`, through the client's `meta.getItems` — as
|
|
4738
|
+
* `{ name, label }` headers for the `position-ref` / `permission-ref` field
|
|
4739
|
+
* widgets (objectui#7611).
|
|
4740
|
+
*
|
|
4741
|
+
* Unlike {@link getObjects}, a failure REJECTS (the `DataSource` contract):
|
|
4742
|
+
* the transport's own error propagates, and an answer without the spec's
|
|
4743
|
+
* `items` list is refused rather than read as an empty registry, so a
|
|
4744
|
+
* failed load can never reach the widget as "this registry declares
|
|
4745
|
+
* nothing". Uncached, like `getObjects`: an item authored in Studio is
|
|
4746
|
+
* listed on the next form that opens.
|
|
4747
|
+
*
|
|
4748
|
+
* An item without a string `name` is not listed (the widget stores the
|
|
4749
|
+
* name, so there is nothing to store); a `label` that is not a string is
|
|
4750
|
+
* left off rather than coerced.
|
|
4751
|
+
*/
|
|
4752
|
+
async listMetadataItems(type) {
|
|
4753
|
+
await this.connect();
|
|
4754
|
+
const result = await this.client.meta.getItems(type);
|
|
4755
|
+
if (!Array.isArray(result?.items)) {
|
|
4756
|
+
throw new Error(`GET /meta/${type} answered without an items list`);
|
|
4757
|
+
}
|
|
4758
|
+
const out = [];
|
|
4759
|
+
for (const it of result.items) {
|
|
4760
|
+
const name = it?.name;
|
|
4761
|
+
if (typeof name !== "string" || !name) continue;
|
|
4762
|
+
const label = it.label;
|
|
4763
|
+
out.push(typeof label === "string" && label ? { name, label } : { name });
|
|
4764
|
+
}
|
|
4765
|
+
return out;
|
|
4766
|
+
}
|
|
4192
4767
|
/**
|
|
4193
4768
|
* Get access to the underlying ObjectStack client for advanced operations.
|
|
4194
4769
|
*/
|
|
@@ -4255,14 +4830,16 @@ var ObjectStackAdapter = class {
|
|
|
4255
4830
|
invalidateViewKeys(objectName, viewName) {
|
|
4256
4831
|
this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
|
|
4257
4832
|
this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
|
|
4833
|
+
this.metadataCache.invalidate?.(VIEW_LIST_KEY);
|
|
4258
4834
|
}
|
|
4259
4835
|
/**
|
|
4260
4836
|
* Batch-fetch all persisted view overrides for an object.
|
|
4261
4837
|
*
|
|
4262
4838
|
* Per-view runtime overrides (density, column widths, sort, hidden
|
|
4263
|
-
* columns, inlineEdit …) live in the SAME metadata namespace the
|
|
4264
|
-
*
|
|
4265
|
-
* {@link
|
|
4839
|
+
* columns, inlineEdit …) live in the SAME metadata namespace the view
|
|
4840
|
+
* writes use: `type='view'`, `name=<viewId>` (see {@link createView} /
|
|
4841
|
+
* {@link updateView}; the toolbar's `updateViewConfig` wrote overlays there
|
|
4842
|
+
* until objectui#12504 retired it). Loading them per-view fires N HTTP GETs
|
|
4266
4843
|
* that 404 for every view the user never customized — console noise on
|
|
4267
4844
|
* every page load. This batch method performs a single
|
|
4268
4845
|
* `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the
|
|
@@ -4286,9 +4863,9 @@ var ObjectStackAdapter = class {
|
|
|
4286
4863
|
*
|
|
4287
4864
|
* Result is cached identically to {@link getView}, and EVERY view write —
|
|
4288
4865
|
* on this adapter and above it — invalidates it through the one seam,
|
|
4289
|
-
* {@link invalidateViewKeys}. For a long time only
|
|
4290
|
-
* did (objectui#4363), which left the other three
|
|
4291
|
-
* for the cache's 5-minute TTL. That gap does not self-heal: the consumer
|
|
4866
|
+
* {@link invalidateViewKeys}. For a long time only `updateViewConfig` (since
|
|
4867
|
+
* retired, objectui#12504) did (objectui#4363), which left the other three
|
|
4868
|
+
* adapter paths stale for the cache's 5-minute TTL. That gap does not self-heal: the consumer
|
|
4292
4869
|
* (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as
|
|
4293
4870
|
* authoritative and deliberately does not re-probe per view — objectui#3774,
|
|
4294
4871
|
* and correct, since re-probing reinstates the 404 flurry the batch read
|
|
@@ -4304,7 +4881,7 @@ var ObjectStackAdapter = class {
|
|
|
4304
4881
|
await this.connect();
|
|
4305
4882
|
const cacheKey = `view-overrides:${objectName}`;
|
|
4306
4883
|
return await this.metadataCache.get(cacheKey, async () => {
|
|
4307
|
-
const result = await this.
|
|
4884
|
+
const result = await this.readViewList();
|
|
4308
4885
|
const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
|
|
4309
4886
|
const out = {};
|
|
4310
4887
|
for (const it of items) {
|
|
@@ -4339,104 +4916,6 @@ var ObjectStackAdapter = class {
|
|
|
4339
4916
|
return null;
|
|
4340
4917
|
}
|
|
4341
4918
|
}
|
|
4342
|
-
/**
|
|
4343
|
-
* Persist a toolbar-driven view config patch — density, column widths,
|
|
4344
|
-
* sort, hidden columns, inline edit. Symmetric counterpart to
|
|
4345
|
-
* {@link getView}: writes the row to the server metadata store via
|
|
4346
|
-
* `client.meta.saveItem`, then invalidates the matching cache entry so the
|
|
4347
|
-
* next {@link getView} reflects the new payload. Returns the persisted item
|
|
4348
|
-
* when the server echoes it, otherwise undefined.
|
|
4349
|
-
*
|
|
4350
|
-
* Called from exactly ONE production site — `ObjectView`'s
|
|
4351
|
-
* `persistViewPatch`, for the toolbar toggle — but that ONE call site
|
|
4352
|
-
* fires for BOTH kinds of active tab: a code-defined **system** view (no
|
|
4353
|
-
* row of its own yet) and a genuinely user-created **saved** view (already
|
|
4354
|
-
* has a row — the toggle is editing ITS OWN definition, not laying an
|
|
4355
|
-
* overlay on top of it). Which one a given call means is NOT re-derived
|
|
4356
|
-
* here from the write's shape (objectui#4227's own lesson: shape inference
|
|
4357
|
-
* on this namespace is exactly what let a system view masquerade as
|
|
4358
|
-
* saved) — the caller already knows, via the same `isSavedViewId`
|
|
4359
|
-
* classification that gates the switcher's readonly flag and its five
|
|
4360
|
-
* mutating handlers, and passes it as {@link opts.isSavedView}.
|
|
4361
|
-
*
|
|
4362
|
-
* - `isSavedView` false/omitted (system-view target, the common case and
|
|
4363
|
-
* the default for backward compatibility): stamps
|
|
4364
|
-
* {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
|
|
4365
|
-
* the original objectui#4227 fix.
|
|
4366
|
-
* - `isSavedView` true: the marker is withheld. Stamping it here would
|
|
4367
|
-
* flag the saved view's OWN row as a personalization overlay, and
|
|
4368
|
-
* `listViews()` would exclude it on the very next read — the user's own
|
|
4369
|
-
* view would vanish from the switcher the moment they toggled its
|
|
4370
|
-
* density (objectui#4227 follow-up, PM review on PR #4713, measured:
|
|
4371
|
-
* `persistViewPatch` has no gate on which kind of tab is active, and
|
|
4372
|
-
* this method writes to the exact same `(type='view', name=viewId)` key
|
|
4373
|
-
* {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
|
|
4374
|
-
* that view, so the write is an upsert onto the saved view's row, not a
|
|
4375
|
-
* new one).
|
|
4376
|
-
*
|
|
4377
|
-
* Per objectstack#7494's ruling, the overlay this writes (system-view
|
|
4378
|
-
* case) is ORG-WIDE shared view settings, not a per-user preference — a
|
|
4379
|
-
* true per-user scope is a parked v18 direction on the platform side.
|
|
4380
|
-
*
|
|
4381
|
-
* @param objectName - Object name (e.g. 'lead')
|
|
4382
|
-
* @param viewId - View identifier (e.g. 'all_leads')
|
|
4383
|
-
* @param config - Full view definition to persist
|
|
4384
|
-
* @param opts.isSavedView - Whether `viewId` already names a saved view
|
|
4385
|
-
* (vs. a system view being personalized for the first time). Omit /
|
|
4386
|
-
* `false` for the default overlay-marking behavior.
|
|
4387
|
-
*/
|
|
4388
|
-
/**
|
|
4389
|
-
* [ADR-0066] Tell this adapter which system capabilities the session holds.
|
|
4390
|
-
*
|
|
4391
|
-
* Pass `undefined` for "no answer was reported" (a backend predating
|
|
4392
|
-
* ADR-0066, or no permission provider mounted). Pass `[]` for a real,
|
|
4393
|
-
* reported, empty grant — {@link maySetViewConfig} treats those two
|
|
4394
|
-
* differently and callers must not collapse them, which is the same
|
|
4395
|
-
* distinction `MePermissionsProvider` preserves natively (objectui#4656).
|
|
4396
|
-
*/
|
|
4397
|
-
setSystemCapabilities(capabilities) {
|
|
4398
|
-
this.systemCapabilities = capabilities ? [...capabilities] : void 0;
|
|
4399
|
-
}
|
|
4400
|
-
/**
|
|
4401
|
-
* May this session write ORG-WIDE view configuration?
|
|
4402
|
-
*
|
|
4403
|
-
* **Unknown fails OPEN**, deliberately, and this is the one judgement in the
|
|
4404
|
-
* gate worth reading twice. It is the doctrine `PermissionContextValue`
|
|
4405
|
-
* .`hasCapabilities` states for every ADR-0066 gate in this repo
|
|
4406
|
-
* (objectui#4656, framework#3923): the server enforces `manage_metadata` on
|
|
4407
|
-
* the metadata door regardless of what the client believes, so a client-side
|
|
4408
|
-
* denial on MISSING DATA cannot protect anything the server was not already
|
|
4409
|
-
* protecting — it can only break a permitted user. Failing closed here would
|
|
4410
|
-
* have refused the write on every deployment predating ADR-0066 and every
|
|
4411
|
-
* host with no permission provider, which is "break the write for everyone"
|
|
4412
|
-
* wearing the costume of a security fix.
|
|
4413
|
-
*
|
|
4414
|
-
* A REPORTED empty array gates strictly: "holds nothing" is a real answer.
|
|
4415
|
-
*/
|
|
4416
|
-
maySetViewConfig() {
|
|
4417
|
-
if (this.systemCapabilities === void 0) return true;
|
|
4418
|
-
return this.systemCapabilities.includes(VIEW_CONFIG_CAPABILITY);
|
|
4419
|
-
}
|
|
4420
|
-
async updateViewConfig(objectName, viewId, config, opts) {
|
|
4421
|
-
if (!this.maySetViewConfig()) {
|
|
4422
|
-
throw new ViewConfigPermissionDeniedError(objectName, viewId);
|
|
4423
|
-
}
|
|
4424
|
-
await this.connect();
|
|
4425
|
-
const merged = {
|
|
4426
|
-
...config || {},
|
|
4427
|
-
object: config?.object || objectName,
|
|
4428
|
-
name: viewId,
|
|
4429
|
-
...opts?.isSavedView ? {} : { [VIEW_OVERLAY_MARKER]: true }
|
|
4430
|
-
};
|
|
4431
|
-
const result = await this.client.meta.saveItem(
|
|
4432
|
-
"view",
|
|
4433
|
-
viewId,
|
|
4434
|
-
merged
|
|
4435
|
-
);
|
|
4436
|
-
this.invalidateViewKeys(objectName, viewId);
|
|
4437
|
-
if (result && result.item) return result.item;
|
|
4438
|
-
return result ?? void 0;
|
|
4439
|
-
}
|
|
4440
4919
|
/**
|
|
4441
4920
|
* List user-created views for a given object via the metadata overlay
|
|
4442
4921
|
* API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
|
|
@@ -4493,7 +4972,7 @@ var ObjectStackAdapter = class {
|
|
|
4493
4972
|
const draftItems = await this.metadataClient().withPreviewDrafts(true).list("view");
|
|
4494
4973
|
items = Array.isArray(draftItems) ? draftItems : [];
|
|
4495
4974
|
} else {
|
|
4496
|
-
const result = await this.
|
|
4975
|
+
const result = await this.readViewList();
|
|
4497
4976
|
items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
|
|
4498
4977
|
}
|
|
4499
4978
|
const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
|
|
@@ -4782,6 +5261,10 @@ var ObjectStackAdapter = class {
|
|
|
4782
5261
|
* the PUBLISHED half after the draft was discarded carries the partial
|
|
4783
5262
|
* state on the error's `outcome` — "draft gone, overlay left" is exactly
|
|
4784
5263
|
* what the old `{ deleted: boolean }` could not express.
|
|
5264
|
+
* Its message is the adapter's "NOT fully removed" sentence followed, on
|
|
5265
|
+
* the next line, by the server's own reason (objectui#12416), so
|
|
5266
|
+
* `formatMetadataError(err)` shows the remedy a refusal names; the
|
|
5267
|
+
* original error stays on `cause`.
|
|
4785
5268
|
*
|
|
4786
5269
|
* Invalidates through {@link invalidateViewKeys}: the deleted row leaves the
|
|
4787
5270
|
* batch override map too, and a ghost entry there is what the object page
|
|
@@ -4807,9 +5290,12 @@ var ObjectStackAdapter = class {
|
|
|
4807
5290
|
draft,
|
|
4808
5291
|
published: { removed: false }
|
|
4809
5292
|
};
|
|
5293
|
+
const status = err?.status;
|
|
5294
|
+
const refused = typeof status === "number" && status >= 400 && status < 500;
|
|
4810
5295
|
throw Object.assign(
|
|
4811
5296
|
new Error(
|
|
4812
|
-
`deleteView: view "${viewName}" on object "${objectName}" is NOT fully removed \u2014 the draft home was ${draft.removed ? "discarded" : "already absent"}, but deleting the published overlay failed. The published row is still serving the view; retry the delete
|
|
5297
|
+
`deleteView: view "${viewName}" on object "${objectName}" is NOT fully removed \u2014 the draft home was ${draft.removed ? "discarded" : "already absent"}, but deleting the published overlay failed. The published row is still serving the view${refused ? "." : "; retry the delete."}
|
|
5298
|
+
` + formatMetadataError(err)
|
|
4813
5299
|
),
|
|
4814
5300
|
{ cause: err, outcome }
|
|
4815
5301
|
);
|
|
@@ -4909,14 +5395,41 @@ var ObjectStackAdapter = class {
|
|
|
4909
5395
|
* @param appName - the app name as it appears in the URL segment
|
|
4910
5396
|
*/
|
|
4911
5397
|
async probeAppAccess(appName) {
|
|
4912
|
-
|
|
5398
|
+
return (await this.probeMetaItemAccess("app", appName)).verdict;
|
|
5399
|
+
}
|
|
5400
|
+
/**
|
|
5401
|
+
* {@link probeAppAccess} for any metadata type whose by-name read refuses a
|
|
5402
|
+
* caller (objectui#12112): why `name` is not in this session's list of
|
|
5403
|
+
* `type`, plus the server's own sentence when it refused.
|
|
5404
|
+
*
|
|
5405
|
+
* Since objectstack#22731 a dashboard and a list view carry the app's gate:
|
|
5406
|
+
* `GET /api/v1/meta/dashboard` and `/meta/view` list only what the caller may
|
|
5407
|
+
* open, so a withheld item and a missing one are the same absence there, and
|
|
5408
|
+
* the by-name read of one the caller does not hold answers `403`
|
|
5409
|
+
* `PERMISSION_DENIED`, "the app arm's whole refusal (same emitter, same
|
|
5410
|
+
* envelope)". The verdicts are therefore read exactly as for an app.
|
|
5411
|
+
*
|
|
5412
|
+
* `message` is the refusal's `message`, as the server wrote it: the one
|
|
5413
|
+
* sentence that names the item and says it was refused. Only on `denied`.
|
|
5414
|
+
*
|
|
5415
|
+
* Nothing here throws, and nothing is cached: a verdict about the CALLER
|
|
5416
|
+
* must not outlive a grant.
|
|
5417
|
+
*
|
|
5418
|
+
* @param type - the singular metadata type (`app`, `dashboard`, `view`)
|
|
5419
|
+
* @param name - the item's name, as the by-name route addresses it
|
|
5420
|
+
*/
|
|
5421
|
+
async probeMetaItemAccess(type, name) {
|
|
5422
|
+
if (!type || !name) return { verdict: "unknown" };
|
|
4913
5423
|
try {
|
|
4914
|
-
const envelope = await this.client.meta.getItem(
|
|
4915
|
-
return metaEnvelopeCarriesItem(envelope) ? "granted" : "not_found";
|
|
5424
|
+
const envelope = await this.client.meta.getItem(type, name);
|
|
5425
|
+
return { verdict: metaEnvelopeCarriesItem(envelope) ? "granted" : "not_found" };
|
|
4916
5426
|
} catch (err) {
|
|
4917
|
-
if (isAppPermissionDeniedError(err))
|
|
4918
|
-
|
|
4919
|
-
|
|
5427
|
+
if (isAppPermissionDeniedError(err)) {
|
|
5428
|
+
const message = err?.message;
|
|
5429
|
+
return typeof message === "string" && message.trim() ? { verdict: "denied", message } : { verdict: "denied" };
|
|
5430
|
+
}
|
|
5431
|
+
if (isMetaItemAbsentError(err)) return { verdict: "not_found" };
|
|
5432
|
+
return { verdict: "unreachable" };
|
|
4920
5433
|
}
|
|
4921
5434
|
}
|
|
4922
5435
|
/**
|
|
@@ -5004,12 +5517,9 @@ var ObjectStackAdapter = class {
|
|
|
5004
5517
|
throw new AnalyticsResultShapeError(resource, contractResult);
|
|
5005
5518
|
}
|
|
5006
5519
|
const rawRows = contractResult.rows;
|
|
5007
|
-
const measureMissing = rawRows.length > 0 && rawRows.every(
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
if (valueKey in row && row[valueKey] != null) return false;
|
|
5011
|
-
return true;
|
|
5012
|
-
});
|
|
5520
|
+
const measureMissing = rawRows.length > 0 && rawRows.every(
|
|
5521
|
+
(row) => row == null || !(measureName in row) && !(valueKey in row)
|
|
5522
|
+
);
|
|
5013
5523
|
if (measureMissing) {
|
|
5014
5524
|
return await this.aggregateViaFind(resource, params);
|
|
5015
5525
|
}
|
|
@@ -5461,13 +5971,12 @@ function createObjectStackAdapter(config) {
|
|
|
5461
5971
|
OBJECT_METADATA_TYPE,
|
|
5462
5972
|
ObjectStackAdapter,
|
|
5463
5973
|
ObjectStackError,
|
|
5974
|
+
PREFERENCE_FAMILY_ROW_LIMIT,
|
|
5464
5975
|
RELATIONSHIP_TYPES_REQUIRING_REFERENCE,
|
|
5465
5976
|
UNRECOGNIZED_DROP_REASON,
|
|
5466
5977
|
UnlowerableAnalyticsFilterError,
|
|
5467
5978
|
UnloweredAggregateWhereError,
|
|
5468
|
-
VIEW_CONFIG_CAPABILITY,
|
|
5469
5979
|
VIEW_OVERLAY_OWNED_KEYS,
|
|
5470
|
-
ViewConfigPermissionDeniedError,
|
|
5471
5980
|
assertObjectMetadataWritable,
|
|
5472
5981
|
classifyAnalyticsFailure,
|
|
5473
5982
|
classifyImportMappingsFailure,
|
|
@@ -5475,13 +5984,17 @@ function createObjectStackAdapter(config) {
|
|
|
5475
5984
|
clearSharedDiscoveryCache,
|
|
5476
5985
|
createErrorFromResponse,
|
|
5477
5986
|
createObjectStackAdapter,
|
|
5987
|
+
createObjectStackUserPreferenceFamily,
|
|
5478
5988
|
createObjectStackUserStateAdapter,
|
|
5479
5989
|
createQuietHttpLogger,
|
|
5990
|
+
draftChangesUnder,
|
|
5991
|
+
dropServedPicklistOptions,
|
|
5480
5992
|
extractDraftBody,
|
|
5481
5993
|
formatHttpFailureMessage,
|
|
5482
5994
|
formatMetadataError,
|
|
5483
5995
|
formatMetadataIssue,
|
|
5484
5996
|
getSharedDiscovery,
|
|
5997
|
+
governingDraftChange,
|
|
5485
5998
|
is404Error,
|
|
5486
5999
|
isAnalyticsNotInstalledError,
|
|
5487
6000
|
isApiAccessDeniedError,
|
|
@@ -5491,9 +6004,9 @@ function createObjectStackAdapter(config) {
|
|
|
5491
6004
|
isMalformedFilterError,
|
|
5492
6005
|
isMetaItemAbsentError,
|
|
5493
6006
|
isObjectStackError,
|
|
5494
|
-
isViewConfigPermissionDeniedError,
|
|
5495
6007
|
narrowPersonalizationOverlay,
|
|
5496
6008
|
normaliseClientError,
|
|
6009
|
+
readDraftChanges,
|
|
5497
6010
|
readSaveAdvisories,
|
|
5498
6011
|
readTransactionalBatchCapability,
|
|
5499
6012
|
serializeOrderBy,
|