@objectstack/metadata 17.1.0 → 17.3.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/dist/node.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { MetadataManager, MetadataManagerOptions, MetadataLoader, MetadataSerializer } from './index.cjs';
2
- export { DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff } from './index.cjs';
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, deriveViewContainerObject, generateDiffSummary, generateSimpleDiff } from './index.cjs';
3
3
  export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, SysMetadataHistoryObject, SysMetadataObject, WatchFilter } from '@objectstack/metadata-core';
4
4
  import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
5
5
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
@@ -37,10 +37,140 @@ declare class FilesystemLoader implements MetadataLoader {
37
37
  constructor(rootDir: string, serializers: Map<MetadataFormat, MetadataSerializer>, logger?: Logger | undefined);
38
38
  load(type: string, name: string, options?: MetadataLoadOptions): Promise<MetadataLoadResult>;
39
39
  loadMany<T = any>(type: string, options?: MetadataLoadOptions): Promise<T[]>;
40
+ /**
41
+ * [#14341] The keyed half of {@link loadMany} — see {@link MetadataKeyedItem}
42
+ * for why the store's key travels BESIDE the body instead of being folded
43
+ * into it.
44
+ *
45
+ * THE RULE, in one sentence: an item is keyed by this loader's own
46
+ * name-to-path derivation — {@link nameFromFilename}, the very basename
47
+ * derivation `list()` reports — ONLY where that derivation is a bijection for
48
+ * the file (it sits directly under `ROOT/TYPE/` and carries one of the
49
+ * extensions {@link findFile} tries, so `findFile(type, key)` resolves back to
50
+ * this same file); every other shape keeps the pre-#14205 behaviour verbatim,
51
+ * keyed by `body.name` when it has one and dropped when it has none.
52
+ *
53
+ * Why the rule stops there (PM ruling on #14341, 2026-09-02, knowingly over
54
+ * triage's "a nested path keeps whatever `list()` reports for it today"):
55
+ * `list()` and `findFile()` DISAGREE outside that shape. For
56
+ * `ROOT/TYPE/crm/account.json`, `list()` reports the bare `account`, but
57
+ * `findFile()` resolves that name against `ROOT/TYPE/account.json` and finds
58
+ * nothing — the only name reaching the file is `crm/account`, which nothing
59
+ * reports. An extension-less file is read by `loadMany()` and reported by
60
+ * `list()`, and `findFile()` resolves neither. Keying by either side would
61
+ * mint a name some other door cannot open, and two directories holding the
62
+ * same basename would collide in silence
63
+ * (`MetadataManager.admitLoaderItems()` keeps the first and says nothing).
64
+ * The card's own fence: "keying items under names nothing else uses … is
65
+ * worse than today's honest drop". So the drop stays exactly where the key is
66
+ * unsettled, and is pinned as a RECORD in
67
+ * `filesystem-loader-keyed-items.test.ts`.
68
+ *
69
+ * [#14486, partial] `list()` and {@link findFile} have since converged on
70
+ * {@link resolvableNameForPath} — the derivation this method already used —
71
+ * so a nested or extension-less file is now neither listed nor resolvable.
72
+ * What did NOT change is the WALK behind this method: `loadManyEntries()`
73
+ * still READS those files, so `loadMany()` still returns their bodies and
74
+ * this method still falls back to `body.name` for them. That half of the
75
+ * #14486 ruling ("nothing unlisted is returned by `loadMany()` either") is
76
+ * deliberately NOT taken here: it would invert the three landed #14341 pins
77
+ * in `filesystem-loader-keyed-items.test.ts:113,167,187` and the
78
+ * `loadMany()` CONTROL at `:196`, and that file was under a concurrent
79
+ * claim (PR #14627) when this landed. The remaining divergence — listed ⊂
80
+ * loaded — is pinned as a RECORD in
81
+ * `filesystem-loader-list-reachability.test.ts` rather than left implicit.
82
+ *
83
+ * One consequence, deliberate: a flat file whose `body.name` DISAGREES with
84
+ * its basename is now keyed by the BASENAME. That is #14205's rule (identity
85
+ * is the key the store holds an item under, not `body.name`) applied to this
86
+ * loader, and it aligns `MetadataManager.list()` with `listNames()` for that
87
+ * shape.
88
+ *
89
+ * The body is handed back by reference, unchanged: nothing is written into a
90
+ * body that deliberately has no `name`. `limit` bounds the items LOADED,
91
+ * exactly as `loadMany()` does — an entry the key rule drops has still been
92
+ * read and still counts against it.
93
+ */
94
+ loadManyKeyed<T = any>(type: string, options?: MetadataLoadOptions): Promise<MetadataKeyedItem<T>[]>;
95
+ /**
96
+ * The single walk behind {@link loadMany} and {@link loadManyKeyed}: one glob,
97
+ * one serializer pass, one `limit`. Shared so the two can never answer with
98
+ * different bodies for the same file — {@link MetadataLoader.loadManyKeyed}
99
+ * requires `data` to be "the same body `loadMany()` would return for the
100
+ * item", and a second copy of this walk is how that would quietly stop being
101
+ * true.
102
+ */
103
+ private loadManyEntries;
40
104
  exists(type: string, name: string): Promise<boolean>;
41
105
  stat(type: string, name: string): Promise<MetadataStats | null>;
106
+ /**
107
+ * [#14486] The names this loader can be asked for, and ONLY those: a file
108
+ * directly under `ROOT/TYPE/` carrying an extension one of this instance's
109
+ * REGISTERED serializers claims. Every name it reports resolves back through
110
+ * {@link findFile}, so `listNames()` and `get()` give the same answer.
111
+ *
112
+ * It used to report `path.basename(file, ext)` for every file the glob found,
113
+ * nested or not, extension or not — and {@link findFile} resolves neither
114
+ * shape. `ROOT/TYPE/crm/account.json` was listed as `account`, which resolves
115
+ * against `ROOT/TYPE/account.json` and finds nothing; an extension-less
116
+ * `ROOT/TYPE/noext` was listed as `noext`, which resolves under no appended
117
+ * extension at all. A name in the list that `get()` answers `null` for is the
118
+ * silent failure an author (human or AI) reads as their own typo, so they
119
+ * retry the same word: the list and the door now agree instead.
120
+ *
121
+ * Ruling (maintainer, via the director seat on #14486, 2026-09-02): narrow
122
+ * the list — direction A, over B (reverse-unify: report `crm/account` and
123
+ * teach `findFile()` path-shaped names), which would have made a slash inside
124
+ * a metadata name every consumer's permanent obligation with no measured
125
+ * demand for it. The two-segment layout follows ADR-0008 §10, which
126
+ * `metadata-fs`'s `parseItemPath()` already enforces for its own store; the
127
+ * EXTENSION set deliberately does NOT follow §10's `.json`-only rule — see
128
+ * {@link resolvableExtensions} for why.
129
+ */
42
130
  list(type: string): Promise<string[]>;
43
131
  save(type: string, name: string, data: any, options?: MetadataSaveOptions): Promise<MetadataSaveResult>;
132
+ /**
133
+ * The inverse of {@link detectFormat}: which file extensions carry which
134
+ * format. Fixed ORDER, because it is also {@link findFile}'s precedence when
135
+ * two files under one type directory share a stem — registration order must
136
+ * not be able to change which file `ROOT/TYPE/NAME` opens.
137
+ */
138
+ private static readonly EXTENSIONS_BY_FORMAT;
139
+ /**
140
+ * [#14486] The extensions a name can be resolved under, for THIS instance:
141
+ * the ones belonging to the serializer set it was constructed with. Shared by
142
+ * {@link findFile}, {@link resolvableNameForPath} and therefore {@link list},
143
+ * so the set a name can be RESOLVED under cannot drift from the set that is
144
+ * LISTED or the set {@link loadManyKeyed} is willing to KEY by.
145
+ *
146
+ * Registered, not hard-coded, and deliberately not ADR-0008 §10's `.json`
147
+ * only. §10 governs the `metadata-fs` store; applying it verbatim here would
148
+ * drop `.yaml` and `.ts` metadata out of `listNames()` — a breakage this card
149
+ * never asked for. Under the manager's DEFAULT format set
150
+ * (`typescript` / `json` / `yaml`, `metadata-manager.ts`) that leaves `.js`
151
+ * out, which is the card's row-4 membership mismatch closing for free: a `.js`
152
+ * file was listed and resolvable while `loadMany()` could never return it and
153
+ * `load()` threw `No serializer found for format: javascript`. Register
154
+ * `javascript` and it is listed, resolvable and loadable together.
155
+ */
156
+ private resolvableExtensions;
157
+ /**
158
+ * The metadata name this loader reports for a file: the basename with its
159
+ * extension stripped. One derivation, shared by {@link list} and
160
+ * {@link loadManyKeyed}, so the two cannot drift for the shape where they
161
+ * agree — `dotted.config.json` is `dotted.config` for both.
162
+ */
163
+ private static nameFromFilename;
164
+ /**
165
+ * The key for a file IF this loader's name-to-path mapping is a bijection for
166
+ * it: a file directly under `ROOT/TYPE/` carrying an extension
167
+ * {@link findFile} tries, so `findFile(type, key)` resolves back to this very
168
+ * file. `null` for every other shape — a nested path, an extension-less file,
169
+ * an extension spelled in a case `findFile()` does not compose — which is why
170
+ * {@link loadManyKeyed} falls back to `body.name` there rather than minting a
171
+ * key no other door can open.
172
+ */
173
+ private resolvableNameForPath;
44
174
  /**
45
175
  * Find file for a given type and name
46
176
  */
@@ -61,4 +191,4 @@ declare class FilesystemLoader implements MetadataLoader {
61
191
  private generateETag;
62
192
  }
63
193
 
64
- export { FilesystemLoader, MetadataLoader, MetadataManager, MetadataManagerOptions, MetadataSerializer, NodeMetadataManager };
194
+ export { FilesystemLoader, MetadataKeyedItem, MetadataLoader, MetadataManager, MetadataManagerOptions, MetadataSerializer, NodeMetadataManager };
package/dist/node.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { MetadataManager, MetadataManagerOptions, MetadataLoader, MetadataSerializer } from './index.js';
2
- export { DatabaseLoader, DatabaseLoaderOptions, HistoryCleanupManager, JSONSerializer, MemoryLoader, MetadataPlugin, Migration, RemoteLoader, SerializeOptions, TypeScriptSerializer, WatchCallback, YAMLSerializer, calculateChecksum, generateDiffSummary, generateSimpleDiff } from './index.js';
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, deriveViewContainerObject, generateDiffSummary, generateSimpleDiff } from './index.js';
3
3
  export { HistoryOptions, MetaRef, MetadataEvent, MetadataItem, MetadataItemHeader, MetadataRepository, SysMetadataHistoryObject, SysMetadataObject, WatchFilter } from '@objectstack/metadata-core';
