@objectstack/metadata 17.0.0-rc.0 → 17.0.0-rc.2
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 +3594 -0
- package/dist/index.cjs +353 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -17
- package/dist/index.d.ts +146 -17
- package/dist/index.js +348 -94
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +353 -102
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +348 -94
- package/dist/node.js.map +1 -1
- package/package.json +12 -10
package/dist/node.d.cts
CHANGED
|
@@ -2,8 +2,8 @@ import { MetadataManager, MetadataManagerOptions, MetadataLoader, MetadataSerial
|
|
|
2
2
|
export { DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff } from './index.cjs';
|
|
3
3
|
export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, SysMetadataHistoryObject, SysMetadataObject, WatchFilter } from '@objectstack/metadata-core';
|
|
4
4
|
import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
|
|
5
|
-
export { MetadataCollectionInfo, MetadataDiffResult,
|
|
6
|
-
export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
|
|
5
|
+
export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
|
|
6
|
+
export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
|
|
7
7
|
export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
|
|
8
8
|
import { Logger } from '@objectstack/core';
|
|
9
9
|
import 'zod';
|
package/dist/node.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { MetadataManager, MetadataManagerOptions, MetadataLoader, MetadataSerial
|
|
|
2
2
|
export { DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff } from './index.js';
|
|
3
3
|
export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, SysMetadataHistoryObject, SysMetadataObject, WatchFilter } from '@objectstack/metadata-core';
|
|
4
4
|
import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
|
|
5
|
-
export { MetadataCollectionInfo, MetadataDiffResult,
|
|
6
|
-
export { IMetadataService, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
|
|
5
|
+
export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
|
|
6
|
+
export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
|
|
7
7
|
export { MetadataBulkResult, MetadataDependency, MetadataPluginConfig, MetadataPluginManifest, MetadataQuery, MetadataQueryResult, MetadataType, MetadataTypeRegistryEntry, MetadataValidationResult } from '@objectstack/spec/kernel';
|
|
8
8
|
import { Logger } from '@objectstack/core';
|
|
9
9
|
import 'zod';
|
package/dist/node.js
CHANGED
|
@@ -169,6 +169,10 @@ var init_hmr_routes = __esm({
|
|
|
169
169
|
|
|
170
170
|
// src/metadata-manager.ts
|
|
171
171
|
import { getMetadataTypeActions } from "@objectstack/spec/kernel";
|
|
172
|
+
import {
|
|
173
|
+
MetadataEventType,
|
|
174
|
+
MetadataEventSchema
|
|
175
|
+
} from "@objectstack/spec/api";
|
|
172
176
|
import { createLogger } from "@objectstack/core";
|
|
173
177
|
|
|
174
178
|
// src/serializers/json-serializer.ts
|
|
@@ -340,6 +344,8 @@ export default metadata;
|
|
|
340
344
|
|
|
341
345
|
// src/loaders/database-loader.ts
|
|
342
346
|
import { SysMetadataObject, SysMetadataHistoryObject } from "@objectstack/metadata-core";
|
|
347
|
+
import { applyConversionsToStoredItem } from "@objectstack/spec";
|
|
348
|
+
import { PLURAL_TO_SINGULAR } from "@objectstack/spec/shared";
|
|
343
349
|
|
|
344
350
|
// src/utils/metadata-history-utils.ts
|
|
345
351
|
async function calculateChecksum(metadata) {
|
|
@@ -505,6 +511,70 @@ var LRUCache = class {
|
|
|
505
511
|
}
|
|
506
512
|
};
|
|
507
513
|
|
|
514
|
+
// src/utils/schema-sync-errors.ts
|
|
515
|
+
var ALREADY_EXISTS = {
|
|
516
|
+
codes: /* @__PURE__ */ new Set([
|
|
517
|
+
// PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)
|
|
518
|
+
"42P07",
|
|
519
|
+
// duplicate_table
|
|
520
|
+
"42701",
|
|
521
|
+
// duplicate_column
|
|
522
|
+
"42710",
|
|
523
|
+
// duplicate_object — index / constraint already exists
|
|
524
|
+
// MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
|
|
525
|
+
"ER_TABLE_EXISTS_ERROR",
|
|
526
|
+
// 1050
|
|
527
|
+
"ER_DUP_FIELDNAME",
|
|
528
|
+
// 1060
|
|
529
|
+
"ER_DUP_KEYNAME"
|
|
530
|
+
// 1061
|
|
531
|
+
]),
|
|
532
|
+
errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
|
|
533
|
+
/**
|
|
534
|
+
* Message fallback for drivers that carry no machine-readable code —
|
|
535
|
+
* notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
|
|
536
|
+
* every DDL failure, so the message is the only signal available:
|
|
537
|
+
* - `table sys_metadata already exists`
|
|
538
|
+
* - `duplicate column name: environment_id`
|
|
539
|
+
* - `index idx_x already exists`
|
|
540
|
+
* Postgres phrases its own as `relation "x" already exists` /
|
|
541
|
+
* `column "x" of relation "y" already exists`, which matches the same test.
|
|
542
|
+
*/
|
|
543
|
+
message: /already exists|duplicate column name|duplicate key name/i
|
|
544
|
+
};
|
|
545
|
+
var MISSING_TABLE = {
|
|
546
|
+
codes: /* @__PURE__ */ new Set([
|
|
547
|
+
"42P01",
|
|
548
|
+
// PostgreSQL undefined_table
|
|
549
|
+
"ER_NO_SUCH_TABLE"
|
|
550
|
+
// MySQL / MariaDB 1146
|
|
551
|
+
]),
|
|
552
|
+
errnos: /* @__PURE__ */ new Set([1146]),
|
|
553
|
+
/**
|
|
554
|
+
* - SQLite / libsql: `no such table: sys_metadata_history`
|
|
555
|
+
* - PostgreSQL: `relation "sys_metadata_history" does not exist`
|
|
556
|
+
* - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
|
|
557
|
+
*/
|
|
558
|
+
message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i
|
|
559
|
+
};
|
|
560
|
+
var MAX_CAUSE_DEPTH = 4;
|
|
561
|
+
function matchesDriverError(error, signature, depth) {
|
|
562
|
+
if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
|
|
563
|
+
if (typeof error === "string") return signature.message.test(error);
|
|
564
|
+
if (typeof error !== "object") return false;
|
|
565
|
+
const err = error;
|
|
566
|
+
if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
|
|
567
|
+
if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
|
|
568
|
+
if (typeof err.message === "string" && signature.message.test(err.message)) return true;
|
|
569
|
+
return matchesDriverError(err.cause, signature, depth + 1);
|
|
570
|
+
}
|
|
571
|
+
function isSchemaAlreadyExistsError(error, depth = 0) {
|
|
572
|
+
return matchesDriverError(error, ALREADY_EXISTS, depth);
|
|
573
|
+
}
|
|
574
|
+
function isMissingTableError(error, depth = 0) {
|
|
575
|
+
return matchesDriverError(error, MISSING_TABLE, depth);
|
|
576
|
+
}
|
|
577
|
+
|
|
508
578
|
// src/migrations/add-sys-metadata-overlay-index.ts
|
|
509
579
|
var INDEX_NAME = "idx_sys_metadata_overlay_active";
|
|
510
580
|
var TABLE = "sys_metadata";
|
|
@@ -615,6 +685,25 @@ var DatabaseLoader = class {
|
|
|
615
685
|
};
|
|
616
686
|
this.schemaReady = false;
|
|
617
687
|
this.historySchemaReady = false;
|
|
688
|
+
/**
|
|
689
|
+
* Whether the loud "DDL failed" report has already been printed for the
|
|
690
|
+
* metadata table / history table respectively. AGENTS.md → "Degradation log
|
|
691
|
+
* levels": say it **once**, at the first degradation, not once per retry.
|
|
692
|
+
*/
|
|
693
|
+
this.schemaFailureReported = false;
|
|
694
|
+
this.historySchemaFailureReported = false;
|
|
695
|
+
/**
|
|
696
|
+
* Same once-only discipline for the #4825 seam: the history table is readable
|
|
697
|
+
* or it is not, and repeating the report per skipped write turns a real
|
|
698
|
+
* degradation into noise people learn to skim.
|
|
699
|
+
*/
|
|
700
|
+
this.historySeqFailureReported = false;
|
|
701
|
+
/**
|
|
702
|
+
* Once-per-process dedupe for stored-row conversion notices — `load` /
|
|
703
|
+
* `loadMany` are hot read paths (cached, but re-hit on every TTL expiry),
|
|
704
|
+
* so a legacy row must warn once, not once per cache miss.
|
|
705
|
+
*/
|
|
706
|
+
this.storedConversionWarned = /* @__PURE__ */ new Set();
|
|
618
707
|
if (!options.driver && !options.engine) {
|
|
619
708
|
throw new Error("DatabaseLoader requires either a driver or engine");
|
|
620
709
|
}
|
|
@@ -718,6 +807,24 @@ var DatabaseLoader = class {
|
|
|
718
807
|
* Reads `MAX(event_seq) + 1` for the configured `organization_id`.
|
|
719
808
|
* Legacy path — not transactional, so concurrent writes can collide.
|
|
720
809
|
* The canonical (transactional) producer is `SysMetadataRepository`.
|
|
810
|
+
*
|
|
811
|
+
* #4825 (same shape as #4728, rule from #4632) — discriminate by error TYPE.
|
|
812
|
+
* This used to `catch { return 1 }`, with a comment that named BOTH reasons a
|
|
813
|
+
* read can fail and then answered both the same way. Exactly one of them is
|
|
814
|
+
* benign: the history table has not been provisioned, so there is no row to
|
|
815
|
+
* be inconsistent with and 1 genuinely IS the next number. Every other reason
|
|
816
|
+
* — connection drop, timeout, insufficient privileges — means the rows are
|
|
817
|
+
* still there and simply were not seen, and answering 1 against a table with
|
|
818
|
+
* N rows **collides with existing rows**: the insert succeeds, the log stays
|
|
819
|
+
* empty, and `event_seq` (the ordering key that history listing and rollback
|
|
820
|
+
* targeting both stand on) is silently wrong from then on. Note this is the
|
|
821
|
+
* costlier half of the #4728 family — not bytes that never landed, but bytes
|
|
822
|
+
* that landed *wrong*, which no retry and no restart repairs.
|
|
823
|
+
*
|
|
824
|
+
* @throws The underlying driver error, unchanged, for every non-benign read
|
|
825
|
+
* failure. Deliberate: a sequence number this method cannot derive
|
|
826
|
+
* from data it actually read is not a number it may invent. The
|
|
827
|
+
* caller ({@link createHistoryRecord}) owns the consequence.
|
|
721
828
|
*/
|
|
722
829
|
async nextEventSeq() {
|
|
723
830
|
const where = this.organizationId ? { organization_id: this.organizationId } : {};
|
|
@@ -729,8 +836,9 @@ var DatabaseLoader = class {
|
|
|
729
836
|
if (v > max) max = v;
|
|
730
837
|
}
|
|
731
838
|
return max + 1;
|
|
732
|
-
} catch {
|
|
733
|
-
return 1;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (isMissingTableError(error)) return 1;
|
|
841
|
+
throw error;
|
|
734
842
|
}
|
|
735
843
|
}
|
|
736
844
|
/**
|
|
@@ -767,17 +875,32 @@ var DatabaseLoader = class {
|
|
|
767
875
|
...SysMetadataObject,
|
|
768
876
|
name: this.tableName
|
|
769
877
|
});
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
878
|
+
} catch (error) {
|
|
879
|
+
if (!isSchemaAlreadyExistsError(error)) {
|
|
880
|
+
if (!this.schemaFailureReported) {
|
|
881
|
+
this.schemaFailureReported = true;
|
|
882
|
+
console.error(
|
|
883
|
+
`[Metadata] DDL for the metadata table \`${this.tableName}\` FAILED \u2014 its table/columns were NOT created or altered. Every metadata write from here on (Studio saves, app installs, org overlays) targets storage that may not exist: writes will error out, or silently drop columns on a lenient driver, while the server keeps reporting healthy. This is NOT the benign "already exists" case \u2014 check the datasource/driver error below (insufficient privileges, datasource not connected, incompatible column type), fix it and restart. Schema sync is retried on the next metadata operation, so a transient cause recovers on its own.`,
|
|
884
|
+
error
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
return;
|
|
778
888
|
}
|
|
889
|
+
}
|
|
890
|
+
if (this.schemaFailureReported) {
|
|
891
|
+
this.schemaFailureReported = false;
|
|
892
|
+
console.info(
|
|
893
|
+
`[Metadata] DDL for the metadata table \`${this.tableName}\` succeeded on retry \u2014 metadata writes are durable again.`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
this.schemaReady = true;
|
|
897
|
+
try {
|
|
898
|
+
await migrateProjectIdToEnvironmentId(this.driver);
|
|
899
|
+
} catch {
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
await addSysMetadataOverlayIndex(this.driver);
|
|
779
903
|
} catch {
|
|
780
|
-
this.schemaReady = true;
|
|
781
904
|
}
|
|
782
905
|
}
|
|
783
906
|
/**
|
|
@@ -795,9 +918,25 @@ var DatabaseLoader = class {
|
|
|
795
918
|
...SysMetadataHistoryObject,
|
|
796
919
|
name: this.historyTableName
|
|
797
920
|
});
|
|
921
|
+
if (this.historySchemaFailureReported) {
|
|
922
|
+
this.historySchemaFailureReported = false;
|
|
923
|
+
console.info(
|
|
924
|
+
`[Metadata] DDL for the metadata history table \`${this.historyTableName}\` succeeded on retry \u2014 change history is being recorded again.`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
798
927
|
this.historySchemaReady = true;
|
|
799
928
|
} catch (error) {
|
|
800
|
-
|
|
929
|
+
if (isSchemaAlreadyExistsError(error)) {
|
|
930
|
+
this.historySchemaReady = true;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (!this.historySchemaFailureReported) {
|
|
934
|
+
this.historySchemaFailureReported = true;
|
|
935
|
+
console.error(
|
|
936
|
+
`[Metadata] DDL for the metadata history table \`${this.historyTableName}\` FAILED \u2014 its table/columns were NOT created. Metadata change history (versions, diffs, rollback) will NOT be persisted while every metadata write keeps succeeding, so the audit trail silently ends here. Fix the datasource/driver error below and restart; the sync is retried on the next metadata operation.`,
|
|
937
|
+
error
|
|
938
|
+
);
|
|
939
|
+
}
|
|
801
940
|
}
|
|
802
941
|
}
|
|
803
942
|
/**
|
|
@@ -838,7 +977,25 @@ var DatabaseLoader = class {
|
|
|
838
977
|
}
|
|
839
978
|
const historyId = generateId();
|
|
840
979
|
const metadataJson = JSON.stringify(metadata);
|
|
841
|
-
|
|
980
|
+
let eventSeq;
|
|
981
|
+
try {
|
|
982
|
+
eventSeq = await this.nextEventSeq();
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (!this.historySeqFailureReported) {
|
|
985
|
+
this.historySeqFailureReported = true;
|
|
986
|
+
console.error(
|
|
987
|
+
`[Metadata] Could not read \`${this.historyTableName}\` to determine the next \`event_seq\` \u2014 the history entry for ${type}/${name} was NOT written, and further entries are being skipped while this persists. The metadata write itself SUCCEEDED, so the server keeps looking healthy while its change history silently develops holes: version timelines and rollback targets will be incomplete. The entry is skipped deliberately \u2014 numbering it from 1 (what this code did before #4825) would collide with existing rows and make \`event_seq\` ordering wrong rather than merely incomplete, which nothing detects and no restart repairs. Fix the datasource/driver error below (connection, timeout, privileges); the next metadata write retries and reports recovery.`,
|
|
988
|
+
error
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (this.historySeqFailureReported) {
|
|
994
|
+
this.historySeqFailureReported = false;
|
|
995
|
+
console.info(
|
|
996
|
+
`[Metadata] \`${this.historyTableName}\` is readable again \u2014 \`event_seq\` numbering recovered and change history is being recorded again. Entries skipped during the outage are not backfilled.`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
842
999
|
const historyRecord = {
|
|
843
1000
|
id: historyId,
|
|
844
1001
|
name,
|
|
@@ -875,13 +1032,33 @@ var DatabaseLoader = class {
|
|
|
875
1032
|
}
|
|
876
1033
|
}
|
|
877
1034
|
/**
|
|
878
|
-
* Convert a database row to a metadata payload.
|
|
879
|
-
*
|
|
1035
|
+
* Convert a LIVE database row to a metadata payload.
|
|
1036
|
+
*
|
|
1037
|
+
* Parses the JSON `metadata` column back into an object, then replays the
|
|
1038
|
+
* full ADR-0087 conversion chain over it (#3903): rows written under a past
|
|
1039
|
+
* protocol are served canonical, exactly like the metadata-protocol's
|
|
1040
|
+
* `sys_metadata` seams. History rows do NOT pass through here — history
|
|
1041
|
+
* readers parse inline and stay verbatim, as a record of what was written.
|
|
1042
|
+
*
|
|
1043
|
+
* `flow` is skipped for the same reason the protocol skips it: flow-node
|
|
1044
|
+
* conversions need the automation engine's live executor registry for their
|
|
1045
|
+
* open-namespace conflict guard; flows canonicalize at `registerFlow`.
|
|
880
1046
|
*/
|
|
881
1047
|
rowToData(row) {
|
|
882
1048
|
if (!row || !row.metadata) return null;
|
|
883
1049
|
const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
884
|
-
|
|
1050
|
+
const singular = PLURAL_TO_SINGULAR[row.type] ?? row.type;
|
|
1051
|
+
if (singular === "flow") return payload;
|
|
1052
|
+
return applyConversionsToStoredItem(singular, payload, {
|
|
1053
|
+
onNotice: (n) => {
|
|
1054
|
+
const key = `${n.conversionId}|${singular}|${String(row.name ?? "")}`;
|
|
1055
|
+
if (this.storedConversionWarned.has(key)) return;
|
|
1056
|
+
this.storedConversionWarned.add(key);
|
|
1057
|
+
console.warn(
|
|
1058
|
+
`[DatabaseLoader] stored ${singular}/${String(row.name ?? "<unnamed>")} carries a pre-protocol shape; ${n.message}`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
885
1062
|
}
|
|
886
1063
|
/**
|
|
887
1064
|
* Convert a database row to a MetadataRecord-like object.
|
|
@@ -1275,6 +1452,17 @@ function generateId() {
|
|
|
1275
1452
|
}
|
|
1276
1453
|
|
|
1277
1454
|
// src/metadata-manager.ts
|
|
1455
|
+
function generateEventUuid() {
|
|
1456
|
+
const c = globalThis.crypto;
|
|
1457
|
+
if (c && typeof c.randomUUID === "function") {
|
|
1458
|
+
return c.randomUUID();
|
|
1459
|
+
}
|
|
1460
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (ch) => {
|
|
1461
|
+
const r = Math.random() * 16 | 0;
|
|
1462
|
+
const v = ch === "x" ? r : r & 3 | 8;
|
|
1463
|
+
return v.toString(16);
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1278
1466
|
var _MetadataManager = class _MetadataManager {
|
|
1279
1467
|
constructor(config) {
|
|
1280
1468
|
this.loaders = /* @__PURE__ */ new Map();
|
|
@@ -1395,6 +1583,60 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1395
1583
|
this.realtimeService = service;
|
|
1396
1584
|
this.logger.info("RealtimeService configured for metadata events");
|
|
1397
1585
|
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Publish a realtime {@link RealtimeMetadataEvent} for a metadata write
|
|
1588
|
+
* (#4602 — contract-first).
|
|
1589
|
+
*
|
|
1590
|
+
* What reaches a `subscribeMetadata` callback must BE the spec's
|
|
1591
|
+
* `MetadataEvent` (`@objectstack/spec/api`): `id` (uuid) at the top level,
|
|
1592
|
+
* flattened `metadataType`/`name`/`definition`, `userId` when the write
|
|
1593
|
+
* carried an actor. The transport keeps its `RealtimeEventPayload`
|
|
1594
|
+
* envelope — `payload` carries the complete `MetadataEvent`, and the client
|
|
1595
|
+
* SDK unwraps + validates it at the boundary.
|
|
1596
|
+
*
|
|
1597
|
+
* Two loud-by-design gates:
|
|
1598
|
+
* - `MetadataEventType` is a CLOSED enum. A metadata type outside it has
|
|
1599
|
+
* no declared realtime event contract, so we skip publishing (debug log)
|
|
1600
|
+
* instead of emitting an event every compliant consumer must reject.
|
|
1601
|
+
* Declared = enforced; widening coverage means widening the spec enum,
|
|
1602
|
+
* not producing off-contract events.
|
|
1603
|
+
* - The event body is `MetadataEventSchema.parse`d before publish, so a
|
|
1604
|
+
* malformed producer fails here (warn log, event not published) rather
|
|
1605
|
+
* than delivering a lie downstream.
|
|
1606
|
+
*/
|
|
1607
|
+
async publishRealtimeMetadataEvent(action, type, name, opts = {}) {
|
|
1608
|
+
if (!this.realtimeService) return;
|
|
1609
|
+
const eventType = `metadata.${type}.${action}`;
|
|
1610
|
+
if (!MetadataEventType.options.includes(eventType)) {
|
|
1611
|
+
this.logger.debug(
|
|
1612
|
+
`Metadata type '${type}' has no declared realtime event type (MetadataEventType) \u2014 skipping publish`,
|
|
1613
|
+
{ eventType, name }
|
|
1614
|
+
);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const event = MetadataEventSchema.parse({
|
|
1619
|
+
id: generateEventUuid(),
|
|
1620
|
+
type: eventType,
|
|
1621
|
+
metadataType: type,
|
|
1622
|
+
name,
|
|
1623
|
+
...typeof opts.packageId === "string" ? { packageId: opts.packageId } : {},
|
|
1624
|
+
...opts.definition !== void 0 ? { definition: opts.definition } : {},
|
|
1625
|
+
...opts.userId ? { userId: opts.userId } : {},
|
|
1626
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1627
|
+
});
|
|
1628
|
+
const envelope = {
|
|
1629
|
+
type: event.type,
|
|
1630
|
+
object: type,
|
|
1631
|
+
payload: { ...event },
|
|
1632
|
+
timestamp: event.timestamp
|
|
1633
|
+
};
|
|
1634
|
+
await this.realtimeService.publish(envelope);
|
|
1635
|
+
this.logger.debug(`Published ${eventType} event`, { name });
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
this.logger.warn(`Failed to publish metadata event`, { type, name, error });
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1398
1640
|
/**
|
|
1399
1641
|
* Register a new metadata loader (data source)
|
|
1400
1642
|
*/
|
|
@@ -1438,25 +1680,11 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1438
1680
|
await loader.save(type, name, data);
|
|
1439
1681
|
}
|
|
1440
1682
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
metadataType: type,
|
|
1447
|
-
name,
|
|
1448
|
-
definition: data,
|
|
1449
|
-
packageId: data?.packageId
|
|
1450
|
-
},
|
|
1451
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1452
|
-
};
|
|
1453
|
-
try {
|
|
1454
|
-
await this.realtimeService.publish(event);
|
|
1455
|
-
this.logger.debug(`Published metadata.${type}.created event`, { name });
|
|
1456
|
-
} catch (error) {
|
|
1457
|
-
this.logger.warn(`Failed to publish metadata event`, { type, name, error });
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1683
|
+
await this.publishRealtimeMetadataEvent(existed ? "updated" : "created", type, name, {
|
|
1684
|
+
definition: data,
|
|
1685
|
+
packageId: data?.packageId,
|
|
1686
|
+
userId: options?.userId
|
|
1687
|
+
});
|
|
1460
1688
|
if (options?.notify !== false) {
|
|
1461
1689
|
this.notifyWatchers(type, {
|
|
1462
1690
|
type: existed ? "changed" : "added",
|
|
@@ -1570,23 +1798,9 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1570
1798
|
}
|
|
1571
1799
|
}
|
|
1572
1800
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
object: type,
|
|
1577
|
-
payload: {
|
|
1578
|
-
metadataType: type,
|
|
1579
|
-
name
|
|
1580
|
-
},
|
|
1581
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1582
|
-
};
|
|
1583
|
-
try {
|
|
1584
|
-
await this.realtimeService.publish(event);
|
|
1585
|
-
this.logger.debug(`Published metadata.${type}.deleted event`, { name });
|
|
1586
|
-
} catch (error) {
|
|
1587
|
-
this.logger.warn(`Failed to publish metadata event`, { type, name, error });
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1801
|
+
await this.publishRealtimeMetadataEvent("deleted", type, name, {
|
|
1802
|
+
userId: options?.userId
|
|
1803
|
+
});
|
|
1590
1804
|
if (options?.notify !== false) {
|
|
1591
1805
|
this.notifyWatchers(type, {
|
|
1592
1806
|
type: "deleted",
|
|
@@ -2296,19 +2510,44 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2296
2510
|
/**
|
|
2297
2511
|
* Load a single metadata item from loaders.
|
|
2298
2512
|
* Iterates through registered loaders until found.
|
|
2513
|
+
*
|
|
2514
|
+
* Returns `null` both when no loader HAS the item and when every loader
|
|
2515
|
+
* FAILED — see {@link loadDiagnosed} when the caller must tell those apart.
|
|
2299
2516
|
*/
|
|
2300
2517
|
async load(type, name, options) {
|
|
2518
|
+
return (await this.loadDiagnosed(type, name, options)).data;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* `load`, plus whether the answer can be trusted as complete.
|
|
2522
|
+
*
|
|
2523
|
+
* [ADR-0110 D3] A miss and an outage are different facts with opposite
|
|
2524
|
+
* security meanings, and plain `load` cannot express the difference: a
|
|
2525
|
+
* loader that throws is warn-logged and skipped, so a database the metadata
|
|
2526
|
+
* plane cannot reach returns the same `null` as a name that was never
|
|
2527
|
+
* declared. Callers that gate on a declaration MUST NOT read that `null` as
|
|
2528
|
+
* "the author declared no gate" — an availability failure would silently
|
|
2529
|
+
* widen access (the REST `/actions` route's fail-open branch, #3935).
|
|
2530
|
+
*
|
|
2531
|
+
* `degraded` is true when at least one loader threw AND no loader answered
|
|
2532
|
+
* with the item. The posture is deliberately conservative: with a loader
|
|
2533
|
+
* down we cannot prove the item is absent, so we decline to claim it is.
|
|
2534
|
+
* A clean miss (every loader answered, none had it) is NOT degraded.
|
|
2535
|
+
*/
|
|
2536
|
+
async loadDiagnosed(type, name, options) {
|
|
2537
|
+
const errors = [];
|
|
2301
2538
|
for (const loader of this.loaders.values()) {
|
|
2302
2539
|
try {
|
|
2303
2540
|
const result = await loader.load(type, name, options);
|
|
2304
2541
|
if (result.data) {
|
|
2305
|
-
return result.data;
|
|
2542
|
+
return { data: result.data, degraded: false, errors };
|
|
2306
2543
|
}
|
|
2307
2544
|
} catch (e) {
|
|
2545
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2546
|
+
errors.push(`${loader.contract.name}: ${message}`);
|
|
2308
2547
|
this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
|
|
2309
2548
|
}
|
|
2310
2549
|
}
|
|
2311
|
-
return null;
|
|
2550
|
+
return { data: null, degraded: errors.length > 0, errors };
|
|
2312
2551
|
}
|
|
2313
2552
|
/**
|
|
2314
2553
|
* Load multiple metadata items from loaders.
|
|
@@ -3256,6 +3495,20 @@ var MetadataPlugin = class {
|
|
|
3256
3495
|
this.name = "com.objectstack.metadata";
|
|
3257
3496
|
this.type = "standard";
|
|
3258
3497
|
this.version = "1.0.0";
|
|
3498
|
+
/**
|
|
3499
|
+
* Services init() UNCONDITIONALLY registers (ADR-0116, #4131) — lets the
|
|
3500
|
+
* kernel name this plugin when a consumer requires `metadata` before it
|
|
3501
|
+
* initializes.
|
|
3502
|
+
*/
|
|
3503
|
+
this.providesServices = ["metadata"];
|
|
3504
|
+
/**
|
|
3505
|
+
* init() registers the metadata system objects through the `manifest`
|
|
3506
|
+
* service ObjectQLPlugin provides — order-if-present so that
|
|
3507
|
+
* registration is deterministic instead of "whichever init ran first"
|
|
3508
|
+
* (ADR-0116, #4471). Soft, not hard: without an engine the plugin
|
|
3509
|
+
* degrades on purpose (objects are discovered via the legacy fallback).
|
|
3510
|
+
*/
|
|
3511
|
+
this.optionalDependencies = ["com.objectstack.engine.objectql"];
|
|
3259
3512
|
this.init = async (ctx) => {
|
|
3260
3513
|
ctx.logger.info("Initializing Metadata Manager", {
|
|
3261
3514
|
root: this.options.rootDir || process.cwd(),
|
|
@@ -3295,27 +3548,27 @@ var MetadataPlugin = class {
|
|
|
3295
3548
|
bootstrap: mode,
|
|
3296
3549
|
artifactSource: src?.mode ?? "none"
|
|
3297
3550
|
});
|
|
3551
|
+
if (src && src.mode !== "local-file") {
|
|
3552
|
+
const bad = src.mode;
|
|
3553
|
+
throw new Error(
|
|
3554
|
+
`[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 }.")
|
|
3555
|
+
);
|
|
3556
|
+
}
|
|
3298
3557
|
if (mode === "artifact-only") {
|
|
3299
|
-
if (src
|
|
3558
|
+
if (src) {
|
|
3300
3559
|
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3301
|
-
} else if (src?.mode === "artifact-api") {
|
|
3302
|
-
await this._loadFromArtifactApi(ctx, src);
|
|
3303
3560
|
} else {
|
|
3304
3561
|
throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
|
|
3305
3562
|
}
|
|
3306
3563
|
} 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);
|
|
3564
|
+
if (src) {
|
|
3565
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3311
3566
|
} else {
|
|
3312
3567
|
ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
|
|
3313
3568
|
}
|
|
3314
3569
|
} 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);
|
|
3570
|
+
if (src) {
|
|
3571
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3319
3572
|
} else {
|
|
3320
3573
|
await this._loadFromFileSystem(ctx);
|
|
3321
3574
|
}
|
|
@@ -3357,7 +3610,14 @@ var MetadataPlugin = class {
|
|
|
3357
3610
|
});
|
|
3358
3611
|
}
|
|
3359
3612
|
try {
|
|
3360
|
-
const
|
|
3613
|
+
const readServer = (name) => {
|
|
3614
|
+
try {
|
|
3615
|
+
return ctx.getService(name);
|
|
3616
|
+
} catch {
|
|
3617
|
+
return void 0;
|
|
3618
|
+
}
|
|
3619
|
+
};
|
|
3620
|
+
const httpServer = readServer("http.server") ?? readServer("http-server");
|
|
3361
3621
|
if (httpServer && typeof httpServer.getRawApp === "function") {
|
|
3362
3622
|
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
3363
3623
|
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
@@ -3470,14 +3730,13 @@ var MetadataPlugin = class {
|
|
|
3470
3730
|
/**
|
|
3471
3731
|
* Fetch JSON content from a URL with configurable timeout.
|
|
3472
3732
|
*/
|
|
3473
|
-
async _fetchJson(url, fetchTimeoutMs
|
|
3733
|
+
async _fetchJson(url, fetchTimeoutMs) {
|
|
3474
3734
|
const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
|
|
3475
3735
|
const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
|
|
3476
3736
|
const controller = new AbortController();
|
|
3477
3737
|
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
3478
3738
|
try {
|
|
3479
3739
|
const headers = { Accept: "application/json, */*;q=0.5" };
|
|
3480
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3481
3740
|
const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
|
|
3482
3741
|
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
3483
3742
|
const content = await res.text();
|
|
@@ -3597,14 +3856,26 @@ var MetadataPlugin = class {
|
|
|
3597
3856
|
* logged but never blocks the reload.
|
|
3598
3857
|
*/
|
|
3599
3858
|
async _reloadAndAnnounce(ctx, src, changed) {
|
|
3600
|
-
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
3859
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs, { optional: true });
|
|
3601
3860
|
try {
|
|
3602
3861
|
await ctx.trigger("metadata:reloaded", { changed, metadata: this.lastParsedMetadata });
|
|
3603
3862
|
} catch (e) {
|
|
3604
3863
|
ctx.logger.warn("[MetadataPlugin] metadata:reloaded subscriber failed", { error: e?.message });
|
|
3605
3864
|
}
|
|
3606
3865
|
}
|
|
3607
|
-
|
|
3866
|
+
/**
|
|
3867
|
+
* @param opts.optional When true, a LOCAL artifact file that does not exist
|
|
3868
|
+
* is "nothing compiled yet" rather than a fault: log and return, leaving
|
|
3869
|
+
* the manager empty and the artifact watcher armed so the first
|
|
3870
|
+
* `os compile` hydrates the running server (#4085). Callers pass it for
|
|
3871
|
+
* the `eager` / `lazy` bootstrap modes — the development-platform paths,
|
|
3872
|
+
* where an app is optional. `artifact-only` (sealed runtime) does NOT:
|
|
3873
|
+
* there the artifact IS the deployment, so its absence must fail loudly
|
|
3874
|
+
* instead of silently serving an empty runtime. Only ENOENT is tolerated;
|
|
3875
|
+
* a present-but-unreadable artifact (malformed JSON, bad permissions) and
|
|
3876
|
+
* every remote-URL failure stay fatal.
|
|
3877
|
+
*/
|
|
3878
|
+
async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs, opts = {}) {
|
|
3608
3879
|
const isUrl = /^https?:\/\//i.test(filePath);
|
|
3609
3880
|
ctx.logger.info(
|
|
3610
3881
|
`[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
|
|
@@ -3619,34 +3890,17 @@ var MetadataPlugin = class {
|
|
|
3619
3890
|
raw = JSON.parse(content);
|
|
3620
3891
|
}
|
|
3621
3892
|
} catch (e) {
|
|
3893
|
+
if (opts.optional && !isUrl && e?.code === "ENOENT") {
|
|
3894
|
+
ctx.logger.info(
|
|
3895
|
+
"[MetadataPlugin] no compiled artifact yet \u2014 starting with no artifact metadata",
|
|
3896
|
+
{ path: filePath }
|
|
3897
|
+
);
|
|
3898
|
+
return;
|
|
3899
|
+
}
|
|
3622
3900
|
throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
|
|
3623
3901
|
}
|
|
3624
3902
|
await this._parseAndRegisterArtifact(ctx, raw, filePath);
|
|
3625
3903
|
}
|
|
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
3904
|
async _loadFromFileSystem(ctx) {
|
|
3651
3905
|
ctx.logger.info("Loading metadata from file system...");
|
|
3652
3906
|
const sortedTypes = [...DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|