@objectstack/metadata 16.1.0 → 17.0.0-rc.1
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 +3278 -0
- package/dist/index.cjs +113 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -15
- package/dist/index.d.ts +83 -15
- package/dist/index.js +105 -45
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +113 -53
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +105 -45
- package/dist/node.js.map +1 -1
- package/package.json +13 -11
package/dist/node.cjs
CHANGED
|
@@ -385,6 +385,8 @@ export default metadata;
|
|
|
385
385
|
|
|
386
386
|
// src/loaders/database-loader.ts
|
|
387
387
|
var import_metadata_core = require("@objectstack/metadata-core");
|
|
388
|
+
var import_spec = require("@objectstack/spec");
|
|
389
|
+
var import_shared = require("@objectstack/spec/shared");
|
|
388
390
|
|
|
389
391
|
// src/utils/metadata-history-utils.ts
|
|
390
392
|
async function calculateChecksum(metadata) {
|
|
@@ -660,6 +662,12 @@ var DatabaseLoader = class {
|
|
|
660
662
|
};
|
|
661
663
|
this.schemaReady = false;
|
|
662
664
|
this.historySchemaReady = false;
|
|
665
|
+
/**
|
|
666
|
+
* Once-per-process dedupe for stored-row conversion notices — `load` /
|
|
667
|
+
* `loadMany` are hot read paths (cached, but re-hit on every TTL expiry),
|
|
668
|
+
* so a legacy row must warn once, not once per cache miss.
|
|
669
|
+
*/
|
|
670
|
+
this.storedConversionWarned = /* @__PURE__ */ new Set();
|
|
663
671
|
if (!options.driver && !options.engine) {
|
|
664
672
|
throw new Error("DatabaseLoader requires either a driver or engine");
|
|
665
673
|
}
|
|
@@ -920,13 +928,33 @@ var DatabaseLoader = class {
|
|
|
920
928
|
}
|
|
921
929
|
}
|
|
922
930
|
/**
|
|
923
|
-
* Convert a database row to a metadata payload.
|
|
924
|
-
*
|
|
931
|
+
* Convert a LIVE database row to a metadata payload.
|
|
932
|
+
*
|
|
933
|
+
* Parses the JSON `metadata` column back into an object, then replays the
|
|
934
|
+
* full ADR-0087 conversion chain over it (#3903): rows written under a past
|
|
935
|
+
* protocol are served canonical, exactly like the metadata-protocol's
|
|
936
|
+
* `sys_metadata` seams. History rows do NOT pass through here — history
|
|
937
|
+
* readers parse inline and stay verbatim, as a record of what was written.
|
|
938
|
+
*
|
|
939
|
+
* `flow` is skipped for the same reason the protocol skips it: flow-node
|
|
940
|
+
* conversions need the automation engine's live executor registry for their
|
|
941
|
+
* open-namespace conflict guard; flows canonicalize at `registerFlow`.
|
|
925
942
|
*/
|
|
926
943
|
rowToData(row) {
|
|
927
944
|
if (!row || !row.metadata) return null;
|
|
928
945
|
const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
929
|
-
|
|
946
|
+
const singular = import_shared.PLURAL_TO_SINGULAR[row.type] ?? row.type;
|
|
947
|
+
if (singular === "flow") return payload;
|
|
948
|
+
return (0, import_spec.applyConversionsToStoredItem)(singular, payload, {
|
|
949
|
+
onNotice: (n) => {
|
|
950
|
+
const key = `${n.conversionId}|${singular}|${String(row.name ?? "")}`;
|
|
951
|
+
if (this.storedConversionWarned.has(key)) return;
|
|
952
|
+
this.storedConversionWarned.add(key);
|
|
953
|
+
console.warn(
|
|
954
|
+
`[DatabaseLoader] stored ${singular}/${String(row.name ?? "<unnamed>")} carries a pre-protocol shape; ${n.message}`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
});
|
|
930
958
|
}
|
|
931
959
|
/**
|
|
932
960
|
* Convert a database row to a MetadataRecord-like object.
|
|
@@ -2341,19 +2369,44 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2341
2369
|
/**
|
|
2342
2370
|
* Load a single metadata item from loaders.
|
|
2343
2371
|
* Iterates through registered loaders until found.
|
|
2372
|
+
*
|
|
2373
|
+
* Returns `null` both when no loader HAS the item and when every loader
|
|
2374
|
+
* FAILED — see {@link loadDiagnosed} when the caller must tell those apart.
|
|
2344
2375
|
*/
|
|
2345
2376
|
async load(type, name, options) {
|
|
2377
|
+
return (await this.loadDiagnosed(type, name, options)).data;
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* `load`, plus whether the answer can be trusted as complete.
|
|
2381
|
+
*
|
|
2382
|
+
* [ADR-0110 D3] A miss and an outage are different facts with opposite
|
|
2383
|
+
* security meanings, and plain `load` cannot express the difference: a
|
|
2384
|
+
* loader that throws is warn-logged and skipped, so a database the metadata
|
|
2385
|
+
* plane cannot reach returns the same `null` as a name that was never
|
|
2386
|
+
* declared. Callers that gate on a declaration MUST NOT read that `null` as
|
|
2387
|
+
* "the author declared no gate" — an availability failure would silently
|
|
2388
|
+
* widen access (the REST `/actions` route's fail-open branch, #3935).
|
|
2389
|
+
*
|
|
2390
|
+
* `degraded` is true when at least one loader threw AND no loader answered
|
|
2391
|
+
* with the item. The posture is deliberately conservative: with a loader
|
|
2392
|
+
* down we cannot prove the item is absent, so we decline to claim it is.
|
|
2393
|
+
* A clean miss (every loader answered, none had it) is NOT degraded.
|
|
2394
|
+
*/
|
|
2395
|
+
async loadDiagnosed(type, name, options) {
|
|
2396
|
+
const errors = [];
|
|
2346
2397
|
for (const loader of this.loaders.values()) {
|
|
2347
2398
|
try {
|
|
2348
2399
|
const result = await loader.load(type, name, options);
|
|
2349
2400
|
if (result.data) {
|
|
2350
|
-
return result.data;
|
|
2401
|
+
return { data: result.data, degraded: false, errors };
|
|
2351
2402
|
}
|
|
2352
2403
|
} catch (e) {
|
|
2404
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2405
|
+
errors.push(`${loader.contract.name}: ${message}`);
|
|
2353
2406
|
this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
|
|
2354
2407
|
}
|
|
2355
2408
|
}
|
|
2356
|
-
return null;
|
|
2409
|
+
return { data: null, degraded: errors.length > 0, errors };
|
|
2357
2410
|
}
|
|
2358
2411
|
/**
|
|
2359
2412
|
* Load multiple metadata items from loaders.
|
|
@@ -3240,10 +3293,10 @@ var MemoryLoader = class {
|
|
|
3240
3293
|
|
|
3241
3294
|
// src/plugin.ts
|
|
3242
3295
|
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
3243
|
-
var
|
|
3296
|
+
var import_shared2 = require("@objectstack/spec/shared");
|
|
3244
3297
|
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
3245
|
-
var import_spec = require("@objectstack/spec");
|
|
3246
3298
|
var import_spec2 = require("@objectstack/spec");
|
|
3299
|
+
var import_spec3 = require("@objectstack/spec");
|
|
3247
3300
|
var queryableMetadataObjects = [
|
|
3248
3301
|
import_metadata_core2.SysMetadataObject,
|
|
3249
3302
|
import_metadata_core2.SysMetadataHistoryObject,
|
|
@@ -3295,6 +3348,12 @@ var MetadataPlugin = class {
|
|
|
3295
3348
|
this.name = "com.objectstack.metadata";
|
|
3296
3349
|
this.type = "standard";
|
|
3297
3350
|
this.version = "1.0.0";
|
|
3351
|
+
/**
|
|
3352
|
+
* Services init() UNCONDITIONALLY registers (ADR-0116, #4131) — lets the
|
|
3353
|
+
* kernel name this plugin when a consumer requires `metadata` before it
|
|
3354
|
+
* initializes.
|
|
3355
|
+
*/
|
|
3356
|
+
this.providesServices = ["metadata"];
|
|
3298
3357
|
this.init = async (ctx) => {
|
|
3299
3358
|
ctx.logger.info("Initializing Metadata Manager", {
|
|
3300
3359
|
root: this.options.rootDir || process.cwd(),
|
|
@@ -3334,27 +3393,27 @@ var MetadataPlugin = class {
|
|
|
3334
3393
|
bootstrap: mode,
|
|
3335
3394
|
artifactSource: src?.mode ?? "none"
|
|
3336
3395
|
});
|
|
3396
|
+
if (src && src.mode !== "local-file") {
|
|
3397
|
+
const bad = src.mode;
|
|
3398
|
+
throw new Error(
|
|
3399
|
+
`[MetadataPlugin] artifactSource.mode '${bad}' is not supported` + (bad === "artifact-api" ? " \u2014 the 'artifact-api' source was removed (#4246). Load the same artifact with { mode: 'local-file', path: '<http(s) URL>' } (e.g. the control plane's /pub/v1/environments/:id/artifact route), or install packages into a running runtime via @objectstack/cloud-connection." : ". The only artifact source is { mode: 'local-file', path }.")
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3337
3402
|
if (mode === "artifact-only") {
|
|
3338
|
-
if (src
|
|
3403
|
+
if (src) {
|
|
3339
3404
|
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3340
|
-
} else if (src?.mode === "artifact-api") {
|
|
3341
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3342
3405
|
} else {
|
|
3343
3406
|
throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
|
|
3344
3407
|
}
|
|
3345
3408
|
} else if (mode === "lazy") {
|
|
3346
|
-
if (src
|
|
3347
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3348
|
-
} else if (src?.mode === "artifact-api") {
|
|
3349
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3409
|
+
if (src) {
|
|
3410
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3350
3411
|
} else {
|
|
3351
3412
|
ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
|
|
3352
3413
|
}
|
|
3353
3414
|
} else {
|
|
3354
|
-
if (src
|
|
3355
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3356
|
-
} else if (src?.mode === "artifact-api") {
|
|
3357
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3415
|
+
if (src) {
|
|
3416
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3358
3417
|
} else {
|
|
3359
3418
|
await this._loadFromFileSystem(ctx);
|
|
3360
3419
|
}
|
|
@@ -3396,7 +3455,14 @@ var MetadataPlugin = class {
|
|
|
3396
3455
|
});
|
|
3397
3456
|
}
|
|
3398
3457
|
try {
|
|
3399
|
-
const
|
|
3458
|
+
const readServer = (name) => {
|
|
3459
|
+
try {
|
|
3460
|
+
return ctx.getService(name);
|
|
3461
|
+
} catch {
|
|
3462
|
+
return void 0;
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
const httpServer = readServer("http.server") ?? readServer("http-server");
|
|
3400
3466
|
if (httpServer && typeof httpServer.getRawApp === "function") {
|
|
3401
3467
|
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
3402
3468
|
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
@@ -3509,14 +3575,13 @@ var MetadataPlugin = class {
|
|
|
3509
3575
|
/**
|
|
3510
3576
|
* Fetch JSON content from a URL with configurable timeout.
|
|
3511
3577
|
*/
|
|
3512
|
-
async _fetchJson(url, fetchTimeoutMs
|
|
3578
|
+
async _fetchJson(url, fetchTimeoutMs) {
|
|
3513
3579
|
const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
|
|
3514
3580
|
const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
|
|
3515
3581
|
const controller = new AbortController();
|
|
3516
3582
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
3517
3583
|
try {
|
|
3518
3584
|
const headers = { Accept: "application/json, */*;q=0.5" };
|
|
3519
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3520
3585
|
const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
|
|
3521
3586
|
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
3522
3587
|
const content = await res.text();
|
|
@@ -3578,21 +3643,21 @@ var MetadataPlugin = class {
|
|
|
3578
3643
|
const items = metadata[field];
|
|
3579
3644
|
if (!Array.isArray(items) || items.length === 0) continue;
|
|
3580
3645
|
for (const item of items) {
|
|
3581
|
-
if (metaType === "view" && (0,
|
|
3646
|
+
if (metaType === "view" && (0, import_spec3.isAggregatedViewContainer)(item)) {
|
|
3582
3647
|
const viewObject = item?.list?.data?.object ?? item?.form?.data?.object;
|
|
3583
3648
|
if (!viewObject) continue;
|
|
3584
|
-
(0,
|
|
3649
|
+
(0, import_shared2.applyProtection)(item, {
|
|
3585
3650
|
packageId: manifestPackageId,
|
|
3586
3651
|
packageVersion: manifestVersion
|
|
3587
3652
|
});
|
|
3588
3653
|
await memLoader.save("view", viewObject, item);
|
|
3589
3654
|
await this.manager.register("view", viewObject, item, { notify: false });
|
|
3590
3655
|
totalRegistered++;
|
|
3591
|
-
for (const vi of (0,
|
|
3656
|
+
for (const vi of (0, import_spec3.expandViewContainer)(viewObject, item)) {
|
|
3592
3657
|
for (const w of vi._diagnostics?.warnings ?? []) {
|
|
3593
3658
|
ctx.logger.warn(`[MetadataPlugin] View expansion warning for '${vi.name}': ${w.message}`);
|
|
3594
3659
|
}
|
|
3595
|
-
(0,
|
|
3660
|
+
(0, import_shared2.applyProtection)(vi, {
|
|
3596
3661
|
packageId: manifestPackageId,
|
|
3597
3662
|
packageVersion: manifestVersion
|
|
3598
3663
|
});
|
|
@@ -3609,7 +3674,7 @@ var MetadataPlugin = class {
|
|
|
3609
3674
|
}
|
|
3610
3675
|
}
|
|
3611
3676
|
if (!name) continue;
|
|
3612
|
-
(0,
|
|
3677
|
+
(0, import_shared2.applyProtection)(item, {
|
|
3613
3678
|
packageId: manifestPackageId,
|
|
3614
3679
|
packageVersion: manifestVersion
|
|
3615
3680
|
});
|
|
@@ -3636,14 +3701,26 @@ var MetadataPlugin = class {
|
|
|
3636
3701
|
* logged but never blocks the reload.
|
|
3637
3702
|
*/
|
|
3638
3703
|
async _reloadAndAnnounce(ctx, src, changed) {
|
|
3639
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3704
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3640
3705
|
try {
|
|
3641
3706
|
await ctx.trigger("metadata:reloaded", { changed, metadata: this.lastParsedMetadata });
|
|
3642
3707
|
} catch (e) {
|
|
3643
3708
|
ctx.logger.warn("[MetadataPlugin] metadata:reloaded subscriber failed", { error: e?.message });
|
|
3644
3709
|
}
|
|
3645
3710
|
}
|
|
3646
|
-
|
|
3711
|
+
/**
|
|
3712
|
+
* @param opts.optional When true, a LOCAL artifact file that does not exist
|
|
3713
|
+
* is "nothing compiled yet" rather than a fault: log and return, leaving
|
|
3714
|
+
* the manager empty and the artifact watcher armed so the first
|
|
3715
|
+
* `os compile` hydrates the running server (#4085). Callers pass it for
|
|
3716
|
+
* the `eager` / `lazy` bootstrap modes — the development-platform paths,
|
|
3717
|
+
* where an app is optional. `artifact-only` (sealed runtime) does NOT:
|
|
3718
|
+
* there the artifact IS the deployment, so its absence must fail loudly
|
|
3719
|
+
* instead of silently serving an empty runtime. Only ENOENT is tolerated;
|
|
3720
|
+
* a present-but-unreadable artifact (malformed JSON, bad permissions) and
|
|
3721
|
+
* every remote-URL failure stay fatal.
|
|
3722
|
+
*/
|
|
3723
|
+
async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs, opts = {}) {
|
|
3647
3724
|
const isUrl = /^https?:\/\//i.test(filePath);
|
|
3648
3725
|
ctx.logger.info(
|
|
3649
3726
|
`[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
|
|
@@ -3658,34 +3735,17 @@ var MetadataPlugin = class {
|
|
|
3658
3735
|
raw = JSON.parse(content);
|
|
3659
3736
|
}
|
|
3660
3737
|
} catch (e) {
|
|
3738
|
+
if (opts.optional && !isUrl && e?.code === "ENOENT") {
|
|
3739
|
+
ctx.logger.info(
|
|
3740
|
+
"[MetadataPlugin] no compiled artifact yet \u2014 starting with no artifact metadata",
|
|
3741
|
+
{ path: filePath }
|
|
3742
|
+
);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3661
3745
|
throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
|
|
3662
3746
|
}
|
|
3663
3747
|
await this._parseAndRegisterArtifact(ctx, raw, filePath);
|
|
3664
3748
|
}
|
|
3665
|
-
/**
|
|
3666
|
-
* P2: Load metadata from the cloud artifact API endpoint.
|
|
3667
|
-
*/
|
|
3668
|
-
async _loadFromArtifactApi(ctx, src) {
|
|
3669
|
-
const environmentId = this.options.environmentId;
|
|
3670
|
-
if (!environmentId) {
|
|
3671
|
-
throw new Error("[MetadataPlugin] artifact-api source requires options.environmentId to be set");
|
|
3672
|
-
}
|
|
3673
|
-
let artifactUrl = src.url.replace(/\/+$/, "");
|
|
3674
|
-
if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
|
|
3675
|
-
artifactUrl = `${artifactUrl}/api/v1/cloud/environments/${environmentId}/artifact`;
|
|
3676
|
-
}
|
|
3677
|
-
if (src.commitId) {
|
|
3678
|
-
artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
|
|
3679
|
-
}
|
|
3680
|
-
ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
|
|
3681
|
-
let raw;
|
|
3682
|
-
try {
|
|
3683
|
-
raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
|
|
3684
|
-
} catch (e) {
|
|
3685
|
-
throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
|
|
3686
|
-
}
|
|
3687
|
-
await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
|
|
3688
|
-
}
|
|
3689
3749
|
async _loadFromFileSystem(ctx) {
|
|
3690
3750
|
ctx.logger.info("Loading metadata from file system...");
|
|
3691
3751
|
const sortedTypes = [...import_kernel2.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
@@ -3700,7 +3760,7 @@ var MetadataPlugin = class {
|
|
|
3700
3760
|
for (const item of items) {
|
|
3701
3761
|
const meta = item;
|
|
3702
3762
|
if (meta?.name) {
|
|
3703
|
-
(0,
|
|
3763
|
+
(0, import_shared2.applyProtection)(meta, {
|
|
3704
3764
|
packageId: this.options.packageId
|
|
3705
3765
|
});
|
|
3706
3766
|
await this.manager.register(entry.type, meta.name, item, { notify: false });
|