4
4
  import { MetadataLoaderContract, MetadataFormat, MetadataLoadOptions, MetadataLoadResult, MetadataStats, MetadataSaveOptions, MetadataSaveResult } from '@objectstack/spec/system';
5
5
  export { MetadataCollectionInfo, MetadataDiffResult, MetadataFormat, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataManagerConfig, MetadataSaveOptions, MetadataSaveResult, MetadataStats, MetadataWatchEvent } from '@objectstack/spec/system';
@@ -37,10 +37,140 @@ declare class FilesystemLoader implements MetadataLoader {
37
37
  constructor(rootDir: string, serializers: Map<MetadataFormat, MetadataSerializer>, logger?: Logger | undefined);
38
38
  load(type: string, name: string, options?: MetadataLoadOptions): Promise<MetadataLoadResult>;
39
39
  loadMany<T = any>(type: string, options?: MetadataLoadOptions): Promise<T[]>;
40
+ /**
41
+ * [#14341] The keyed half of {@link loadMany} — see {@link MetadataKeyedItem}
42
+ * for why the store's key travels BESIDE the body instead of being folded
43
+ * into it.
44
+ *
45
+ * THE RULE, in one sentence: an item is keyed by this loader's own
46
+ * name-to-path derivation — {@link nameFromFilename}, the very basename
47
+ * derivation `list()` reports — ONLY where that derivation is a bijection for
48
+ * the file (it sits directly under `ROOT/TYPE/` and carries one of the
49
+ * extensions {@link findFile} tries, so `findFile(type, key)` resolves back to
50
+ * this same file); every other shape keeps the pre-#14205 behaviour verbatim,
51
+ * keyed by `body.name` when it has one and dropped when it has none.
52
+ *
53
+ * Why the rule stops there (PM ruling on #14341, 2026-09-02, knowingly over
54
+ * triage's "a nested path keeps whatever `list()` reports for it today"):
55
+ * `list()` and `findFile()` DISAGREE outside that shape. For
56
+ * `ROOT/TYPE/crm/account.json`, `list()` reports the bare `account`, but
57
+ * `findFile()` resolves that name against `ROOT/TYPE/account.json` and finds
58
+ * nothing — the only name reaching the file is `crm/account`, which nothing
59
+ * reports. An extension-less file is read by `loadMany()` and reported by
60
+ * `list()`, and `findFile()` resolves neither. Keying by either side would
61
+ * mint a name some other door cannot open, and two directories holding the
62
+ * same basename would collide in silence
63
+ * (`MetadataManager.admitLoaderItems()` keeps the first and says nothing).
64
+ * The card's own fence: "keying items under names nothing else uses … is
65
+ * worse than today's honest drop". So the drop stays exactly where the key is
66
+ * unsettled, and is pinned as a RECORD in
67
+ * `filesystem-loader-keyed-items.test.ts`.
68
+ *
69
+ * [#14486, partial] `list()` and {@link findFile} have since converged on
70
+ * {@link resolvableNameForPath} — the derivation this method already used —
71
+ * so a nested or extension-less file is now neither listed nor resolvable.
72
+ * What did NOT change is the WALK behind this method: `loadManyEntries()`
73
+ * still READS those files, so `loadMany()` still returns their bodies and
74
+ * this method still falls back to `body.name` for them. That half of the
75
+ * #14486 ruling ("nothing unlisted is returned by `loadMany()` either") is
76
+ * deliberately NOT taken here: it would invert the three landed #14341 pins
77
+ * in `filesystem-loader-keyed-items.test.ts:113,167,187` and the
78
+ * `loadMany()` CONTROL at `:196`, and that file was under a concurrent
79
+ * claim (PR #14627) when this landed. The remaining divergence — listed ⊂
80
+ * loaded — is pinned as a RECORD in
81
+ * `filesystem-loader-list-reachability.test.ts` rather than left implicit.
82
+ *
83
+ * One consequence, deliberate: a flat file whose `body.name` DISAGREES with
84
+ * its basename is now keyed by the BASENAME. That is #14205's rule (identity
85
+ * is the key the store holds an item under, not `body.name`) applied to this
86
+ * loader, and it aligns `MetadataManager.list()` with `listNames()` for that
87
+ * shape.
88
+ *
89
+ * The body is handed back by reference, unchanged: nothing is written into a
90
+ * body that deliberately has no `name`. `limit` bounds the items LOADED,
91
+ * exactly as `loadMany()` does — an entry the key rule drops has still been
92
+ * read and still counts against it.
93
+ */
94
+ loadManyKeyed<T = any>(type: string, options?: MetadataLoadOptions): Promise<MetadataKeyedItem<T>[]>;
95
+ /**
96
+ * The single walk behind {@link loadMany} and {@link loadManyKeyed}: one glob,
97
+ * one serializer pass, one `limit`. Shared so the two can never answer with
98
+ * different bodies for the same file — {@link MetadataLoader.loadManyKeyed}
99
+ * requires `data` to be "the same body `loadMany()` would return for the
100
+ * item", and a second copy of this walk is how that would quietly stop being
101
+ * true.
102
+ */
103
+ private loadManyEntries;
40
104
  exists(type: string, name: string): Promise<boolean>;
41
105
  stat(type: string, name: string): Promise<MetadataStats | null>;
106
+ /**
107
+ * [#14486] The names this loader can be asked for, and ONLY those: a file
108
+ * directly under `ROOT/TYPE/` carrying an extension one of this instance's
109
+ * REGISTERED serializers claims. Every name it reports resolves back through
110
+ * {@link findFile}, so `listNames()` and `get()` give the same answer.
111
+ *
112
+ * It used to report `path.basename(file, ext)` for every file the glob found,
113
+ * nested or not, extension or not — and {@link findFile} resolves neither
114
+ * shape. `ROOT/TYPE/crm/account.json` was listed as `account`, which resolves
115
+ * against `ROOT/TYPE/account.json` and finds nothing; an extension-less
116
+ * `ROOT/TYPE/noext` was listed as `noext`, which resolves under no appended
117
+ * extension at all. A name in the list that `get()` answers `null` for is the
118
+ * silent failure an author (human or AI) reads as their own typo, so they
119
+ * retry the same word: the list and the door now agree instead.
120
+ *
121
+ * Ruling (maintainer, via the director seat on #14486, 2026-09-02): narrow
122
+ * the list — direction A, over B (reverse-unify: report `crm/account` and
123
+ * teach `findFile()` path-shaped names), which would have made a slash inside
124
+ * a metadata name every consumer's permanent obligation with no measured
125
+ * demand for it. The two-segment layout follows ADR-0008 §10, which
126
+ * `metadata-fs`'s `parseItemPath()` already enforces for its own store; the
127
+ * EXTENSION set deliberately does NOT follow §10's `.json`-only rule — see
128
+ * {@link resolvableExtensions} for why.
129
+ */
42
130
  list(type: string): Promise<string[]>;
43
131
  save(type: string, name: string, data: any, options?: MetadataSaveOptions): Promise<MetadataSaveResult>;
132
+ /**
133
+ * The inverse of {@link detectFormat}: which file extensions carry which
134
+ * format. Fixed ORDER, because it is also {@link findFile}'s precedence when
135
+ * two files under one type directory share a stem — registration order must
136
+ * not be able to change which file `ROOT/TYPE/NAME` opens.
137
+ */
138
+ private static readonly EXTENSIONS_BY_FORMAT;
139
+ /**
140
+ * [#14486] The extensions a name can be resolved under, for THIS instance:
141
+ * the ones belonging to the serializer set it was constructed with. Shared by
142
+ * {@link findFile}, {@link resolvableNameForPath} and therefore {@link list},
143
+ * so the set a name can be RESOLVED under cannot drift from the set that is
144
+ * LISTED or the set {@link loadManyKeyed} is willing to KEY by.
145
+ *
146
+ * Registered, not hard-coded, and deliberately not ADR-0008 §10's `.json`
147
+ * only. §10 governs the `metadata-fs` store; applying it verbatim here would
148
+ * drop `.yaml` and `.ts` metadata out of `listNames()` — a breakage this card
149
+ * never asked for. Under the manager's DEFAULT format set
150
+ * (`typescript` / `json` / `yaml`, `metadata-manager.ts`) that leaves `.js`
151
+ * out, which is the card's row-4 membership mismatch closing for free: a `.js`
152
+ * file was listed and resolvable while `loadMany()` could never return it and
153
+ * `load()` threw `No serializer found for format: javascript`. Register
154
+ * `javascript` and it is listed, resolvable and loadable together.
155
+ */
156
+ private resolvableExtensions;
157
+ /**
158
+ * The metadata name this loader reports for a file: the basename with its
159
+ * extension stripped. One derivation, shared by {@link list} and
160
+ * {@link loadManyKeyed}, so the two cannot drift for the shape where they
161
+ * agree — `dotted.config.json` is `dotted.config` for both.
162
+ */
163
+ private static nameFromFilename;
164
+ /**
165
+ * The key for a file IF this loader's name-to-path mapping is a bijection for
166
+ * it: a file directly under `ROOT/TYPE/` carrying an extension
167
+ * {@link findFile} tries, so `findFile(type, key)` resolves back to this very
168
+ * file. `null` for every other shape — a nested path, an extension-less file,
169
+ * an extension spelled in a case `findFile()` does not compose — which is why
170
+ * {@link loadManyKeyed} falls back to `body.name` there rather than minting a
171
+ * key no other door can open.
172
+ */
173
+ private resolvableNameForPath;
44
174
  /**
45
175
  * Find file for a given type and name
46
176
  */
@@ -61,4 +191,4 @@ declare class FilesystemLoader implements MetadataLoader {
61
191
  private generateETag;
62
192
  }
63
193
 
64
- export { FilesystemLoader, MetadataLoader, MetadataManager, MetadataManagerOptions, MetadataSerializer, NodeMetadataManager };
194
+ export { FilesystemLoader, MetadataKeyedItem, MetadataLoader, MetadataManager, MetadataManagerOptions, MetadataSerializer, NodeMetadataManager };