@objectstack/metadata 17.2.0 → 17.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1967 -0
- package/README.md +6 -4
- package/dist/errors.cjs +1 -85
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +1 -22
- package/dist/errors.d.ts +1 -22
- package/dist/errors.js +2 -84
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +1014 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +496 -29
- package/dist/index.d.ts +496 -29
- package/dist/index.js +1013 -299
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +175 -67
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +97 -15
- package/dist/migrations/index.d.ts +97 -15
- package/dist/migrations/index.js +178 -67
- package/dist/migrations/index.js.map +1 -1
- package/dist/node.cjs +1014 -302
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +156 -3
- package/dist/node.d.ts +156 -3
- package/dist/node.js +1013 -299
- package/dist/node.js.map +1 -1
- package/dist/view-container.cjs +37 -0
- package/dist/view-container.cjs.map +1 -0
- package/dist/view-container.d.cts +76 -0
- package/dist/view-container.d.ts +76 -0
- package/dist/view-container.js +12 -0
- package/dist/view-container.js.map +1 -0
- package/package.json +52 -21
package/dist/node.cjs
CHANGED
|
@@ -38,9 +38,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
38
38
|
// src/routes/hmr-routes.ts
|
|
39
39
|
var hmr_routes_exports = {};
|
|
40
40
|
__export(hmr_routes_exports, {
|
|
41
|
+
isDevMetadataEndpointEnabled: () => isDevMetadataEndpointEnabled,
|
|
41
42
|
registerMetadataHmrRoutes: () => registerMetadataHmrRoutes
|
|
42
43
|
});
|
|
44
|
+
function isDevMetadataEndpointEnabled(env = process.env) {
|
|
45
|
+
return (env.NODE_ENV ?? "").trim().toLowerCase() === "development";
|
|
46
|
+
}
|
|
43
47
|
function registerMetadataHmrRoutes(app, manager, options = {}) {
|
|
48
|
+
if (!isDevMetadataEndpointEnabled()) return null;
|
|
44
49
|
const routePath = options.path ?? "/api/v1/dev/metadata-events";
|
|
45
50
|
const listeners = /* @__PURE__ */ new Set();
|
|
46
51
|
const broadcast = (evt) => {
|
|
@@ -192,6 +197,9 @@ var init_hmr_routes = __esm({
|
|
|
192
197
|
// src/node.ts
|
|
193
198
|
var node_exports = {};
|
|
194
199
|
__export(node_exports, {
|
|
200
|
+
AMBIGUOUS_METADATA_STEM_CODE: () => AMBIGUOUS_METADATA_STEM_CODE,
|
|
201
|
+
AMBIGUOUS_METADATA_STEM_STATUS: () => AMBIGUOUS_METADATA_STEM_STATUS,
|
|
202
|
+
AmbiguousMetadataStemError: () => AmbiguousMetadataStemError,
|
|
195
203
|
DatabaseLoader: () => DatabaseLoader,
|
|
196
204
|
FilesystemLoader: () => FilesystemLoader,
|
|
197
205
|
HistoryCleanupManager: () => HistoryCleanupManager,
|
|
@@ -202,13 +210,15 @@ __export(node_exports, {
|
|
|
202
210
|
Migration: () => migration_exports,
|
|
203
211
|
NodeMetadataManager: () => NodeMetadataManager,
|
|
204
212
|
RemoteLoader: () => RemoteLoader,
|
|
205
|
-
SysMetadataHistoryObject: () =>
|
|
206
|
-
SysMetadataObject: () =>
|
|
213
|
+
SysMetadataHistoryObject: () => import_metadata_core4.SysMetadataHistoryObject,
|
|
214
|
+
SysMetadataObject: () => import_metadata_core4.SysMetadataObject,
|
|
207
215
|
TypeScriptSerializer: () => TypeScriptSerializer,
|
|
208
216
|
YAMLSerializer: () => YAMLSerializer,
|
|
209
217
|
calculateChecksum: () => calculateChecksum,
|
|
218
|
+
deriveViewContainerObject: () => deriveViewContainerObject,
|
|
210
219
|
generateDiffSummary: () => generateDiffSummary,
|
|
211
|
-
generateSimpleDiff: () => generateSimpleDiff
|
|
220
|
+
generateSimpleDiff: () => generateSimpleDiff,
|
|
221
|
+
isAmbiguousMetadataStemError: () => isAmbiguousMetadataStemError
|
|
212
222
|
});
|
|
213
223
|
module.exports = __toCommonJS(node_exports);
|
|
214
224
|
|
|
@@ -386,7 +396,7 @@ export default metadata;
|
|
|
386
396
|
};
|
|
387
397
|
|
|
388
398
|
// src/loaders/database-loader.ts
|
|
389
|
-
var
|
|
399
|
+
var import_metadata_core2 = require("@objectstack/metadata-core");
|
|
390
400
|
var import_spec = require("@objectstack/spec");
|
|
391
401
|
var import_shared = require("@objectstack/spec/shared");
|
|
392
402
|
|
|
@@ -554,141 +564,49 @@ var LRUCache = class {
|
|
|
554
564
|
}
|
|
555
565
|
};
|
|
556
566
|
|
|
557
|
-
// src/
|
|
567
|
+
// src/loaders/database-loader.ts
|
|
558
568
|
var import_types = require("@objectstack/types");
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
"42710",
|
|
567
|
-
// duplicate_object — index / constraint already exists
|
|
568
|
-
// MySQL / MariaDB (mysql2 puts the symbolic name on `code`)
|
|
569
|
-
"ER_TABLE_EXISTS_ERROR",
|
|
570
|
-
// 1050
|
|
571
|
-
"ER_DUP_FIELDNAME",
|
|
572
|
-
// 1060
|
|
573
|
-
"ER_DUP_KEYNAME"
|
|
574
|
-
// 1061
|
|
575
|
-
]),
|
|
576
|
-
errnos: /* @__PURE__ */ new Set([1050, 1060, 1061]),
|
|
577
|
-
/**
|
|
578
|
-
* Message fallback for drivers that carry no machine-readable code —
|
|
579
|
-
* notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for
|
|
580
|
-
* every DDL failure, so the message is the only signal available:
|
|
581
|
-
* - `table sys_metadata already exists`
|
|
582
|
-
* - `duplicate column name: environment_id`
|
|
583
|
-
* - `index idx_x already exists`
|
|
584
|
-
* Postgres phrases its own as `relation "x" already exists` /
|
|
585
|
-
* `column "x" of relation "y" already exists`, which matches the same test.
|
|
586
|
-
*/
|
|
587
|
-
message: /already exists|duplicate column name|duplicate key name/i
|
|
588
|
-
};
|
|
589
|
-
var MISSING_TABLE = {
|
|
590
|
-
codes: /* @__PURE__ */ new Set([
|
|
591
|
-
"42P01",
|
|
592
|
-
// PostgreSQL undefined_table
|
|
593
|
-
"ER_NO_SUCH_TABLE"
|
|
594
|
-
// MySQL / MariaDB 1146
|
|
595
|
-
]),
|
|
596
|
-
errnos: /* @__PURE__ */ new Set([1146]),
|
|
597
|
-
/**
|
|
598
|
-
* - SQLite / libsql: `no such table: sys_metadata_history`
|
|
599
|
-
* - PostgreSQL: `relation "sys_metadata_history" does not exist`
|
|
600
|
-
* - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
|
|
601
|
-
*/
|
|
602
|
-
message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i,
|
|
603
|
-
excludes: {
|
|
604
|
-
/**
|
|
605
|
-
* Exactly the three SQLSTATEs the docblock above already names as
|
|
606
|
-
* must-stay-loud neighbours of `does not exist`. They are listed here
|
|
607
|
-
* rather than merely trusted to miss the message test, because two of
|
|
608
|
-
* them (42703 columns, 42704 constraints/triggers) have a phrasing that
|
|
609
|
-
* *does* hit it, and because a code is a fact where prose is a guess.
|
|
610
|
-
*
|
|
611
|
-
* Postgres-shaped on purpose: measured, neither MySQL
|
|
612
|
-
* (`Unknown column 'label' in 'field list'`) nor SQLite
|
|
613
|
-
* (`no such column: bogus`, `table t has no column named label`)
|
|
614
|
-
* phrases a sub-object failure so that a missing-table phrase falls out
|
|
615
|
-
* of it, so there is nothing there to exclude. Adding their codes would
|
|
616
|
-
* be surface with no defect behind it.
|
|
617
|
-
*/
|
|
618
|
-
codes: /* @__PURE__ */ new Set([
|
|
619
|
-
"42703",
|
|
620
|
-
// undefined_column
|
|
621
|
-
"42704",
|
|
622
|
-
// undefined_object — constraint, trigger, role, type, …
|
|
623
|
-
"3D000"
|
|
624
|
-
// invalid_catalog_name — `database "x" does not exist`
|
|
625
|
-
]),
|
|
626
|
-
/**
|
|
627
|
-
* `«sub-object» "x" of relation "y" …` — Postgres' phrasing for a
|
|
628
|
-
* failure about something *inside* a relation, which therefore says the
|
|
629
|
-
* relation itself is present. The two in-repo siblings that carry this
|
|
630
|
-
* phrase are `mapDataError` (`packages/rest`, #5352) and
|
|
631
|
-
* `service-analytics`'s missing-column subtraction (#6035/PR #6346).
|
|
632
|
-
*
|
|
633
|
-
* [#6615] All three now read one home — `@objectstack/types` — instead
|
|
634
|
-
* of three hand-kept copies, so the phrase can no longer be taught to
|
|
635
|
-
* the repo a fourth time or drift in one package only. The **width**
|
|
636
|
-
* difference that used to justify the copy is preserved and is the
|
|
637
|
-
* reason the home exports two functions rather than one: those two
|
|
638
|
-
* *extract* the column name to phrase a better error, so a miss costs a
|
|
639
|
-
* vaguer message; this one *excludes*, so a miss restores the
|
|
640
|
-
* corruption. {@link isRelationSubObjectPhrase} is therefore the wider
|
|
641
|
-
* question — it drops their `column`/`[a-z0-9_]+`/`does not exist`
|
|
642
|
-
* anchors: any sub-object, any quoted identifier, any verdict.
|
|
643
|
-
* Over-matching here only ever converts a benign verdict into a loud
|
|
644
|
-
* one, which is the direction this whole module already errs in.
|
|
645
|
-
*/
|
|
646
|
-
matchesMessage: import_types.isRelationSubObjectPhrase
|
|
569
|
+
|
|
570
|
+
// src/migrations/driver-exec.ts
|
|
571
|
+
function resolveDriverExec(driver) {
|
|
572
|
+
const candidate = driver;
|
|
573
|
+
if (!candidate) return void 0;
|
|
574
|
+
if (typeof candidate.execute === "function") {
|
|
575
|
+
return (sql, bindings) => candidate.execute(sql, bindings ? [...bindings] : []);
|
|
647
576
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (typeof error === "string") {
|
|
653
|
-
if (signature.excludes?.matchesMessage(error)) return false;
|
|
654
|
-
return signature.message.test(error);
|
|
655
|
-
}
|
|
656
|
-
if (typeof error !== "object") return false;
|
|
657
|
-
const err = error;
|
|
658
|
-
const excludes = signature.excludes;
|
|
659
|
-
if (excludes) {
|
|
660
|
-
if (typeof err.code === "string" && excludes.codes.has(err.code)) return false;
|
|
661
|
-
if (typeof err.message === "string" && excludes.matchesMessage(err.message)) return false;
|
|
662
|
-
}
|
|
663
|
-
if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
|
|
664
|
-
if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
|
|
665
|
-
if (typeof err.message === "string" && signature.message.test(err.message)) return true;
|
|
666
|
-
return matchesDriverError(err.cause, signature, depth + 1);
|
|
667
|
-
}
|
|
668
|
-
function isSchemaAlreadyExistsError(error, depth = 0) {
|
|
669
|
-
return matchesDriverError(error, ALREADY_EXISTS, depth);
|
|
577
|
+
if (typeof candidate.raw === "function") {
|
|
578
|
+
return (sql, bindings) => candidate.raw(sql, bindings ? [...bindings] : []);
|
|
579
|
+
}
|
|
580
|
+
return void 0;
|
|
670
581
|
}
|
|
671
|
-
function
|
|
672
|
-
return
|
|
582
|
+
function driverExecRefusal(helper) {
|
|
583
|
+
return `${helper}: driver must expose an .execute(sql, bindings?) or .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) exposes .execute(), as does its SqliteWasmDriver subclass; cloud-side TursoDriver also conforms.`;
|
|
673
584
|
}
|
|
674
585
|
|
|
675
586
|
// src/migrations/migrate-project-id-to-environment-id.ts
|
|
676
|
-
var
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
];
|
|
587
|
+
var import_metadata_core = require("@objectstack/metadata-core");
|
|
588
|
+
var SOURCE_COLUMN = "project_id";
|
|
589
|
+
var TARGET_COLUMN = "environment_id";
|
|
590
|
+
var CANDIDATE_OBJECTS = [import_metadata_core.SysMetadataObject, import_metadata_core.SysMetadataHistoryObject];
|
|
591
|
+
function declaresColumn(object, column) {
|
|
592
|
+
return Object.prototype.hasOwnProperty.call(object.fields ?? {}, column);
|
|
593
|
+
}
|
|
594
|
+
var CANDIDATE_TABLES = CANDIDATE_OBJECTS.map((o) => o.name);
|
|
595
|
+
var AFFECTED_TABLES = CANDIDATE_OBJECTS.filter((o) => declaresColumn(o, TARGET_COLUMN)).map((o) => o.name);
|
|
680
596
|
async function migrateProjectIdToEnvironmentId(driver) {
|
|
681
|
-
const
|
|
682
|
-
if (
|
|
683
|
-
throw new Error(
|
|
684
|
-
"migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. migrateProjectIdToEnvironmentId: driver must expose a .raw(sql, bindings?) method. SqlDriver (better-sqlite3/knex) supports this; cloud-side TursoDriver also conforms."
|
|
685
|
-
);
|
|
597
|
+
const exec = resolveDriverExec(driver);
|
|
598
|
+
if (!exec) {
|
|
599
|
+
throw new Error(driverExecRefusal("migrateProjectIdToEnvironmentId"));
|
|
686
600
|
}
|
|
687
601
|
const results = [];
|
|
688
|
-
for (const table of
|
|
602
|
+
for (const table of CANDIDATE_TABLES) {
|
|
603
|
+
if (!AFFECTED_TABLES.includes(table)) {
|
|
604
|
+
results.push({ table, status: "skipped_not_declared" });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
689
607
|
try {
|
|
690
|
-
const hasColumn = await _columnExists(
|
|
691
|
-
const alreadyMigrated = await _columnExists(
|
|
608
|
+
const hasColumn = await _columnExists(exec, table, SOURCE_COLUMN);
|
|
609
|
+
const alreadyMigrated = await _columnExists(exec, table, TARGET_COLUMN);
|
|
692
610
|
if (alreadyMigrated && !hasColumn) {
|
|
693
611
|
results.push({ table, status: "already_done" });
|
|
694
612
|
continue;
|
|
@@ -697,8 +615,8 @@ async function migrateProjectIdToEnvironmentId(driver) {
|
|
|
697
615
|
results.push({ table, status: "table_missing" });
|
|
698
616
|
continue;
|
|
699
617
|
}
|
|
700
|
-
await
|
|
701
|
-
`ALTER TABLE "${table}" RENAME COLUMN
|
|
618
|
+
await exec(
|
|
619
|
+
`ALTER TABLE "${table}" RENAME COLUMN ${SOURCE_COLUMN} TO ${TARGET_COLUMN}`
|
|
702
620
|
);
|
|
703
621
|
results.push({ table, status: "renamed" });
|
|
704
622
|
} catch (err) {
|
|
@@ -707,14 +625,14 @@ async function migrateProjectIdToEnvironmentId(driver) {
|
|
|
707
625
|
}
|
|
708
626
|
return results;
|
|
709
627
|
}
|
|
710
|
-
async function _columnExists(
|
|
628
|
+
async function _columnExists(exec, table, column) {
|
|
711
629
|
try {
|
|
712
|
-
const rows = await
|
|
630
|
+
const rows = await exec(`PRAGMA table_info("${table}")`);
|
|
713
631
|
if (Array.isArray(rows) && rows.length > 0) {
|
|
714
632
|
const list2 = Array.isArray(rows[0]) ? rows[0] : rows;
|
|
715
633
|
return list2.some((r) => r?.name === column);
|
|
716
634
|
}
|
|
717
|
-
const result = await
|
|
635
|
+
const result = await exec(
|
|
718
636
|
`SELECT column_name FROM information_schema.columns WHERE table_name = ? AND column_name = ?`,
|
|
719
637
|
[table, column]
|
|
720
638
|
);
|
|
@@ -726,6 +644,16 @@ async function _columnExists(driver, table, column) {
|
|
|
726
644
|
}
|
|
727
645
|
|
|
728
646
|
// src/loaders/database-loader.ts
|
|
647
|
+
function canonicalIsoInstant(value) {
|
|
648
|
+
if (value === null || value === void 0) return void 0;
|
|
649
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value.toISOString();
|
|
650
|
+
if (typeof value === "string") return value;
|
|
651
|
+
return String(value);
|
|
652
|
+
}
|
|
653
|
+
function isoFromValidDate(value) {
|
|
654
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
729
657
|
var DatabaseLoader = class {
|
|
730
658
|
constructor(options) {
|
|
731
659
|
this.contract = {
|
|
@@ -774,7 +702,7 @@ var DatabaseLoader = class {
|
|
|
774
702
|
if (cacheEnabled) {
|
|
775
703
|
const lruOpts = {
|
|
776
704
|
maxSize: cacheOpts?.maxSize ?? 500,
|
|
777
|
-
ttl: cacheOpts?.
|
|
705
|
+
ttl: cacheOpts?.ttlMs ?? 6e4
|
|
778
706
|
};
|
|
779
707
|
this.loadCache = new LRUCache(lruOpts);
|
|
780
708
|
this.loadManyCache = new LRUCache(lruOpts);
|
|
@@ -862,9 +790,11 @@ var DatabaseLoader = class {
|
|
|
862
790
|
}
|
|
863
791
|
return this.driver.create(table, data);
|
|
864
792
|
}
|
|
793
|
+
// `null` is the driver path's not-found answer (`IDataDriver.update()`,
|
|
794
|
+
// #13878); both callers here resolve the row first and discard the result.
|
|
865
795
|
async _update(table, id, data) {
|
|
866
796
|
if (this.engine) {
|
|
867
|
-
return this.engine.update(table, {
|
|
797
|
+
return this.engine.update(table, { ...data, id });
|
|
868
798
|
}
|
|
869
799
|
return this.driver.update(table, id, data);
|
|
870
800
|
}
|
|
@@ -909,7 +839,7 @@ var DatabaseLoader = class {
|
|
|
909
839
|
}
|
|
910
840
|
return max + 1;
|
|
911
841
|
} catch (error) {
|
|
912
|
-
if (isMissingTableError(error)) return 1;
|
|
842
|
+
if ((0, import_types.isMissingTableError)(error, this.historyTableName)) return 1;
|
|
913
843
|
throw error;
|
|
914
844
|
}
|
|
915
845
|
}
|
|
@@ -947,11 +877,11 @@ var DatabaseLoader = class {
|
|
|
947
877
|
}
|
|
948
878
|
try {
|
|
949
879
|
await this.driver.syncSchema(this.tableName, {
|
|
950
|
-
...
|
|
880
|
+
...import_metadata_core2.SysMetadataObject,
|
|
951
881
|
name: this.tableName
|
|
952
882
|
});
|
|
953
883
|
} catch (error) {
|
|
954
|
-
if (!isSchemaAlreadyExistsError(error)) {
|
|
884
|
+
if (!(0, import_types.isSchemaAlreadyExistsError)(error)) {
|
|
955
885
|
if (!this.schemaFailureReported) {
|
|
956
886
|
this.schemaFailureReported = true;
|
|
957
887
|
console.error(
|
|
@@ -986,7 +916,7 @@ var DatabaseLoader = class {
|
|
|
986
916
|
}
|
|
987
917
|
try {
|
|
988
918
|
await this.driver.syncSchema(this.historyTableName, {
|
|
989
|
-
...
|
|
919
|
+
...import_metadata_core2.SysMetadataHistoryObject,
|
|
990
920
|
name: this.historyTableName
|
|
991
921
|
});
|
|
992
922
|
if (this.historySchemaFailureReported) {
|
|
@@ -997,7 +927,7 @@ var DatabaseLoader = class {
|
|
|
997
927
|
}
|
|
998
928
|
this.historySchemaReady = true;
|
|
999
929
|
} catch (error) {
|
|
1000
|
-
if (isSchemaAlreadyExistsError(error)) {
|
|
930
|
+
if ((0, import_types.isSchemaAlreadyExistsError)(error)) {
|
|
1001
931
|
this.historySchemaReady = true;
|
|
1002
932
|
return;
|
|
1003
933
|
}
|
|
@@ -1155,9 +1085,9 @@ var DatabaseLoader = class {
|
|
|
1155
1085
|
source: row.source,
|
|
1156
1086
|
tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
|
|
1157
1087
|
createdBy: row.created_by,
|
|
1158
|
-
createdAt: row.created_at,
|
|
1088
|
+
createdAt: isoFromValidDate(row.created_at),
|
|
1159
1089
|
updatedBy: row.updated_by,
|
|
1160
|
-
updatedAt: row.updated_at
|
|
1090
|
+
updatedAt: isoFromValidDate(row.updated_at)
|
|
1161
1091
|
};
|
|
1162
1092
|
}
|
|
1163
1093
|
// ==========================================
|
|
@@ -1208,7 +1138,7 @@ var DatabaseLoader = class {
|
|
|
1208
1138
|
* with its empty value.
|
|
1209
1139
|
*/
|
|
1210
1140
|
rethrowUnlessTableUnprovisioned(error) {
|
|
1211
|
-
if (isMissingTableError(error)) return;
|
|
1141
|
+
if ((0, import_types.isMissingTableError)(error, this.tableName)) return;
|
|
1212
1142
|
throw error;
|
|
1213
1143
|
}
|
|
1214
1144
|
// ==========================================
|
|
@@ -1258,17 +1188,38 @@ var DatabaseLoader = class {
|
|
|
1258
1188
|
};
|
|
1259
1189
|
}
|
|
1260
1190
|
}
|
|
1261
|
-
|
|
1191
|
+
/**
|
|
1192
|
+
* The one type-wide read both plural readers share: every row of `type`, each
|
|
1193
|
+
* body paired with the `name` COLUMN it was stored under.
|
|
1194
|
+
*
|
|
1195
|
+
* [#14205] `name` is `null` only for a row whose key column does not hold a
|
|
1196
|
+
* string. Such a row is still a body {@link loadMany} must return — dropping
|
|
1197
|
+
* it would change what consumers see today — but it has no usable identity,
|
|
1198
|
+
* so {@link loadManyKeyed} filters it out rather than invent one.
|
|
1199
|
+
*
|
|
1200
|
+
* One query and one cache entry serve both methods: `loadMany()` used to own
|
|
1201
|
+
* them, and splitting them would have made every keyed `list()` read miss the
|
|
1202
|
+
* cache and re-hit the database.
|
|
1203
|
+
*/
|
|
1204
|
+
async readTypeRows(type) {
|
|
1262
1205
|
await this.ensureSchema();
|
|
1263
1206
|
if (this.loadManyCache) {
|
|
1264
1207
|
const cached = this.loadManyCache.get(type);
|
|
1265
|
-
if (cached !== void 0)
|
|
1208
|
+
if (cached !== void 0) {
|
|
1209
|
+
return cached;
|
|
1210
|
+
}
|
|
1266
1211
|
}
|
|
1267
1212
|
try {
|
|
1268
1213
|
const rows = await this._find(this.tableName, {
|
|
1269
1214
|
where: this.baseFilter(type)
|
|
1270
1215
|
});
|
|
1271
|
-
const result =
|
|
1216
|
+
const result = [];
|
|
1217
|
+
for (const row of rows) {
|
|
1218
|
+
const data = this.rowToData(row);
|
|
1219
|
+
if (data === null) continue;
|
|
1220
|
+
const name = row.name;
|
|
1221
|
+
result.push({ name: typeof name === "string" && name !== "" ? name : null, data });
|
|
1222
|
+
}
|
|
1272
1223
|
this.loadManyCache?.set(type, result);
|
|
1273
1224
|
return result;
|
|
1274
1225
|
} catch (error) {
|
|
@@ -1276,6 +1227,29 @@ var DatabaseLoader = class {
|
|
|
1276
1227
|
return [];
|
|
1277
1228
|
}
|
|
1278
1229
|
}
|
|
1230
|
+
async loadMany(type, _options) {
|
|
1231
|
+
return (await this.readTypeRows(type)).map((entry) => entry.data);
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* [#14205] The keyed half of {@link loadMany} — see
|
|
1235
|
+
* {@link MetadataKeyedItem} for why the row key travels beside the body
|
|
1236
|
+
* instead of inside it.
|
|
1237
|
+
*
|
|
1238
|
+
* `DatabaseLoader` is where the defect was measured: an aggregated view
|
|
1239
|
+
* container is written by `register('view', OBJECT, container)` and stored
|
|
1240
|
+
* verbatim, so its `sys_metadata` row carries the identity in the `name`
|
|
1241
|
+
* COLUMN and the body has none. {@link rowToData} returns that body without
|
|
1242
|
+
* folding the column in — deliberately, and unchanged here.
|
|
1243
|
+
*/
|
|
1244
|
+
async loadManyKeyed(type, _options) {
|
|
1245
|
+
const entries = await this.readTypeRows(type);
|
|
1246
|
+
const keyed = [];
|
|
1247
|
+
for (const entry of entries) {
|
|
1248
|
+
if (entry.name === null) continue;
|
|
1249
|
+
keyed.push({ name: entry.name, data: entry.data });
|
|
1250
|
+
}
|
|
1251
|
+
return keyed;
|
|
1252
|
+
}
|
|
1279
1253
|
async exists(type, name) {
|
|
1280
1254
|
await this.ensureSchema();
|
|
1281
1255
|
if (this.loadCache) {
|
|
@@ -1311,7 +1285,7 @@ var DatabaseLoader = class {
|
|
|
1311
1285
|
const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
|
|
1312
1286
|
const stats = {
|
|
1313
1287
|
size: metadataStr.length,
|
|
1314
|
-
mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1288
|
+
mtime: canonicalIsoInstant(record.updatedAt ?? record.createdAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1315
1289
|
format: "json",
|
|
1316
1290
|
etag: record.checksum
|
|
1317
1291
|
};
|
|
@@ -1372,7 +1346,7 @@ var DatabaseLoader = class {
|
|
|
1372
1346
|
changeNote: row.change_note,
|
|
1373
1347
|
organizationId: row.organization_id,
|
|
1374
1348
|
recordedBy: row.recorded_by,
|
|
1375
|
-
recordedAt: row.recorded_at
|
|
1349
|
+
recordedAt: isoFromValidDate(row.recorded_at)
|
|
1376
1350
|
};
|
|
1377
1351
|
}
|
|
1378
1352
|
/**
|
|
@@ -1429,7 +1403,7 @@ var DatabaseLoader = class {
|
|
|
1429
1403
|
changeNote: row.change_note,
|
|
1430
1404
|
organizationId: row.organization_id,
|
|
1431
1405
|
recordedBy: row.recorded_by,
|
|
1432
|
-
recordedAt: row.recorded_at
|
|
1406
|
+
recordedAt: isoFromValidDate(row.recorded_at)
|
|
1433
1407
|
};
|
|
1434
1408
|
});
|
|
1435
1409
|
return { records: result, total, hasMore };
|
|
@@ -1578,6 +1552,33 @@ function generateId() {
|
|
|
1578
1552
|
return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
1579
1553
|
}
|
|
1580
1554
|
|
|
1555
|
+
// src/loaders/ambiguous-metadata-stem.ts
|
|
1556
|
+
var AMBIGUOUS_METADATA_STEM_CODE = "AMBIGUOUS_METADATA_STEM";
|
|
1557
|
+
var AMBIGUOUS_METADATA_STEM_STATUS = 500;
|
|
1558
|
+
var AMBIGUOUS_METADATA_STEM_BRAND = /* @__PURE__ */ Symbol.for("objectstack.metadata.ambiguousStem");
|
|
1559
|
+
var _a, _b;
|
|
1560
|
+
var AmbiguousMetadataStemError = class extends (_b = Error, _a = AMBIGUOUS_METADATA_STEM_BRAND, _b) {
|
|
1561
|
+
constructor(type, stem, paths) {
|
|
1562
|
+
const sorted = [...paths].sort();
|
|
1563
|
+
super(
|
|
1564
|
+
`Ambiguous metadata name \`${stem}\` for type \`${type}\`: ${sorted.length} files resolve to the same name \u2014 ${sorted.map((p) => `\`${p}\``).join(", ")}. Only the first would ever be served (extension precedence: .json, .yaml, .yml, .ts, .js), so the others are listed and unreachable. Delete or rename all but one.`
|
|
1565
|
+
);
|
|
1566
|
+
/** Brand — see the module doc on why this is not `instanceof`. */
|
|
1567
|
+
this[_a] = true;
|
|
1568
|
+
/** ADR-0112 wire code. */
|
|
1569
|
+
this.code = AMBIGUOUS_METADATA_STEM_CODE;
|
|
1570
|
+
/** HTTP status a transport should answer. */
|
|
1571
|
+
this.status = AMBIGUOUS_METADATA_STEM_STATUS;
|
|
1572
|
+
this.name = "AmbiguousMetadataStemError";
|
|
1573
|
+
this.type = type;
|
|
1574
|
+
this.stem = stem;
|
|
1575
|
+
this.paths = sorted;
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
function isAmbiguousMetadataStemError(err) {
|
|
1579
|
+
return typeof err === "object" && err !== null && err[AMBIGUOUS_METADATA_STEM_BRAND] === true;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1581
1582
|
// src/endpoint-matcher.ts
|
|
1582
1583
|
var import_api = require("@objectstack/spec/api");
|
|
1583
1584
|
|
|
@@ -1725,6 +1726,36 @@ var EndpointMatcher = class {
|
|
|
1725
1726
|
}
|
|
1726
1727
|
};
|
|
1727
1728
|
|
|
1729
|
+
// src/view-container-expansion.ts
|
|
1730
|
+
var import_spec2 = require("@objectstack/spec");
|
|
1731
|
+
var import_shared2 = require("@objectstack/spec/shared");
|
|
1732
|
+
|
|
1733
|
+
// src/view-container.ts
|
|
1734
|
+
function deriveViewContainerObject(container) {
|
|
1735
|
+
if (!container || typeof container !== "object") return void 0;
|
|
1736
|
+
const c = container;
|
|
1737
|
+
const own = typeof c.object === "string" && c.object ? c.object : void 0;
|
|
1738
|
+
const byName = typeof c.name === "string" && c.name ? c.name : void 0;
|
|
1739
|
+
return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// src/view-container-expansion.ts
|
|
1743
|
+
function expandRuntimeViewContainer(data) {
|
|
1744
|
+
if (!(0, import_spec2.isAggregatedViewContainer)(data)) return [];
|
|
1745
|
+
const container = data;
|
|
1746
|
+
const viewObject = deriveViewContainerObject(container);
|
|
1747
|
+
if (!viewObject) return [];
|
|
1748
|
+
const out = [];
|
|
1749
|
+
for (const vi of (0, import_spec2.expandViewContainer)(viewObject, container)) {
|
|
1750
|
+
(0, import_shared2.applyProtection)(vi, {
|
|
1751
|
+
packageId: container._packageId,
|
|
1752
|
+
packageVersion: container._packageVersion
|
|
1753
|
+
});
|
|
1754
|
+
out.push(vi);
|
|
1755
|
+
}
|
|
1756
|
+
return out;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1728
1759
|
// src/metadata-manager.ts
|
|
1729
1760
|
var WRITABLE_LOADER_METHODS = ["save", "delete"];
|
|
1730
1761
|
var WRITABLE_LOADER_METHOD_SIGNATURE = {
|
|
@@ -1764,8 +1795,6 @@ var _MetadataManager = class _MetadataManager {
|
|
|
1764
1795
|
this.watchCallbacks = /* @__PURE__ */ new Map();
|
|
1765
1796
|
// In-memory metadata registry: type -> name -> data
|
|
1766
1797
|
this.registry = /* @__PURE__ */ new Map();
|
|
1767
|
-
// Overlay storage: "type:name:scope" -> MetadataOverlay
|
|
1768
|
-
this.overlays = /* @__PURE__ */ new Map();
|
|
1769
1798
|
// Type registry for metadata type info
|
|
1770
1799
|
this.typeRegistry = [];
|
|
1771
1800
|
// Dependency tracking: "type:name" -> dependencies
|
|
@@ -2340,6 +2369,67 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2340
2369
|
* result may be memoized depends on what happened to the read's registration
|
|
2341
2370
|
* while it ran, which only `list()` can see.
|
|
2342
2371
|
*/
|
|
2372
|
+
/**
|
|
2373
|
+
* Merge one loader's answer for `type` into `items`, under the identity that
|
|
2374
|
+
* loader holds each item by.
|
|
2375
|
+
*
|
|
2376
|
+
* ## [#14205] The identity of a loader-held item is its ROW KEY
|
|
2377
|
+
*
|
|
2378
|
+
* Both plural readers used to key a loader's items by `body.name`, and admit
|
|
2379
|
+
* an item only when the body carried a string one:
|
|
2380
|
+
*
|
|
2381
|
+
* ```ts
|
|
2382
|
+
* if (itemAny && typeof itemAny.name === 'string' && !items.has(itemAny.name))
|
|
2383
|
+
* ```
|
|
2384
|
+
*
|
|
2385
|
+
* A body is not required to name itself. `register(type, name, data)` takes
|
|
2386
|
+
* the key as its ARGUMENT, and `assertMetadataRegisterContract` says in as
|
|
2387
|
+
* many words that "A document with NO `name` of its own is fine — the argument
|
|
2388
|
+
* is the key". An aggregated `defineView` container is exactly that: no own
|
|
2389
|
+
* `name` by design, identity carried in the row's `name` column.
|
|
2390
|
+
*
|
|
2391
|
+
* So the old gate dropped every such item the moment the registry went cold
|
|
2392
|
+
* and only the loader could answer — a persisted view container vanished from
|
|
2393
|
+
* `list('view')` after a restart, and `listDiagnosed()` called the short
|
|
2394
|
+
* answer complete because no loader had thrown. Same gate, same effect, in
|
|
2395
|
+
* `listForIndex()`: a nameless `api` row fell out of the endpoint index, where
|
|
2396
|
+
* a miss reads as "nothing declares this route".
|
|
2397
|
+
*
|
|
2398
|
+
* The repair is to ask the loader for the key instead of guessing it from the
|
|
2399
|
+
* body ({@link MetadataLoader.loadManyKeyed}), and to keep the key BESIDE the
|
|
2400
|
+
* body: nothing is written into a body that deliberately has none, so the
|
|
2401
|
+
* register contract's refusal of a disagreeing `data.name` still means what it
|
|
2402
|
+
* says.
|
|
2403
|
+
*
|
|
2404
|
+
* Nothing consumers see today changes shape. For any item that went through
|
|
2405
|
+
* `register()`, a `data.name` that exists is required to EQUAL the key, so the
|
|
2406
|
+
* keyed merge produces the identical map entry; what is new is only the
|
|
2407
|
+
* entries the old gate refused. The `loadMany()` fallback below is the
|
|
2408
|
+
* pre-#14205 behaviour verbatim, for loaders that cannot produce keys
|
|
2409
|
+
* (`RemoteLoader`'s wire format carries bodies only).
|
|
2410
|
+
*
|
|
2411
|
+
* Read failures are NOT caught here: `readListUncached` warns-and-continues,
|
|
2412
|
+
* `listForIndex` deliberately throws, and that difference is each caller's to
|
|
2413
|
+
* keep.
|
|
2414
|
+
*/
|
|
2415
|
+
async admitLoaderItems(loader, type, items) {
|
|
2416
|
+
if (typeof loader.loadManyKeyed === "function") {
|
|
2417
|
+
const keyed = await loader.loadManyKeyed(type);
|
|
2418
|
+
for (const entry of keyed) {
|
|
2419
|
+
if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
|
|
2420
|
+
if (items.has(entry.name)) continue;
|
|
2421
|
+
items.set(entry.name, entry.data);
|
|
2422
|
+
}
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
const loaderItems = await loader.loadMany(type);
|
|
2426
|
+
for (const item of loaderItems) {
|
|
2427
|
+
const itemAny = item;
|
|
2428
|
+
if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
|
|
2429
|
+
items.set(itemAny.name, item);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2343
2433
|
async readListUncached(type) {
|
|
2344
2434
|
const items = /* @__PURE__ */ new Map();
|
|
2345
2435
|
const typeStore = this.registry.get(type);
|
|
@@ -2352,15 +2442,12 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2352
2442
|
const errors = [];
|
|
2353
2443
|
for (const loader of this.loaders.values()) {
|
|
2354
2444
|
try {
|
|
2355
|
-
|
|
2356
|
-
for (const item of loaderItems) {
|
|
2357
|
-
const itemAny = item;
|
|
2358
|
-
if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
|
|
2359
|
-
items.set(itemAny.name, item);
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2445
|
+
await this.admitLoaderItems(loader, type, items);
|
|
2362
2446
|
this.reportLoaderReadRecovered(loader.contract.name);
|
|
2363
2447
|
} catch (e) {
|
|
2448
|
+
if (isAmbiguousMetadataStemError(e)) {
|
|
2449
|
+
throw e;
|
|
2450
|
+
}
|
|
2364
2451
|
degraded = true;
|
|
2365
2452
|
errors.push(`${loader.contract.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2366
2453
|
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
@@ -2511,13 +2598,7 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2511
2598
|
}
|
|
2512
2599
|
}
|
|
2513
2600
|
for (const loader of this.loaders.values()) {
|
|
2514
|
-
|
|
2515
|
-
for (const item of loaderItems) {
|
|
2516
|
-
const itemAny = item;
|
|
2517
|
-
if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
|
|
2518
|
-
items.set(itemAny.name, item);
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2601
|
+
await this.admitLoaderItems(loader, type, items);
|
|
2521
2602
|
}
|
|
2522
2603
|
return Array.from(items.values());
|
|
2523
2604
|
}
|
|
@@ -2704,6 +2785,30 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2704
2785
|
}
|
|
2705
2786
|
/**
|
|
2706
2787
|
* List all names of metadata items of a given type
|
|
2788
|
+
*
|
|
2789
|
+
* ## [#14423] One loader's fault does not take the whole enumeration down
|
|
2790
|
+
*
|
|
2791
|
+
* This loop used to be bare — `const result = await loader.list(type)` with
|
|
2792
|
+
* no `try`, while the two sibling plural reads (`list()` via
|
|
2793
|
+
* {@link admitLoaderItems}, and {@link loadMany}) have carried a per-loader
|
|
2794
|
+
* `catch` since #5108. That asymmetry is the defect, independent of any one
|
|
2795
|
+
* caller: the SAME storage outage was swallowed by one plural read and
|
|
2796
|
+
* thrown out of the other, so which answer a caller got depended only on
|
|
2797
|
+
* which method it happened to call. A caller reading both — the action
|
|
2798
|
+
* governance audit is one — saw `loadMany` report a short-but-successful
|
|
2799
|
+
* set and `listNames` throw, and had no way to tell that one fact was
|
|
2800
|
+
* behind both.
|
|
2801
|
+
*
|
|
2802
|
+
* Same shape as `loadMany`'s, deliberately, down to the helpers: the outage
|
|
2803
|
+
* is spoken once per loader through {@link reportLoaderReadFailure} and
|
|
2804
|
+
* un-said through {@link reportLoaderReadRecovered}. ⛔ Not a third spelling
|
|
2805
|
+
* for "a loader faulted" — a second vocabulary for one event is how the two
|
|
2806
|
+
* reads drifted apart in the first place.
|
|
2807
|
+
*
|
|
2808
|
+
* The degradation is the same one `list()` documents and is graded the same
|
|
2809
|
+
* way (AGENTS.md → "Degradation log levels"): the caller still gets an
|
|
2810
|
+
* array, nothing 500s, and the set is quietly short — so it is reported at
|
|
2811
|
+
* `error`, by the shared helper, rather than being re-graded here.
|
|
2707
2812
|
*/
|
|
2708
2813
|
async listNames(type) {
|
|
2709
2814
|
type = (0, import_core.canonicalMetadataServiceType)(type);
|
|
@@ -2715,8 +2820,16 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2715
2820
|
}
|
|
2716
2821
|
}
|
|
2717
2822
|
for (const loader of this.loaders.values()) {
|
|
2718
|
-
|
|
2719
|
-
|
|
2823
|
+
try {
|
|
2824
|
+
const result = await loader.list(type);
|
|
2825
|
+
result.forEach((item) => names.add(item));
|
|
2826
|
+
this.reportLoaderReadRecovered(loader.contract.name);
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
if (isAmbiguousMetadataStemError(e)) {
|
|
2829
|
+
throw e;
|
|
2830
|
+
}
|
|
2831
|
+
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
2832
|
+
}
|
|
2720
2833
|
}
|
|
2721
2834
|
return Array.from(names);
|
|
2722
2835
|
}
|
|
@@ -2762,12 +2875,54 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2762
2875
|
* Runtime-authored `shared` / `personal` views (`sys_view_definition`) are
|
|
2763
2876
|
* merged in by the REST layer; this method returns the `package` layer that
|
|
2764
2877
|
* was registered from source.
|
|
2878
|
+
*
|
|
2879
|
+
* ## [#13913] Aggregated containers are expanded inline, per read
|
|
2880
|
+
*
|
|
2881
|
+
* `this.list('view')` is `MetadataManager`'s OWN loader-based store — the
|
|
2882
|
+
* in-memory registry plus every registered loader — and is a completely
|
|
2883
|
+
* different store from the `sys_metadata` rows `getMetaItems` reads. #13407
|
|
2884
|
+
* taught `getMetaItems` to expand a runtime-authored aggregated container
|
|
2885
|
+
* inline; this exit never called it and had no equivalent step, so a
|
|
2886
|
+
* container that `GET /meta/view?object=` now serves still answered **empty**
|
|
2887
|
+
* here.
|
|
2888
|
+
*
|
|
2889
|
+
* Merely getting the container into the store would not have helped: the
|
|
2890
|
+
* filter also requires `viewKind`, and a container has none. Loosening that
|
|
2891
|
+
* requirement is NOT the repair — it would answer with the container itself
|
|
2892
|
+
* as a view, the behaviour #7163 ruled wrong — so what is added below is the
|
|
2893
|
+
* container's **expansion**, whose items each carry the `viewKind` + `object`
|
|
2894
|
+
* pair this filter has always tested. The filter itself is untouched: it
|
|
2895
|
+
* reads the top-level `object`, exactly as `ViewSchema.object` declares.
|
|
2896
|
+
*
|
|
2897
|
+
* Registry-free and per-read, mirroring #13407's choice at the other exit and
|
|
2898
|
+
* for the same reason — the registry is process-wide, so a read must not
|
|
2899
|
+
* graft rows into it (see `view-container-expansion.ts`'s header, which also
|
|
2900
|
+
* records why the protocol's copy of this logic cannot be imported).
|
|
2901
|
+
*
|
|
2902
|
+
* Already-present items win: an expansion contributes only names the store
|
|
2903
|
+
* does not already hold, so a container whose expanded ViewItems were
|
|
2904
|
+
* registered by a source registrar (the ObjectQL boot loop, the artifact/HMR
|
|
2905
|
+
* loader) still answers with those registered, fully-enriched items and this
|
|
2906
|
+
* step adds nothing.
|
|
2765
2907
|
*/
|
|
2766
2908
|
async getViewsByObject(object) {
|
|
2767
2909
|
const views = await this.list("view");
|
|
2768
|
-
|
|
2910
|
+
const matches = views.filter(
|
|
2769
2911
|
(v) => v && typeof v === "object" && v.viewKind && v.object === object
|
|
2770
|
-
)
|
|
2912
|
+
);
|
|
2913
|
+
const known = /* @__PURE__ */ new Set();
|
|
2914
|
+
for (const v of views) {
|
|
2915
|
+
if (v && typeof v === "object" && typeof v.name === "string") known.add(v.name);
|
|
2916
|
+
}
|
|
2917
|
+
for (const v of views) {
|
|
2918
|
+
for (const item of expandRuntimeViewContainer(v)) {
|
|
2919
|
+
if (!item.viewKind || item.object !== object) continue;
|
|
2920
|
+
if (known.has(item.name)) continue;
|
|
2921
|
+
known.add(item.name);
|
|
2922
|
+
matches.push(item);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
return matches.sort(
|
|
2771
2926
|
(a, b) => (a.order ?? 0) - (b.order ?? 0) || String(a.name).localeCompare(String(b.name))
|
|
2772
2927
|
);
|
|
2773
2928
|
}
|
|
@@ -3185,66 +3340,18 @@ var _MetadataManager = class _MetadataManager {
|
|
|
3185
3340
|
};
|
|
3186
3341
|
}
|
|
3187
3342
|
// ==========================================
|
|
3188
|
-
// Overlay / Customization Management
|
|
3343
|
+
// Overlay / Customization Management — REMOVED (#13135, ADR-0049)
|
|
3189
3344
|
// ==========================================
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
* Save/update an overlay for a metadata item
|
|
3201
|
-
*/
|
|
3202
|
-
async saveOverlay(overlay) {
|
|
3203
|
-
if (this.config.persistence?.overlayWritable === false) {
|
|
3204
|
-
const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
|
|
3205
|
-
if (this.config.validation?.throwOnError) {
|
|
3206
|
-
throw new Error(msg);
|
|
3207
|
-
}
|
|
3208
|
-
this.logger.warn(msg);
|
|
3209
|
-
return;
|
|
3210
|
-
}
|
|
3211
|
-
const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
|
|
3212
|
-
this.overlays.set(key, overlay);
|
|
3213
|
-
}
|
|
3214
|
-
/**
|
|
3215
|
-
* Remove an overlay, reverting to the base definition
|
|
3216
|
-
*/
|
|
3217
|
-
async removeOverlay(type, name, scope) {
|
|
3218
|
-
this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
|
|
3219
|
-
}
|
|
3220
|
-
/**
|
|
3221
|
-
* Get the effective (merged) metadata after applying all overlays.
|
|
3222
|
-
* Resolution order: system ← merge(platform) ← merge(user)
|
|
3223
|
-
*/
|
|
3224
|
-
async getEffective(type, name, context) {
|
|
3225
|
-
const base = await this.get(type, name);
|
|
3226
|
-
if (!base) return void 0;
|
|
3227
|
-
let effective = { ...base };
|
|
3228
|
-
const platformOverlay = await this.getOverlay(type, name, "platform");
|
|
3229
|
-
if (platformOverlay?.active && platformOverlay.patch) {
|
|
3230
|
-
effective = { ...effective, ...platformOverlay.patch };
|
|
3231
|
-
}
|
|
3232
|
-
if (context?.userId) {
|
|
3233
|
-
const userOverlayKey = this.overlayKey(type, name, "user") + `:${context.userId}`;
|
|
3234
|
-
const userOverlay = this.overlays.get(userOverlayKey) ?? await this.getOverlay(type, name, "user");
|
|
3235
|
-
if (userOverlay?.active && userOverlay.patch) {
|
|
3236
|
-
if (!userOverlay.owner || userOverlay.owner === context.userId) {
|
|
3237
|
-
effective = { ...effective, ...userOverlay.patch };
|
|
3238
|
-
}
|
|
3239
|
-
}
|
|
3240
|
-
} else {
|
|
3241
|
-
const userOverlay = await this.getOverlay(type, name, "user");
|
|
3242
|
-
if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
|
|
3243
|
-
effective = { ...effective, ...userOverlay.patch };
|
|
3244
|
-
}
|
|
3245
|
-
}
|
|
3246
|
-
return effective;
|
|
3247
|
-
}
|
|
3345
|
+
//
|
|
3346
|
+
// The in-memory overlay limb (`getOverlay` / `saveOverlay` / `removeOverlay`
|
|
3347
|
+
// / `getEffective`, keyed `type:name:scope`) implemented the paper
|
|
3348
|
+
// metadata-customization protocol removed from `@objectstack/spec` in the
|
|
3349
|
+
// same change: no route ever served the paper `.../overlay` or
|
|
3350
|
+
// `.../effective` endpoints, and the only callers of these methods were this
|
|
3351
|
+
// package's own unit tests. ADR-0126 supersedes the protocol on the record.
|
|
3352
|
+
// The org-scoped customization that actually ships is ADR-0005's
|
|
3353
|
+
// `sys_metadata` overlay (`getMetaItemLayered` in metadata-protocol), which
|
|
3354
|
+
// never lived here.
|
|
3248
3355
|
// ==========================================
|
|
3249
3356
|
// Watch / Subscribe (IMetadataService)
|
|
3250
3357
|
// ==========================================
|
|
@@ -3582,6 +3689,99 @@ var _MetadataManager = class _MetadataManager {
|
|
|
3582
3689
|
}
|
|
3583
3690
|
return results;
|
|
3584
3691
|
}
|
|
3692
|
+
/**
|
|
3693
|
+
* [#14423] {@link loadMany}, read under the identity the STORE holds each
|
|
3694
|
+
* item by — the keyed plural read, beside the unkeyed one.
|
|
3695
|
+
*
|
|
3696
|
+
* ## Why a second method and not a widened `loadMany`
|
|
3697
|
+
*
|
|
3698
|
+
* `loadMany` keys nothing: it returns bodies, and every consumer that needs
|
|
3699
|
+
* an identity reads `body.name` off them. #14205 already ruled what identity
|
|
3700
|
+
* IS — the key the store holds the item under (`register(type, name, data)`
|
|
3701
|
+
* takes it as the ARGUMENT, and a body is not required to name itself) — so
|
|
3702
|
+
* `body.name` is a guess that happens to be right for most items and drops
|
|
3703
|
+
* the rest ENTIRELY: an item whose body carries no `name` is served by
|
|
3704
|
+
* `load(type, name)` and is not nameable from `loadMany`'s answer at all.
|
|
3705
|
+
*
|
|
3706
|
+
* Widening `loadMany`'s return would fix that and break every consumer of a
|
|
3707
|
+
* published shape (the ones counted on this card all read `body.name` as the
|
|
3708
|
+
* identity). So this is additive: `loadMany`'s return shape is untouched,
|
|
3709
|
+
* and a caller that needs the key asks for the key.
|
|
3710
|
+
*
|
|
3711
|
+
* ## What it reads — the same population `loadMany` reads
|
|
3712
|
+
*
|
|
3713
|
+
* Loaders only, deliberately, so this is `loadMany` keyed and nothing more.
|
|
3714
|
+
* It is NOT `list()`/{@link listNames}, which also merge the in-memory
|
|
3715
|
+
* `register()` registry; a caller wanting that set has those. Reading the
|
|
3716
|
+
* loaders alone is also what makes this the enumerable twin of
|
|
3717
|
+
* {@link loadDiagnosed}, which walks the same loaders by name — that pairing
|
|
3718
|
+
* is the point on the audit side of #14423, where an enumeration and a
|
|
3719
|
+
* by-name read that disagree about a population make one subsystem accuse
|
|
3720
|
+
* another of a defect neither has.
|
|
3721
|
+
*
|
|
3722
|
+
* ## Delegate first, fall back second — and why that order is not a style
|
|
3723
|
+
*
|
|
3724
|
+
* Per loader: {@link MetadataLoader.loadManyKeyed} where the loader offers
|
|
3725
|
+
* one, else its `list()` + a per-name `load()`. Measured, on
|
|
3726
|
+
* `DatabaseLoader`: the keyed method shares `loadMany`'s single query
|
|
3727
|
+
* (`{find:1, findOne:0}` — zero extra cost), while enumerate-then-read-each
|
|
3728
|
+
* on that same loader is a real N+1 (`{find:1, findOne:5}` for five items).
|
|
3729
|
+
* The fallback exists for loaders that cannot produce keys at all
|
|
3730
|
+
* (`RemoteLoader`'s wire format carries bodies only), and it recovers the
|
|
3731
|
+
* nameless item the pre-#14205 `loadMany`-and-key-by-`body.name` fallback
|
|
3732
|
+
* drops — which is why it is `list()` + `load()` and not `loadMany()`.
|
|
3733
|
+
*
|
|
3734
|
+
* ## Failure posture
|
|
3735
|
+
*
|
|
3736
|
+
* Per-loader `try`/`catch`, the same seam and the same helpers as
|
|
3737
|
+
* {@link loadMany} and `list()` — one loader's outage does not take the
|
|
3738
|
+
* enumeration down, and it is reported once through
|
|
3739
|
+
* {@link reportLoaderReadFailure} rather than in a third vocabulary.
|
|
3740
|
+
* Earlier loaders win a key collision, mirroring `list()`.
|
|
3741
|
+
*/
|
|
3742
|
+
async loadManyKeyed(type, options) {
|
|
3743
|
+
const items = /* @__PURE__ */ new Map();
|
|
3744
|
+
for (const loader of this.loaders.values()) {
|
|
3745
|
+
try {
|
|
3746
|
+
await this.admitKeyedLoaderItems(loader, type, items, options);
|
|
3747
|
+
this.reportLoaderReadRecovered(loader.contract.name);
|
|
3748
|
+
} catch (e) {
|
|
3749
|
+
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
return Array.from(items, ([name, data]) => ({ name, data }));
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Merge ONE loader's answer for `type` into `items`, keyed by that loader's
|
|
3756
|
+
* own key for each item — {@link loadManyKeyed}'s per-loader body.
|
|
3757
|
+
*
|
|
3758
|
+
* Distinct from {@link admitLoaderItems} on exactly one axis, and that axis
|
|
3759
|
+
* is the whole of #14423: the fallback for a loader with no
|
|
3760
|
+
* `loadManyKeyed`. `admitLoaderItems` falls back to `loadMany` keyed by
|
|
3761
|
+
* `data.name` — the pre-#14205 behaviour, verbatim, which drops a nameless
|
|
3762
|
+
* body. Here the fallback is `list()` + a per-name `load()`, so a loader
|
|
3763
|
+
* that cannot enumerate keys and bodies together still answers with both.
|
|
3764
|
+
*
|
|
3765
|
+
* Read failures are NOT caught here — the caller owns that verdict, as in
|
|
3766
|
+
* {@link admitLoaderItems}.
|
|
3767
|
+
*/
|
|
3768
|
+
async admitKeyedLoaderItems(loader, type, items, options) {
|
|
3769
|
+
if (typeof loader.loadManyKeyed === "function") {
|
|
3770
|
+
const keyed = await loader.loadManyKeyed(type, options);
|
|
3771
|
+
for (const entry of keyed) {
|
|
3772
|
+
if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
|
|
3773
|
+
if (items.has(entry.name)) continue;
|
|
3774
|
+
items.set(entry.name, entry.data);
|
|
3775
|
+
}
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3778
|
+
for (const name of await loader.list(type)) {
|
|
3779
|
+
if (typeof name !== "string" || name === "" || items.has(name)) continue;
|
|
3780
|
+
const result = await loader.load(type, name, options);
|
|
3781
|
+
if (result?.data == null) continue;
|
|
3782
|
+
items.set(name, result.data);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3585
3785
|
/**
|
|
3586
3786
|
* Save metadata item to a loader
|
|
3587
3787
|
*/
|
|
@@ -4018,6 +4218,7 @@ var MetadataManager = _MetadataManager;
|
|
|
4018
4218
|
// src/plugin.ts
|
|
4019
4219
|
var import_promises = require("fs/promises");
|
|
4020
4220
|
var import_node_crypto2 = require("crypto");
|
|
4221
|
+
var import_core2 = require("@objectstack/core");
|
|
4021
4222
|
|
|
4022
4223
|
// src/node-metadata-manager.ts
|
|
4023
4224
|
var path2 = __toESM(require("path"), 1);
|
|
@@ -4028,7 +4229,11 @@ var fs = __toESM(require("fs/promises"), 1);
|
|
|
4028
4229
|
var path = __toESM(require("path"), 1);
|
|
4029
4230
|
var import_glob = require("glob");
|
|
4030
4231
|
var import_node_crypto = require("crypto");
|
|
4031
|
-
|
|
4232
|
+
function ownNameOf(data) {
|
|
4233
|
+
const own = data?.name;
|
|
4234
|
+
return typeof own === "string" && own !== "" ? own : null;
|
|
4235
|
+
}
|
|
4236
|
+
var _FilesystemLoader = class _FilesystemLoader {
|
|
4032
4237
|
constructor(rootDir, serializers, logger) {
|
|
4033
4238
|
this.rootDir = rootDir;
|
|
4034
4239
|
this.serializers = serializers;
|
|
@@ -4126,6 +4331,82 @@ var FilesystemLoader = class {
|
|
|
4126
4331
|
}
|
|
4127
4332
|
}
|
|
4128
4333
|
async loadMany(type, options) {
|
|
4334
|
+
return (await this.loadManyEntries(type, options)).map((entry) => entry.data);
|
|
4335
|
+
}
|
|
4336
|
+
/**
|
|
4337
|
+
* [#14341] The keyed half of {@link loadMany} — see {@link MetadataKeyedItem}
|
|
4338
|
+
* for why the store's key travels BESIDE the body instead of being folded
|
|
4339
|
+
* into it.
|
|
4340
|
+
*
|
|
4341
|
+
* THE RULE, in one sentence: an item is keyed by this loader's own
|
|
4342
|
+
* name-to-path derivation — {@link nameFromFilename}, the very basename
|
|
4343
|
+
* derivation `list()` reports — ONLY where that derivation is a bijection for
|
|
4344
|
+
* the file (it sits directly under `ROOT/TYPE/` and carries one of the
|
|
4345
|
+
* extensions {@link findFile} tries, so `findFile(type, key)` resolves back to
|
|
4346
|
+
* this same file); every other shape keeps the pre-#14205 behaviour verbatim,
|
|
4347
|
+
* keyed by `body.name` when it has one and dropped when it has none.
|
|
4348
|
+
*
|
|
4349
|
+
* Why the rule stops there (PM ruling on #14341, 2026-09-02, knowingly over
|
|
4350
|
+
* triage's "a nested path keeps whatever `list()` reports for it today"):
|
|
4351
|
+
* `list()` and `findFile()` DISAGREE outside that shape. For
|
|
4352
|
+
* `ROOT/TYPE/crm/account.json`, `list()` reports the bare `account`, but
|
|
4353
|
+
* `findFile()` resolves that name against `ROOT/TYPE/account.json` and finds
|
|
4354
|
+
* nothing — the only name reaching the file is `crm/account`, which nothing
|
|
4355
|
+
* reports. An extension-less file is read by `loadMany()` and reported by
|
|
4356
|
+
* `list()`, and `findFile()` resolves neither. Keying by either side would
|
|
4357
|
+
* mint a name some other door cannot open, and two directories holding the
|
|
4358
|
+
* same basename would collide in silence
|
|
4359
|
+
* (`MetadataManager.admitLoaderItems()` keeps the first and says nothing).
|
|
4360
|
+
* The card's own fence: "keying items under names nothing else uses … is
|
|
4361
|
+
* worse than today's honest drop". So the drop stays exactly where the key is
|
|
4362
|
+
* unsettled, and is pinned as a RECORD in
|
|
4363
|
+
* `filesystem-loader-keyed-items.test.ts`.
|
|
4364
|
+
*
|
|
4365
|
+
* [#14486, partial] `list()` and {@link findFile} have since converged on
|
|
4366
|
+
* {@link resolvableNameForPath} — the derivation this method already used —
|
|
4367
|
+
* so a nested or extension-less file is now neither listed nor resolvable.
|
|
4368
|
+
* What did NOT change is the WALK behind this method: `loadManyEntries()`
|
|
4369
|
+
* still READS those files, so `loadMany()` still returns their bodies and
|
|
4370
|
+
* this method still falls back to `body.name` for them. That half of the
|
|
4371
|
+
* #14486 ruling ("nothing unlisted is returned by `loadMany()` either") is
|
|
4372
|
+
* deliberately NOT taken here: it would invert the three landed #14341 pins
|
|
4373
|
+
* in `filesystem-loader-keyed-items.test.ts:113,167,187` and the
|
|
4374
|
+
* `loadMany()` CONTROL at `:196`, and that file was under a concurrent
|
|
4375
|
+
* claim (PR #14627) when this landed. The remaining divergence — listed ⊂
|
|
4376
|
+
* loaded — is pinned as a RECORD in
|
|
4377
|
+
* `filesystem-loader-list-reachability.test.ts` rather than left implicit.
|
|
4378
|
+
*
|
|
4379
|
+
* One consequence, deliberate: a flat file whose `body.name` DISAGREES with
|
|
4380
|
+
* its basename is now keyed by the BASENAME. That is #14205's rule (identity
|
|
4381
|
+
* is the key the store holds an item under, not `body.name`) applied to this
|
|
4382
|
+
* loader, and it aligns `MetadataManager.list()` with `listNames()` for that
|
|
4383
|
+
* shape.
|
|
4384
|
+
*
|
|
4385
|
+
* The body is handed back by reference, unchanged: nothing is written into a
|
|
4386
|
+
* body that deliberately has no `name`. `limit` bounds the items LOADED,
|
|
4387
|
+
* exactly as `loadMany()` does — an entry the key rule drops has still been
|
|
4388
|
+
* read and still counts against it.
|
|
4389
|
+
*/
|
|
4390
|
+
async loadManyKeyed(type, options) {
|
|
4391
|
+
const typeDir = path.join(this.rootDir, type);
|
|
4392
|
+
const keyed = [];
|
|
4393
|
+
for (const entry of await this.loadManyEntries(type, options)) {
|
|
4394
|
+
const name = this.resolvableNameForPath(typeDir, entry.file) ?? ownNameOf(entry.data);
|
|
4395
|
+
if (name) {
|
|
4396
|
+
keyed.push({ name, data: entry.data });
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
return keyed;
|
|
4400
|
+
}
|
|
4401
|
+
/**
|
|
4402
|
+
* The single walk behind {@link loadMany} and {@link loadManyKeyed}: one glob,
|
|
4403
|
+
* one serializer pass, one `limit`. Shared so the two can never answer with
|
|
4404
|
+
* different bodies for the same file — {@link MetadataLoader.loadManyKeyed}
|
|
4405
|
+
* requires `data` to be "the same body `loadMany()` would return for the
|
|
4406
|
+
* item", and a second copy of this walk is how that would quietly stop being
|
|
4407
|
+
* true.
|
|
4408
|
+
*/
|
|
4409
|
+
async loadManyEntries(type, options) {
|
|
4129
4410
|
const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
|
|
4130
4411
|
const typeDir = path.join(this.rootDir, type);
|
|
4131
4412
|
const items = [];
|
|
@@ -4133,33 +4414,34 @@ var FilesystemLoader = class {
|
|
|
4133
4414
|
const globPatterns = patterns.map(
|
|
4134
4415
|
(pattern) => path.join(typeDir, pattern)
|
|
4135
4416
|
);
|
|
4417
|
+
const files = [];
|
|
4136
4418
|
for (const pattern of globPatterns) {
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
const content = await fs.readFile(file, "utf-8");
|
|
4147
|
-
const format = this.detectFormat(file);
|
|
4148
|
-
const serializer = this.getSerializer(format);
|
|
4149
|
-
if (serializer) {
|
|
4150
|
-
const data = serializer.deserialize(content);
|
|
4151
|
-
items.push(data);
|
|
4152
|
-
}
|
|
4153
|
-
} catch (error) {
|
|
4154
|
-
this.logger?.warn("Failed to load file", {
|
|
4155
|
-
file,
|
|
4156
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4157
|
-
});
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4419
|
+
files.push(
|
|
4420
|
+
...await (0, import_glob.glob)(pattern, {
|
|
4421
|
+
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
|
|
4422
|
+
nodir: true
|
|
4423
|
+
})
|
|
4424
|
+
);
|
|
4425
|
+
}
|
|
4426
|
+
this.resolvableNames(type, typeDir, files);
|
|
4427
|
+
for (const file of files) {
|
|
4160
4428
|
if (limit && items.length >= limit) {
|
|
4161
4429
|
break;
|
|
4162
4430
|
}
|
|
4431
|
+
try {
|
|
4432
|
+
const content = await fs.readFile(file, "utf-8");
|
|
4433
|
+
const format = this.detectFormat(file);
|
|
4434
|
+
const serializer = this.getSerializer(format);
|
|
4435
|
+
if (serializer) {
|
|
4436
|
+
const data = serializer.deserialize(content);
|
|
4437
|
+
items.push({ file, data });
|
|
4438
|
+
}
|
|
4439
|
+
} catch (error) {
|
|
4440
|
+
this.logger?.warn("Failed to load file", {
|
|
4441
|
+
file,
|
|
4442
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4163
4445
|
}
|
|
4164
4446
|
return items;
|
|
4165
4447
|
} catch (error) {
|
|
@@ -4202,19 +4484,39 @@ var FilesystemLoader = class {
|
|
|
4202
4484
|
return null;
|
|
4203
4485
|
}
|
|
4204
4486
|
}
|
|
4487
|
+
/**
|
|
4488
|
+
* [#14486] The names this loader can be asked for, and ONLY those: a file
|
|
4489
|
+
* directly under `ROOT/TYPE/` carrying an extension one of this instance's
|
|
4490
|
+
* REGISTERED serializers claims. Every name it reports resolves back through
|
|
4491
|
+
* {@link findFile}, so `listNames()` and `get()` give the same answer.
|
|
4492
|
+
*
|
|
4493
|
+
* It used to report `path.basename(file, ext)` for every file the glob found,
|
|
4494
|
+
* nested or not, extension or not — and {@link findFile} resolves neither
|
|
4495
|
+
* shape. `ROOT/TYPE/crm/account.json` was listed as `account`, which resolves
|
|
4496
|
+
* against `ROOT/TYPE/account.json` and finds nothing; an extension-less
|
|
4497
|
+
* `ROOT/TYPE/noext` was listed as `noext`, which resolves under no appended
|
|
4498
|
+
* extension at all. A name in the list that `get()` answers `null` for is the
|
|
4499
|
+
* silent failure an author (human or AI) reads as their own typo, so they
|
|
4500
|
+
* retry the same word: the list and the door now agree instead.
|
|
4501
|
+
*
|
|
4502
|
+
* Ruling (maintainer, via the director seat on #14486, 2026-09-02): narrow
|
|
4503
|
+
* the list — direction A, over B (reverse-unify: report `crm/account` and
|
|
4504
|
+
* teach `findFile()` path-shaped names), which would have made a slash inside
|
|
4505
|
+
* a metadata name every consumer's permanent obligation with no measured
|
|
4506
|
+
* demand for it. The two-segment layout follows ADR-0008 §10, which
|
|
4507
|
+
* `metadata-fs`'s `parseItemPath()` already enforces for its own store; the
|
|
4508
|
+
* EXTENSION set deliberately does NOT follow §10's `.json`-only rule — see
|
|
4509
|
+
* {@link resolvableExtensions} for why.
|
|
4510
|
+
*/
|
|
4205
4511
|
async list(type) {
|
|
4206
4512
|
const typeDir = path.join(this.rootDir, type);
|
|
4513
|
+
let files;
|
|
4207
4514
|
try {
|
|
4208
|
-
|
|
4515
|
+
files = await (0, import_glob.glob)("**/*", {
|
|
4209
4516
|
cwd: typeDir,
|
|
4210
4517
|
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
4211
4518
|
nodir: true
|
|
4212
4519
|
});
|
|
4213
|
-
return files.map((file) => {
|
|
4214
|
-
const ext = path.extname(file);
|
|
4215
|
-
const basename3 = path.basename(file, ext);
|
|
4216
|
-
return basename3;
|
|
4217
|
-
});
|
|
4218
4520
|
} catch (error) {
|
|
4219
4521
|
this.logger?.error("Failed to list", void 0, {
|
|
4220
4522
|
type,
|
|
@@ -4222,6 +4524,7 @@ var FilesystemLoader = class {
|
|
|
4222
4524
|
});
|
|
4223
4525
|
return [];
|
|
4224
4526
|
}
|
|
4527
|
+
return this.resolvableNames(type, typeDir, files.map((file) => path.join(typeDir, file)));
|
|
4225
4528
|
}
|
|
4226
4529
|
async save(type, name, data, options) {
|
|
4227
4530
|
const startTime = Date.now();
|
|
@@ -4292,12 +4595,109 @@ var FilesystemLoader = class {
|
|
|
4292
4595
|
throw error;
|
|
4293
4596
|
}
|
|
4294
4597
|
}
|
|
4598
|
+
/**
|
|
4599
|
+
* [#14486] The extensions a name can be resolved under, for THIS instance:
|
|
4600
|
+
* the ones belonging to the serializer set it was constructed with. Shared by
|
|
4601
|
+
* {@link findFile}, {@link resolvableNameForPath} and therefore {@link list},
|
|
4602
|
+
* so the set a name can be RESOLVED under cannot drift from the set that is
|
|
4603
|
+
* LISTED or the set {@link loadManyKeyed} is willing to KEY by.
|
|
4604
|
+
*
|
|
4605
|
+
* Registered, not hard-coded, and deliberately not ADR-0008 §10's `.json`
|
|
4606
|
+
* only. §10 governs the `metadata-fs` store; applying it verbatim here would
|
|
4607
|
+
* drop `.yaml` and `.ts` metadata out of `listNames()` — a breakage this card
|
|
4608
|
+
* never asked for. Under the manager's DEFAULT format set
|
|
4609
|
+
* (`typescript` / `json` / `yaml`, `metadata-manager.ts`) that leaves `.js`
|
|
4610
|
+
* out, which is the card's row-4 membership mismatch closing for free: a `.js`
|
|
4611
|
+
* file was listed and resolvable while `loadMany()` could never return it and
|
|
4612
|
+
* `load()` threw `No serializer found for format: javascript`. Register
|
|
4613
|
+
* `javascript` and it is listed, resolvable and loadable together.
|
|
4614
|
+
*/
|
|
4615
|
+
resolvableExtensions() {
|
|
4616
|
+
const extensions = [];
|
|
4617
|
+
for (const [format, formatExtensions] of _FilesystemLoader.EXTENSIONS_BY_FORMAT) {
|
|
4618
|
+
if (this.serializers.has(format)) {
|
|
4619
|
+
extensions.push(...formatExtensions);
|
|
4620
|
+
}
|
|
4621
|
+
}
|
|
4622
|
+
return extensions;
|
|
4623
|
+
}
|
|
4624
|
+
/**
|
|
4625
|
+
* The metadata name this loader reports for a file: the basename with its
|
|
4626
|
+
* extension stripped. One derivation, shared by {@link list} and
|
|
4627
|
+
* {@link loadManyKeyed}, so the two cannot drift for the shape where they
|
|
4628
|
+
* agree — `dotted.config.json` is `dotted.config` for both.
|
|
4629
|
+
*/
|
|
4630
|
+
static nameFromFilename(file) {
|
|
4631
|
+
return path.basename(file, path.extname(file));
|
|
4632
|
+
}
|
|
4633
|
+
/**
|
|
4634
|
+
* The key for a file IF this loader's name-to-path mapping is a bijection for
|
|
4635
|
+
* it: a file directly under `ROOT/TYPE/` carrying an extension
|
|
4636
|
+
* {@link findFile} tries, so `findFile(type, key)` resolves back to this very
|
|
4637
|
+
* file. `null` for every other shape — a nested path, an extension-less file,
|
|
4638
|
+
* an extension spelled in a case `findFile()` does not compose — which is why
|
|
4639
|
+
* {@link loadManyKeyed} falls back to `body.name` there rather than minting a
|
|
4640
|
+
* key no other door can open.
|
|
4641
|
+
*/
|
|
4642
|
+
resolvableNameForPath(typeDir, file) {
|
|
4643
|
+
const rel = path.relative(typeDir, file);
|
|
4644
|
+
if (rel === "" || rel.split(path.sep).length !== 1) {
|
|
4645
|
+
return null;
|
|
4646
|
+
}
|
|
4647
|
+
if (!this.resolvableExtensions().includes(path.extname(rel))) {
|
|
4648
|
+
return null;
|
|
4649
|
+
}
|
|
4650
|
+
return _FilesystemLoader.nameFromFilename(rel);
|
|
4651
|
+
}
|
|
4652
|
+
/**
|
|
4653
|
+
* [#14921] The names this loader reports for `files` — and the ONE place an
|
|
4654
|
+
* ambiguous stem is refused.
|
|
4655
|
+
*
|
|
4656
|
+
* Shared by {@link list} and {@link loadManyEntries} so the two can never
|
|
4657
|
+
* disagree about which trees are admissible: a stem that `list()` refuses
|
|
4658
|
+
* must not still be walked and returned as two bodies by `loadMany()`, which
|
|
4659
|
+
* is exactly the split this card measured.
|
|
4660
|
+
*
|
|
4661
|
+
* Refuses on the FIRST colliding name in sorted order, so a tree holding more
|
|
4662
|
+
* than one collision always names the same one — a refusal that moves
|
|
4663
|
+
* between runs reads as flakiness rather than as the fixed authoring error it
|
|
4664
|
+
* is. Paths are deduplicated because two overlapping `patterns` legitimately
|
|
4665
|
+
* match one file twice, and counting that as a collision would refuse a
|
|
4666
|
+
* perfectly good tree.
|
|
4667
|
+
*
|
|
4668
|
+
* ⛔ Not a precedence resolver. Picking a winner here is what the ruling
|
|
4669
|
+
* declined (option 2, keep the precedence and log): the loser would stay
|
|
4670
|
+
* unreachable and the listed set would stay different from the addressable
|
|
4671
|
+
* one.
|
|
4672
|
+
*/
|
|
4673
|
+
resolvableNames(type, typeDir, files) {
|
|
4674
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4675
|
+
for (const file of files) {
|
|
4676
|
+
const name = this.resolvableNameForPath(typeDir, file);
|
|
4677
|
+
if (name === null) {
|
|
4678
|
+
continue;
|
|
4679
|
+
}
|
|
4680
|
+
let paths = byName.get(name);
|
|
4681
|
+
if (!paths) {
|
|
4682
|
+
paths = /* @__PURE__ */ new Set();
|
|
4683
|
+
byName.set(name, paths);
|
|
4684
|
+
}
|
|
4685
|
+
paths.add(file);
|
|
4686
|
+
}
|
|
4687
|
+
for (const name of [...byName.keys()].sort()) {
|
|
4688
|
+
const paths = byName.get(name);
|
|
4689
|
+
if (paths.size > 1) {
|
|
4690
|
+
throw new AmbiguousMetadataStemError(type, name, [...paths]);
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
return [...byName.keys()];
|
|
4694
|
+
}
|
|
4295
4695
|
/**
|
|
4296
4696
|
* Find file for a given type and name
|
|
4297
4697
|
*/
|
|
4298
4698
|
async findFile(type, name) {
|
|
4299
4699
|
const typeDir = path.join(this.rootDir, type);
|
|
4300
|
-
const extensions =
|
|
4700
|
+
const extensions = this.resolvableExtensions();
|
|
4301
4701
|
for (const ext of extensions) {
|
|
4302
4702
|
const filePath = path.join(typeDir, `${name}${ext}`);
|
|
4303
4703
|
try {
|
|
@@ -4343,6 +4743,19 @@ var FilesystemLoader = class {
|
|
|
4343
4743
|
return `"${hash}"`;
|
|
4344
4744
|
}
|
|
4345
4745
|
};
|
|
4746
|
+
/**
|
|
4747
|
+
* The inverse of {@link detectFormat}: which file extensions carry which
|
|
4748
|
+
* format. Fixed ORDER, because it is also {@link findFile}'s precedence when
|
|
4749
|
+
* two files under one type directory share a stem — registration order must
|
|
4750
|
+
* not be able to change which file `ROOT/TYPE/NAME` opens.
|
|
4751
|
+
*/
|
|
4752
|
+
_FilesystemLoader.EXTENSIONS_BY_FORMAT = [
|
|
4753
|
+
["json", [".json"]],
|
|
4754
|
+
["yaml", [".yaml", ".yml"]],
|
|
4755
|
+
["typescript", [".ts"]],
|
|
4756
|
+
["javascript", [".js"]]
|
|
4757
|
+
];
|
|
4758
|
+
var FilesystemLoader = _FilesystemLoader;
|
|
4346
4759
|
|
|
4347
4760
|
// src/node-metadata-manager.ts
|
|
4348
4761
|
var NodeMetadataManager = class extends MetadataManager {
|
|
@@ -4467,6 +4880,20 @@ var MemoryLoader = class {
|
|
|
4467
4880
|
if (!typeStore) return [];
|
|
4468
4881
|
return Array.from(typeStore.values());
|
|
4469
4882
|
}
|
|
4883
|
+
/**
|
|
4884
|
+
* [#14205] The keyed half of {@link loadMany}. The storage map is already
|
|
4885
|
+
* `Type -> Name -> Data`, so the key this loader holds an item under is the
|
|
4886
|
+
* map key — `loadMany()` was simply discarding it, which dropped every
|
|
4887
|
+
* nameless body out of `MetadataManager.list()` and out of the endpoint index.
|
|
4888
|
+
*
|
|
4889
|
+
* The body is handed back by reference, unchanged: the key travels beside it,
|
|
4890
|
+
* never folded into it.
|
|
4891
|
+
*/
|
|
4892
|
+
async loadManyKeyed(type, _options) {
|
|
4893
|
+
const typeStore = this.storage.get(type);
|
|
4894
|
+
if (!typeStore) return [];
|
|
4895
|
+
return Array.from(typeStore, ([name, data]) => ({ name, data }));
|
|
4896
|
+
}
|
|
4470
4897
|
async exists(type, name) {
|
|
4471
4898
|
return this.storage.get(type)?.has(name) ?? false;
|
|
4472
4899
|
}
|
|
@@ -4513,20 +4940,20 @@ var MemoryLoader = class {
|
|
|
4513
4940
|
|
|
4514
4941
|
// src/plugin.ts
|
|
4515
4942
|
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
4516
|
-
var
|
|
4517
|
-
var
|
|
4518
|
-
var import_spec2 = require("@objectstack/spec");
|
|
4943
|
+
var import_shared3 = require("@objectstack/spec/shared");
|
|
4944
|
+
var import_metadata_core3 = require("@objectstack/metadata-core");
|
|
4519
4945
|
var import_spec3 = require("@objectstack/spec");
|
|
4946
|
+
var import_spec4 = require("@objectstack/spec");
|
|
4520
4947
|
var queryableMetadataObjects = [
|
|
4521
|
-
|
|
4522
|
-
|
|
4948
|
+
import_metadata_core3.SysMetadataObject,
|
|
4949
|
+
import_metadata_core3.SysMetadataHistoryObject,
|
|
4523
4950
|
// ADR-0067 commit log — sibling of sys_metadata_history (see note above).
|
|
4524
|
-
|
|
4525
|
-
|
|
4951
|
+
import_metadata_core3.SysMetadataCommitObject,
|
|
4952
|
+
import_metadata_core3.SysMetadataAuditObject,
|
|
4526
4953
|
// Runtime view storage (shared / personal). Must always be provisioned so
|
|
4527
4954
|
// end-user view creation via the generic data API has a place to write —
|
|
4528
4955
|
// mirroring why sys_metadata is always provisioned for PUT /meta.
|
|
4529
|
-
|
|
4956
|
+
import_metadata_core3.SysViewDefinitionObject
|
|
4530
4957
|
];
|
|
4531
4958
|
var REPO_SUBDIR = ".objectstack/metadata";
|
|
4532
4959
|
var ARTIFACT_FIELD_TO_TYPE = {
|
|
@@ -4549,8 +4976,37 @@ var ARTIFACT_FIELD_TO_TYPE = {
|
|
|
4549
4976
|
// positions from artifact ingestion.
|
|
4550
4977
|
positions: "position",
|
|
4551
4978
|
permissions: "permission",
|
|
4979
|
+
// [ADR-0066 D1] `capabilities` reaches the door at #12892 step 1, the
|
|
4980
|
+
// maintainer's `option 1` ruling ("the door owns the registration
|
|
4981
|
+
// route" for the five artifact security collections). Until #12894
|
|
4982
|
+
// measured it, `AppPlugin`'s `SECURITY_FIELDS` block
|
|
4983
|
+
// (packages/runtime/src/app-plugin.ts) was this collection's SOLE
|
|
4984
|
+
// registrar on an artifact boot — the one security collection the door
|
|
4985
|
+
// could not reach — so a declared capability was registered from bytes
|
|
4986
|
+
// nothing strict-parses, with no schema default and no ADR-0010
|
|
4987
|
+
// provenance. Measured on the two-reader harness, the door's copy adds
|
|
4988
|
+
// exactly four keys the raw copy lacks: `scope` (the schema default)
|
|
4989
|
+
// and `_packageId` / `_packageVersion` / `_provenance`.
|
|
4990
|
+
//
|
|
4991
|
+
// ⚠️ This entry makes the door a SECOND writer, not yet the only one:
|
|
4992
|
+
// `AppPlugin` still registers `capabilities`, and it runs last, so the
|
|
4993
|
+
// raw copy still wins a real artifact boot. Step 2 of the ruling (that
|
|
4994
|
+
// block stops registering these five on the artifact path, after a
|
|
4995
|
+
// census of the non-artifact boot paths) is what makes this the only
|
|
4996
|
+
// copy. Until then the divergence is the interim reality the ruling
|
|
4997
|
+
// explicitly permits, and #12878's pins are what keep it visible.
|
|
4998
|
+
capabilities: "capability",
|
|
4552
4999
|
sharingRules: "sharing_rule",
|
|
4553
|
-
policies:
|
|
5000
|
+
// `policies: 'policy'` removed at #12894: the stack schema is a
|
|
5001
|
+
// `strictObject` that declares no top-level `policies` key, so a
|
|
5002
|
+
// definition carrying one is refused by the strict parse a few lines
|
|
5003
|
+
// below — the entry could never match, and nothing was ever registered
|
|
5004
|
+
// under `policy` from this map. The word is real, but it lives ONE LEVEL
|
|
5005
|
+
// DOWN: on a permission set it is an alias for `rowLevelSecurity`
|
|
5006
|
+
// (`PERMISSION_SET_KEY_ALIASES`, packages/spec/src/security/permission.zod.ts)
|
|
5007
|
+
// — a key on an ITEM, never a collection. Third retirement of this exact
|
|
5008
|
+
// shape in this map (`themes` and `roles` above); the reasons are kept
|
|
5009
|
+
// in place because the first two are what made this one findable.
|
|
4554
5010
|
apis: "api",
|
|
4555
5011
|
webhooks: "webhook",
|
|
4556
5012
|
agents: "agent",
|
|
@@ -4593,6 +5049,21 @@ var MetadataPlugin = class {
|
|
|
4593
5049
|
* degrades on purpose (objects are discovered via the legacy fallback).
|
|
4594
5050
|
*/
|
|
4595
5051
|
this.optionalDependencies = ["com.objectstack.engine.objectql"];
|
|
5052
|
+
/**
|
|
5053
|
+
* Once-per-process dedupe for the summaries the versioned artifact window
|
|
5054
|
+
* emits. The artifact watcher replays `_parseAndRegisterArtifact` on every
|
|
5055
|
+
* file change, so without this a dev loop over a legacy artifact would
|
|
5056
|
+
* re-announce the same finding on every reload — the same shape
|
|
5057
|
+
* `Protocol.storedConversionWarned` guards on the stored-row pass, which
|
|
5058
|
+
* this surfacing is modeled on.
|
|
5059
|
+
*
|
|
5060
|
+
* Two key families share the set, because they share the replay:
|
|
5061
|
+
* `<conversionId>|<label>` for a forward-conversion summary (#12772), and
|
|
5062
|
+
* `unbound-form-predicate-root|<label>` for the unbound-root notice
|
|
5063
|
+
* (#12915) — one line per artifact there, not one per conversion, since
|
|
5064
|
+
* the notice already aggregates every finding it made.
|
|
5065
|
+
*/
|
|
5066
|
+
this.artifactConversionWarned = /* @__PURE__ */ new Set();
|
|
4596
5067
|
this.init = async (ctx) => {
|
|
4597
5068
|
this.initCtx = ctx;
|
|
4598
5069
|
ctx.logger.info("Initializing Metadata Manager", {
|
|
@@ -4601,7 +5072,6 @@ var MetadataPlugin = class {
|
|
|
4601
5072
|
artifactSource: this.options.artifactSource?.mode
|
|
4602
5073
|
});
|
|
4603
5074
|
ctx.registerService("metadata", this.manager);
|
|
4604
|
-
console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
|
|
4605
5075
|
const registerSysObjects = this.options.registerSystemObjects !== false;
|
|
4606
5076
|
if (registerSysObjects) {
|
|
4607
5077
|
try {
|
|
@@ -4623,7 +5093,7 @@ var MetadataPlugin = class {
|
|
|
4623
5093
|
}
|
|
4624
5094
|
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
4625
5095
|
mode: this.options.artifactSource?.mode ?? "file-system",
|
|
4626
|
-
features: ["watch", "multi-format", "query", "
|
|
5096
|
+
features: ["watch", "multi-format", "query", "type-registry"]
|
|
4627
5097
|
});
|
|
4628
5098
|
};
|
|
4629
5099
|
this.start = async (ctx) => {
|
|
@@ -4706,7 +5176,7 @@ var MetadataPlugin = class {
|
|
|
4706
5176
|
if (httpServer && typeof httpServer.getRawApp === "function") {
|
|
4707
5177
|
const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
|
|
4708
5178
|
const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
|
|
4709
|
-
hub
|
|
5179
|
+
hub?.setOnPostReload(async (body = {}) => {
|
|
4710
5180
|
const src3 = this.options.artifactSource;
|
|
4711
5181
|
if (src3?.mode === "local-file") {
|
|
4712
5182
|
try {
|
|
@@ -4746,7 +5216,7 @@ var MetadataPlugin = class {
|
|
|
4746
5216
|
pending = true;
|
|
4747
5217
|
try {
|
|
4748
5218
|
await this._reloadAndAnnounce(ctx, src2, [src2.path]);
|
|
4749
|
-
hub
|
|
5219
|
+
hub?.broadcastReload("artifact-file-changed", [src2.path]);
|
|
4750
5220
|
ctx.logger.info("[MetadataPlugin] artifact auto-reloaded (file watcher)", {
|
|
4751
5221
|
path: src2.path
|
|
4752
5222
|
});
|
|
@@ -4768,7 +5238,13 @@ var MetadataPlugin = class {
|
|
|
4768
5238
|
ctx.logger.warn("[MetadataPlugin] artifact watcher failed to start", { error: e?.message });
|
|
4769
5239
|
}
|
|
4770
5240
|
}
|
|
4771
|
-
|
|
5241
|
+
if (hub) {
|
|
5242
|
+
console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
|
|
5243
|
+
} else {
|
|
5244
|
+
console.log(
|
|
5245
|
+
`[MetadataPlugin] dev metadata-HMR endpoints NOT mounted \u2014 they require NODE_ENV=development (this process: ${process.env.NODE_ENV ? `NODE_ENV=${process.env.NODE_ENV}` : "NODE_ENV unset, treated as production"})`
|
|
5246
|
+
);
|
|
5247
|
+
}
|
|
4772
5248
|
} else {
|
|
4773
5249
|
console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
|
|
4774
5250
|
}
|
|
@@ -4864,6 +5340,106 @@ var MetadataPlugin = class {
|
|
|
4864
5340
|
if (timer) clearTimeout(timer);
|
|
4865
5341
|
}
|
|
4866
5342
|
}
|
|
5343
|
+
/**
|
|
5344
|
+
* Versioned ADR-0087 forward conversion at the artifact-ingestion door
|
|
5345
|
+
* (#12772) — runs BEFORE the strict schema parse below, because the parse
|
|
5346
|
+
* is the refusal point.
|
|
5347
|
+
*
|
|
5348
|
+
* A compiled artifact is data at rest with a version stamp: built by
|
|
5349
|
+
* released tooling, then unchanged while the platform moves on. When a
|
|
5350
|
+
* spec release retires an authorable key inside a protocol line (spec
|
|
5351
|
+
* 17.1 → 17.2 retired the `allowRestore`/`allowPurge` permission bits),
|
|
5352
|
+
* every already-built artifact carrying the key becomes unbootable at the
|
|
5353
|
+
* tombstone — with no operator remedy, since `os migrate meta` targets
|
|
5354
|
+
* sources, not built artifacts. The stored-row read path already replays
|
|
5355
|
+
* the conversion chain for exactly this reason
|
|
5356
|
+
* (`applyConversionsToStoredItem`, ADR-0087 addendum); this is the same
|
|
5357
|
+
* policy at the artifact door, **keyed off the artifact's own declared
|
|
5358
|
+
* `engines.protocol` floor**: an artifact authored below the running spec
|
|
5359
|
+
* version converts forward, an artifact authored at the current (or a
|
|
5360
|
+
* newer) surface converts nothing and answers to the strict parse,
|
|
5361
|
+
* tombstones included. The version key is what keeps this a conversion
|
|
5362
|
+
* rather than an amnesty — the retired keys return with the M2 lifecycle
|
|
5363
|
+
* batch (#1883), and artifacts authored against that surface must never
|
|
5364
|
+
* have them stripped by history.
|
|
5365
|
+
*
|
|
5366
|
+
* Notices surface the way the stored-row pass's do — operator-visible and
|
|
5367
|
+
* deduped — as one summary line per conversion per artifact rather than
|
|
5368
|
+
* one per rewritten path (a real 17.1 artifact carried 150 strips of the
|
|
5369
|
+
* same two keys; 150 identical warn lines would bury the boot log).
|
|
5370
|
+
*/
|
|
5371
|
+
_convertArtifactForward(ctx, definition, label) {
|
|
5372
|
+
const result = (0, import_metadata_core3.applyArtifactForwardConversions)(definition);
|
|
5373
|
+
this._warnUnboundFormPredicateRoots(ctx, result, label);
|
|
5374
|
+
if (result.notices.length === 0) return result.definition;
|
|
5375
|
+
const byConversion = /* @__PURE__ */ new Map();
|
|
5376
|
+
for (const n of result.notices) {
|
|
5377
|
+
const existing = byConversion.get(n.conversionId);
|
|
5378
|
+
if (existing) existing.count += 1;
|
|
5379
|
+
else byConversion.set(n.conversionId, { count: 1, firstPath: n.path, message: n.message });
|
|
5380
|
+
}
|
|
5381
|
+
for (const [conversionId, agg] of byConversion) {
|
|
5382
|
+
const key = `${conversionId}|${label}`;
|
|
5383
|
+
if (this.artifactConversionWarned.has(key)) continue;
|
|
5384
|
+
this.artifactConversionWarned.add(key);
|
|
5385
|
+
ctx.logger.warn(
|
|
5386
|
+
`[MetadataPlugin] artifact '${label}' predates this runtime's spec (authored engines.protocol floor ${result.authoredFloor ?? "<undeclared>"}, runtime spec ${result.runtimeSpecVersion}) \u2014 converted ${agg.count} site(s) forward via ADR-0087 conversion '${conversionId}' (first at ${agg.firstPath}). ${agg.message} The artifact file itself is unchanged \u2014 rebuild it with current tooling ('os build') to persist the canonical shape.`
|
|
5387
|
+
);
|
|
5388
|
+
}
|
|
5389
|
+
return result.definition;
|
|
5390
|
+
}
|
|
5391
|
+
/**
|
|
5392
|
+
* Operator-facing boot notice for form-view predicates that fault OPEN on
|
|
5393
|
+
* this runtime (#12915 scope C — maintainer ruling 2026-08-28, 「同意C」).
|
|
5394
|
+
*
|
|
5395
|
+
* A form-view predicate binds `record` / `previous` / `parent` (runtime
|
|
5396
|
+
* record forms) or `data` (metadata-editing forms) — and a FIELD-level one
|
|
5397
|
+
* also binds `current_user` and its ADR-0068 aliases (objectui#6010),
|
|
5398
|
+
* which a SECTION-level one does not. The contract states beside that
|
|
5399
|
+
* vocabulary that a bare identifier is UNBOUND and the predicate faults,
|
|
5400
|
+
* and `visibleWhen`'s fault fallback is `true`. On a real
|
|
5401
|
+
* 17.1-built artifact that combination dead-ends record creation in the
|
|
5402
|
+
* console: the conditionally hidden field renders, and its unconditional
|
|
5403
|
+
* `required: true` — authored to be gated by the visibility that no longer
|
|
5404
|
+
* applies — blocks every submit, while the same payload POSTs 201 through
|
|
5405
|
+
* REST. Nothing refused, nothing logged, and only the operator can fix it
|
|
5406
|
+
* (by rebuilding the artifact), so this is the channel the ruling picked:
|
|
5407
|
+
* service startup, server-side, never a console surface — the person at
|
|
5408
|
+
* the form cannot act on "your artifact is stale".
|
|
5409
|
+
*
|
|
5410
|
+
* **Detection only.** No refusal, no rewrite, no behaviour change: the
|
|
5411
|
+
* predicate keeps faulting open exactly as before. Rewriting a bare root to
|
|
5412
|
+
* `record.` is the ADR-0087 conversion (#12915 scope A), deferred by the
|
|
5413
|
+
* same ruling with an explicit start line.
|
|
5414
|
+
*
|
|
5415
|
+
* **Same versioned window as the conversion replay above** — and read off
|
|
5416
|
+
* that pass's own verdict rather than recomputed, so the two can never
|
|
5417
|
+
* disagree about which artifacts are "old". An artifact declaring the
|
|
5418
|
+
* current (or a newer) floor answers to the strict parse and gets nothing
|
|
5419
|
+
* from here even when it does carry bare roots; that boundary is what keeps
|
|
5420
|
+
* a notice about legacy artifacts out of contract territory. An undeclared
|
|
5421
|
+
* range is treated as old data at rest, matching the grandfathering posture
|
|
5422
|
+
* the window already takes (`converted-undeclared`).
|
|
5423
|
+
*/
|
|
5424
|
+
_warnUnboundFormPredicateRoots(ctx, result, label) {
|
|
5425
|
+
if (result.verdict !== "converted-forward" && result.verdict !== "converted-undeclared") return;
|
|
5426
|
+
const findings = (0, import_metadata_core3.detectUnboundFormViewPredicateRoots)(result.definition);
|
|
5427
|
+
if (findings.length === 0) return;
|
|
5428
|
+
const key = `unbound-form-predicate-root|${label}`;
|
|
5429
|
+
if (this.artifactConversionWarned.has(key)) return;
|
|
5430
|
+
this.artifactConversionWarned.add(key);
|
|
5431
|
+
const views = [...new Set(findings.map((f) => f.view))];
|
|
5432
|
+
const roots = [...new Set(findings.map((f) => f.root))];
|
|
5433
|
+
const quote = (list) => list.map((v) => `'${v}'`).join(", ");
|
|
5434
|
+
const surfaces = new Set(findings.map((f) => f.surface));
|
|
5435
|
+
const vocabulary = [
|
|
5436
|
+
surfaces.has("field") ? `on a form FIELD: ${quote(import_metadata_core3.BOUND_FORM_FIELD_PREDICATE_ROOTS)}` : null,
|
|
5437
|
+
surfaces.has("section") ? `on a form SECTION: ${quote(import_metadata_core3.BOUND_FORM_VIEW_PREDICATE_ROOTS)}` : null
|
|
5438
|
+
].filter(Boolean).join("; ");
|
|
5439
|
+
ctx.logger.warn(
|
|
5440
|
+
`[MetadataPlugin] artifact '${label}' predates this runtime's spec (authored engines.protocol floor ${result.authoredFloor ?? "<undeclared>"}, runtime spec ${result.runtimeSpecVersion}) and carries ${findings.length} form-view predicate(s) whose root identifier is NOT bound where it evaluates \u2014 ${quote(roots)} (bound roots ${vocabulary}) \u2014 across ${views.length} view(s): ${views.join(", ")} (first at ${findings[0].path}). Each one faults at evaluation and visibility fails OPEN, so a field the predicate was authored to HIDE renders anyway \u2014 and an unconditional 'required: true' on such a field dead-ends record creation in the console while the REST door still accepts it. Nothing was rewritten and no behaviour changed here; rebuild the artifact with current tooling ('os build') so its predicates carry a bound root ('record.<field>').`
|
|
5441
|
+
);
|
|
5442
|
+
}
|
|
4867
5443
|
/**
|
|
4868
5444
|
* Parse raw artifact JSON (envelope or bare definition) and register all
|
|
4869
5445
|
* metadata items into the MetadataManager.
|
|
@@ -4882,16 +5458,22 @@ var MetadataPlugin = class {
|
|
|
4882
5458
|
let metadata;
|
|
4883
5459
|
const obj = raw;
|
|
4884
5460
|
if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
|
|
4885
|
-
const artifact = EnvironmentArtifactSchema.parse(
|
|
5461
|
+
const artifact = EnvironmentArtifactSchema.parse({
|
|
5462
|
+
...obj,
|
|
5463
|
+
metadata: this._convertArtifactForward(ctx, obj.metadata, label)
|
|
5464
|
+
});
|
|
4886
5465
|
metadata = artifact.metadata;
|
|
4887
5466
|
} else if (obj?.success && obj?.data?.metadata) {
|
|
4888
|
-
const artifact = EnvironmentArtifactSchema.parse(
|
|
5467
|
+
const artifact = EnvironmentArtifactSchema.parse({
|
|
5468
|
+
...obj.data,
|
|
5469
|
+
metadata: this._convertArtifactForward(ctx, obj.data.metadata, label)
|
|
5470
|
+
});
|
|
4889
5471
|
metadata = artifact.metadata;
|
|
4890
5472
|
} else {
|
|
4891
|
-
const def = ObjectStackDefinitionSchema.parse(obj);
|
|
5473
|
+
const def = ObjectStackDefinitionSchema.parse(this._convertArtifactForward(ctx, obj, label));
|
|
4892
5474
|
const canonical = JSON.stringify(def, Object.keys(def).sort());
|
|
4893
5475
|
const checksum = (0, import_node_crypto2.createHash)("sha256").update(canonical).digest("hex");
|
|
4894
|
-
const environmentId = this.options.environmentId ?? "
|
|
5476
|
+
const environmentId = this.options.environmentId ?? "env_local";
|
|
4895
5477
|
EnvironmentArtifactSchema.parse({
|
|
4896
5478
|
schemaVersion: "0.1",
|
|
4897
5479
|
environmentId,
|
|
@@ -4905,53 +5487,127 @@ var MetadataPlugin = class {
|
|
|
4905
5487
|
const memLoader = new MemoryLoader();
|
|
4906
5488
|
const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
|
|
4907
5489
|
const manifestVersion = metadata?.manifest?.version ?? metadata?.version ?? void 0;
|
|
5490
|
+
const carriesPackages = Array.isArray(metadata?.packages);
|
|
5491
|
+
const bodies = (0, import_core2.resolveArtifactPackageOrder)(metadata);
|
|
5492
|
+
const ownedByPackage = /* @__PURE__ */ new Map();
|
|
5493
|
+
const claim = (type, name) => {
|
|
5494
|
+
let names = ownedByPackage.get(type);
|
|
5495
|
+
if (!names) ownedByPackage.set(type, names = /* @__PURE__ */ new Set());
|
|
5496
|
+
names.add(name);
|
|
5497
|
+
};
|
|
5498
|
+
const claimed = (type, name) => ownedByPackage.get(type)?.has(name) === true;
|
|
5499
|
+
let totalRegistered = 0;
|
|
5500
|
+
for (const body of bodies) {
|
|
5501
|
+
totalRegistered += await this._registerArtifactBodyCollections(
|
|
5502
|
+
ctx,
|
|
5503
|
+
memLoader,
|
|
5504
|
+
body,
|
|
5505
|
+
carriesPackages ? {
|
|
5506
|
+
packageId: (0, import_core2.artifactPackageId)(body),
|
|
5507
|
+
packageVersion: body?.version ?? void 0
|
|
5508
|
+
} : { packageId: manifestPackageId, packageVersion: manifestVersion },
|
|
5509
|
+
{ claim: carriesPackages ? claim : void 0 }
|
|
5510
|
+
);
|
|
5511
|
+
}
|
|
5512
|
+
if (carriesPackages) {
|
|
5513
|
+
const residual = await this._registerArtifactBodyCollections(
|
|
5514
|
+
ctx,
|
|
5515
|
+
memLoader,
|
|
5516
|
+
metadata,
|
|
5517
|
+
{ packageId: manifestPackageId, packageVersion: manifestVersion },
|
|
5518
|
+
{ skip: claimed }
|
|
5519
|
+
);
|
|
5520
|
+
totalRegistered += residual;
|
|
5521
|
+
if (residual > 0) {
|
|
5522
|
+
ctx.logger.warn(
|
|
5523
|
+
`[MetadataPlugin] artifact '${label}' carries ${residual} top-level metadata item(s) that none of its ${bodies.length} package bodies declare. They were registered under the artifact's own manifest id ('${manifestPackageId ?? "<none>"}') because no package in the artifact claims them, so every door will report that id as their owner. Rebuild the artifact so each collection it ships is carried by the package that owns it.`
|
|
5524
|
+
);
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
this.manager.registerLoader(memLoader);
|
|
5528
|
+
ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
|
|
5529
|
+
return totalRegistered;
|
|
5530
|
+
}
|
|
5531
|
+
/**
|
|
5532
|
+
* Register ONE artifact body's collections into the MetadataManager.
|
|
5533
|
+
*
|
|
5534
|
+
* A "body" is either the whole artifact (the single-package branch, where
|
|
5535
|
+
* the artifact and its one package are the same object) or one entry of
|
|
5536
|
+
* `packages[]` (ADR-0130 D4), which is an assembled
|
|
5537
|
+
* `{ ...manifest, ...collections }` payload carrying the same collection
|
|
5538
|
+
* keys the top level does. The loop is identical for both — that is the
|
|
5539
|
+
* point: there is one ingestion of a collection here, not one per shape.
|
|
5540
|
+
*
|
|
5541
|
+
* @param provenance - The `(packageId, packageVersion)` every item found in
|
|
5542
|
+
* this body is stamped with (ADR-0010 §3.7, via `applyProtection`). It is
|
|
5543
|
+
* the body's OWN identity, never the enclosing artifact's, which is what
|
|
5544
|
+
* makes a multi-package artifact's items agree with the registry and with
|
|
5545
|
+
* `GET /api/v1/packages` about who owns them.
|
|
5546
|
+
* @param slots.claim - Called with every `(type, name)` this pass
|
|
5547
|
+
* registered. Passed when reading package bodies; the residual sweep uses
|
|
5548
|
+
* what it recorded.
|
|
5549
|
+
* @param slots.skip - Consulted before registering each `(type, name)`.
|
|
5550
|
+
* Passed ONLY by the residual sweep, so a package body's copy is never
|
|
5551
|
+
* overwritten by the flattened top-level copy of the same definition —
|
|
5552
|
+
* the overwrite that re-attributed the item to the artifact's manifest.
|
|
5553
|
+
* ⛔ It is never passed while reading the bodies themselves: two items of
|
|
5554
|
+
* one name inside one body still register as they always have (last
|
|
5555
|
+
* wins), because suppressing that would be a behaviour change on the
|
|
5556
|
+
* single-package branch D7 pins.
|
|
5557
|
+
* @returns How many items this body registered.
|
|
5558
|
+
*/
|
|
5559
|
+
async _registerArtifactBodyCollections(ctx, memLoader, body, provenance, slots = {}) {
|
|
5560
|
+
const { packageId, packageVersion } = provenance;
|
|
4908
5561
|
let totalRegistered = 0;
|
|
4909
5562
|
for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
|
|
4910
|
-
const items =
|
|
5563
|
+
const items = body[field];
|
|
4911
5564
|
if (!Array.isArray(items) || items.length === 0) continue;
|
|
4912
5565
|
for (const item of items) {
|
|
4913
|
-
if (metaType === "view" && (0,
|
|
4914
|
-
const viewObject = item
|
|
5566
|
+
if (metaType === "view" && (0, import_spec4.isAggregatedViewContainer)(item)) {
|
|
5567
|
+
const viewObject = deriveViewContainerObject(item);
|
|
4915
5568
|
if (!viewObject) continue;
|
|
4916
|
-
(
|
|
4917
|
-
|
|
4918
|
-
|
|
5569
|
+
if (slots.skip?.("view", viewObject)) continue;
|
|
5570
|
+
(0, import_shared3.applyProtection)(item, {
|
|
5571
|
+
packageId,
|
|
5572
|
+
packageVersion
|
|
4919
5573
|
});
|
|
4920
5574
|
await memLoader.save("view", viewObject, item);
|
|
4921
5575
|
await this.manager.register("view", viewObject, item, { notify: false });
|
|
4922
5576
|
totalRegistered++;
|
|
4923
|
-
|
|
5577
|
+
slots.claim?.("view", viewObject);
|
|
5578
|
+
for (const vi of (0, import_spec4.expandViewContainer)(viewObject, item)) {
|
|
4924
5579
|
for (const w of vi._diagnostics?.warnings ?? []) {
|
|
4925
5580
|
ctx.logger.warn(`[MetadataPlugin] View expansion warning for '${vi.name}': ${w.message}`);
|
|
4926
5581
|
}
|
|
4927
|
-
(0,
|
|
4928
|
-
packageId
|
|
4929
|
-
packageVersion
|
|
5582
|
+
(0, import_shared3.applyProtection)(vi, {
|
|
5583
|
+
packageId,
|
|
5584
|
+
packageVersion
|
|
4930
5585
|
});
|
|
4931
5586
|
await memLoader.save("view", vi.name, vi);
|
|
4932
5587
|
await this.manager.register("view", vi.name, vi, { notify: false });
|
|
4933
5588
|
totalRegistered++;
|
|
5589
|
+
slots.claim?.("view", vi.name);
|
|
4934
5590
|
}
|
|
4935
5591
|
continue;
|
|
4936
5592
|
}
|
|
4937
5593
|
let name = item?.name;
|
|
4938
5594
|
if (!name) {
|
|
4939
5595
|
if (metaType === "view") {
|
|
4940
|
-
name = item
|
|
5596
|
+
name = deriveViewContainerObject(item);
|
|
4941
5597
|
}
|
|
4942
5598
|
}
|
|
4943
5599
|
if (!name) continue;
|
|
4944
|
-
(
|
|
4945
|
-
|
|
4946
|
-
|
|
5600
|
+
if (slots.skip?.(metaType, name)) continue;
|
|
5601
|
+
(0, import_shared3.applyProtection)(item, {
|
|
5602
|
+
packageId,
|
|
5603
|
+
packageVersion
|
|
4947
5604
|
});
|
|
4948
5605
|
await memLoader.save(metaType, name, item);
|
|
4949
5606
|
await this.manager.register(metaType, name, item, { notify: false });
|
|
4950
5607
|
totalRegistered++;
|
|
5608
|
+
slots.claim?.(metaType, name);
|
|
4951
5609
|
}
|
|
4952
5610
|
}
|
|
4953
|
-
this.manager.registerLoader(memLoader);
|
|
4954
|
-
ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
|
|
4955
5611
|
return totalRegistered;
|
|
4956
5612
|
}
|
|
4957
5613
|
/**
|
|
@@ -5027,7 +5683,7 @@ var MetadataPlugin = class {
|
|
|
5027
5683
|
for (const item of items) {
|
|
5028
5684
|
const meta = item;
|
|
5029
5685
|
if (meta?.name) {
|
|
5030
|
-
(0,
|
|
5686
|
+
(0, import_shared3.applyProtection)(meta, {
|
|
5031
5687
|
packageId: this.options.packageId
|
|
5032
5688
|
});
|
|
5033
5689
|
await this.manager.register(entry.type, meta.name, item, { notify: false });
|
|
@@ -5123,9 +5779,43 @@ var RemoteLoader = class {
|
|
|
5123
5779
|
format: "json"
|
|
5124
5780
|
};
|
|
5125
5781
|
}
|
|
5782
|
+
/**
|
|
5783
|
+
* [#15037] Report only the names that ARE names.
|
|
5784
|
+
*
|
|
5785
|
+
* This read used to be `loadMany<{ name: string }>(type)` mapped straight to
|
|
5786
|
+
* `items.map(i => i.name)`. That type argument is an ASSERTION about bodies
|
|
5787
|
+
* that arrived over HTTP, and nothing checked it: a body with no top-level
|
|
5788
|
+
* `name` yielded `undefined`, which went into an array this signature
|
|
5789
|
+
* declares as `string[]` and reached consumers through
|
|
5790
|
+
* `MetadataManager.listNames()` — a runtime violation of a declared type,
|
|
5791
|
+
* not an untidy entry. A consumer that keys by it, lower-cases it, or feeds
|
|
5792
|
+
* it back to a by-name `load()` gets `undefined` where the type says it
|
|
5793
|
+
* cannot be.
|
|
5794
|
+
*
|
|
5795
|
+
* The guard is `DatabaseLoader.list()`'s, one file away: same cast-then-map
|
|
5796
|
+
* spelling, one `typeof` filter behind it. Silently dropping is the landed
|
|
5797
|
+
* direction, not a preference — `DatabaseLoader` drops rather than throws,
|
|
5798
|
+
* and `FilesystemLoader`'s narrowing carries a maintainer ruling (via the
|
|
5799
|
+
* director seat on #14486, 2026-09-02) that chose narrowing (A) over
|
|
5800
|
+
* refusing loudly (B), because a name in the list that the door answers
|
|
5801
|
+
* `null` for is the silent failure an author reads as their own typo. An
|
|
5802
|
+
* `undefined` here is the extreme form of that name.
|
|
5803
|
+
*
|
|
5804
|
+
* ⛔ NOT copied from the siblings: `MemoryLoader` answers with its store
|
|
5805
|
+
* keys, and #14205 ruled that identity is the key the store holds an item
|
|
5806
|
+
* under rather than `body.name`. This loader reads over HTTP and holds no
|
|
5807
|
+
* store key, so `body.name` is the only identity it has — the list is
|
|
5808
|
+
* narrowed to agree with the door instead. `loadMany()` is deliberately
|
|
5809
|
+
* untouched: it keys nothing, so a nameless body is still served there.
|
|
5810
|
+
*
|
|
5811
|
+
* The predicate is spelled as a type guard, and the mapped element type left
|
|
5812
|
+
* `unknown`, so `tsc` PROVES the declared `string[]` instead of a cast
|
|
5813
|
+
* asserting it — otherwise the compiler reads the filter as always-true and
|
|
5814
|
+
* a later reader deletes it as dead.
|
|
5815
|
+
*/
|
|
5126
5816
|
async list(type) {
|
|
5127
5817
|
const items = await this.loadMany(type);
|
|
5128
|
-
return items.map((
|
|
5818
|
+
return items.map((item) => item.name).filter((name) => typeof name === "string");
|
|
5129
5819
|
}
|
|
5130
5820
|
async save(type, name, data, _options) {
|
|
5131
5821
|
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
@@ -5145,7 +5835,7 @@ var RemoteLoader = class {
|
|
|
5145
5835
|
};
|
|
5146
5836
|
|
|
5147
5837
|
// src/index.ts
|
|
5148
|
-
var
|
|
5838
|
+
var import_metadata_core4 = require("@objectstack/metadata-core");
|
|
5149
5839
|
|
|
5150
5840
|
// src/utils/history-cleanup.ts
|
|
5151
5841
|
var import_kernel3 = require("@objectstack/spec/kernel");
|
|
@@ -5165,9 +5855,9 @@ var HistoryCleanupManager = class {
|
|
|
5165
5855
|
return;
|
|
5166
5856
|
}
|
|
5167
5857
|
const intervalMs = (this.policy.cleanupIntervalHours ?? 24) * 60 * 60 * 1e3;
|
|
5168
|
-
void this
|
|
5858
|
+
void runCleanupAndReport(this);
|
|
5169
5859
|
this.cleanupTimer = setInterval(() => {
|
|
5170
|
-
void this
|
|
5860
|
+
void runCleanupAndReport(this);
|
|
5171
5861
|
}, intervalMs);
|
|
5172
5862
|
}
|
|
5173
5863
|
/**
|
|
@@ -5194,7 +5884,7 @@ var HistoryCleanupManager = class {
|
|
|
5194
5884
|
try {
|
|
5195
5885
|
if (this.policy.maxAgeDays) {
|
|
5196
5886
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
5197
|
-
cutoffDate.
|
|
5887
|
+
cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
|
|
5198
5888
|
const cutoffISO = cutoffDate.toISOString();
|
|
5199
5889
|
const filter = {
|
|
5200
5890
|
recorded_at: { $lt: cutoffISO }
|
|
@@ -5314,7 +6004,7 @@ var HistoryCleanupManager = class {
|
|
|
5314
6004
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
5315
6005
|
if (this.policy.maxAgeDays) {
|
|
5316
6006
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
5317
|
-
cutoffDate.
|
|
6007
|
+
cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
|
|
5318
6008
|
const cutoffISO = cutoffDate.toISOString();
|
|
5319
6009
|
const filter = {
|
|
5320
6010
|
recorded_at: { $lt: cutoffISO },
|
|
@@ -5361,6 +6051,23 @@ var HistoryCleanupManager = class {
|
|
|
5361
6051
|
};
|
|
5362
6052
|
}
|
|
5363
6053
|
};
|
|
6054
|
+
async function runCleanupAndReport(manager) {
|
|
6055
|
+
let outcome;
|
|
6056
|
+
try {
|
|
6057
|
+
outcome = await manager.runCleanup();
|
|
6058
|
+
} catch (error) {
|
|
6059
|
+
console.error(
|
|
6060
|
+
"History cleanup: the run did not complete, so no history row past the retention policy was deleted and the table keeps growing while the system reports healthy. Fix: the cause below comes from the configured data driver, not from the retention policy; call `runCleanup()` directly to reproduce it. Cause:",
|
|
6061
|
+
error
|
|
6062
|
+
);
|
|
6063
|
+
return;
|
|
6064
|
+
}
|
|
6065
|
+
if (outcome.errors > 0) {
|
|
6066
|
+
console.error(
|
|
6067
|
+
`History cleanup: ${outcome.errors} delete operation(s) failed and ${outcome.deleted} row(s) were deleted. The history rows those deletes were meant to remove are still in the table, nothing retries them, and the table grows past the retention policy while the system keeps reporting healthy. Fix: check the data driver delete path for the metadata history table. The per-failure causes are not carried out of \`runCleanup()\`, so reproduce them against the driver directly.`
|
|
6068
|
+
);
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
5364
6071
|
|
|
5365
6072
|
// src/migration/index.ts
|
|
5366
6073
|
var migration_exports = {};
|
|
@@ -5419,6 +6126,9 @@ var MigrationExecutor = class {
|
|
|
5419
6126
|
};
|
|
5420
6127
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5421
6128
|
0 && (module.exports = {
|
|
6129
|
+
AMBIGUOUS_METADATA_STEM_CODE,
|
|
6130
|
+
AMBIGUOUS_METADATA_STEM_STATUS,
|
|
6131
|
+
AmbiguousMetadataStemError,
|
|
5422
6132
|
DatabaseLoader,
|
|
5423
6133
|
FilesystemLoader,
|
|
5424
6134
|
HistoryCleanupManager,
|
|
@@ -5434,7 +6144,9 @@ var MigrationExecutor = class {
|
|
|
5434
6144
|
TypeScriptSerializer,
|
|
5435
6145
|
YAMLSerializer,
|
|
5436
6146
|
calculateChecksum,
|
|
6147
|
+
deriveViewContainerObject,
|
|
5437
6148
|
generateDiffSummary,
|
|
5438
|
-
generateSimpleDiff
|
|
6149
|
+
generateSimpleDiff,
|
|
6150
|
+
isAmbiguousMetadataStemError
|
|
5439
6151
|
});
|
|
5440
6152
|
//# sourceMappingURL=node.cjs.map
|