@objectstack/metadata 17.0.0-rc.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 +11 -9
package/dist/index.d.cts
CHANGED
|
@@ -444,8 +444,32 @@ declare class MetadataManager implements IMetadataService {
|
|
|
444
444
|
/**
|
|
445
445
|
* Load a single metadata item from loaders.
|
|
446
446
|
* Iterates through registered loaders until found.
|
|
447
|
+
*
|
|
448
|
+
* Returns `null` both when no loader HAS the item and when every loader
|
|
449
|
+
* FAILED — see {@link loadDiagnosed} when the caller must tell those apart.
|
|
447
450
|
*/
|
|
448
451
|
load<T = any>(type: string, name: string, options?: MetadataLoadOptions): Promise<T | null>;
|
|
452
|
+
/**
|
|
453
|
+
* `load`, plus whether the answer can be trusted as complete.
|
|
454
|
+
*
|
|
455
|
+
* [ADR-0110 D3] A miss and an outage are different facts with opposite
|
|
456
|
+
* security meanings, and plain `load` cannot express the difference: a
|
|
457
|
+
* loader that throws is warn-logged and skipped, so a database the metadata
|
|
458
|
+
* plane cannot reach returns the same `null` as a name that was never
|
|
459
|
+
* declared. Callers that gate on a declaration MUST NOT read that `null` as
|
|
460
|
+
* "the author declared no gate" — an availability failure would silently
|
|
461
|
+
* widen access (the REST `/actions` route's fail-open branch, #3935).
|
|
462
|
+
*
|
|
463
|
+
* `degraded` is true when at least one loader threw AND no loader answered
|
|
464
|
+
* with the item. The posture is deliberately conservative: with a loader
|
|
465
|
+
* down we cannot prove the item is absent, so we decline to claim it is.
|
|
466
|
+
* A clean miss (every loader answered, none had it) is NOT degraded.
|
|
467
|
+
*/
|
|
468
|
+
loadDiagnosed<T = any>(type: string, name: string, options?: MetadataLoadOptions): Promise<{
|
|
469
|
+
data: T | null;
|
|
470
|
+
degraded: boolean;
|
|
471
|
+
errors: string[];
|
|
472
|
+
}>;
|
|
449
473
|
/**
|
|
450
474
|
* Load multiple metadata items from loaders.
|
|
451
475
|
* Aggregates results from all loaders.
|
|
@@ -568,23 +592,38 @@ interface MetadataPluginOptions {
|
|
|
568
592
|
config?: Partial<MetadataPluginConfig>;
|
|
569
593
|
/** Organization ID for metadata-scoped consumers; MetadataPlugin itself does not persist runtime metadata. */
|
|
570
594
|
organizationId?: string;
|
|
571
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* Environment ID used by local artifact envelopes and metadata-scoped
|
|
597
|
+
* consumers. (The v5.0 rename retired the "project ID" wording this
|
|
598
|
+
* comment used to carry; see ADR-0006.)
|
|
599
|
+
*/
|
|
572
600
|
environmentId?: string;
|
|
573
601
|
/**
|
|
574
|
-
* When set, MetadataPlugin loads metadata from
|
|
575
|
-
* the filesystem
|
|
576
|
-
*
|
|
602
|
+
* When set, MetadataPlugin loads metadata from a compiled artifact instead
|
|
603
|
+
* of scanning the filesystem, honored by all three bootstrap modes
|
|
604
|
+
* (`eager` / `lazy` / `artifact-only`) — see `start()`.
|
|
605
|
+
*
|
|
606
|
+
* `path` is a filesystem path, or an `http(s)://` URL fetched verbatim —
|
|
607
|
+
* the control plane's public artifact route
|
|
608
|
+
* (`/pub/v1/environments/:id/artifact[?commit=…]`) serves exactly such
|
|
609
|
+
* URLs, so a sealed runtime can boot straight off a published revision.
|
|
610
|
+
* Remote reads honor `fetchTimeoutMs` / `OS_ARTIFACT_FETCH_TIMEOUT_MS`;
|
|
611
|
+
* the artifact-file HMR watcher ({@link artifactWatch}) applies to
|
|
612
|
+
* non-URL paths only.
|
|
613
|
+
*
|
|
614
|
+
* `local-file` is the only mode. A second `artifact-api` mode (a
|
|
615
|
+
* Bearer-authenticated control-plane pull) existed here through v17 with
|
|
616
|
+
* zero consumers in any repo — the cloud runtime uses its own
|
|
617
|
+
* `ArtifactApiClient`, and package distribution into a running OSS
|
|
618
|
+
* instance goes through `@objectstack/cloud-connection` — and was removed
|
|
619
|
+
* when #4246 forced the declared-vs-enforced question. `start()` rejects
|
|
620
|
+
* an unknown mode loudly rather than silently scanning the filesystem
|
|
621
|
+
* instead.
|
|
577
622
|
*/
|
|
578
623
|
artifactSource?: {
|
|
579
624
|
mode: 'local-file';
|
|
580
625
|
path: string;
|
|
581
626
|
fetchTimeoutMs?: number;
|
|
582
|
-
} | {
|
|
583
|
-
mode: 'artifact-api';
|
|
584
|
-
url: string;
|
|
585
|
-
token?: string;
|
|
586
|
-
commitId?: string;
|
|
587
|
-
fetchTimeoutMs?: number;
|
|
588
627
|
};
|
|
589
628
|
/**
|
|
590
629
|
* Register the `sys_metadata` + `sys_metadata_history` storage objects
|
|
@@ -615,6 +654,12 @@ declare class MetadataPlugin implements Plugin {
|
|
|
615
654
|
name: string;
|
|
616
655
|
type: string;
|
|
617
656
|
version: string;
|
|
657
|
+
/**
|
|
658
|
+
* Services init() UNCONDITIONALLY registers (ADR-0116, #4131) — lets the
|
|
659
|
+
* kernel name this plugin when a consumer requires `metadata` before it
|
|
660
|
+
* initializes.
|
|
661
|
+
*/
|
|
662
|
+
providesServices: string[];
|
|
618
663
|
private manager;
|
|
619
664
|
private options;
|
|
620
665
|
private repository?;
|
|
@@ -663,11 +708,19 @@ declare class MetadataPlugin implements Plugin {
|
|
|
663
708
|
* logged but never blocks the reload.
|
|
664
709
|
*/
|
|
665
710
|
private _reloadAndAnnounce;
|
|
666
|
-
private _loadFromLocalFile;
|
|
667
711
|
/**
|
|
668
|
-
*
|
|
712
|
+
* @param opts.optional When true, a LOCAL artifact file that does not exist
|
|
713
|
+
* is "nothing compiled yet" rather than a fault: log and return, leaving
|
|
714
|
+
* the manager empty and the artifact watcher armed so the first
|
|
715
|
+
* `os compile` hydrates the running server (#4085). Callers pass it for
|
|
716
|
+
* the `eager` / `lazy` bootstrap modes — the development-platform paths,
|
|
717
|
+
* where an app is optional. `artifact-only` (sealed runtime) does NOT:
|
|
718
|
+
* there the artifact IS the deployment, so its absence must fail loudly
|
|
719
|
+
* instead of silently serving an empty runtime. Only ENOENT is tolerated;
|
|
720
|
+
* a present-but-unreadable artifact (malformed JSON, bad permissions) and
|
|
721
|
+
* every remote-URL failure stay fatal.
|
|
669
722
|
*/
|
|
670
|
-
private
|
|
723
|
+
private _loadFromLocalFile;
|
|
671
724
|
private _loadFromFileSystem;
|
|
672
725
|
}
|
|
673
726
|
|
|
@@ -915,8 +968,23 @@ declare class DatabaseLoader implements MetadataLoader {
|
|
|
915
968
|
*/
|
|
916
969
|
private createHistoryRecord;
|
|
917
970
|
/**
|
|
918
|
-
*
|
|
919
|
-
*
|
|
971
|
+
* Once-per-process dedupe for stored-row conversion notices — `load` /
|
|
972
|
+
* `loadMany` are hot read paths (cached, but re-hit on every TTL expiry),
|
|
973
|
+
* so a legacy row must warn once, not once per cache miss.
|
|
974
|
+
*/
|
|
975
|
+
private storedConversionWarned;
|
|
976
|
+
/**
|
|
977
|
+
* Convert a LIVE database row to a metadata payload.
|
|
978
|
+
*
|
|
979
|
+
* Parses the JSON `metadata` column back into an object, then replays the
|
|
980
|
+
* full ADR-0087 conversion chain over it (#3903): rows written under a past
|
|
981
|
+
* protocol are served canonical, exactly like the metadata-protocol's
|
|
982
|
+
* `sys_metadata` seams. History rows do NOT pass through here — history
|
|
983
|
+
* readers parse inline and stay verbatim, as a record of what was written.
|
|
984
|
+
*
|
|
985
|
+
* `flow` is skipped for the same reason the protocol skips it: flow-node
|
|
986
|
+
* conversions need the automation engine's live executor registry for their
|
|
987
|
+
* open-namespace conflict guard; flows canonicalize at `registerFlow`.
|
|
920
988
|
*/
|
|
921
989
|
private rowToData;
|
|
922
990
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -444,8 +444,32 @@ declare class MetadataManager implements IMetadataService {
|
|
|
444
444
|
/**
|
|
445
445
|
* Load a single metadata item from loaders.
|
|
446
446
|
* Iterates through registered loaders until found.
|
|
447
|
+
*
|
|
448
|
+
* Returns `null` both when no loader HAS the item and when every loader
|
|
449
|
+
* FAILED — see {@link loadDiagnosed} when the caller must tell those apart.
|
|
447
450
|
*/
|
|
448
451
|
load<T = any>(type: string, name: string, options?: MetadataLoadOptions): Promise<T | null>;
|
|
452
|
+
/**
|
|
453
|
+
* `load`, plus whether the answer can be trusted as complete.
|
|
454
|
+
*
|
|
455
|
+
* [ADR-0110 D3] A miss and an outage are different facts with opposite
|
|
456
|
+
* security meanings, and plain `load` cannot express the difference: a
|
|
457
|
+
* loader that throws is warn-logged and skipped, so a database the metadata
|
|
458
|
+
* plane cannot reach returns the same `null` as a name that was never
|
|
459
|
+
* declared. Callers that gate on a declaration MUST NOT read that `null` as
|
|
460
|
+
* "the author declared no gate" — an availability failure would silently
|
|
461
|
+
* widen access (the REST `/actions` route's fail-open branch, #3935).
|
|
462
|
+
*
|
|
463
|
+
* `degraded` is true when at least one loader threw AND no loader answered
|
|
464
|
+
* with the item. The posture is deliberately conservative: with a loader
|
|
465
|
+
* down we cannot prove the item is absent, so we decline to claim it is.
|
|
466
|
+
* A clean miss (every loader answered, none had it) is NOT degraded.
|
|
467
|
+
*/
|
|
468
|
+
loadDiagnosed<T = any>(type: string, name: string, options?: MetadataLoadOptions): Promise<{
|
|
469
|
+
data: T | null;
|
|
470
|
+
degraded: boolean;
|
|
471
|
+
errors: string[];
|
|
472
|
+
}>;
|
|
449
473
|
/**
|
|
450
474
|
* Load multiple metadata items from loaders.
|
|
451
475
|
* Aggregates results from all loaders.
|
|
@@ -568,23 +592,38 @@ interface MetadataPluginOptions {
|
|
|
568
592
|
config?: Partial<MetadataPluginConfig>;
|
|
569
593
|
/** Organization ID for metadata-scoped consumers; MetadataPlugin itself does not persist runtime metadata. */
|
|
570
594
|
organizationId?: string;
|
|
571
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* Environment ID used by local artifact envelopes and metadata-scoped
|
|
597
|
+
* consumers. (The v5.0 rename retired the "project ID" wording this
|
|
598
|
+
* comment used to carry; see ADR-0006.)
|
|
599
|
+
*/
|
|
572
600
|
environmentId?: string;
|
|
573
601
|
/**
|
|
574
|
-
* When set, MetadataPlugin loads metadata from
|
|
575
|
-
* the filesystem
|
|
576
|
-
*
|
|
602
|
+
* When set, MetadataPlugin loads metadata from a compiled artifact instead
|
|
603
|
+
* of scanning the filesystem, honored by all three bootstrap modes
|
|
604
|
+
* (`eager` / `lazy` / `artifact-only`) — see `start()`.
|
|
605
|
+
*
|
|
606
|
+
* `path` is a filesystem path, or an `http(s)://` URL fetched verbatim —
|
|
607
|
+
* the control plane's public artifact route
|
|
608
|
+
* (`/pub/v1/environments/:id/artifact[?commit=…]`) serves exactly such
|
|
609
|
+
* URLs, so a sealed runtime can boot straight off a published revision.
|
|
610
|
+
* Remote reads honor `fetchTimeoutMs` / `OS_ARTIFACT_FETCH_TIMEOUT_MS`;
|
|
611
|
+
* the artifact-file HMR watcher ({@link artifactWatch}) applies to
|
|
612
|
+
* non-URL paths only.
|
|
613
|
+
*
|
|
614
|
+
* `local-file` is the only mode. A second `artifact-api` mode (a
|
|
615
|
+
* Bearer-authenticated control-plane pull) existed here through v17 with
|
|
616
|
+
* zero consumers in any repo — the cloud runtime uses its own
|
|
617
|
+
* `ArtifactApiClient`, and package distribution into a running OSS
|
|
618
|
+
* instance goes through `@objectstack/cloud-connection` — and was removed
|
|
619
|
+
* when #4246 forced the declared-vs-enforced question. `start()` rejects
|
|
620
|
+
* an unknown mode loudly rather than silently scanning the filesystem
|
|
621
|
+
* instead.
|
|
577
622
|
*/
|
|
578
623
|
artifactSource?: {
|
|
579
624
|
mode: 'local-file';
|
|
580
625
|
path: string;
|
|
581
626
|
fetchTimeoutMs?: number;
|
|
582
|
-
} | {
|
|
583
|
-
mode: 'artifact-api';
|
|
584
|
-
url: string;
|
|
585
|
-
token?: string;
|
|
586
|
-
commitId?: string;
|
|
587
|
-
fetchTimeoutMs?: number;
|
|
588
627
|
};
|
|
589
628
|
/**
|
|
590
629
|
* Register the `sys_metadata` + `sys_metadata_history` storage objects
|
|
@@ -615,6 +654,12 @@ declare class MetadataPlugin implements Plugin {
|
|
|
615
654
|
name: string;
|
|
616
655
|
type: string;
|
|
617
656
|
version: string;
|
|
657
|
+
/**
|
|
658
|
+
* Services init() UNCONDITIONALLY registers (ADR-0116, #4131) — lets the
|
|
659
|
+
* kernel name this plugin when a consumer requires `metadata` before it
|
|
660
|
+
* initializes.
|
|
661
|
+
*/
|
|
662
|
+
providesServices: string[];
|
|
618
663
|
private manager;
|
|
619
664
|
private options;
|
|
620
665
|
private repository?;
|
|
@@ -663,11 +708,19 @@ declare class MetadataPlugin implements Plugin {
|
|
|
663
708
|
* logged but never blocks the reload.
|
|
664
709
|
*/
|
|
665
710
|
private _reloadAndAnnounce;
|
|
666
|
-
private _loadFromLocalFile;
|
|
667
711
|
/**
|
|
668
|
-
*
|
|
712
|
+
* @param opts.optional When true, a LOCAL artifact file that does not exist
|
|
713
|
+
* is "nothing compiled yet" rather than a fault: log and return, leaving
|
|
714
|
+
* the manager empty and the artifact watcher armed so the first
|
|
715
|
+
* `os compile` hydrates the running server (#4085). Callers pass it for
|
|
716
|
+
* the `eager` / `lazy` bootstrap modes — the development-platform paths,
|
|
717
|
+
* where an app is optional. `artifact-only` (sealed runtime) does NOT:
|
|
718
|
+
* there the artifact IS the deployment, so its absence must fail loudly
|
|
719
|
+
* instead of silently serving an empty runtime. Only ENOENT is tolerated;
|
|
720
|
+
* a present-but-unreadable artifact (malformed JSON, bad permissions) and
|
|
721
|
+
* every remote-URL failure stay fatal.
|
|
669
722
|
*/
|
|
670
|
-
private
|
|
723
|
+
private _loadFromLocalFile;
|
|
671
724
|
private _loadFromFileSystem;
|
|
672
725
|
}
|
|
673
726
|
|
|
@@ -915,8 +968,23 @@ declare class DatabaseLoader implements MetadataLoader {
|
|
|
915
968
|
*/
|
|
916
969
|
private createHistoryRecord;
|
|
917
970
|
/**
|
|
918
|
-
*
|
|
919
|
-
*
|
|
971
|
+
* Once-per-process dedupe for stored-row conversion notices — `load` /
|
|
972
|
+
* `loadMany` are hot read paths (cached, but re-hit on every TTL expiry),
|
|
973
|
+
* so a legacy row must warn once, not once per cache miss.
|
|
974
|
+
*/
|
|
975
|
+
private storedConversionWarned;
|
|
976
|
+
/**
|
|
977
|
+
* Convert a LIVE database row to a metadata payload.
|
|
978
|
+
*
|
|
979
|
+
* Parses the JSON `metadata` column back into an object, then replays the
|
|
980
|
+
* full ADR-0087 conversion chain over it (#3903): rows written under a past
|
|
981
|
+
* protocol are served canonical, exactly like the metadata-protocol's
|
|
982
|
+
* `sys_metadata` seams. History rows do NOT pass through here — history
|
|
983
|
+
* readers parse inline and stay verbatim, as a record of what was written.
|
|
984
|
+
*
|
|
985
|
+
* `flow` is skipped for the same reason the protocol skips it: flow-node
|
|
986
|
+
* conversions need the automation engine's live executor registry for their
|
|
987
|
+
* open-namespace conflict guard; flows canonicalize at `registerFlow`.
|
|
920
988
|
*/
|
|
921
989
|
private rowToData;
|
|
922
990
|
/**
|
package/dist/index.js
CHANGED
|
@@ -340,6 +340,8 @@ export default metadata;
|
|
|
340
340
|
|
|
341
341
|
// src/loaders/database-loader.ts
|
|
342
342
|
import { SysMetadataObject, SysMetadataHistoryObject } from "@objectstack/metadata-core";
|
|
343
|
+
import { applyConversionsToStoredItem } from "@objectstack/spec";
|
|
344
|
+
import { PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
|
|
343
345
|
|
|
344
346
|
// src/utils/metadata-history-utils.ts
|
|
345
347
|
async function calculateChecksum(metadata) {
|
|
@@ -615,6 +617,12 @@ var DatabaseLoader = class {
|
|
|
615
617
|
};
|
|
616
618
|
this.schemaReady = false;
|
|
617
619
|
this.historySchemaReady = false;
|
|
620
|
+
/**
|
|
621
|
+
* Once-per-process dedupe for stored-row conversion notices — `load` /
|
|
622
|
+
* `loadMany` are hot read paths (cached, but re-hit on every TTL expiry),
|
|
623
|
+
* so a legacy row must warn once, not once per cache miss.
|
|
624
|
+
*/
|
|
625
|
+
this.storedConversionWarned = /* @__PURE__ */ new Set();
|
|
618
626
|
if (!options.driver && !options.engine) {
|
|
619
627
|
throw new Error("DatabaseLoader requires either a driver or engine");
|
|
620
628
|
}
|
|
@@ -875,13 +883,33 @@ var DatabaseLoader = class {
|
|
|
875
883
|
}
|
|
876
884
|
}
|
|
877
885
|
/**
|
|
878
|
-
* Convert a database row to a metadata payload.
|
|
879
|
-
*
|
|
886
|
+
* Convert a LIVE database row to a metadata payload.
|
|
887
|
+
*
|
|
888
|
+
* Parses the JSON `metadata` column back into an object, then replays the
|
|
889
|
+
* full ADR-0087 conversion chain over it (#3903): rows written under a past
|
|
890
|
+
* protocol are served canonical, exactly like the metadata-protocol's
|
|
891
|
+
* `sys_metadata` seams. History rows do NOT pass through here — history
|
|
892
|
+
* readers parse inline and stay verbatim, as a record of what was written.
|
|
893
|
+
*
|
|
894
|
+
* `flow` is skipped for the same reason the protocol skips it: flow-node
|
|
895
|
+
* conversions need the automation engine's live executor registry for their
|
|
896
|
+
* open-namespace conflict guard; flows canonicalize at `registerFlow`.
|
|
880
897
|
*/
|
|
881
898
|
rowToData(row) {
|
|
882
899
|
if (!row || !row.metadata) return null;
|
|
883
900
|
const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
884
|
-
|
|
901
|
+
const singular = PLURAL_TO_SINGULAR[row.type] ?? row.type;
|
|
902
|
+
if (singular === "flow") return payload;
|
|
903
|
+
return applyConversionsToStoredItem(singular, payload, {
|
|
904
|
+
onNotice: (n) => {
|
|
905
|
+
const key = `${n.conversionId}|${singular}|${String(row.name ?? "")}`;
|
|
906
|
+
if (this.storedConversionWarned.has(key)) return;
|
|
907
|
+
this.storedConversionWarned.add(key);
|
|
908
|
+
console.warn(
|
|
909
|
+
`[DatabaseLoader] stored ${singular}/${String(row.name ?? "<unnamed>")} carries a pre-protocol shape; ${n.message}`
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
});
|
|
885
913
|
}
|
|
886
914
|
/**
|
|
887
915
|
* Convert a database row to a MetadataRecord-like object.
|
|
@@ -2296,19 +2324,44 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2296
2324
|
/**
|
|
2297
2325
|
* Load a single metadata item from loaders.
|
|
2298
2326
|
* Iterates through registered loaders until found.
|
|
2327
|
+
*
|
|
2328
|
+
* Returns `null` both when no loader HAS the item and when every loader
|
|
2329
|
+
* FAILED — see {@link loadDiagnosed} when the caller must tell those apart.
|
|
2299
2330
|
*/
|
|
2300
2331
|
async load(type, name, options) {
|
|
2332
|
+
return (await this.loadDiagnosed(type, name, options)).data;
|
|
2333
|
+
}
|
|
2334
|
+
/**
|
|
2335
|
+
* `load`, plus whether the answer can be trusted as complete.
|
|
2336
|
+
*
|
|
2337
|
+
* [ADR-0110 D3] A miss and an outage are different facts with opposite
|
|
2338
|
+
* security meanings, and plain `load` cannot express the difference: a
|
|
2339
|
+
* loader that throws is warn-logged and skipped, so a database the metadata
|
|
2340
|
+
* plane cannot reach returns the same `null` as a name that was never
|
|
2341
|
+
* declared. Callers that gate on a declaration MUST NOT read that `null` as
|
|
2342
|
+
* "the author declared no gate" — an availability failure would silently
|
|
2343
|
+
* widen access (the REST `/actions` route's fail-open branch, #3935).
|
|
2344
|
+
*
|
|
2345
|
+
* `degraded` is true when at least one loader threw AND no loader answered
|
|
2346
|
+
* with the item. The posture is deliberately conservative: with a loader
|
|
2347
|
+
* down we cannot prove the item is absent, so we decline to claim it is.
|
|
2348
|
+
* A clean miss (every loader answered, none had it) is NOT degraded.
|
|
2349
|
+
*/
|
|
2350
|
+
async loadDiagnosed(type, name, options) {
|
|
2351
|
+
const errors = [];
|
|
2301
2352
|
for (const loader of this.loaders.values()) {
|
|
2302
2353
|
try {
|
|
2303
2354
|
const result = await loader.load(type, name, options);
|
|
2304
2355
|
if (result.data) {
|
|
2305
|
-
return result.data;
|
|
2356
|
+
return { data: result.data, degraded: false, errors };
|
|
2306
2357
|
}
|
|
2307
2358
|
} catch (e) {
|
|
2359
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2360
|
+
errors.push(`${loader.contract.name}: ${message}`);
|
|
2308
2361
|
this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
|
|
2309
2362
|
}
|
|
2310
2363
|
}
|
|
2311
|
-
return null;
|
|
2364
|
+
return { data: null, degraded: errors.length > 0, errors };
|
|
2312
2365
|
}
|
|
2313
2366
|
/**
|
|
2314
2367
|
* Load multiple metadata items from loaders.
|
|
@@ -3256,6 +3309,12 @@ var MetadataPlugin = class {
|
|
|
3256
3309
|
this.name = "com.objectstack.metadata";
|
|
3257
3310
|
this.type = "standard";
|
|
3258
3311
|
this.version = "1.0.0";
|
|
3312
|
+
/**
|
|
3313
|
+
* Services init() UNCONDITIONALLY registers (ADR-0116, #4131) — lets the
|
|
3314
|
+
* kernel name this plugin when a consumer requires `metadata` before it
|
|
3315
|
+
* initializes.
|
|
3316
|
+
*/
|
|
3317
|
+
this.providesServices = ["metadata"];
|
|
3259
3318
|
this.init = async (ctx) => {
|
|
3260
3319
|
ctx.logger.info("Initializing Metadata Manager", {
|
|
3261
3320
|
root: this.options.rootDir || process.cwd(),
|
|
@@ -3295,27 +3354,27 @@ var MetadataPlugin = class {
|
|
|
3295
3354
|
bootstrap: mode,
|
|
3296
3355
|
artifactSource: src?.mode ?? "none"
|
|
3297
3356
|
});
|
|
3357
|
+
if (src && src.mode !== "local-file") {
|
|
3358
|
+
const bad = src.mode;
|
|
3359
|
+
throw new Error(
|
|
3360
|
+
`[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 }.")
|
|
3361
|
+
);
|
|
3362
|
+
}
|
|
3298
3363
|
if (mode === "artifact-only") {
|
|
3299
|
-
if (src
|
|
3364
|
+
if (src) {
|
|
3300
3365
|
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3301
|
-
} else if (src?.mode === "artifact-api") {
|
|
3302
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3303
3366
|
} else {
|
|
3304
3367
|
throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
|
|
3305
3368
|
}
|
|
3306
3369
|
} else if (mode === "lazy") {
|
|
3307
|
-
if (src
|
|
3308
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3309
|
-
} else if (src?.mode === "artifact-api") {
|
|
3310
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3370
|
+
if (src) {
|
|
3371
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3311
3372
|
} else {
|
|
3312
3373
|
ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
|
|
3313
3374
|
}
|
|
3314
3375
|
} else {
|
|
3315
|
-
if (src
|
|
3316
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3317
|
-
} else if (src?.mode === "artifact-api") {
|
|
3318
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3376
|
+
if (src) {
|
|
3377
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3319
3378
|
} else {
|
|
3320
3379
|
await this._loadFromFileSystem(ctx);
|
|
3321
3380
|
}
|
|
@@ -3357,7 +3416,14 @@ var MetadataPlugin = class {
|
|
|
3357
3416
|
});
|
|
3358
3417
|
}
|
|
3359
3418
|
try {
|
|
3360
|
-
const
|
|
3419
|
+
const readServer = (name) => {
|
|
3420
|
+
try {
|
|
3421
|
+
return ctx.getService(name);
|
|
3422
|
+
} catch {
|
|
3423
|
+
return void 0;
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
const httpServer = readServer("http.server") ?? readServer("http-server");
|
|
3361
3427
|
if (httpServer && typeof httpServer.getRawApp === "function") {
|
|
3362
3428
|
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
3363
3429
|
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
@@ -3470,14 +3536,13 @@ var MetadataPlugin = class {
|
|
|
3470
3536
|
/**
|
|
3471
3537
|
* Fetch JSON content from a URL with configurable timeout.
|
|
3472
3538
|
*/
|
|
3473
|
-
async _fetchJson(url, fetchTimeoutMs
|
|
3539
|
+
async _fetchJson(url, fetchTimeoutMs) {
|
|
3474
3540
|
const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
|
|
3475
3541
|
const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
|
|
3476
3542
|
const controller = new AbortController();
|
|
3477
3543
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
3478
3544
|
try {
|
|
3479
3545
|
const headers = { Accept: "application/json, */*;q=0.5" };
|
|
3480
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3481
3546
|
const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
|
|
3482
3547
|
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
3483
3548
|
const content = await res.text();
|
|
@@ -3597,14 +3662,26 @@ var MetadataPlugin = class {
|
|
|
3597
3662
|
* logged but never blocks the reload.
|
|
3598
3663
|
*/
|
|
3599
3664
|
async _reloadAndAnnounce(ctx, src, changed) {
|
|
3600
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3665
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3601
3666
|
try {
|
|
3602
3667
|
await ctx.trigger("metadata:reloaded", { changed, metadata: this.lastParsedMetadata });
|
|
3603
3668
|
} catch (e) {
|
|
3604
3669
|
ctx.logger.warn("[MetadataPlugin] metadata:reloaded subscriber failed", { error: e?.message });
|
|
3605
3670
|
}
|
|
3606
3671
|
}
|
|
3607
|
-
|
|
3672
|
+
/**
|
|
3673
|
+
* @param opts.optional When true, a LOCAL artifact file that does not exist
|
|
3674
|
+
* is "nothing compiled yet" rather than a fault: log and return, leaving
|
|
3675
|
+
* the manager empty and the artifact watcher armed so the first
|
|
3676
|
+
* `os compile` hydrates the running server (#4085). Callers pass it for
|
|
3677
|
+
* the `eager` / `lazy` bootstrap modes — the development-platform paths,
|
|
3678
|
+
* where an app is optional. `artifact-only` (sealed runtime) does NOT:
|
|
3679
|
+
* there the artifact IS the deployment, so its absence must fail loudly
|
|
3680
|
+
* instead of silently serving an empty runtime. Only ENOENT is tolerated;
|
|
3681
|
+
* a present-but-unreadable artifact (malformed JSON, bad permissions) and
|
|
3682
|
+
* every remote-URL failure stay fatal.
|
|
3683
|
+
*/
|
|
3684
|
+
async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs, opts = {}) {
|
|
3608
3685
|
const isUrl = /^https?:\/\//i.test(filePath);
|
|
3609
3686
|
ctx.logger.info(
|
|
3610
3687
|
`[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
|
|
@@ -3619,34 +3696,17 @@ var MetadataPlugin = class {
|
|
|
3619
3696
|
raw = JSON.parse(content);
|
|
3620
3697
|
}
|
|
3621
3698
|
} catch (e) {
|
|
3699
|
+
if (opts.optional && !isUrl && e?.code === "ENOENT") {
|
|
3700
|
+
ctx.logger.info(
|
|
3701
|
+
"[MetadataPlugin] no compiled artifact yet \u2014 starting with no artifact metadata",
|
|
3702
|
+
{ path: filePath }
|
|
3703
|
+
);
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3622
3706
|
throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
|
|
3623
3707
|
}
|
|
3624
3708
|
await this._parseAndRegisterArtifact(ctx, raw, filePath);
|
|
3625
3709
|
}
|
|
3626
|
-
/**
|
|
3627
|
-
* P2: Load metadata from the cloud artifact API endpoint.
|
|
3628
|
-
*/
|
|
3629
|
-
async _loadFromArtifactApi(ctx, src) {
|
|
3630
|
-
const environmentId = this.options.environmentId;
|
|
3631
|
-
if (!environmentId) {
|
|
3632
|
-
throw new Error("[MetadataPlugin] artifact-api source requires options.environmentId to be set");
|
|
3633
|
-
}
|
|
3634
|
-
let artifactUrl = src.url.replace(/\/+$/, "");
|
|
3635
|
-
if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
|
|
3636
|
-
artifactUrl = `${artifactUrl}/api/v1/cloud/environments/${environmentId}/artifact`;
|
|
3637
|
-
}
|
|
3638
|
-
if (src.commitId) {
|
|
3639
|
-
artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
|
|
3640
|
-
}
|
|
3641
|
-
ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
|
|
3642
|
-
let raw;
|
|
3643
|
-
try {
|
|
3644
|
-
raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
|
|
3645
|
-
} catch (e) {
|
|
3646
|
-
throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
|
|
3647
|
-
}
|
|
3648
|
-
await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
|
|
3649
|
-
}
|
|
3650
3710
|
async _loadFromFileSystem(ctx) {
|
|
3651
3711
|
ctx.logger.info("Loading metadata from file system...");
|
|
3652
3712
|
const sortedTypes = [...DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|