@objectstack/metadata 17.3.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 +532 -0
- package/README.md +6 -4
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.cjs +299 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -60
- package/dist/index.d.ts +227 -60
- package/dist/index.js +294 -35
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +104 -22
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +40 -1
- package/dist/migrations/index.d.ts +40 -1
- package/dist/migrations/index.js +107 -22
- package/dist/migrations/index.js.map +1 -1
- package/dist/node.cjs +299 -36
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +24 -1
- package/dist/node.d.ts +24 -1
- package/dist/node.js +294 -35
- 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 +18 -8
package/dist/node.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MetadataManager, MetadataManagerOptions, MetadataLoader, MetadataSerializer, MetadataKeyedItem } from './index.cjs';
|
|
2
|
-
export { DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum,
|
|
2
|
+
export { AMBIGUOUS_METADATA_STEM_CODE, AMBIGUOUS_METADATA_STEM_STATUS, AmbiguousMetadataStemError, DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff, isAmbiguousMetadataStemError } from './index.cjs';
|
|
3
3
|
export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, SysMetadataHistoryObject, SysMetadataObject, WatchFilter } from '@objectstack/metadata-core';
|
|
4
|
+
export { deriveViewContainerObject } from './view-container.cjs';
|
|
4
5
|
import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
|
|
5
6
|
export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
|
|
6
7
|
export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
|
|
@@ -171,6 +172,28 @@ declare class FilesystemLoader implements MetadataLoader {
|
|
|
171
172
|
* key no other door can open.
|
|
172
173
|
*/
|
|
173
174
|
private resolvableNameForPath;
|
|
175
|
+
/**
|
|
176
|
+
* [#14921] The names this loader reports for `files` — and the ONE place an
|
|
177
|
+
* ambiguous stem is refused.
|
|
178
|
+
*
|
|
179
|
+
* Shared by {@link list} and {@link loadManyEntries} so the two can never
|
|
180
|
+
* disagree about which trees are admissible: a stem that `list()` refuses
|
|
181
|
+
* must not still be walked and returned as two bodies by `loadMany()`, which
|
|
182
|
+
* is exactly the split this card measured.
|
|
183
|
+
*
|
|
184
|
+
* Refuses on the FIRST colliding name in sorted order, so a tree holding more
|
|
185
|
+
* than one collision always names the same one — a refusal that moves
|
|
186
|
+
* between runs reads as flakiness rather than as the fixed authoring error it
|
|
187
|
+
* is. Paths are deduplicated because two overlapping `patterns` legitimately
|
|
188
|
+
* match one file twice, and counting that as a collision would refuse a
|
|
189
|
+
* perfectly good tree.
|
|
190
|
+
*
|
|
191
|
+
* ⛔ Not a precedence resolver. Picking a winner here is what the ruling
|
|
192
|
+
* declined (option 2, keep the precedence and log): the loser would stay
|
|
193
|
+
* unreachable and the listed set would stay different from the addressable
|
|
194
|
+
* one.
|
|
195
|
+
*/
|
|
196
|
+
private resolvableNames;
|
|
174
197
|
/**
|
|
175
198
|
* Find file for a given type and name
|
|
176
199
|
*/
|
package/dist/node.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MetadataManager, MetadataManagerOptions, MetadataLoader, MetadataSerializer, MetadataKeyedItem } from './index.js';
|
|
2
|
-
export { DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum,
|
|
2
|
+
export { AMBIGUOUS_METADATA_STEM_CODE, AMBIGUOUS_METADATA_STEM_STATUS, AmbiguousMetadataStemError, DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff, isAmbiguousMetadataStemError } from './index.js';
|
|
3
3
|
export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, SysMetadataHistoryObject, SysMetadataObject, WatchFilter } from '@objectstack/metadata-core';
|
|
4
|
+
export { deriveViewContainerObject } from './view-container.js';
|
|
4
5
|
import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
|
|
5
6
|
export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
|
|
6
7
|
export { IMetadataService, MetadataExportOptions, MetadataImportOptions, MetadataImportResult, MetadataTypeInfo, MetadataWatchCallback, MetadataWatchHandle } from '@objectstack/spec/contracts';
|
|
@@ -171,6 +172,28 @@ declare class FilesystemLoader implements MetadataLoader {
|
|
|
171
172
|
* key no other door can open.
|
|
172
173
|
*/
|
|
173
174
|
private resolvableNameForPath;
|
|
175
|
+
/**
|
|
176
|
+
* [#14921] The names this loader reports for `files` — and the ONE place an
|
|
177
|
+
* ambiguous stem is refused.
|
|
178
|
+
*
|
|
179
|
+
* Shared by {@link list} and {@link loadManyEntries} so the two can never
|
|
180
|
+
* disagree about which trees are admissible: a stem that `list()` refuses
|
|
181
|
+
* must not still be walked and returned as two bodies by `loadMany()`, which
|
|
182
|
+
* is exactly the split this card measured.
|
|
183
|
+
*
|
|
184
|
+
* Refuses on the FIRST colliding name in sorted order, so a tree holding more
|
|
185
|
+
* than one collision always names the same one — a refusal that moves
|
|
186
|
+
* between runs reads as flakiness rather than as the fixed authoring error it
|
|
187
|
+
* is. Paths are deduplicated because two overlapping `patterns` legitimately
|
|
188
|
+
* match one file twice, and counting that as a collision would refuse a
|
|
189
|
+
* perfectly good tree.
|
|
190
|
+
*
|
|
191
|
+
* ⛔ Not a precedence resolver. Picking a winner here is what the ruling
|
|
192
|
+
* declined (option 2, keep the precedence and log): the loser would stay
|
|
193
|
+
* unreachable and the listed set would stay different from the addressable
|
|
194
|
+
* one.
|
|
195
|
+
*/
|
|
196
|
+
private resolvableNames;
|
|
174
197
|
/**
|
|
175
198
|
* Find file for a given type and name
|
|
176
199
|
*/
|
package/dist/node.js
CHANGED
|
@@ -606,7 +606,7 @@ async function _columnExists(exec, table, column) {
|
|
|
606
606
|
// src/loaders/database-loader.ts
|
|
607
607
|
function canonicalIsoInstant(value) {
|
|
608
608
|
if (value === null || value === void 0) return void 0;
|
|
609
|
-
if (value instanceof Date) return value.toISOString();
|
|
609
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? void 0 : value.toISOString();
|
|
610
610
|
if (typeof value === "string") return value;
|
|
611
611
|
return String(value);
|
|
612
612
|
}
|
|
@@ -662,7 +662,7 @@ var DatabaseLoader = class {
|
|
|
662
662
|
if (cacheEnabled) {
|
|
663
663
|
const lruOpts = {
|
|
664
664
|
maxSize: cacheOpts?.maxSize ?? 500,
|
|
665
|
-
ttl: cacheOpts?.
|
|
665
|
+
ttl: cacheOpts?.ttlMs ?? 6e4
|
|
666
666
|
};
|
|
667
667
|
this.loadCache = new LRUCache(lruOpts);
|
|
668
668
|
this.loadManyCache = new LRUCache(lruOpts);
|
|
@@ -1512,6 +1512,33 @@ function generateId() {
|
|
|
1512
1512
|
return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
1513
1513
|
}
|
|
1514
1514
|
|
|
1515
|
+
// src/loaders/ambiguous-metadata-stem.ts
|
|
1516
|
+
var AMBIGUOUS_METADATA_STEM_CODE = "AMBIGUOUS_METADATA_STEM";
|
|
1517
|
+
var AMBIGUOUS_METADATA_STEM_STATUS = 500;
|
|
1518
|
+
var AMBIGUOUS_METADATA_STEM_BRAND = /* @__PURE__ */ Symbol.for("objectstack.metadata.ambiguousStem");
|
|
1519
|
+
var _a, _b;
|
|
1520
|
+
var AmbiguousMetadataStemError = class extends (_b = Error, _a = AMBIGUOUS_METADATA_STEM_BRAND, _b) {
|
|
1521
|
+
constructor(type, stem, paths) {
|
|
1522
|
+
const sorted = [...paths].sort();
|
|
1523
|
+
super(
|
|
1524
|
+
`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.`
|
|
1525
|
+
);
|
|
1526
|
+
/** Brand — see the module doc on why this is not `instanceof`. */
|
|
1527
|
+
this[_a] = true;
|
|
1528
|
+
/** ADR-0112 wire code. */
|
|
1529
|
+
this.code = AMBIGUOUS_METADATA_STEM_CODE;
|
|
1530
|
+
/** HTTP status a transport should answer. */
|
|
1531
|
+
this.status = AMBIGUOUS_METADATA_STEM_STATUS;
|
|
1532
|
+
this.name = "AmbiguousMetadataStemError";
|
|
1533
|
+
this.type = type;
|
|
1534
|
+
this.stem = stem;
|
|
1535
|
+
this.paths = sorted;
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
function isAmbiguousMetadataStemError(err) {
|
|
1539
|
+
return typeof err === "object" && err !== null && err[AMBIGUOUS_METADATA_STEM_BRAND] === true;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1515
1542
|
// src/endpoint-matcher.ts
|
|
1516
1543
|
import {
|
|
1517
1544
|
ApiEndpointSchema,
|
|
@@ -1669,6 +1696,8 @@ import {
|
|
|
1669
1696
|
isAggregatedViewContainer
|
|
1670
1697
|
} from "@objectstack/spec";
|
|
1671
1698
|
import { applyProtection } from "@objectstack/spec/shared";
|
|
1699
|
+
|
|
1700
|
+
// src/view-container.ts
|
|
1672
1701
|
function deriveViewContainerObject(container) {
|
|
1673
1702
|
if (!container || typeof container !== "object") return void 0;
|
|
1674
1703
|
const c = container;
|
|
@@ -1676,6 +1705,8 @@ function deriveViewContainerObject(container) {
|
|
|
1676
1705
|
const byName = typeof c.name === "string" && c.name ? c.name : void 0;
|
|
1677
1706
|
return own ?? c?.list?.data?.object ?? c?.form?.data?.object ?? byName;
|
|
1678
1707
|
}
|
|
1708
|
+
|
|
1709
|
+
// src/view-container-expansion.ts
|
|
1679
1710
|
function expandRuntimeViewContainer(data) {
|
|
1680
1711
|
if (!isAggregatedViewContainer(data)) return [];
|
|
1681
1712
|
const container = data;
|
|
@@ -2381,6 +2412,9 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2381
2412
|
await this.admitLoaderItems(loader, type, items);
|
|
2382
2413
|
this.reportLoaderReadRecovered(loader.contract.name);
|
|
2383
2414
|
} catch (e) {
|
|
2415
|
+
if (isAmbiguousMetadataStemError(e)) {
|
|
2416
|
+
throw e;
|
|
2417
|
+
}
|
|
2384
2418
|
degraded = true;
|
|
2385
2419
|
errors.push(`${loader.contract.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
2386
2420
|
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
@@ -2718,6 +2752,30 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2718
2752
|
}
|
|
2719
2753
|
/**
|
|
2720
2754
|
* List all names of metadata items of a given type
|
|
2755
|
+
*
|
|
2756
|
+
* ## [#14423] One loader's fault does not take the whole enumeration down
|
|
2757
|
+
*
|
|
2758
|
+
* This loop used to be bare — `const result = await loader.list(type)` with
|
|
2759
|
+
* no `try`, while the two sibling plural reads (`list()` via
|
|
2760
|
+
* {@link admitLoaderItems}, and {@link loadMany}) have carried a per-loader
|
|
2761
|
+
* `catch` since #5108. That asymmetry is the defect, independent of any one
|
|
2762
|
+
* caller: the SAME storage outage was swallowed by one plural read and
|
|
2763
|
+
* thrown out of the other, so which answer a caller got depended only on
|
|
2764
|
+
* which method it happened to call. A caller reading both — the action
|
|
2765
|
+
* governance audit is one — saw `loadMany` report a short-but-successful
|
|
2766
|
+
* set and `listNames` throw, and had no way to tell that one fact was
|
|
2767
|
+
* behind both.
|
|
2768
|
+
*
|
|
2769
|
+
* Same shape as `loadMany`'s, deliberately, down to the helpers: the outage
|
|
2770
|
+
* is spoken once per loader through {@link reportLoaderReadFailure} and
|
|
2771
|
+
* un-said through {@link reportLoaderReadRecovered}. ⛔ Not a third spelling
|
|
2772
|
+
* for "a loader faulted" — a second vocabulary for one event is how the two
|
|
2773
|
+
* reads drifted apart in the first place.
|
|
2774
|
+
*
|
|
2775
|
+
* The degradation is the same one `list()` documents and is graded the same
|
|
2776
|
+
* way (AGENTS.md → "Degradation log levels"): the caller still gets an
|
|
2777
|
+
* array, nothing 500s, and the set is quietly short — so it is reported at
|
|
2778
|
+
* `error`, by the shared helper, rather than being re-graded here.
|
|
2721
2779
|
*/
|
|
2722
2780
|
async listNames(type) {
|
|
2723
2781
|
type = canonicalMetadataServiceType(type);
|
|
@@ -2729,8 +2787,16 @@ var _MetadataManager = class _MetadataManager {
|
|
|
2729
2787
|
}
|
|
2730
2788
|
}
|
|
2731
2789
|
for (const loader of this.loaders.values()) {
|
|
2732
|
-
|
|
2733
|
-
|
|
2790
|
+
try {
|
|
2791
|
+
const result = await loader.list(type);
|
|
2792
|
+
result.forEach((item) => names.add(item));
|
|
2793
|
+
this.reportLoaderReadRecovered(loader.contract.name);
|
|
2794
|
+
} catch (e) {
|
|
2795
|
+
if (isAmbiguousMetadataStemError(e)) {
|
|
2796
|
+
throw e;
|
|
2797
|
+
}
|
|
2798
|
+
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
2799
|
+
}
|
|
2734
2800
|
}
|
|
2735
2801
|
return Array.from(names);
|
|
2736
2802
|
}
|
|
@@ -3590,6 +3656,99 @@ var _MetadataManager = class _MetadataManager {
|
|
|
3590
3656
|
}
|
|
3591
3657
|
return results;
|
|
3592
3658
|
}
|
|
3659
|
+
/**
|
|
3660
|
+
* [#14423] {@link loadMany}, read under the identity the STORE holds each
|
|
3661
|
+
* item by — the keyed plural read, beside the unkeyed one.
|
|
3662
|
+
*
|
|
3663
|
+
* ## Why a second method and not a widened `loadMany`
|
|
3664
|
+
*
|
|
3665
|
+
* `loadMany` keys nothing: it returns bodies, and every consumer that needs
|
|
3666
|
+
* an identity reads `body.name` off them. #14205 already ruled what identity
|
|
3667
|
+
* IS — the key the store holds the item under (`register(type, name, data)`
|
|
3668
|
+
* takes it as the ARGUMENT, and a body is not required to name itself) — so
|
|
3669
|
+
* `body.name` is a guess that happens to be right for most items and drops
|
|
3670
|
+
* the rest ENTIRELY: an item whose body carries no `name` is served by
|
|
3671
|
+
* `load(type, name)` and is not nameable from `loadMany`'s answer at all.
|
|
3672
|
+
*
|
|
3673
|
+
* Widening `loadMany`'s return would fix that and break every consumer of a
|
|
3674
|
+
* published shape (the ones counted on this card all read `body.name` as the
|
|
3675
|
+
* identity). So this is additive: `loadMany`'s return shape is untouched,
|
|
3676
|
+
* and a caller that needs the key asks for the key.
|
|
3677
|
+
*
|
|
3678
|
+
* ## What it reads — the same population `loadMany` reads
|
|
3679
|
+
*
|
|
3680
|
+
* Loaders only, deliberately, so this is `loadMany` keyed and nothing more.
|
|
3681
|
+
* It is NOT `list()`/{@link listNames}, which also merge the in-memory
|
|
3682
|
+
* `register()` registry; a caller wanting that set has those. Reading the
|
|
3683
|
+
* loaders alone is also what makes this the enumerable twin of
|
|
3684
|
+
* {@link loadDiagnosed}, which walks the same loaders by name — that pairing
|
|
3685
|
+
* is the point on the audit side of #14423, where an enumeration and a
|
|
3686
|
+
* by-name read that disagree about a population make one subsystem accuse
|
|
3687
|
+
* another of a defect neither has.
|
|
3688
|
+
*
|
|
3689
|
+
* ## Delegate first, fall back second — and why that order is not a style
|
|
3690
|
+
*
|
|
3691
|
+
* Per loader: {@link MetadataLoader.loadManyKeyed} where the loader offers
|
|
3692
|
+
* one, else its `list()` + a per-name `load()`. Measured, on
|
|
3693
|
+
* `DatabaseLoader`: the keyed method shares `loadMany`'s single query
|
|
3694
|
+
* (`{find:1, findOne:0}` — zero extra cost), while enumerate-then-read-each
|
|
3695
|
+
* on that same loader is a real N+1 (`{find:1, findOne:5}` for five items).
|
|
3696
|
+
* The fallback exists for loaders that cannot produce keys at all
|
|
3697
|
+
* (`RemoteLoader`'s wire format carries bodies only), and it recovers the
|
|
3698
|
+
* nameless item the pre-#14205 `loadMany`-and-key-by-`body.name` fallback
|
|
3699
|
+
* drops — which is why it is `list()` + `load()` and not `loadMany()`.
|
|
3700
|
+
*
|
|
3701
|
+
* ## Failure posture
|
|
3702
|
+
*
|
|
3703
|
+
* Per-loader `try`/`catch`, the same seam and the same helpers as
|
|
3704
|
+
* {@link loadMany} and `list()` — one loader's outage does not take the
|
|
3705
|
+
* enumeration down, and it is reported once through
|
|
3706
|
+
* {@link reportLoaderReadFailure} rather than in a third vocabulary.
|
|
3707
|
+
* Earlier loaders win a key collision, mirroring `list()`.
|
|
3708
|
+
*/
|
|
3709
|
+
async loadManyKeyed(type, options) {
|
|
3710
|
+
const items = /* @__PURE__ */ new Map();
|
|
3711
|
+
for (const loader of this.loaders.values()) {
|
|
3712
|
+
try {
|
|
3713
|
+
await this.admitKeyedLoaderItems(loader, type, items, options);
|
|
3714
|
+
this.reportLoaderReadRecovered(loader.contract.name);
|
|
3715
|
+
} catch (e) {
|
|
3716
|
+
this.reportLoaderReadFailure(loader.contract.name, type, e);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
return Array.from(items, ([name, data]) => ({ name, data }));
|
|
3720
|
+
}
|
|
3721
|
+
/**
|
|
3722
|
+
* Merge ONE loader's answer for `type` into `items`, keyed by that loader's
|
|
3723
|
+
* own key for each item — {@link loadManyKeyed}'s per-loader body.
|
|
3724
|
+
*
|
|
3725
|
+
* Distinct from {@link admitLoaderItems} on exactly one axis, and that axis
|
|
3726
|
+
* is the whole of #14423: the fallback for a loader with no
|
|
3727
|
+
* `loadManyKeyed`. `admitLoaderItems` falls back to `loadMany` keyed by
|
|
3728
|
+
* `data.name` — the pre-#14205 behaviour, verbatim, which drops a nameless
|
|
3729
|
+
* body. Here the fallback is `list()` + a per-name `load()`, so a loader
|
|
3730
|
+
* that cannot enumerate keys and bodies together still answers with both.
|
|
3731
|
+
*
|
|
3732
|
+
* Read failures are NOT caught here — the caller owns that verdict, as in
|
|
3733
|
+
* {@link admitLoaderItems}.
|
|
3734
|
+
*/
|
|
3735
|
+
async admitKeyedLoaderItems(loader, type, items, options) {
|
|
3736
|
+
if (typeof loader.loadManyKeyed === "function") {
|
|
3737
|
+
const keyed = await loader.loadManyKeyed(type, options);
|
|
3738
|
+
for (const entry of keyed) {
|
|
3739
|
+
if (!entry || typeof entry.name !== "string" || entry.name === "") continue;
|
|
3740
|
+
if (items.has(entry.name)) continue;
|
|
3741
|
+
items.set(entry.name, entry.data);
|
|
3742
|
+
}
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
for (const name of await loader.list(type)) {
|
|
3746
|
+
if (typeof name !== "string" || name === "" || items.has(name)) continue;
|
|
3747
|
+
const result = await loader.load(type, name, options);
|
|
3748
|
+
if (result?.data == null) continue;
|
|
3749
|
+
items.set(name, result.data);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3593
3752
|
/**
|
|
3594
3753
|
* Save metadata item to a loader
|
|
3595
3754
|
*/
|
|
@@ -4222,33 +4381,34 @@ var _FilesystemLoader = class _FilesystemLoader {
|
|
|
4222
4381
|
const globPatterns = patterns.map(
|
|
4223
4382
|
(pattern) => path.join(typeDir, pattern)
|
|
4224
4383
|
);
|
|
4384
|
+
const files = [];
|
|
4225
4385
|
for (const pattern of globPatterns) {
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
const content = await fs.readFile(file, "utf-8");
|
|
4236
|
-
const format = this.detectFormat(file);
|
|
4237
|
-
const serializer = this.getSerializer(format);
|
|
4238
|
-
if (serializer) {
|
|
4239
|
-
const data = serializer.deserialize(content);
|
|
4240
|
-
items.push({ file, data });
|
|
4241
|
-
}
|
|
4242
|
-
} catch (error) {
|
|
4243
|
-
this.logger?.warn("Failed to load file", {
|
|
4244
|
-
file,
|
|
4245
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4246
|
-
});
|
|
4247
|
-
}
|
|
4248
|
-
}
|
|
4386
|
+
files.push(
|
|
4387
|
+
...await glob(pattern, {
|
|
4388
|
+
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
|
|
4389
|
+
nodir: true
|
|
4390
|
+
})
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
this.resolvableNames(type, typeDir, files);
|
|
4394
|
+
for (const file of files) {
|
|
4249
4395
|
if (limit && items.length >= limit) {
|
|
4250
4396
|
break;
|
|
4251
4397
|
}
|
|
4398
|
+
try {
|
|
4399
|
+
const content = await fs.readFile(file, "utf-8");
|
|
4400
|
+
const format = this.detectFormat(file);
|
|
4401
|
+
const serializer = this.getSerializer(format);
|
|
4402
|
+
if (serializer) {
|
|
4403
|
+
const data = serializer.deserialize(content);
|
|
4404
|
+
items.push({ file, data });
|
|
4405
|
+
}
|
|
4406
|
+
} catch (error) {
|
|
4407
|
+
this.logger?.warn("Failed to load file", {
|
|
4408
|
+
file,
|
|
4409
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4252
4412
|
}
|
|
4253
4413
|
return items;
|
|
4254
4414
|
} catch (error) {
|
|
@@ -4317,13 +4477,13 @@ var _FilesystemLoader = class _FilesystemLoader {
|
|
|
4317
4477
|
*/
|
|
4318
4478
|
async list(type) {
|
|
4319
4479
|
const typeDir = path.join(this.rootDir, type);
|
|
4480
|
+
let files;
|
|
4320
4481
|
try {
|
|
4321
|
-
|
|
4482
|
+
files = await glob("**/*", {
|
|
4322
4483
|
cwd: typeDir,
|
|
4323
4484
|
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
4324
4485
|
nodir: true
|
|
4325
4486
|
});
|
|
4326
|
-
return files.map((file) => this.resolvableNameForPath(typeDir, path.join(typeDir, file))).filter((name) => name !== null);
|
|
4327
4487
|
} catch (error) {
|
|
4328
4488
|
this.logger?.error("Failed to list", void 0, {
|
|
4329
4489
|
type,
|
|
@@ -4331,6 +4491,7 @@ var _FilesystemLoader = class _FilesystemLoader {
|
|
|
4331
4491
|
});
|
|
4332
4492
|
return [];
|
|
4333
4493
|
}
|
|
4494
|
+
return this.resolvableNames(type, typeDir, files.map((file) => path.join(typeDir, file)));
|
|
4334
4495
|
}
|
|
4335
4496
|
async save(type, name, data, options) {
|
|
4336
4497
|
const startTime = Date.now();
|
|
@@ -4455,6 +4616,49 @@ var _FilesystemLoader = class _FilesystemLoader {
|
|
|
4455
4616
|
}
|
|
4456
4617
|
return _FilesystemLoader.nameFromFilename(rel);
|
|
4457
4618
|
}
|
|
4619
|
+
/**
|
|
4620
|
+
* [#14921] The names this loader reports for `files` — and the ONE place an
|
|
4621
|
+
* ambiguous stem is refused.
|
|
4622
|
+
*
|
|
4623
|
+
* Shared by {@link list} and {@link loadManyEntries} so the two can never
|
|
4624
|
+
* disagree about which trees are admissible: a stem that `list()` refuses
|
|
4625
|
+
* must not still be walked and returned as two bodies by `loadMany()`, which
|
|
4626
|
+
* is exactly the split this card measured.
|
|
4627
|
+
*
|
|
4628
|
+
* Refuses on the FIRST colliding name in sorted order, so a tree holding more
|
|
4629
|
+
* than one collision always names the same one — a refusal that moves
|
|
4630
|
+
* between runs reads as flakiness rather than as the fixed authoring error it
|
|
4631
|
+
* is. Paths are deduplicated because two overlapping `patterns` legitimately
|
|
4632
|
+
* match one file twice, and counting that as a collision would refuse a
|
|
4633
|
+
* perfectly good tree.
|
|
4634
|
+
*
|
|
4635
|
+
* ⛔ Not a precedence resolver. Picking a winner here is what the ruling
|
|
4636
|
+
* declined (option 2, keep the precedence and log): the loser would stay
|
|
4637
|
+
* unreachable and the listed set would stay different from the addressable
|
|
4638
|
+
* one.
|
|
4639
|
+
*/
|
|
4640
|
+
resolvableNames(type, typeDir, files) {
|
|
4641
|
+
const byName = /* @__PURE__ */ new Map();
|
|
4642
|
+
for (const file of files) {
|
|
4643
|
+
const name = this.resolvableNameForPath(typeDir, file);
|
|
4644
|
+
if (name === null) {
|
|
4645
|
+
continue;
|
|
4646
|
+
}
|
|
4647
|
+
let paths = byName.get(name);
|
|
4648
|
+
if (!paths) {
|
|
4649
|
+
paths = /* @__PURE__ */ new Set();
|
|
4650
|
+
byName.set(name, paths);
|
|
4651
|
+
}
|
|
4652
|
+
paths.add(file);
|
|
4653
|
+
}
|
|
4654
|
+
for (const name of [...byName.keys()].sort()) {
|
|
4655
|
+
const paths = byName.get(name);
|
|
4656
|
+
if (paths.size > 1) {
|
|
4657
|
+
throw new AmbiguousMetadataStemError(type, name, [...paths]);
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
return [...byName.keys()];
|
|
4661
|
+
}
|
|
4458
4662
|
/**
|
|
4459
4663
|
* Find file for a given type and name
|
|
4460
4664
|
*/
|
|
@@ -5552,9 +5756,43 @@ var RemoteLoader = class {
|
|
|
5552
5756
|
format: "json"
|
|
5553
5757
|
};
|
|
5554
5758
|
}
|
|
5759
|
+
/**
|
|
5760
|
+
* [#15037] Report only the names that ARE names.
|
|
5761
|
+
*
|
|
5762
|
+
* This read used to be `loadMany<{ name: string }>(type)` mapped straight to
|
|
5763
|
+
* `items.map(i => i.name)`. That type argument is an ASSERTION about bodies
|
|
5764
|
+
* that arrived over HTTP, and nothing checked it: a body with no top-level
|
|
5765
|
+
* `name` yielded `undefined`, which went into an array this signature
|
|
5766
|
+
* declares as `string[]` and reached consumers through
|
|
5767
|
+
* `MetadataManager.listNames()` — a runtime violation of a declared type,
|
|
5768
|
+
* not an untidy entry. A consumer that keys by it, lower-cases it, or feeds
|
|
5769
|
+
* it back to a by-name `load()` gets `undefined` where the type says it
|
|
5770
|
+
* cannot be.
|
|
5771
|
+
*
|
|
5772
|
+
* The guard is `DatabaseLoader.list()`'s, one file away: same cast-then-map
|
|
5773
|
+
* spelling, one `typeof` filter behind it. Silently dropping is the landed
|
|
5774
|
+
* direction, not a preference — `DatabaseLoader` drops rather than throws,
|
|
5775
|
+
* and `FilesystemLoader`'s narrowing carries a maintainer ruling (via the
|
|
5776
|
+
* director seat on #14486, 2026-09-02) that chose narrowing (A) over
|
|
5777
|
+
* refusing loudly (B), because a name in the list that the door answers
|
|
5778
|
+
* `null` for is the silent failure an author reads as their own typo. An
|
|
5779
|
+
* `undefined` here is the extreme form of that name.
|
|
5780
|
+
*
|
|
5781
|
+
* ⛔ NOT copied from the siblings: `MemoryLoader` answers with its store
|
|
5782
|
+
* keys, and #14205 ruled that identity is the key the store holds an item
|
|
5783
|
+
* under rather than `body.name`. This loader reads over HTTP and holds no
|
|
5784
|
+
* store key, so `body.name` is the only identity it has — the list is
|
|
5785
|
+
* narrowed to agree with the door instead. `loadMany()` is deliberately
|
|
5786
|
+
* untouched: it keys nothing, so a nameless body is still served there.
|
|
5787
|
+
*
|
|
5788
|
+
* The predicate is spelled as a type guard, and the mapped element type left
|
|
5789
|
+
* `unknown`, so `tsc` PROVES the declared `string[]` instead of a cast
|
|
5790
|
+
* asserting it — otherwise the compiler reads the filter as always-true and
|
|
5791
|
+
* a later reader deletes it as dead.
|
|
5792
|
+
*/
|
|
5555
5793
|
async list(type) {
|
|
5556
5794
|
const items = await this.loadMany(type);
|
|
5557
|
-
return items.map((
|
|
5795
|
+
return items.map((item) => item.name).filter((name) => typeof name === "string");
|
|
5558
5796
|
}
|
|
5559
5797
|
async save(type, name, data, _options) {
|
|
5560
5798
|
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
@@ -5594,9 +5832,9 @@ var HistoryCleanupManager = class {
|
|
|
5594
5832
|
return;
|
|
5595
5833
|
}
|
|
5596
5834
|
const intervalMs = (this.policy.cleanupIntervalHours ?? 24) * 60 * 60 * 1e3;
|
|
5597
|
-
void this
|
|
5835
|
+
void runCleanupAndReport(this);
|
|
5598
5836
|
this.cleanupTimer = setInterval(() => {
|
|
5599
|
-
void this
|
|
5837
|
+
void runCleanupAndReport(this);
|
|
5600
5838
|
}, intervalMs);
|
|
5601
5839
|
}
|
|
5602
5840
|
/**
|
|
@@ -5623,7 +5861,7 @@ var HistoryCleanupManager = class {
|
|
|
5623
5861
|
try {
|
|
5624
5862
|
if (this.policy.maxAgeDays) {
|
|
5625
5863
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
5626
|
-
cutoffDate.
|
|
5864
|
+
cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
|
|
5627
5865
|
const cutoffISO = cutoffDate.toISOString();
|
|
5628
5866
|
const filter = {
|
|
5629
5867
|
recorded_at: { $lt: cutoffISO }
|
|
@@ -5743,7 +5981,7 @@ var HistoryCleanupManager = class {
|
|
|
5743
5981
|
if (organizationId) baseWhere.organization_id = organizationId;
|
|
5744
5982
|
if (this.policy.maxAgeDays) {
|
|
5745
5983
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
5746
|
-
cutoffDate.
|
|
5984
|
+
cutoffDate.setUTCDate(cutoffDate.getUTCDate() - this.policy.maxAgeDays);
|
|
5747
5985
|
const cutoffISO = cutoffDate.toISOString();
|
|
5748
5986
|
const filter = {
|
|
5749
5987
|
recorded_at: { $lt: cutoffISO },
|
|
@@ -5790,6 +6028,23 @@ var HistoryCleanupManager = class {
|
|
|
5790
6028
|
};
|
|
5791
6029
|
}
|
|
5792
6030
|
};
|
|
6031
|
+
async function runCleanupAndReport(manager) {
|
|
6032
|
+
let outcome;
|
|
6033
|
+
try {
|
|
6034
|
+
outcome = await manager.runCleanup();
|
|
6035
|
+
} catch (error) {
|
|
6036
|
+
console.error(
|
|
6037
|
+
"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:",
|
|
6038
|
+
error
|
|
6039
|
+
);
|
|
6040
|
+
return;
|
|
6041
|
+
}
|
|
6042
|
+
if (outcome.errors > 0) {
|
|
6043
|
+
console.error(
|
|
6044
|
+
`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.`
|
|
6045
|
+
);
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
5793
6048
|
|
|
5794
6049
|
// src/migration/index.ts
|
|
5795
6050
|
var migration_exports = {};
|
|
@@ -5847,6 +6102,9 @@ var MigrationExecutor = class {
|
|
|
5847
6102
|
}
|
|
5848
6103
|
};
|
|
5849
6104
|
export {
|
|
6105
|
+
AMBIGUOUS_METADATA_STEM_CODE,
|
|
6106
|
+
AMBIGUOUS_METADATA_STEM_STATUS,
|
|
6107
|
+
AmbiguousMetadataStemError,
|
|
5850
6108
|
DatabaseLoader,
|
|
5851
6109
|
FilesystemLoader,
|
|
5852
6110
|
HistoryCleanupManager,
|
|
@@ -5864,6 +6122,7 @@ export {
|
|
|
5864
6122
|
calculateChecksum,
|
|
5865
6123
|
deriveViewContainerObject,
|
|
5866
6124
|
generateDiffSummary,
|
|
5867
|
-
generateSimpleDiff
|
|
6125
|
+
generateSimpleDiff,
|
|
6126
|
+
isAmbiguousMetadataStemError
|
|
5868
6127
|
};
|
|
5869
6128
|
//# sourceMappingURL=node.js.map
|