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