@object-ui/data-objectstack 15.0.0 → 16.1.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 +210 -0
- package/README.md +63 -0
- package/dist/index.cjs +1022 -872
- package/dist/index.d.cts +79 -14
- package/dist/index.d.ts +79 -14
- package/dist/index.js +1021 -872
- package/package.json +4 -4
- package/src/exportDownload.test.ts +9 -3
- package/src/index.ts +259 -105
- package/src/integration.ts +8 -0
- package/src/listViews.test.ts +97 -0
- package/src/metadata-client.test.ts +8 -0
- package/src/metadata-client.ts +9 -0
- package/src/onMutation.test.ts +267 -2
package/dist/index.js
CHANGED
|
@@ -71,6 +71,72 @@ function convertFiltersToAST(filter) {
|
|
|
71
71
|
return ["and", ...conditions];
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// ../core/src/adapters/batchTransaction.ts
|
|
75
|
+
function idOf(rec) {
|
|
76
|
+
if (rec == null) return void 0;
|
|
77
|
+
return rec.id ?? rec._id ?? rec.recordId;
|
|
78
|
+
}
|
|
79
|
+
function resolveRefs(data, results) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [key, value] of Object.entries(data)) {
|
|
82
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.$ref === "number") {
|
|
83
|
+
const refIndex = value.$ref;
|
|
84
|
+
const id = idOf(results[refIndex]);
|
|
85
|
+
if (id === void 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`batchTransaction: field "${key}" references operation ${refIndex}, which has not produced an id (forward or invalid $ref)`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
out[key] = id;
|
|
91
|
+
} else {
|
|
92
|
+
out[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
async function emulateBatchTransaction(dataSource, operations) {
|
|
98
|
+
const results = [];
|
|
99
|
+
const created = [];
|
|
100
|
+
try {
|
|
101
|
+
for (const op of operations) {
|
|
102
|
+
const action = op.action ?? "create";
|
|
103
|
+
if (action === "create") {
|
|
104
|
+
const record = await dataSource.create(
|
|
105
|
+
op.object,
|
|
106
|
+
resolveRefs(op.data ?? {}, results)
|
|
107
|
+
);
|
|
108
|
+
results.push(record);
|
|
109
|
+
const id = idOf(record);
|
|
110
|
+
if (id) created.push({ object: op.object, id });
|
|
111
|
+
} else if (action === "update") {
|
|
112
|
+
if (op.id == null) {
|
|
113
|
+
throw new Error(`batchTransaction: update on "${op.object}" requires an id`);
|
|
114
|
+
}
|
|
115
|
+
results.push(
|
|
116
|
+
await dataSource.update(
|
|
117
|
+
op.object,
|
|
118
|
+
op.id,
|
|
119
|
+
resolveRefs(op.data ?? {}, results)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
if (op.id == null) {
|
|
124
|
+
throw new Error(`batchTransaction: delete on "${op.object}" requires an id`);
|
|
125
|
+
}
|
|
126
|
+
results.push(await dataSource.delete(op.object, op.id));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { results };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
if (created.length > 0) {
|
|
132
|
+
await Promise.allSettled(
|
|
133
|
+
[...created].reverse().map((c) => dataSource.delete(c.object, c.id))
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
// ../core/src/utils/reference-keys.ts
|
|
75
141
|
function normalizeFieldReferenceKeys(fieldDef) {
|
|
76
142
|
if (!fieldDef || typeof fieldDef !== "object") return fieldDef;
|
|
@@ -271,893 +337,898 @@ var MetadataCache = class {
|
|
|
271
337
|
}
|
|
272
338
|
};
|
|
273
339
|
|
|
274
|
-
// src/
|
|
275
|
-
var
|
|
340
|
+
// src/metadata-client.ts
|
|
341
|
+
var META_PREFIX = "/meta";
|
|
342
|
+
var API_PREFIX = "/api/v1";
|
|
343
|
+
function buildBase(config) {
|
|
344
|
+
const trimmed = config.baseUrl.replace(/\/+$/, "");
|
|
345
|
+
const scoped = config.environmentId ? `${API_PREFIX}/environments/${encodeURIComponent(config.environmentId)}${META_PREFIX}` : `${API_PREFIX}${META_PREFIX}`;
|
|
346
|
+
if (/\/api\/v\d+$/i.test(trimmed)) {
|
|
347
|
+
const stripped = trimmed.replace(/\/api\/v\d+$/i, "");
|
|
348
|
+
const scopedNoApi = config.environmentId ? `${API_PREFIX}/environments/${encodeURIComponent(config.environmentId)}${META_PREFIX}` : `${API_PREFIX}${META_PREFIX}`;
|
|
349
|
+
return `${stripped}${scopedNoApi}`;
|
|
350
|
+
}
|
|
351
|
+
return `${trimmed}${scoped}`;
|
|
352
|
+
}
|
|
353
|
+
async function parseError(res) {
|
|
354
|
+
let body;
|
|
355
|
+
try {
|
|
356
|
+
body = await res.json();
|
|
357
|
+
} catch {
|
|
358
|
+
body = await res.text().catch(() => void 0);
|
|
359
|
+
}
|
|
360
|
+
const bodyObj = body && typeof body === "object" ? body : void 0;
|
|
361
|
+
const errNode = bodyObj?.error;
|
|
362
|
+
const message = (errNode && typeof errNode === "object" ? errNode.message : errNode) ?? bodyObj?.message ?? `Metadata request failed: ${res.status} ${res.statusText}`;
|
|
363
|
+
const err = new Error(String(message));
|
|
364
|
+
err.status = res.status;
|
|
365
|
+
err.body = body;
|
|
366
|
+
const details = errNode && typeof errNode === "object" ? errNode.details : void 0;
|
|
367
|
+
const issues = details?.issues ?? (errNode && typeof errNode === "object" ? errNode.issues : void 0) ?? bodyObj?.issues;
|
|
368
|
+
if (Array.isArray(issues)) err.issues = issues;
|
|
369
|
+
const code = details?.code ?? (errNode && typeof errNode === "object" ? errNode.code : void 0) ?? bodyObj?.code;
|
|
370
|
+
if (code != null) err.code = String(code);
|
|
371
|
+
return err;
|
|
372
|
+
}
|
|
373
|
+
var MetadataClient = class _MetadataClient {
|
|
374
|
+
constructor(config) {
|
|
375
|
+
__publicField(this, "base");
|
|
376
|
+
__publicField(this, "fetchImpl");
|
|
377
|
+
__publicField(this, "headers");
|
|
378
|
+
/** ADR-0037: when true, reads render the draft-overlaid world. */
|
|
379
|
+
__publicField(this, "previewDrafts");
|
|
380
|
+
this.base = buildBase(config);
|
|
381
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
382
|
+
this.headers = { Accept: "application/json", ...config.headers ?? {} };
|
|
383
|
+
this.previewDrafts = config.previewDrafts === true;
|
|
384
|
+
}
|
|
385
|
+
/** Update the client's environment scope at runtime. */
|
|
386
|
+
withEnvironment(environmentId) {
|
|
387
|
+
return new _MetadataClient({
|
|
388
|
+
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
389
|
+
environmentId,
|
|
390
|
+
fetch: this.fetchImpl,
|
|
391
|
+
headers: this.headers,
|
|
392
|
+
previewDrafts: this.previewDrafts
|
|
393
|
+
});
|
|
394
|
+
}
|
|
276
395
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* @param code - Unique error code for programmatic handling
|
|
281
|
-
* @param statusCode - Optional HTTP status code
|
|
282
|
-
* @param details - Optional additional error details for debugging
|
|
396
|
+
* Derive a client whose reads render the draft-overlaid world (or not).
|
|
397
|
+
* Same base/fetch/headers; used by the app-shell to switch the whole
|
|
398
|
+
* renderer tree into ADR-0037 preview mode off one URL flag.
|
|
283
399
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
400
|
+
withPreviewDrafts(previewDrafts) {
|
|
401
|
+
if (previewDrafts === this.previewDrafts) return this;
|
|
402
|
+
const envMatch = this.base.match(/\/api\/v\d+\/environments\/([^/]+)\/meta$/);
|
|
403
|
+
return new _MetadataClient({
|
|
404
|
+
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
405
|
+
...envMatch ? { environmentId: decodeURIComponent(envMatch[1]) } : {},
|
|
406
|
+
fetch: this.fetchImpl,
|
|
407
|
+
headers: this.headers,
|
|
408
|
+
previewDrafts
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/** List all registered metadata types (returns the registry rows). */
|
|
412
|
+
async listTypes() {
|
|
413
|
+
const res = await this.fetchImpl(this.base, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
414
|
+
if (!res.ok) throw await parseError(res);
|
|
415
|
+
const data = await res.json();
|
|
416
|
+
if (Array.isArray(data)) return data;
|
|
417
|
+
if (data && Array.isArray(data.entries)) return data.entries;
|
|
418
|
+
if (data && Array.isArray(data.items)) return data.items;
|
|
419
|
+
if (data && Array.isArray(data.types)) {
|
|
420
|
+
return data.types.map((t) => ({ type: t }));
|
|
421
|
+
}
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
/** List items of a metadata type (e.g. `object`, `field`, `view`). */
|
|
425
|
+
async list(type, options = {}) {
|
|
426
|
+
const params = [];
|
|
427
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
428
|
+
if (this.previewDrafts) params.push("preview=draft");
|
|
429
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
430
|
+
const url = `${this.base}/${encodeURIComponent(type)}${qs}`;
|
|
431
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
432
|
+
if (!res.ok) throw await parseError(res);
|
|
433
|
+
const data = await res.json();
|
|
434
|
+
if (Array.isArray(data)) return data;
|
|
435
|
+
if (data && Array.isArray(data.items)) {
|
|
436
|
+
return data.items;
|
|
292
437
|
}
|
|
438
|
+
return [];
|
|
293
439
|
}
|
|
294
440
|
/**
|
|
295
|
-
*
|
|
441
|
+
* List pending DRAFT items (ADR-0033) — what an AI authored but nobody
|
|
442
|
+
* published yet. `list()` only sees published/active metadata, so a
|
|
443
|
+
* just-built app package looks empty there; this surfaces the drafts so the
|
|
444
|
+
* console can show a "pending changes" view and draft-aware package contents.
|
|
445
|
+
* Optionally narrow by `packageId` and/or `type`. Returns light headers
|
|
446
|
+
* (no body) carrying `packageId` for grouping.
|
|
296
447
|
*/
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
448
|
+
async listDrafts(options = {}) {
|
|
449
|
+
const params = [];
|
|
450
|
+
if (options.packageId) params.push(`packageId=${encodeURIComponent(options.packageId)}`);
|
|
451
|
+
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
452
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
453
|
+
const url = `${this.base}/_drafts${qs}`;
|
|
454
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
455
|
+
if (!res.ok) throw await parseError(res);
|
|
456
|
+
const data = await res.json();
|
|
457
|
+
if (Array.isArray(data)) return data;
|
|
458
|
+
return data?.drafts ?? data?.data?.drafts ?? [];
|
|
306
459
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
);
|
|
316
|
-
|
|
460
|
+
/**
|
|
461
|
+
* Get a single metadata item. Returns the unwrapped item content
|
|
462
|
+
* (matching the framework REST handler which calls `res.json(item)`).
|
|
463
|
+
* Returns `null` on 404 to keep the call-site ergonomic.
|
|
464
|
+
*/
|
|
465
|
+
async get(type, name, options = {}) {
|
|
466
|
+
const params = [];
|
|
467
|
+
if (options.state === "draft") params.push("state=draft");
|
|
468
|
+
else if (this.previewDrafts) params.push("preview=draft");
|
|
469
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
470
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
471
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
472
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
473
|
+
if (res.status === 404) return null;
|
|
474
|
+
if (!res.ok) throw await parseError(res);
|
|
475
|
+
return await res.json();
|
|
317
476
|
}
|
|
318
|
-
};
|
|
319
|
-
var BulkOperationError = class extends ObjectStackError {
|
|
320
477
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
478
|
+
* Read the pending draft body for an item (`?state=draft`). Returns
|
|
479
|
+
* `null` when there is no draft pending. Draft reads do NOT fall
|
|
480
|
+
* back to the published overlay or the artifact registry — a `null`
|
|
481
|
+
* unambiguously means "nothing to publish".
|
|
482
|
+
*
|
|
483
|
+
* Note: the framework wraps draft responses in an envelope
|
|
484
|
+
* `{ type, name, item }` (matching `getMetaItem`); callers should
|
|
485
|
+
* read `.item` to get the body. The legacy `get()` returns the
|
|
486
|
+
* unwrapped body, so this method preserves that asymmetry by
|
|
487
|
+
* returning whatever the server sent.
|
|
328
488
|
*/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`Bulk ${operation} operation failed: ${successCount} succeeded, ${failureCount} failed`,
|
|
332
|
-
"BULK_OPERATION_ERROR",
|
|
333
|
-
500,
|
|
334
|
-
{
|
|
335
|
-
operation,
|
|
336
|
-
successCount,
|
|
337
|
-
failureCount,
|
|
338
|
-
errors,
|
|
339
|
-
...details
|
|
340
|
-
}
|
|
341
|
-
);
|
|
342
|
-
__publicField(this, "successCount", successCount);
|
|
343
|
-
__publicField(this, "failureCount", failureCount);
|
|
344
|
-
__publicField(this, "errors", errors);
|
|
345
|
-
this.name = "BulkOperationError";
|
|
489
|
+
async getDraft(type, name, options = {}) {
|
|
490
|
+
return this.get(type, name, { state: "draft", ...options.packageId ? { packageId: options.packageId } : {} });
|
|
346
491
|
}
|
|
347
492
|
/**
|
|
348
|
-
*
|
|
493
|
+
* Save (PUT) a metadata item. The framework accepts both the bare
|
|
494
|
+
* item payload and the `{ item: ... }` / `{ metadata: ... }`
|
|
495
|
+
* envelopes; we send bare for consistency. Pass `mode: 'draft'` to
|
|
496
|
+
* stage the change without publishing (Studio's "Save" button).
|
|
349
497
|
*/
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
498
|
+
async save(type, name, item, options = {}) {
|
|
499
|
+
if (!name || !String(name).trim()) {
|
|
500
|
+
throw new Error(
|
|
501
|
+
`MetadataClient.save: name must be non-empty (type="${type}"). The PUT /meta/:type/:name route requires a name segment.`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const params = [];
|
|
505
|
+
if (options.force) params.push("force=true");
|
|
506
|
+
if (options.mode === "draft") params.push("mode=draft");
|
|
507
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
508
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
509
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
510
|
+
const headers = {
|
|
511
|
+
...this.headers,
|
|
512
|
+
"Content-Type": "application/json"
|
|
360
513
|
};
|
|
514
|
+
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
515
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
516
|
+
const res = await this.fetchImpl(url, {
|
|
517
|
+
method: "PUT",
|
|
518
|
+
headers,
|
|
519
|
+
body: JSON.stringify(item)
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) throw await parseError(res);
|
|
522
|
+
return await res.json();
|
|
361
523
|
}
|
|
362
|
-
};
|
|
363
|
-
var ConnectionError = class extends ObjectStackError {
|
|
364
|
-
constructor(message, url, details, statusCode) {
|
|
365
|
-
super(
|
|
366
|
-
`Connection error: ${message}`,
|
|
367
|
-
"CONNECTION_ERROR",
|
|
368
|
-
statusCode || 503,
|
|
369
|
-
{ url, ...details }
|
|
370
|
-
);
|
|
371
|
-
__publicField(this, "url", url);
|
|
372
|
-
this.name = "ConnectionError";
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
var AuthenticationError = class extends ObjectStackError {
|
|
376
|
-
constructor(message = "Authentication failed", details, statusCode) {
|
|
377
|
-
super(
|
|
378
|
-
message,
|
|
379
|
-
"AUTHENTICATION_ERROR",
|
|
380
|
-
statusCode || 401,
|
|
381
|
-
details
|
|
382
|
-
);
|
|
383
|
-
this.name = "AuthenticationError";
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
var ValidationError = class extends ObjectStackError {
|
|
387
524
|
/**
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
525
|
+
* Publish a single pending draft BY REFERENCE — promotes the draft row for
|
|
526
|
+
* `(type, name)` into the active overlay and drops the draft. Works for ANY
|
|
527
|
+
* draft, including ones with no `packageId` binding (which the package-scoped
|
|
528
|
+
* `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
|
|
529
|
+
* exact set returned by {@link listDrafts} without needing a package.
|
|
530
|
+
*
|
|
531
|
+
* Returns the server result. For `seed` drafts the protocol also materializes
|
|
532
|
+
* the rows and reports under `seedApplied` — a data problem never fails the
|
|
533
|
+
* publish, so callers should check `seedApplied?.success` and warn the user
|
|
534
|
+
* rather than assume the data went live.
|
|
394
535
|
*/
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"
|
|
399
|
-
|
|
400
|
-
{
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
__publicField(this, "field", field);
|
|
407
|
-
__publicField(this, "validationErrors", validationErrors);
|
|
408
|
-
this.name = "ValidationError";
|
|
536
|
+
async publishDraft(type, name) {
|
|
537
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
538
|
+
const res = await this.fetchImpl(url, {
|
|
539
|
+
method: "POST",
|
|
540
|
+
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
541
|
+
body: "{}"
|
|
542
|
+
});
|
|
543
|
+
if (!res.ok) throw await parseError(res);
|
|
544
|
+
const body = await res.json().catch(() => ({}));
|
|
545
|
+
const inner = body?.data && typeof body.data === "object" ? body.data : body;
|
|
546
|
+
return inner;
|
|
409
547
|
}
|
|
410
548
|
/**
|
|
411
|
-
* Get
|
|
549
|
+
* Get the 3-state layered view of a metadata item (Phase 3a). Returns
|
|
550
|
+
* `code` (the artifact / fallback default), `overlay` (the saved
|
|
551
|
+
* customisation, if any), and `effective` (what the runtime sees).
|
|
412
552
|
*/
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const message = response?.message || response?.statusText || "Unknown error";
|
|
420
|
-
const details = {
|
|
421
|
-
context,
|
|
422
|
-
response: {
|
|
423
|
-
status,
|
|
424
|
-
data: response?.data,
|
|
425
|
-
headers: response?.headers
|
|
553
|
+
async layered(type, name, options = {}) {
|
|
554
|
+
const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : "";
|
|
555
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
|
|
556
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
557
|
+
if (res.status === 404) {
|
|
558
|
+
return { code: null, overlay: null, overlayScope: null, effective: null };
|
|
426
559
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
function isErrorType(error, errorClass) {
|
|
458
|
-
return error instanceof errorClass;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// src/cloud.ts
|
|
462
|
-
var CloudOperations = class {
|
|
463
|
-
constructor(getClient) {
|
|
464
|
-
__publicField(this, "getClient", getClient);
|
|
560
|
+
if (!res.ok) throw await parseError(res);
|
|
561
|
+
const body = await res.json();
|
|
562
|
+
const hasEnvelope = body && ("code" in body || "overlay" in body || "effective" in body);
|
|
563
|
+
if (!hasEnvelope) {
|
|
564
|
+
return {
|
|
565
|
+
code: null,
|
|
566
|
+
overlay: null,
|
|
567
|
+
overlayScope: null,
|
|
568
|
+
effective: body
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
code: body.code ?? null,
|
|
573
|
+
overlay: body.overlay ?? null,
|
|
574
|
+
overlayScope: body.overlayScope ?? null,
|
|
575
|
+
effective: body.effective ?? null,
|
|
576
|
+
...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
|
|
577
|
+
// ADR-0010 Phase 4 — pass through the metadata-protection envelope so
|
|
578
|
+
// the editor can render lock affordances without a second round trip.
|
|
579
|
+
...body.lock !== void 0 ? { lock: body.lock } : {},
|
|
580
|
+
...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
|
|
581
|
+
...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
|
|
582
|
+
...body.lockDocsUrl !== void 0 ? { lockDocsUrl: body.lockDocsUrl } : {},
|
|
583
|
+
...body.provenance !== void 0 ? { provenance: body.provenance } : {},
|
|
584
|
+
...body.packageId !== void 0 ? { packageId: body.packageId } : {},
|
|
585
|
+
...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
|
|
586
|
+
...body.editable !== void 0 ? { editable: body.editable } : {},
|
|
587
|
+
...body.deletable !== void 0 ? { deletable: body.deletable } : {},
|
|
588
|
+
...body.resettable !== void 0 ? { resettable: body.resettable } : {}
|
|
589
|
+
};
|
|
465
590
|
}
|
|
466
591
|
/**
|
|
467
|
-
*
|
|
592
|
+
* Cross-type sweep of load-time validation results — calls
|
|
593
|
+
* `GET /meta/diagnostics`. Returns every entry the framework
|
|
594
|
+
* considers invalid (or, when `severity: 'warning'`, also entries
|
|
595
|
+
* with only warnings).
|
|
596
|
+
*
|
|
597
|
+
* Used by the Studio's governance overview page and by the
|
|
598
|
+
* directory page to show "N invalid" badges per metadata type.
|
|
468
599
|
*/
|
|
469
|
-
async
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
600
|
+
async diagnostics(options = {}) {
|
|
601
|
+
const params = [];
|
|
602
|
+
if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
|
|
603
|
+
if (options.severity) params.push(`severity=${encodeURIComponent(options.severity)}`);
|
|
604
|
+
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
605
|
+
const qs = params.length ? `?${params.join("&")}` : "";
|
|
606
|
+
const url = `${this.base}/diagnostics${qs}`;
|
|
607
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
608
|
+
if (res.status === 404) {
|
|
609
|
+
return { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} };
|
|
610
|
+
}
|
|
611
|
+
if (!res.ok) throw await parseError(res);
|
|
612
|
+
const data = await res.json();
|
|
613
|
+
return {
|
|
614
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
615
|
+
total: typeof data.total === "number" ? data.total : 0,
|
|
616
|
+
scannedTypes: typeof data.scannedTypes === "number" ? data.scannedTypes : 0,
|
|
617
|
+
scannedItems: typeof data.scannedItems === "number" ? data.scannedItems : 0,
|
|
618
|
+
stats: data.stats && typeof data.stats === "object" ? data.stats : {}
|
|
619
|
+
};
|
|
473
620
|
}
|
|
474
621
|
/**
|
|
475
|
-
*
|
|
622
|
+
* Find every metadata item that references this one (Phase 3a). Useful
|
|
623
|
+
* for pre-delete impact analysis: "Are any views pointing at this
|
|
624
|
+
* object before I drop it?".
|
|
476
625
|
*/
|
|
477
|
-
async
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
|
|
626
|
+
async references(type, name) {
|
|
627
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/references`;
|
|
628
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
629
|
+
if (res.status === 404) return [];
|
|
630
|
+
if (!res.ok) throw await parseError(res);
|
|
631
|
+
const data = await res.json();
|
|
632
|
+
return Array.isArray(data) ? data : data?.items ?? [];
|
|
481
633
|
}
|
|
482
634
|
/**
|
|
483
|
-
*
|
|
635
|
+
* Reset a metadata customization overlay back to the artifact default.
|
|
636
|
+
* Idempotent: returns the result even when no overlay row existed.
|
|
637
|
+
* Pass `state: 'draft'` to discard the pending draft only (keeps the
|
|
638
|
+
* published overlay intact) — useful for a Studio "Discard draft" button.
|
|
484
639
|
*/
|
|
485
|
-
async
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
640
|
+
async reset(type, name, options = {}) {
|
|
641
|
+
const qs = options.state === "draft" ? "?state=draft" : "";
|
|
642
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
643
|
+
const headers = { ...this.headers };
|
|
644
|
+
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
645
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
646
|
+
const res = await this.fetchImpl(url, { method: "DELETE", headers });
|
|
647
|
+
if (!res.ok) throw await parseError(res);
|
|
648
|
+
return await res.json();
|
|
489
649
|
}
|
|
490
650
|
/**
|
|
491
|
-
*
|
|
651
|
+
* Promote the pending draft of an item to the active overlay
|
|
652
|
+
* (POST `/meta/:type/:name/publish`). Returns
|
|
653
|
+
* `{ success, version, seq, message }`. Throws a `404 no_draft` if
|
|
654
|
+
* nothing is pending and `409 metadata_conflict` if the published
|
|
655
|
+
* overlay moved while the draft was sitting.
|
|
492
656
|
*/
|
|
493
|
-
async
|
|
494
|
-
const
|
|
495
|
-
const
|
|
496
|
-
|
|
657
|
+
async publish(type, name, options = {}) {
|
|
658
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
|
|
659
|
+
const headers = {
|
|
660
|
+
...this.headers,
|
|
661
|
+
"Content-Type": "application/json"
|
|
662
|
+
};
|
|
663
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
664
|
+
const res = await this.fetchImpl(url, {
|
|
665
|
+
method: "POST",
|
|
666
|
+
headers,
|
|
667
|
+
body: JSON.stringify(options.message ? { message: options.message } : {})
|
|
668
|
+
});
|
|
669
|
+
if (!res.ok) throw await parseError(res);
|
|
670
|
+
return await res.json();
|
|
497
671
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
errors.push({ field: "exports", message: "At least one export is required", code: "NO_EXPORTS" });
|
|
512
|
-
}
|
|
513
|
-
if (contract.exports) {
|
|
514
|
-
const validTypes = ["component", "hook", "utility", "provider"];
|
|
515
|
-
for (const exp of contract.exports) {
|
|
516
|
-
if (!exp.name) {
|
|
517
|
-
errors.push({ field: "exports.name", message: "Export name is required", code: "MISSING_EXPORT_NAME" });
|
|
518
|
-
}
|
|
519
|
-
if (!validTypes.includes(exp.type)) {
|
|
520
|
-
errors.push({ field: "exports.type", message: `Invalid export type: ${exp.type}`, code: "INVALID_EXPORT_TYPE" });
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (!contract.permissions || contract.permissions.length === 0) {
|
|
525
|
-
warnings.push("No permissions declared \u2014 plugin will have minimal access");
|
|
526
|
-
}
|
|
527
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
528
|
-
}
|
|
529
|
-
function generateContractManifest(contract) {
|
|
530
|
-
return {
|
|
531
|
-
$schema: "https://objectui.org/schemas/plugin-contract-v1.json",
|
|
532
|
-
name: contract.name,
|
|
533
|
-
version: contract.version,
|
|
534
|
-
peerDependencies: contract.peerDependencies ?? {},
|
|
535
|
-
exports: contract.exports.map((exp) => ({
|
|
536
|
-
name: exp.name,
|
|
537
|
-
type: exp.type,
|
|
538
|
-
description: exp.description ?? ""
|
|
539
|
-
})),
|
|
540
|
-
permissions: contract.permissions ?? [],
|
|
541
|
-
api: contract.api ?? {},
|
|
542
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// src/userState.ts
|
|
547
|
-
var DEFAULT_RESOURCE = "sys_user_preference";
|
|
548
|
-
function createObjectStackUserStateAdapter(options) {
|
|
549
|
-
const {
|
|
550
|
-
dataSource,
|
|
551
|
-
userId,
|
|
552
|
-
key,
|
|
553
|
-
resource = DEFAULT_RESOURCE,
|
|
554
|
-
onError = () => {
|
|
555
|
-
}
|
|
556
|
-
} = options;
|
|
557
|
-
let cachedRowId = null;
|
|
558
|
-
let saveChain = Promise.resolve();
|
|
559
|
-
const findExisting = async () => {
|
|
560
|
-
const params = {
|
|
561
|
-
$filter: {
|
|
562
|
-
user_id: userId,
|
|
563
|
-
key
|
|
564
|
-
},
|
|
565
|
-
$top: 1
|
|
672
|
+
/**
|
|
673
|
+
* Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
|
|
674
|
+
* The server reads the history row at `toVersion`, writes its body back
|
|
675
|
+
* as the active overlay with `operation_type='revert'`. Returns
|
|
676
|
+
* `{ success, version, seq, restoredFromVersion, message }`. Throws
|
|
677
|
+
* `404 version_not_found` for unknown versions and `409 version_not_restorable`
|
|
678
|
+
* when the target version is a delete tombstone.
|
|
679
|
+
*/
|
|
680
|
+
async rollback(type, name, toVersion, options = {}) {
|
|
681
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`;
|
|
682
|
+
const headers = {
|
|
683
|
+
...this.headers,
|
|
684
|
+
"Content-Type": "application/json"
|
|
566
685
|
};
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
686
|
+
if (options.actor) headers["X-Actor"] = options.actor;
|
|
687
|
+
const res = await this.fetchImpl(url, {
|
|
688
|
+
method: "POST",
|
|
689
|
+
headers,
|
|
690
|
+
body: JSON.stringify({
|
|
691
|
+
toVersion,
|
|
692
|
+
...options.message ? { message: options.message } : {}
|
|
693
|
+
})
|
|
574
694
|
});
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
const decodeValue = (value) => {
|
|
578
|
-
if (Array.isArray(value)) return value;
|
|
579
|
-
if (typeof value === "string") {
|
|
580
|
-
try {
|
|
581
|
-
const parsed = JSON.parse(value);
|
|
582
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
583
|
-
} catch {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
return [];
|
|
588
|
-
};
|
|
589
|
-
const upsert = async (items) => {
|
|
590
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
591
|
-
if (cachedRowId !== null) {
|
|
592
|
-
try {
|
|
593
|
-
await dataSource.update(resource, cachedRowId, { value: items, updated_at: now });
|
|
594
|
-
return;
|
|
595
|
-
} catch (updateError) {
|
|
596
|
-
cachedRowId = null;
|
|
597
|
-
onError("save", updateError);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const existing = await findExisting();
|
|
601
|
-
if (existing && existing.id !== void 0 && existing.id !== null) {
|
|
602
|
-
cachedRowId = existing.id;
|
|
603
|
-
await dataSource.update(resource, existing.id, { value: items, updated_at: now });
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
606
|
-
try {
|
|
607
|
-
const created = await dataSource.create(resource, {
|
|
608
|
-
user_id: userId,
|
|
609
|
-
key,
|
|
610
|
-
value: items,
|
|
611
|
-
updated_at: now
|
|
612
|
-
});
|
|
613
|
-
const newId = created?.id;
|
|
614
|
-
if (newId !== void 0 && newId !== null) cachedRowId = newId;
|
|
615
|
-
} catch (createError) {
|
|
616
|
-
const recovered = await findExisting();
|
|
617
|
-
if (recovered && recovered.id !== void 0 && recovered.id !== null) {
|
|
618
|
-
cachedRowId = recovered.id;
|
|
619
|
-
await dataSource.update(resource, recovered.id, { value: items, updated_at: now });
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
throw createError;
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
return {
|
|
626
|
-
async load() {
|
|
627
|
-
try {
|
|
628
|
-
const row = await findExisting();
|
|
629
|
-
if (!row) {
|
|
630
|
-
cachedRowId = null;
|
|
631
|
-
return [];
|
|
632
|
-
}
|
|
633
|
-
if (row.id !== void 0 && row.id !== null) cachedRowId = row.id;
|
|
634
|
-
return decodeValue(row.value);
|
|
635
|
-
} catch (error) {
|
|
636
|
-
onError("load", error);
|
|
637
|
-
return [];
|
|
638
|
-
}
|
|
639
|
-
},
|
|
640
|
-
async save(items) {
|
|
641
|
-
const run = saveChain.then(() => upsert(items)).catch((error) => {
|
|
642
|
-
onError("save", error);
|
|
643
|
-
});
|
|
644
|
-
saveChain = run;
|
|
645
|
-
return run;
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// src/integration.ts
|
|
651
|
-
var IntegrationManager = class {
|
|
652
|
-
constructor() {
|
|
653
|
-
__publicField(this, "integrations", /* @__PURE__ */ new Map());
|
|
695
|
+
if (!res.ok) throw await parseError(res);
|
|
696
|
+
return await res.json();
|
|
654
697
|
}
|
|
655
698
|
/**
|
|
656
|
-
*
|
|
699
|
+
* Compute a structured top-level key diff between two history versions
|
|
700
|
+
* (GET `/meta/:type/:name/diff?from=&to=`). Returns
|
|
701
|
+
* `{ added, removed, changed }` where each entry carries a `path` and
|
|
702
|
+
* the relevant value(s). Useful for "what changed in this draft?" and
|
|
703
|
+
* pre-rollback previews.
|
|
657
704
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
705
|
+
async diff(type, name, fromVersion, toVersion) {
|
|
706
|
+
const params = new URLSearchParams();
|
|
707
|
+
if (fromVersion !== void 0) params.set("from", String(fromVersion));
|
|
708
|
+
if (toVersion !== void 0) params.set("to", String(toVersion));
|
|
709
|
+
const qs = params.toString();
|
|
710
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ""}`;
|
|
711
|
+
const res = await this.fetchImpl(url, {
|
|
712
|
+
method: "GET",
|
|
713
|
+
headers: this.headers,
|
|
714
|
+
cache: "no-store"
|
|
715
|
+
});
|
|
716
|
+
if (!res.ok) throw await parseError(res);
|
|
717
|
+
return await res.json();
|
|
660
718
|
}
|
|
661
|
-
/**
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
719
|
+
/** Fetch the durable history (change log) for one metadata item. */
|
|
720
|
+
async history(type, name, options = {}) {
|
|
721
|
+
const params = new URLSearchParams();
|
|
722
|
+
if (options.sinceSeq !== void 0) params.set("sinceSeq", String(options.sinceSeq));
|
|
723
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
724
|
+
const qs = params.toString();
|
|
725
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/history${qs ? `?${qs}` : ""}`;
|
|
726
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
727
|
+
if (!res.ok) throw await parseError(res);
|
|
728
|
+
return await res.json();
|
|
666
729
|
}
|
|
667
730
|
/**
|
|
668
|
-
*
|
|
731
|
+
* Fetch the protection-audit trail for one metadata item
|
|
732
|
+
* (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
|
|
733
|
+
* delete/reset attempt — allowed, denied, or forced — so the
|
|
734
|
+
* Studio "审计日志 / Audit log" tab can render who tried what
|
|
735
|
+
* and whether a lock blocked it.
|
|
669
736
|
*/
|
|
670
|
-
|
|
671
|
-
|
|
737
|
+
async audit(type, name, options = {}) {
|
|
738
|
+
const params = new URLSearchParams();
|
|
739
|
+
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
740
|
+
const qs = params.toString();
|
|
741
|
+
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs ? `?${qs}` : ""}`;
|
|
742
|
+
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
743
|
+
if (!res.ok) throw await parseError(res);
|
|
744
|
+
return await res.json();
|
|
672
745
|
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
// src/errors.ts
|
|
749
|
+
var ObjectStackError = class extends Error {
|
|
673
750
|
/**
|
|
674
|
-
*
|
|
751
|
+
* Create a new ObjectStackError
|
|
752
|
+
*
|
|
753
|
+
* @param message - Human-readable error message
|
|
754
|
+
* @param code - Unique error code for programmatic handling
|
|
755
|
+
* @param statusCode - Optional HTTP status code
|
|
756
|
+
* @param details - Optional additional error details for debugging
|
|
675
757
|
*/
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
);
|
|
758
|
+
constructor(message, code, statusCode, details) {
|
|
759
|
+
super(message);
|
|
760
|
+
__publicField(this, "code", code);
|
|
761
|
+
__publicField(this, "statusCode", statusCode);
|
|
762
|
+
__publicField(this, "details", details);
|
|
763
|
+
this.name = "ObjectStackError";
|
|
764
|
+
if (Error.captureStackTrace) {
|
|
765
|
+
Error.captureStackTrace(this, this.constructor);
|
|
766
|
+
}
|
|
680
767
|
}
|
|
681
768
|
/**
|
|
682
|
-
*
|
|
683
|
-
* Returns results for each integration.
|
|
769
|
+
* Convert error to JSON for logging/debugging
|
|
684
770
|
*/
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
771
|
+
toJSON() {
|
|
772
|
+
return {
|
|
773
|
+
name: this.name,
|
|
774
|
+
message: this.message,
|
|
775
|
+
code: this.code,
|
|
776
|
+
statusCode: this.statusCode,
|
|
777
|
+
details: this.details,
|
|
778
|
+
stack: this.stack
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
var MetadataNotFoundError = class extends ObjectStackError {
|
|
783
|
+
constructor(objectName, details) {
|
|
784
|
+
super(
|
|
785
|
+
`Metadata not found for object: ${objectName}`,
|
|
786
|
+
"METADATA_NOT_FOUND",
|
|
787
|
+
404,
|
|
788
|
+
{ objectName, ...details }
|
|
789
|
+
);
|
|
790
|
+
this.name = "MetadataNotFoundError";
|
|
698
791
|
}
|
|
792
|
+
};
|
|
793
|
+
var BulkOperationError = class extends ObjectStackError {
|
|
699
794
|
/**
|
|
700
|
-
*
|
|
795
|
+
* Create a new BulkOperationError
|
|
796
|
+
*
|
|
797
|
+
* @param operation - The bulk operation that failed (create, update, delete)
|
|
798
|
+
* @param successCount - Number of successful operations
|
|
799
|
+
* @param failureCount - Number of failed operations
|
|
800
|
+
* @param errors - Array of individual errors
|
|
801
|
+
* @param details - Additional error details
|
|
701
802
|
*/
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (e instanceof TypeError) {
|
|
714
|
-
throw new Error(`Invalid webhook URL: ${url}`);
|
|
715
|
-
}
|
|
716
|
-
throw e;
|
|
717
|
-
}
|
|
718
|
-
await fetch(url, {
|
|
719
|
-
method: cfg.method,
|
|
720
|
-
headers: {
|
|
721
|
-
"Content-Type": "application/json",
|
|
722
|
-
...cfg.headers
|
|
723
|
-
},
|
|
724
|
-
body: JSON.stringify(payload)
|
|
725
|
-
});
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
case "slack": {
|
|
729
|
-
const cfg = integration.config;
|
|
730
|
-
const url = cfg.webhookUrl;
|
|
731
|
-
try {
|
|
732
|
-
const parsed = new URL(url);
|
|
733
|
-
if (parsed.protocol !== "https:") {
|
|
734
|
-
throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
|
|
735
|
-
}
|
|
736
|
-
} catch (e) {
|
|
737
|
-
if (e instanceof TypeError) {
|
|
738
|
-
throw new Error(`Invalid Slack webhook URL: ${url}`);
|
|
739
|
-
}
|
|
740
|
-
throw e;
|
|
741
|
-
}
|
|
742
|
-
await fetch(url, {
|
|
743
|
-
method: "POST",
|
|
744
|
-
headers: { "Content-Type": "application/json" },
|
|
745
|
-
body: JSON.stringify({
|
|
746
|
-
channel: cfg.channel,
|
|
747
|
-
username: cfg.username,
|
|
748
|
-
icon_emoji: cfg.iconEmoji,
|
|
749
|
-
text: JSON.stringify(payload)
|
|
750
|
-
})
|
|
751
|
-
});
|
|
752
|
-
break;
|
|
803
|
+
constructor(operation, successCount, failureCount, errors, details) {
|
|
804
|
+
super(
|
|
805
|
+
`Bulk ${operation} operation failed: ${successCount} succeeded, ${failureCount} failed`,
|
|
806
|
+
"BULK_OPERATION_ERROR",
|
|
807
|
+
500,
|
|
808
|
+
{
|
|
809
|
+
operation,
|
|
810
|
+
successCount,
|
|
811
|
+
failureCount,
|
|
812
|
+
errors,
|
|
813
|
+
...details
|
|
753
814
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
};
|
|
760
|
-
|
|
761
|
-
// src/metadata-client.ts
|
|
762
|
-
var META_PREFIX = "/meta";
|
|
763
|
-
var API_PREFIX = "/api/v1";
|
|
764
|
-
function buildBase(config) {
|
|
765
|
-
const trimmed = config.baseUrl.replace(/\/+$/, "");
|
|
766
|
-
const scoped = config.environmentId ? `${API_PREFIX}/environments/${encodeURIComponent(config.environmentId)}${META_PREFIX}` : `${API_PREFIX}${META_PREFIX}`;
|
|
767
|
-
if (/\/api\/v\d+$/i.test(trimmed)) {
|
|
768
|
-
const stripped = trimmed.replace(/\/api\/v\d+$/i, "");
|
|
769
|
-
const scopedNoApi = config.environmentId ? `${API_PREFIX}/environments/${encodeURIComponent(config.environmentId)}${META_PREFIX}` : `${API_PREFIX}${META_PREFIX}`;
|
|
770
|
-
return `${stripped}${scopedNoApi}`;
|
|
771
|
-
}
|
|
772
|
-
return `${trimmed}${scoped}`;
|
|
773
|
-
}
|
|
774
|
-
async function parseError(res) {
|
|
775
|
-
let body;
|
|
776
|
-
try {
|
|
777
|
-
body = await res.json();
|
|
778
|
-
} catch {
|
|
779
|
-
body = await res.text().catch(() => void 0);
|
|
780
|
-
}
|
|
781
|
-
const bodyObj = body && typeof body === "object" ? body : void 0;
|
|
782
|
-
const errNode = bodyObj?.error;
|
|
783
|
-
const message = (errNode && typeof errNode === "object" ? errNode.message : errNode) ?? bodyObj?.message ?? `Metadata request failed: ${res.status} ${res.statusText}`;
|
|
784
|
-
const err = new Error(String(message));
|
|
785
|
-
err.status = res.status;
|
|
786
|
-
err.body = body;
|
|
787
|
-
const details = errNode && typeof errNode === "object" ? errNode.details : void 0;
|
|
788
|
-
const issues = details?.issues ?? (errNode && typeof errNode === "object" ? errNode.issues : void 0) ?? bodyObj?.issues;
|
|
789
|
-
if (Array.isArray(issues)) err.issues = issues;
|
|
790
|
-
const code = details?.code ?? (errNode && typeof errNode === "object" ? errNode.code : void 0) ?? bodyObj?.code;
|
|
791
|
-
if (code != null) err.code = String(code);
|
|
792
|
-
return err;
|
|
793
|
-
}
|
|
794
|
-
var MetadataClient = class _MetadataClient {
|
|
795
|
-
constructor(config) {
|
|
796
|
-
__publicField(this, "base");
|
|
797
|
-
__publicField(this, "fetchImpl");
|
|
798
|
-
__publicField(this, "headers");
|
|
799
|
-
/** ADR-0037: when true, reads render the draft-overlaid world. */
|
|
800
|
-
__publicField(this, "previewDrafts");
|
|
801
|
-
this.base = buildBase(config);
|
|
802
|
-
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
803
|
-
this.headers = { Accept: "application/json", ...config.headers ?? {} };
|
|
804
|
-
this.previewDrafts = config.previewDrafts === true;
|
|
805
|
-
}
|
|
806
|
-
/** Update the client's environment scope at runtime. */
|
|
807
|
-
withEnvironment(environmentId) {
|
|
808
|
-
return new _MetadataClient({
|
|
809
|
-
baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
|
|
810
|
-
environmentId,
|
|
811
|
-
fetch: this.fetchImpl,
|
|
812
|
-
headers: this.headers,
|
|
813
|
-
previewDrafts: this.previewDrafts
|
|
814
|
-
});
|
|
815
|
+
);
|
|
816
|
+
__publicField(this, "successCount", successCount);
|
|
817
|
+
__publicField(this, "failureCount", failureCount);
|
|
818
|
+
__publicField(this, "errors", errors);
|
|
819
|
+
this.name = "BulkOperationError";
|
|
815
820
|
}
|
|
816
821
|
/**
|
|
817
|
-
*
|
|
818
|
-
* Same base/fetch/headers; used by the app-shell to switch the whole
|
|
819
|
-
* renderer tree into ADR-0037 preview mode off one URL flag.
|
|
822
|
+
* Get a summary of the bulk operation failure
|
|
820
823
|
*/
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const
|
|
824
|
-
return
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
824
|
+
getSummary() {
|
|
825
|
+
const total = this.successCount + this.failureCount;
|
|
826
|
+
const failureRate = total > 0 ? this.failureCount / total : 0;
|
|
827
|
+
return {
|
|
828
|
+
operation: this.details?.operation,
|
|
829
|
+
total,
|
|
830
|
+
successful: this.successCount,
|
|
831
|
+
failed: this.failureCount,
|
|
832
|
+
failureRate,
|
|
833
|
+
errors: this.errors
|
|
834
|
+
};
|
|
831
835
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
return [];
|
|
836
|
+
};
|
|
837
|
+
var ConnectionError = class extends ObjectStackError {
|
|
838
|
+
constructor(message, url, details, statusCode) {
|
|
839
|
+
super(
|
|
840
|
+
`Connection error: ${message}`,
|
|
841
|
+
"CONNECTION_ERROR",
|
|
842
|
+
statusCode || 503,
|
|
843
|
+
{ url, ...details }
|
|
844
|
+
);
|
|
845
|
+
__publicField(this, "url", url);
|
|
846
|
+
this.name = "ConnectionError";
|
|
844
847
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
if (Array.isArray(data)) return data;
|
|
856
|
-
if (data && Array.isArray(data.items)) {
|
|
857
|
-
return data.items;
|
|
858
|
-
}
|
|
859
|
-
return [];
|
|
848
|
+
};
|
|
849
|
+
var AuthenticationError = class extends ObjectStackError {
|
|
850
|
+
constructor(message = "Authentication failed", details, statusCode) {
|
|
851
|
+
super(
|
|
852
|
+
message,
|
|
853
|
+
"AUTHENTICATION_ERROR",
|
|
854
|
+
statusCode || 401,
|
|
855
|
+
details
|
|
856
|
+
);
|
|
857
|
+
this.name = "AuthenticationError";
|
|
860
858
|
}
|
|
859
|
+
};
|
|
860
|
+
var ValidationError = class extends ObjectStackError {
|
|
861
861
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
862
|
+
* Create a new ValidationError
|
|
863
|
+
*
|
|
864
|
+
* @param message - Human-readable error message
|
|
865
|
+
* @param field - The field that failed validation (optional)
|
|
866
|
+
* @param validationErrors - Array of validation error details
|
|
867
|
+
* @param details - Additional error details
|
|
868
868
|
*/
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
869
|
+
constructor(message, field, validationErrors, details) {
|
|
870
|
+
super(
|
|
871
|
+
message,
|
|
872
|
+
"VALIDATION_ERROR",
|
|
873
|
+
400,
|
|
874
|
+
{
|
|
875
|
+
field,
|
|
876
|
+
validationErrors,
|
|
877
|
+
...details
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
__publicField(this, "field", field);
|
|
881
|
+
__publicField(this, "validationErrors", validationErrors);
|
|
882
|
+
this.name = "ValidationError";
|
|
880
883
|
}
|
|
881
884
|
/**
|
|
882
|
-
* Get
|
|
883
|
-
* (matching the framework REST handler which calls `res.json(item)`).
|
|
884
|
-
* Returns `null` on 404 to keep the call-site ergonomic.
|
|
885
|
+
* Get all validation errors as a formatted list
|
|
885
886
|
*/
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
887
|
+
getValidationErrors() {
|
|
888
|
+
return this.validationErrors || [];
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
function createErrorFromResponse(response, context) {
|
|
892
|
+
const status = response?.status || response?.statusCode || 500;
|
|
893
|
+
const message = response?.message || response?.statusText || "Unknown error";
|
|
894
|
+
const details = {
|
|
895
|
+
context,
|
|
896
|
+
response: {
|
|
897
|
+
status,
|
|
898
|
+
data: response?.data,
|
|
899
|
+
headers: response?.headers
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
switch (status) {
|
|
903
|
+
case 401:
|
|
904
|
+
return new AuthenticationError(message, details, 401);
|
|
905
|
+
case 403:
|
|
906
|
+
return new AuthenticationError(message, details, 403);
|
|
907
|
+
case 404:
|
|
908
|
+
if (context?.includes("metadata") || context?.includes("schema") || context?.includes("getObjectSchema")) {
|
|
909
|
+
const objectName = extractObjectName(context);
|
|
910
|
+
return new MetadataNotFoundError(objectName, details);
|
|
911
|
+
}
|
|
912
|
+
return new ObjectStackError(message, "NOT_FOUND", 404, details);
|
|
913
|
+
case 400:
|
|
914
|
+
return new ValidationError(message, void 0, response?.data?.errors, details);
|
|
915
|
+
case 503:
|
|
916
|
+
return new ConnectionError(message, response?.config?.url, details, 503);
|
|
917
|
+
case 504:
|
|
918
|
+
return new ConnectionError(message, response?.config?.url, details, 504);
|
|
919
|
+
default:
|
|
920
|
+
return new ObjectStackError(message, "UNKNOWN_ERROR", status, details);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function extractObjectName(context) {
|
|
924
|
+
if (!context) return "unknown";
|
|
925
|
+
const match = context.match(/\(([^)]+)\)/);
|
|
926
|
+
return match ? match[1] : "unknown";
|
|
927
|
+
}
|
|
928
|
+
function isObjectStackError(error) {
|
|
929
|
+
return error instanceof ObjectStackError;
|
|
930
|
+
}
|
|
931
|
+
function isErrorType(error, errorClass) {
|
|
932
|
+
return error instanceof errorClass;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// src/cloud.ts
|
|
936
|
+
var CloudOperations = class {
|
|
937
|
+
constructor(getClient) {
|
|
938
|
+
__publicField(this, "getClient", getClient);
|
|
897
939
|
}
|
|
898
940
|
/**
|
|
899
|
-
*
|
|
900
|
-
* `null` when there is no draft pending. Draft reads do NOT fall
|
|
901
|
-
* back to the published overlay or the artifact registry — a `null`
|
|
902
|
-
* unambiguously means "nothing to publish".
|
|
903
|
-
*
|
|
904
|
-
* Note: the framework wraps draft responses in an envelope
|
|
905
|
-
* `{ type, name, item }` (matching `getMetaItem`); callers should
|
|
906
|
-
* read `.item` to get the body. The legacy `get()` returns the
|
|
907
|
-
* unwrapped body, so this method preserves that asymmetry by
|
|
908
|
-
* returning whatever the server sent.
|
|
941
|
+
* Deploy an application to the cloud.
|
|
909
942
|
*/
|
|
910
|
-
async
|
|
911
|
-
|
|
943
|
+
async deploy(appId, config) {
|
|
944
|
+
const client = this.getClient();
|
|
945
|
+
const result = await client.cloud?.deploy?.(appId, config);
|
|
946
|
+
return result ?? { deploymentId: `deploy-${Date.now()}`, status: "pending" };
|
|
912
947
|
}
|
|
913
948
|
/**
|
|
914
|
-
*
|
|
915
|
-
* item payload and the `{ item: ... }` / `{ metadata: ... }`
|
|
916
|
-
* envelopes; we send bare for consistency. Pass `mode: 'draft'` to
|
|
917
|
-
* stage the change without publishing (Studio's "Save" button).
|
|
949
|
+
* Get deployment status.
|
|
918
950
|
*/
|
|
919
|
-
async
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
|
|
924
|
-
const qs = params.length ? `?${params.join("&")}` : "";
|
|
925
|
-
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
926
|
-
const headers = {
|
|
927
|
-
...this.headers,
|
|
928
|
-
"Content-Type": "application/json"
|
|
929
|
-
};
|
|
930
|
-
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
931
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
932
|
-
const res = await this.fetchImpl(url, {
|
|
933
|
-
method: "PUT",
|
|
934
|
-
headers,
|
|
935
|
-
body: JSON.stringify(item)
|
|
936
|
-
});
|
|
937
|
-
if (!res.ok) throw await parseError(res);
|
|
938
|
-
return await res.json();
|
|
951
|
+
async getDeploymentStatus(deploymentId) {
|
|
952
|
+
const client = this.getClient();
|
|
953
|
+
const result = await client.cloud?.getDeployment?.(deploymentId);
|
|
954
|
+
return result ?? { status: "unknown" };
|
|
939
955
|
}
|
|
940
956
|
/**
|
|
941
|
-
*
|
|
942
|
-
* `(type, name)` into the active overlay and drops the draft. Works for ANY
|
|
943
|
-
* draft, including ones with no `packageId` binding (which the package-scoped
|
|
944
|
-
* `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
|
|
945
|
-
* exact set returned by {@link listDrafts} without needing a package.
|
|
946
|
-
*
|
|
947
|
-
* Returns the server result. For `seed` drafts the protocol also materializes
|
|
948
|
-
* the rows and reports under `seedApplied` — a data problem never fails the
|
|
949
|
-
* publish, so callers should check `seedApplied?.success` and warn the user
|
|
950
|
-
* rather than assume the data went live.
|
|
957
|
+
* Search marketplace entries.
|
|
951
958
|
*/
|
|
952
|
-
async
|
|
953
|
-
const
|
|
954
|
-
const
|
|
955
|
-
|
|
956
|
-
headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
|
|
957
|
-
body: "{}"
|
|
958
|
-
});
|
|
959
|
-
if (!res.ok) throw await parseError(res);
|
|
960
|
-
const body = await res.json().catch(() => ({}));
|
|
961
|
-
const inner = body?.data && typeof body.data === "object" ? body.data : body;
|
|
962
|
-
return inner;
|
|
959
|
+
async searchMarketplace(query, category) {
|
|
960
|
+
const client = this.getClient();
|
|
961
|
+
const result = await client.cloud?.marketplace?.search?.({ query, category });
|
|
962
|
+
return result?.items ?? [];
|
|
963
963
|
}
|
|
964
964
|
/**
|
|
965
|
-
*
|
|
966
|
-
* `code` (the artifact / fallback default), `overlay` (the saved
|
|
967
|
-
* customisation, if any), and `effective` (what the runtime sees).
|
|
965
|
+
* Install a marketplace plugin.
|
|
968
966
|
*/
|
|
969
|
-
async
|
|
970
|
-
const
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
967
|
+
async installPlugin(pluginId) {
|
|
968
|
+
const client = this.getClient();
|
|
969
|
+
const result = await client.cloud?.marketplace?.install?.(pluginId);
|
|
970
|
+
return result ?? { success: false };
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
// src/contracts.ts
|
|
975
|
+
function validatePluginContract(contract) {
|
|
976
|
+
const errors = [];
|
|
977
|
+
const warnings = [];
|
|
978
|
+
if (!contract.name || contract.name.trim().length === 0) {
|
|
979
|
+
errors.push({ field: "name", message: "Plugin name is required", code: "MISSING_NAME" });
|
|
980
|
+
}
|
|
981
|
+
if (!contract.version || !/^\d+\.\d+\.\d+/.test(contract.version)) {
|
|
982
|
+
errors.push({ field: "version", message: "Valid semver version is required", code: "INVALID_VERSION" });
|
|
983
|
+
}
|
|
984
|
+
if (!contract.exports || contract.exports.length === 0) {
|
|
985
|
+
errors.push({ field: "exports", message: "At least one export is required", code: "NO_EXPORTS" });
|
|
986
|
+
}
|
|
987
|
+
if (contract.exports) {
|
|
988
|
+
const validTypes = ["component", "hook", "utility", "provider"];
|
|
989
|
+
for (const exp of contract.exports) {
|
|
990
|
+
if (!exp.name) {
|
|
991
|
+
errors.push({ field: "exports.name", message: "Export name is required", code: "MISSING_EXPORT_NAME" });
|
|
992
|
+
}
|
|
993
|
+
if (!validTypes.includes(exp.type)) {
|
|
994
|
+
errors.push({ field: "exports.type", message: `Invalid export type: ${exp.type}`, code: "INVALID_EXPORT_TYPE" });
|
|
995
|
+
}
|
|
986
996
|
}
|
|
987
|
-
return {
|
|
988
|
-
code: body.code ?? null,
|
|
989
|
-
overlay: body.overlay ?? null,
|
|
990
|
-
overlayScope: body.overlayScope ?? null,
|
|
991
|
-
effective: body.effective ?? null,
|
|
992
|
-
...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
|
|
993
|
-
// ADR-0010 Phase 4 — pass through the metadata-protection envelope so
|
|
994
|
-
// the editor can render lock affordances without a second round trip.
|
|
995
|
-
...body.lock !== void 0 ? { lock: body.lock } : {},
|
|
996
|
-
...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
|
|
997
|
-
...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
|
|
998
|
-
...body.lockDocsUrl !== void 0 ? { lockDocsUrl: body.lockDocsUrl } : {},
|
|
999
|
-
...body.provenance !== void 0 ? { provenance: body.provenance } : {},
|
|
1000
|
-
...body.packageId !== void 0 ? { packageId: body.packageId } : {},
|
|
1001
|
-
...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
|
|
1002
|
-
...body.editable !== void 0 ? { editable: body.editable } : {},
|
|
1003
|
-
...body.deletable !== void 0 ? { deletable: body.deletable } : {},
|
|
1004
|
-
...body.resettable !== void 0 ? { resettable: body.resettable } : {}
|
|
1005
|
-
};
|
|
1006
997
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
998
|
+
if (!contract.permissions || contract.permissions.length === 0) {
|
|
999
|
+
warnings.push("No permissions declared \u2014 plugin will have minimal access");
|
|
1000
|
+
}
|
|
1001
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
1002
|
+
}
|
|
1003
|
+
function generateContractManifest(contract) {
|
|
1004
|
+
return {
|
|
1005
|
+
$schema: "https://objectui.org/schemas/plugin-contract-v1.json",
|
|
1006
|
+
name: contract.name,
|
|
1007
|
+
version: contract.version,
|
|
1008
|
+
peerDependencies: contract.peerDependencies ?? {},
|
|
1009
|
+
exports: contract.exports.map((exp) => ({
|
|
1010
|
+
name: exp.name,
|
|
1011
|
+
type: exp.type,
|
|
1012
|
+
description: exp.description ?? ""
|
|
1013
|
+
})),
|
|
1014
|
+
permissions: contract.permissions ?? [],
|
|
1015
|
+
api: contract.api ?? {},
|
|
1016
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/userState.ts
|
|
1021
|
+
var DEFAULT_RESOURCE = "sys_user_preference";
|
|
1022
|
+
function createObjectStackUserStateAdapter(options) {
|
|
1023
|
+
const {
|
|
1024
|
+
dataSource,
|
|
1025
|
+
userId,
|
|
1026
|
+
key,
|
|
1027
|
+
resource = DEFAULT_RESOURCE,
|
|
1028
|
+
onError = () => {
|
|
1026
1029
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1030
|
+
} = options;
|
|
1031
|
+
let cachedRowId = null;
|
|
1032
|
+
let saveChain = Promise.resolve();
|
|
1033
|
+
const findExisting = async () => {
|
|
1034
|
+
const params = {
|
|
1035
|
+
$filter: {
|
|
1036
|
+
user_id: userId,
|
|
1037
|
+
key
|
|
1038
|
+
},
|
|
1039
|
+
$top: 1
|
|
1035
1040
|
};
|
|
1041
|
+
const result = await dataSource.find(resource, params);
|
|
1042
|
+
const rows = result?.data ?? [];
|
|
1043
|
+
const match = rows.find((r) => {
|
|
1044
|
+
if (!r) return false;
|
|
1045
|
+
const userMismatch = r.user_id !== void 0 && r.user_id !== userId;
|
|
1046
|
+
const keyMismatch = r.key !== void 0 && r.key !== key;
|
|
1047
|
+
return !userMismatch && !keyMismatch;
|
|
1048
|
+
});
|
|
1049
|
+
return match ?? null;
|
|
1050
|
+
};
|
|
1051
|
+
const decodeValue = (value) => {
|
|
1052
|
+
if (Array.isArray(value)) return value;
|
|
1053
|
+
if (typeof value === "string") {
|
|
1054
|
+
try {
|
|
1055
|
+
const parsed = JSON.parse(value);
|
|
1056
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1057
|
+
} catch {
|
|
1058
|
+
return [];
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return [];
|
|
1062
|
+
};
|
|
1063
|
+
const upsert = async (items) => {
|
|
1064
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1065
|
+
if (cachedRowId !== null) {
|
|
1066
|
+
try {
|
|
1067
|
+
await dataSource.update(resource, cachedRowId, { value: items, updated_at: now });
|
|
1068
|
+
return;
|
|
1069
|
+
} catch (updateError) {
|
|
1070
|
+
cachedRowId = null;
|
|
1071
|
+
onError("save", updateError);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const existing = await findExisting();
|
|
1075
|
+
if (existing && existing.id !== void 0 && existing.id !== null) {
|
|
1076
|
+
cachedRowId = existing.id;
|
|
1077
|
+
await dataSource.update(resource, existing.id, { value: items, updated_at: now });
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
const created = await dataSource.create(resource, {
|
|
1082
|
+
user_id: userId,
|
|
1083
|
+
key,
|
|
1084
|
+
value: items,
|
|
1085
|
+
updated_at: now
|
|
1086
|
+
});
|
|
1087
|
+
const newId = created?.id;
|
|
1088
|
+
if (newId !== void 0 && newId !== null) cachedRowId = newId;
|
|
1089
|
+
} catch (createError) {
|
|
1090
|
+
const recovered = await findExisting();
|
|
1091
|
+
if (recovered && recovered.id !== void 0 && recovered.id !== null) {
|
|
1092
|
+
cachedRowId = recovered.id;
|
|
1093
|
+
await dataSource.update(resource, recovered.id, { value: items, updated_at: now });
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
throw createError;
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
return {
|
|
1100
|
+
async load() {
|
|
1101
|
+
try {
|
|
1102
|
+
const row = await findExisting();
|
|
1103
|
+
if (!row) {
|
|
1104
|
+
cachedRowId = null;
|
|
1105
|
+
return [];
|
|
1106
|
+
}
|
|
1107
|
+
if (row.id !== void 0 && row.id !== null) cachedRowId = row.id;
|
|
1108
|
+
return decodeValue(row.value);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
onError("load", error);
|
|
1111
|
+
return [];
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
async save(items) {
|
|
1115
|
+
const run = saveChain.then(() => upsert(items)).catch((error) => {
|
|
1116
|
+
onError("save", error);
|
|
1117
|
+
});
|
|
1118
|
+
saveChain = run;
|
|
1119
|
+
return run;
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/integration.ts
|
|
1125
|
+
var IntegrationManager = class {
|
|
1126
|
+
constructor() {
|
|
1127
|
+
__publicField(this, "integrations", /* @__PURE__ */ new Map());
|
|
1036
1128
|
}
|
|
1037
1129
|
/**
|
|
1038
|
-
*
|
|
1039
|
-
* for pre-delete impact analysis: "Are any views pointing at this
|
|
1040
|
-
* object before I drop it?".
|
|
1130
|
+
* Register a new integration.
|
|
1041
1131
|
*/
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
1045
|
-
if (res.status === 404) return [];
|
|
1046
|
-
if (!res.ok) throw await parseError(res);
|
|
1047
|
-
const data = await res.json();
|
|
1048
|
-
return Array.isArray(data) ? data : data?.items ?? [];
|
|
1132
|
+
register(id, config) {
|
|
1133
|
+
this.integrations.set(id, config);
|
|
1049
1134
|
}
|
|
1050
1135
|
/**
|
|
1051
|
-
*
|
|
1052
|
-
* Idempotent: returns the result even when no overlay row existed.
|
|
1053
|
-
* Pass `state: 'draft'` to discard the pending draft only (keeps the
|
|
1054
|
-
* published overlay intact) — useful for a Studio "Discard draft" button.
|
|
1136
|
+
* Remove an integration.
|
|
1055
1137
|
*/
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
|
|
1059
|
-
const headers = { ...this.headers };
|
|
1060
|
-
if (options.ifMatch) headers["If-Match"] = options.ifMatch;
|
|
1061
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1062
|
-
const res = await this.fetchImpl(url, { method: "DELETE", headers });
|
|
1063
|
-
if (!res.ok) throw await parseError(res);
|
|
1064
|
-
return await res.json();
|
|
1138
|
+
unregister(id) {
|
|
1139
|
+
this.integrations.delete(id);
|
|
1065
1140
|
}
|
|
1066
1141
|
/**
|
|
1067
|
-
*
|
|
1068
|
-
* (POST `/meta/:type/:name/publish`). Returns
|
|
1069
|
-
* `{ success, version, seq, message }`. Throws a `404 no_draft` if
|
|
1070
|
-
* nothing is pending and `409 metadata_conflict` if the published
|
|
1071
|
-
* overlay moved while the draft was sitting.
|
|
1142
|
+
* Get all registered integrations.
|
|
1072
1143
|
*/
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
const headers = {
|
|
1076
|
-
...this.headers,
|
|
1077
|
-
"Content-Type": "application/json"
|
|
1078
|
-
};
|
|
1079
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1080
|
-
const res = await this.fetchImpl(url, {
|
|
1081
|
-
method: "POST",
|
|
1082
|
-
headers,
|
|
1083
|
-
body: JSON.stringify(options.message ? { message: options.message } : {})
|
|
1084
|
-
});
|
|
1085
|
-
if (!res.ok) throw await parseError(res);
|
|
1086
|
-
return await res.json();
|
|
1144
|
+
getAll() {
|
|
1145
|
+
return new Map(this.integrations);
|
|
1087
1146
|
}
|
|
1088
1147
|
/**
|
|
1089
|
-
*
|
|
1090
|
-
* The server reads the history row at `toVersion`, writes its body back
|
|
1091
|
-
* as the active overlay with `operation_type='revert'`. Returns
|
|
1092
|
-
* `{ success, version, seq, restoredFromVersion, message }`. Throws
|
|
1093
|
-
* `404 version_not_found` for unknown versions and `409 version_not_restorable`
|
|
1094
|
-
* when the target version is a delete tombstone.
|
|
1148
|
+
* Get integrations that match a specific event.
|
|
1095
1149
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
"Content-Type": "application/json"
|
|
1101
|
-
};
|
|
1102
|
-
if (options.actor) headers["X-Actor"] = options.actor;
|
|
1103
|
-
const res = await this.fetchImpl(url, {
|
|
1104
|
-
method: "POST",
|
|
1105
|
-
headers,
|
|
1106
|
-
body: JSON.stringify({
|
|
1107
|
-
toVersion,
|
|
1108
|
-
...options.message ? { message: options.message } : {}
|
|
1109
|
-
})
|
|
1110
|
-
});
|
|
1111
|
-
if (!res.ok) throw await parseError(res);
|
|
1112
|
-
return await res.json();
|
|
1150
|
+
getForEvent(event) {
|
|
1151
|
+
return Array.from(this.integrations.values()).filter(
|
|
1152
|
+
(integration) => integration.enabled && integration.triggers?.some((t) => t.event === event)
|
|
1153
|
+
);
|
|
1113
1154
|
}
|
|
1114
1155
|
/**
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
* `{ added, removed, changed }` where each entry carries a `path` and
|
|
1118
|
-
* the relevant value(s). Useful for "what changed in this draft?" and
|
|
1119
|
-
* pre-rollback previews.
|
|
1156
|
+
* Dispatch an event to all matching integrations.
|
|
1157
|
+
* Returns results for each integration.
|
|
1120
1158
|
*/
|
|
1121
|
-
async
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
return
|
|
1134
|
-
}
|
|
1135
|
-
/** Fetch the durable history (change log) for one metadata item. */
|
|
1136
|
-
async history(type, name, options = {}) {
|
|
1137
|
-
const params = new URLSearchParams();
|
|
1138
|
-
if (options.sinceSeq !== void 0) params.set("sinceSeq", String(options.sinceSeq));
|
|
1139
|
-
if (options.limit !== void 0) params.set("limit", String(options.limit));
|
|
1140
|
-
const qs = params.toString();
|
|
1141
|
-
const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/history${qs ? `?${qs}` : ""}`;
|
|
1142
|
-
const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
|
|
1143
|
-
if (!res.ok) throw await parseError(res);
|
|
1144
|
-
return await res.json();
|
|
1159
|
+
async dispatch(event, payload) {
|
|
1160
|
+
const matching = this.getForEvent(event);
|
|
1161
|
+
const results = [];
|
|
1162
|
+
for (const [id, integration] of this.integrations) {
|
|
1163
|
+
if (!matching.includes(integration)) continue;
|
|
1164
|
+
try {
|
|
1165
|
+
await this.send(integration, payload);
|
|
1166
|
+
results.push({ id, success: true });
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
results.push({ id, success: false, error: err.message });
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return results;
|
|
1145
1172
|
}
|
|
1146
1173
|
/**
|
|
1147
|
-
*
|
|
1148
|
-
* (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
|
|
1149
|
-
* delete/reset attempt — allowed, denied, or forced — so the
|
|
1150
|
-
* Studio "审计日志 / Audit log" tab can render who tried what
|
|
1151
|
-
* and whether a lock blocked it.
|
|
1174
|
+
* Send payload to a specific integration.
|
|
1152
1175
|
*/
|
|
1153
|
-
async
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1176
|
+
async send(integration, payload) {
|
|
1177
|
+
switch (integration.provider) {
|
|
1178
|
+
case "webhook": {
|
|
1179
|
+
const cfg = integration.config;
|
|
1180
|
+
const url = cfg.url;
|
|
1181
|
+
try {
|
|
1182
|
+
const parsed = new URL(url);
|
|
1183
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
1184
|
+
throw new Error(`Invalid URL protocol: ${parsed.protocol}`);
|
|
1185
|
+
}
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
if (e instanceof TypeError) {
|
|
1188
|
+
throw new Error(`Invalid webhook URL: ${url}`);
|
|
1189
|
+
}
|
|
1190
|
+
throw e;
|
|
1191
|
+
}
|
|
1192
|
+
await fetch(url, {
|
|
1193
|
+
method: cfg.method,
|
|
1194
|
+
headers: {
|
|
1195
|
+
"Content-Type": "application/json",
|
|
1196
|
+
...cfg.headers
|
|
1197
|
+
},
|
|
1198
|
+
body: JSON.stringify(payload)
|
|
1199
|
+
});
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
case "slack": {
|
|
1203
|
+
const cfg = integration.config;
|
|
1204
|
+
const url = cfg.webhookUrl;
|
|
1205
|
+
try {
|
|
1206
|
+
const parsed = new URL(url);
|
|
1207
|
+
if (parsed.protocol !== "https:") {
|
|
1208
|
+
throw new Error(`Invalid Slack webhook URL protocol: ${parsed.protocol}`);
|
|
1209
|
+
}
|
|
1210
|
+
} catch (e) {
|
|
1211
|
+
if (e instanceof TypeError) {
|
|
1212
|
+
throw new Error(`Invalid Slack webhook URL: ${url}`);
|
|
1213
|
+
}
|
|
1214
|
+
throw e;
|
|
1215
|
+
}
|
|
1216
|
+
await fetch(url, {
|
|
1217
|
+
method: "POST",
|
|
1218
|
+
headers: { "Content-Type": "application/json" },
|
|
1219
|
+
body: JSON.stringify({
|
|
1220
|
+
channel: cfg.channel,
|
|
1221
|
+
username: cfg.username,
|
|
1222
|
+
icon_emoji: cfg.iconEmoji,
|
|
1223
|
+
text: JSON.stringify(payload)
|
|
1224
|
+
})
|
|
1225
|
+
});
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
// Email and other providers would require server-side implementation
|
|
1229
|
+
default:
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1161
1232
|
}
|
|
1162
1233
|
};
|
|
1163
1234
|
|
|
@@ -1398,6 +1469,16 @@ async function getSharedDiscovery(baseUrl, fetcher) {
|
|
|
1398
1469
|
function clearSharedDiscoveryCache() {
|
|
1399
1470
|
discoveryCache.clear();
|
|
1400
1471
|
}
|
|
1472
|
+
function readTransactionalBatchCapability(discovery) {
|
|
1473
|
+
const caps = discovery?.capabilities;
|
|
1474
|
+
if (!caps || typeof caps !== "object") return void 0;
|
|
1475
|
+
const value = caps.transactionalBatch;
|
|
1476
|
+
if (typeof value === "boolean") return value;
|
|
1477
|
+
if (value && typeof value === "object" && typeof value.enabled === "boolean") {
|
|
1478
|
+
return value.enabled;
|
|
1479
|
+
}
|
|
1480
|
+
return void 0;
|
|
1481
|
+
}
|
|
1401
1482
|
function is404Error(error) {
|
|
1402
1483
|
if (!error || typeof error !== "object") return false;
|
|
1403
1484
|
const err = error;
|
|
@@ -1498,6 +1579,21 @@ var ObjectStackAdapter = class {
|
|
|
1498
1579
|
// so optional collections like sys_presence don't hammer the server with
|
|
1499
1580
|
// failing requests on every record open / panel render.
|
|
1500
1581
|
__publicField(this, "missingResources", /* @__PURE__ */ new Set());
|
|
1582
|
+
// Set once the server has told us it can't do a cross-object transactional
|
|
1583
|
+
// batch (the client SDK's data.batchTransaction threw HTTP 404/405/501). After
|
|
1584
|
+
// that, batchTransaction skips the SDK call and serves every call via the
|
|
1585
|
+
// client-side emulation — the non-atomic fallback lives HERE, isolated to the
|
|
1586
|
+
// one adapter that has to cope with a backend lacking server atomicity (#2679).
|
|
1587
|
+
__publicField(this, "batchUnsupported", false);
|
|
1588
|
+
// The server's declared cross-object atomic-batch capability, read from
|
|
1589
|
+
// discovery at connect() (framework #3298 / objectui #2693). `true` → the
|
|
1590
|
+
// backend GUARANTEES an atomic `/batch`, so batchTransaction trusts it and
|
|
1591
|
+
// never degrades to the non-atomic emulation (any failure surfaces as a real
|
|
1592
|
+
// error). `false` or `undefined` (capability absent → backend predates #3298)
|
|
1593
|
+
// → keep the legacy runtime-probe + emulation fallback so a save is still
|
|
1594
|
+
// possible; dropping it there would turn "saves, less safe" into "no save
|
|
1595
|
+
// path" on older backends (#2679 compatibility constraint).
|
|
1596
|
+
__publicField(this, "atomicBatchCapability");
|
|
1501
1597
|
// Subscribers registered via onMutation(). Emitted after each successful
|
|
1502
1598
|
// create/update/delete so data-bound views (ListView, ObjectView, kanban,
|
|
1503
1599
|
// calendar) auto-refresh — the interface ListView relies on to reflect
|
|
@@ -1536,6 +1632,7 @@ var ObjectStackAdapter = class {
|
|
|
1536
1632
|
return body && typeof body.success === "boolean" && "data" in body ? body.data : body;
|
|
1537
1633
|
});
|
|
1538
1634
|
this.client.discoveryInfo = data;
|
|
1635
|
+
this.atomicBatchCapability = readTransactionalBatchCapability(data);
|
|
1539
1636
|
this.connected = true;
|
|
1540
1637
|
this.reconnectAttempts = 0;
|
|
1541
1638
|
this.setConnectionState("connected");
|
|
@@ -1885,33 +1982,84 @@ var ObjectStackAdapter = class {
|
|
|
1885
1982
|
* @returns Promise resolving to array of results
|
|
1886
1983
|
*/
|
|
1887
1984
|
/**
|
|
1888
|
-
* Cross-object transactional batch (ObjectStack #1604
|
|
1889
|
-
* in ONE server transaction — commit all or roll back
|
|
1890
|
-
* `{ $ref: <earlier op index> }` resolves to that op's
|
|
1891
|
-
* can reference its parent created earlier in the same
|
|
1985
|
+
* Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4).
|
|
1986
|
+
* Runs the operations in ONE server transaction — commit all or roll back
|
|
1987
|
+
* all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
|
|
1988
|
+
* created id, so a child can reference its parent created earlier in the same
|
|
1989
|
+
* batch (master-detail).
|
|
1990
|
+
*
|
|
1991
|
+
* Transport: the published `@objectstack/client` SDK method
|
|
1992
|
+
* `data.batchTransaction` (framework #3271; shipped since client v16, our
|
|
1993
|
+
* dependency floor). Per AGENTS.md §7 data always flows through the client —
|
|
1994
|
+
* never a hand-rolled `fetch('/api/v1/batch')`.
|
|
1995
|
+
*
|
|
1996
|
+
* Fallback decision — declarative capability negotiation (framework #3298 /
|
|
1997
|
+
* objectui #2693). At connect() we read `capabilities.transactionalBatch`
|
|
1998
|
+
* from discovery:
|
|
1999
|
+
* - Declared `true` → the backend GUARANTEES atomicity (declared ===
|
|
2000
|
+
* enforced). We TRUST it: any batch failure — including 404/405/501 —
|
|
2001
|
+
* surfaces as a real error. No non-atomic client-side compensation. This
|
|
2002
|
+
* is the path modern backends take.
|
|
2003
|
+
* - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
|
|
2004
|
+
* server atomicity, so we keep the legacy behaviour: on 404/405 (no
|
|
2005
|
+
* endpoint) or 501 (runtime without transactions) degrade to the
|
|
2006
|
+
* client-side, NON-atomic {@link emulateBatchTransaction} so a save is
|
|
2007
|
+
* still possible. Removing that here would regress older backends from
|
|
2008
|
+
* "saves, less safe" to "no save path" (#2679 compatibility constraint).
|
|
2009
|
+
* The non-atomic fallback stays isolated to THIS adapter.
|
|
1892
2010
|
*/
|
|
1893
2011
|
async batchTransaction(operations) {
|
|
1894
|
-
|
|
1895
|
-
const
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2012
|
+
await this.connect();
|
|
2013
|
+
const guaranteed = this.atomicBatchCapability === true;
|
|
2014
|
+
if (!guaranteed && this.batchUnsupported) {
|
|
2015
|
+
return emulateBatchTransaction(this, operations);
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
const payload = await this.client.data.batchTransaction(operations);
|
|
2019
|
+
this.emitBatchMutations(operations, payload?.results);
|
|
2020
|
+
return payload;
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
const status = this.errorStatusOf(err);
|
|
2023
|
+
if (!guaranteed && this.batchStatusUnsupported(status)) {
|
|
2024
|
+
return this.fallbackToEmulation(operations, status);
|
|
2025
|
+
}
|
|
2026
|
+
throw err;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
/** True for statuses that mean "this backend can't do a transactional batch". */
|
|
2030
|
+
batchStatusUnsupported(status) {
|
|
2031
|
+
return status === 404 || status === 405 || status === 501;
|
|
2032
|
+
}
|
|
2033
|
+
/** Best-effort HTTP status extraction from a thrown SDK/client error. */
|
|
2034
|
+
errorStatusOf(err) {
|
|
2035
|
+
if (!err || typeof err !== "object") return void 0;
|
|
2036
|
+
const e = err;
|
|
2037
|
+
const s = e.httpStatus ?? e.status ?? e.statusCode;
|
|
2038
|
+
return typeof s === "number" ? s : void 0;
|
|
2039
|
+
}
|
|
2040
|
+
/** Mark the endpoint unsupported (warn once) and serve via emulation. */
|
|
2041
|
+
fallbackToEmulation(operations, status) {
|
|
2042
|
+
if (!this.batchUnsupported) {
|
|
2043
|
+
this.batchUnsupported = true;
|
|
2044
|
+
console.warn(
|
|
2045
|
+
`ObjectStackAdapter: POST /api/v1/batch unavailable (HTTP ${status ?? "?"}) \u2014 falling back to non-atomic client-side batch emulation. Cross-object saves on this backend are best-effort, not transactional.`
|
|
1911
2046
|
);
|
|
1912
2047
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
2048
|
+
return emulateBatchTransaction(this, operations);
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* Emit one MutationEvent per committed operation so the invalidation bus
|
|
2052
|
+
* (#2269) sees writes that went through /batch exactly like single
|
|
2053
|
+
* create/update/delete calls — master-detail ModalForm saves otherwise leave
|
|
2054
|
+
* related lists and count badges stale (#2582). `results` is index-aligned
|
|
2055
|
+
* with `operations`; creates take id/record from the server echo.
|
|
2056
|
+
*
|
|
2057
|
+
* Only called on the server-committed paths. The emulation branch drives the
|
|
2058
|
+
* adapter's own create/update/delete primitives, which already emit — so it
|
|
2059
|
+
* must NOT be routed through here, or events would double-fire.
|
|
2060
|
+
*/
|
|
2061
|
+
emitBatchMutations(operations, rawResults) {
|
|
2062
|
+
const results = Array.isArray(rawResults) ? rawResults : [];
|
|
1915
2063
|
operations.forEach((op, i) => {
|
|
1916
2064
|
const action = op.action ?? "create";
|
|
1917
2065
|
const echo = results[i];
|
|
@@ -1923,7 +2071,6 @@ var ObjectStackAdapter = class {
|
|
|
1923
2071
|
this.emitMutation({ type: "delete", resource: op.object, id: op.id });
|
|
1924
2072
|
}
|
|
1925
2073
|
});
|
|
1926
|
-
return payload;
|
|
1927
2074
|
}
|
|
1928
2075
|
async bulk(resource, operation, data) {
|
|
1929
2076
|
await this.connect();
|
|
@@ -2644,22 +2791,18 @@ var ObjectStackAdapter = class {
|
|
|
2644
2791
|
*/
|
|
2645
2792
|
async updateViewConfig(objectName, viewId, config) {
|
|
2646
2793
|
await this.connect();
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
return result ?? void 0;
|
|
2660
|
-
} catch (err) {
|
|
2661
|
-
throw err;
|
|
2662
|
-
}
|
|
2794
|
+
const merged = { ...config || {}, object: config?.object || objectName, name: viewId };
|
|
2795
|
+
const result = await this.client.meta.saveItem(
|
|
2796
|
+
"view",
|
|
2797
|
+
viewId,
|
|
2798
|
+
merged
|
|
2799
|
+
);
|
|
2800
|
+
const cacheKey = `view:${objectName}:${viewId}`;
|
|
2801
|
+
this.metadataCache.invalidate?.(cacheKey);
|
|
2802
|
+
this.metadataCache.invalidate?.(`view-overrides:${objectName}`);
|
|
2803
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2804
|
+
if (result && result.item) return result.item;
|
|
2805
|
+
return result ?? void 0;
|
|
2663
2806
|
}
|
|
2664
2807
|
/**
|
|
2665
2808
|
* List user-created views for a given object via the metadata overlay
|
|
@@ -2671,11 +2814,17 @@ var ObjectStackAdapter = class {
|
|
|
2671
2814
|
* Filters by `data.object === objectName` (or top-level `object`)
|
|
2672
2815
|
* client-side because the metadata index is name-only, not field-typed.
|
|
2673
2816
|
*/
|
|
2674
|
-
async listViews(objectName) {
|
|
2817
|
+
async listViews(objectName, options) {
|
|
2675
2818
|
await this.connect();
|
|
2676
2819
|
try {
|
|
2677
|
-
|
|
2678
|
-
|
|
2820
|
+
let items;
|
|
2821
|
+
if (options?.previewDrafts) {
|
|
2822
|
+
const draftItems = await this.metadataClient().withPreviewDrafts(true).list("view");
|
|
2823
|
+
items = Array.isArray(draftItems) ? draftItems : [];
|
|
2824
|
+
} else {
|
|
2825
|
+
const result = await this.client.meta.getItems("view");
|
|
2826
|
+
items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
|
|
2827
|
+
}
|
|
2679
2828
|
const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
|
|
2680
2829
|
return items.filter((v) => {
|
|
2681
2830
|
if (!v) return false;
|
|
@@ -2686,21 +2835,36 @@ var ObjectStackAdapter = class {
|
|
|
2686
2835
|
return !(viewKind && FORM_FAMILY.has(viewKind));
|
|
2687
2836
|
}).map((v) => {
|
|
2688
2837
|
const spec = v.list ?? v;
|
|
2838
|
+
const isDraft = v._draft === true || spec?._draft === true;
|
|
2689
2839
|
if (spec && spec.config && typeof spec.config === "object") {
|
|
2690
2840
|
return {
|
|
2691
2841
|
...spec.config,
|
|
2692
2842
|
name: spec.name ?? spec.config.name,
|
|
2693
2843
|
label: spec.label ?? spec.config.label,
|
|
2694
|
-
isDefault: !!spec.isDefault
|
|
2844
|
+
isDefault: !!spec.isDefault,
|
|
2845
|
+
...isDraft ? { _draft: true } : {}
|
|
2695
2846
|
};
|
|
2696
2847
|
}
|
|
2697
|
-
return spec;
|
|
2848
|
+
return isDraft ? { ...spec, _draft: true } : spec;
|
|
2698
2849
|
});
|
|
2699
2850
|
} catch (err) {
|
|
2700
2851
|
console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
|
|
2701
2852
|
return [];
|
|
2702
2853
|
}
|
|
2703
2854
|
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Build a {@link MetadataClient} bound to this adapter's server + auth. Used
|
|
2857
|
+
* by draft-aware reads (`listViews({ previewDrafts })`) so the `/meta` route,
|
|
2858
|
+
* `?preview=draft` flag, and environment scoping live in the SDK rather than
|
|
2859
|
+
* being hand-assembled at each call site (#2767 P3).
|
|
2860
|
+
*/
|
|
2861
|
+
metadataClient() {
|
|
2862
|
+
return new MetadataClient({
|
|
2863
|
+
baseUrl: this.baseUrl,
|
|
2864
|
+
fetch: this.fetchImpl,
|
|
2865
|
+
...this.token ? { headers: { Authorization: `Bearer ${this.token}` } } : {}
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2704
2868
|
/**
|
|
2705
2869
|
* List registered import `mapping` artifacts targeting a given object
|
|
2706
2870
|
* (framework #2611). Reads the `mapping` metadata kind via the overlay API
|
|
@@ -2745,14 +2909,10 @@ var ObjectStackAdapter = class {
|
|
|
2745
2909
|
object: spec?.object || objectName,
|
|
2746
2910
|
data: spec?.data || { provider: "object", object: objectName }
|
|
2747
2911
|
};
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
return fullSpec;
|
|
2753
|
-
} catch (err) {
|
|
2754
|
-
throw err;
|
|
2755
|
-
}
|
|
2912
|
+
const result = await this.client.meta.saveItem("view", name, fullSpec);
|
|
2913
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2914
|
+
if (result && result.item) return result.item;
|
|
2915
|
+
return fullSpec;
|
|
2756
2916
|
}
|
|
2757
2917
|
/**
|
|
2758
2918
|
* Apply a partial update to an existing overlay view. Reads the current
|
|
@@ -2775,15 +2935,11 @@ var ObjectStackAdapter = class {
|
|
|
2775
2935
|
name: viewName,
|
|
2776
2936
|
object: current?.object || current?.data?.object || objectName
|
|
2777
2937
|
};
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
return merged;
|
|
2784
|
-
} catch (err) {
|
|
2785
|
-
throw err;
|
|
2786
|
-
}
|
|
2938
|
+
const result = await this.client.meta.saveItem("view", viewName, merged);
|
|
2939
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2940
|
+
this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
|
|
2941
|
+
if (result && result.item) return result.item;
|
|
2942
|
+
return merged;
|
|
2787
2943
|
}
|
|
2788
2944
|
/**
|
|
2789
2945
|
* Delete an overlay view (reset to artifact default if one exists, or
|
|
@@ -2792,14 +2948,10 @@ var ObjectStackAdapter = class {
|
|
|
2792
2948
|
*/
|
|
2793
2949
|
async deleteView(objectName, viewName) {
|
|
2794
2950
|
await this.connect();
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
|
|
2800
|
-
} catch (err) {
|
|
2801
|
-
throw err;
|
|
2802
|
-
}
|
|
2951
|
+
const result = await this.client.meta.deleteItem("view", viewName);
|
|
2952
|
+
this.metadataCache.invalidate?.(`views:${objectName}`);
|
|
2953
|
+
this.metadataCache.invalidate?.(`view:${objectName}:${viewName}`);
|
|
2954
|
+
return { deleted: !!(result?.deleted ?? result?.reset ?? true) };
|
|
2803
2955
|
}
|
|
2804
2956
|
/**
|
|
2805
2957
|
* Get an application definition by name or ID.
|
|
@@ -2855,19 +3007,15 @@ var ObjectStackAdapter = class {
|
|
|
2855
3007
|
*/
|
|
2856
3008
|
async updateDashboard(dashboardName, schema) {
|
|
2857
3009
|
await this.connect();
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
return result ?? void 0;
|
|
2868
|
-
} catch (err) {
|
|
2869
|
-
throw err;
|
|
2870
|
-
}
|
|
3010
|
+
const result = await this.client.meta.saveItem(
|
|
3011
|
+
"dashboard",
|
|
3012
|
+
dashboardName,
|
|
3013
|
+
schema
|
|
3014
|
+
);
|
|
3015
|
+
this.metadataCache.invalidate?.("dashboards");
|
|
3016
|
+
this.metadataCache.invalidate?.(`dashboard:${dashboardName}`);
|
|
3017
|
+
if (result && result.item) return result.item;
|
|
3018
|
+
return result ?? void 0;
|
|
2871
3019
|
}
|
|
2872
3020
|
/**
|
|
2873
3021
|
* Perform server-side aggregation via the ObjectStack analytics API.
|
|
@@ -3238,6 +3386,7 @@ export {
|
|
|
3238
3386
|
isErrorType,
|
|
3239
3387
|
isObjectStackError,
|
|
3240
3388
|
normaliseClientError,
|
|
3389
|
+
readTransactionalBatchCapability,
|
|
3241
3390
|
snapToGrid,
|
|
3242
3391
|
validatePluginContract
|
|
3243
3392
|
};
|