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