@fgv/ts-utils 5.0.2-0 → 5.1.0-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.
Files changed (105) hide show
  1. package/CHANGELOG.json +21 -0
  2. package/README.md +152 -84
  3. package/dist/index.js +2 -2
  4. package/dist/packlets/base/messageAggregator.js +6 -6
  5. package/dist/packlets/base/result.js +39 -39
  6. package/dist/packlets/base/utils.js +47 -0
  7. package/dist/packlets/collections/aggregatedResultMap.js +722 -0
  8. package/dist/packlets/collections/collector.js +9 -9
  9. package/dist/packlets/collections/collectorValidator.js +4 -4
  10. package/dist/packlets/collections/converters.js +39 -0
  11. package/dist/packlets/collections/convertingCollectorValidator.js +3 -3
  12. package/dist/packlets/collections/convertingResultMap.js +208 -0
  13. package/dist/packlets/collections/index.js +4 -0
  14. package/dist/packlets/collections/keyValueConverters.js +1 -4
  15. package/dist/packlets/collections/readOnlyConvertingResultMap.js +193 -0
  16. package/dist/packlets/collections/resultMapValidator.js +50 -6
  17. package/dist/packlets/collections/validatingConvertingResultMap.js +78 -0
  18. package/dist/packlets/conversion/advancedConverters.js +249 -0
  19. package/dist/packlets/conversion/baseConverter.js +23 -16
  20. package/dist/packlets/conversion/basicConverters.js +571 -0
  21. package/dist/packlets/conversion/converters.js +3 -617
  22. package/dist/packlets/conversion/defaultingConverter.js +21 -15
  23. package/dist/packlets/logging/bootLogger.js +122 -0
  24. package/dist/packlets/logging/index.js +1 -0
  25. package/dist/packlets/logging/logReporter.js +32 -1
  26. package/dist/packlets/logging/logger.js +25 -5
  27. package/dist/packlets/validation/classes.js +1 -0
  28. package/dist/packlets/validation/compositeId.js +60 -0
  29. package/dist/packlets/validation/field.js +1 -1
  30. package/dist/packlets/validation/genericValidator.js +24 -10
  31. package/dist/packlets/validation/object.js +3 -6
  32. package/dist/packlets/validation/validators.js +12 -0
  33. package/dist/ts-utils.d.ts +1633 -200
  34. package/dist/tsdoc-metadata.json +1 -1
  35. package/lib/index.d.ts +2 -2
  36. package/lib/index.js +2 -1
  37. package/lib/packlets/base/mapResults.d.ts +3 -3
  38. package/lib/packlets/base/messageAggregator.d.ts +6 -6
  39. package/lib/packlets/base/messageAggregator.js +6 -6
  40. package/lib/packlets/base/result.d.ts +74 -51
  41. package/lib/packlets/base/result.js +39 -39
  42. package/lib/packlets/base/utils.d.ts +42 -0
  43. package/lib/packlets/base/utils.js +52 -0
  44. package/lib/packlets/collections/aggregatedResultMap.d.ts +433 -0
  45. package/lib/packlets/collections/aggregatedResultMap.js +728 -0
  46. package/lib/packlets/collections/collector.d.ts +11 -10
  47. package/lib/packlets/collections/collector.js +9 -9
  48. package/lib/packlets/collections/collectorValidator.d.ts +12 -10
  49. package/lib/packlets/collections/collectorValidator.js +4 -4
  50. package/lib/packlets/collections/converters.d.ts +18 -0
  51. package/lib/packlets/collections/converters.js +42 -0
  52. package/lib/packlets/collections/convertingCollector.d.ts +4 -4
  53. package/lib/packlets/collections/convertingCollectorValidator.d.ts +10 -7
  54. package/lib/packlets/collections/convertingCollectorValidator.js +3 -3
  55. package/lib/packlets/collections/convertingResultMap.d.ts +176 -0
  56. package/lib/packlets/collections/convertingResultMap.js +213 -0
  57. package/lib/packlets/collections/index.d.ts +4 -0
  58. package/lib/packlets/collections/index.js +4 -0
  59. package/lib/packlets/collections/keyValueConverters.d.ts +1 -4
  60. package/lib/packlets/collections/keyValueConverters.js +1 -4
  61. package/lib/packlets/collections/readOnlyConvertingResultMap.d.ts +153 -0
  62. package/lib/packlets/collections/readOnlyConvertingResultMap.js +197 -0
  63. package/lib/packlets/collections/readonlyResultMap.d.ts +7 -7
  64. package/lib/packlets/collections/resultMap.d.ts +108 -2
  65. package/lib/packlets/collections/resultMapValidator.d.ts +45 -11
  66. package/lib/packlets/collections/resultMapValidator.js +52 -7
  67. package/lib/packlets/collections/validatingCollector.d.ts +5 -5
  68. package/lib/packlets/collections/validatingConvertingCollector.d.ts +3 -3
  69. package/lib/packlets/collections/validatingConvertingResultMap.d.ts +102 -0
  70. package/lib/packlets/collections/validatingConvertingResultMap.js +83 -0
  71. package/lib/packlets/collections/validatingResultMap.d.ts +1 -1
  72. package/lib/packlets/conversion/advancedConverters.d.ts +170 -0
  73. package/lib/packlets/conversion/advancedConverters.js +258 -0
  74. package/lib/packlets/conversion/baseConverter.d.ts +39 -23
  75. package/lib/packlets/conversion/baseConverter.js +23 -16
  76. package/lib/packlets/conversion/basicConverters.d.ts +470 -0
  77. package/lib/packlets/conversion/basicConverters.js +595 -0
  78. package/lib/packlets/conversion/converter.d.ts +7 -0
  79. package/lib/packlets/conversion/converters.d.ts +2 -535
  80. package/lib/packlets/conversion/converters.js +17 -639
  81. package/lib/packlets/conversion/defaultingConverter.d.ts +20 -16
  82. package/lib/packlets/conversion/defaultingConverter.js +21 -15
  83. package/lib/packlets/logging/bootLogger.d.ts +84 -0
  84. package/lib/packlets/logging/bootLogger.js +126 -0
  85. package/lib/packlets/logging/index.d.ts +1 -0
  86. package/lib/packlets/logging/index.js +1 -0
  87. package/lib/packlets/logging/logReporter.d.ts +18 -3
  88. package/lib/packlets/logging/logReporter.js +31 -0
  89. package/lib/packlets/logging/logger.d.ts +39 -6
  90. package/lib/packlets/logging/logger.js +26 -5
  91. package/lib/packlets/validation/classes.d.ts +1 -0
  92. package/lib/packlets/validation/classes.js +3 -1
  93. package/lib/packlets/validation/compositeId.d.ts +29 -0
  94. package/lib/packlets/validation/compositeId.js +64 -0
  95. package/lib/packlets/validation/field.d.ts +1 -1
  96. package/lib/packlets/validation/field.js +1 -1
  97. package/lib/packlets/validation/genericValidator.d.ts +15 -11
  98. package/lib/packlets/validation/genericValidator.js +24 -10
  99. package/lib/packlets/validation/object.d.ts +3 -6
  100. package/lib/packlets/validation/object.js +3 -6
  101. package/lib/packlets/validation/traits.d.ts +3 -3
  102. package/lib/packlets/validation/validator.d.ts +7 -0
  103. package/lib/packlets/validation/validators.d.ts +10 -0
  104. package/lib/packlets/validation/validators.js +13 -0
  105. package/package.json +14 -14
@@ -0,0 +1,722 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ *
4
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ * of this software and associated documentation files (the "Software"), to deal
6
+ * in the Software without restriction, including without limitation the rights
7
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ * copies of the Software, and to permit persons to whom the Software is
9
+ * furnished to do so, subject to the following conditions:
10
+ *
11
+ * The above copyright notice and this permission notice shall be included in all
12
+ * copies or substantial portions of the Software.
13
+ *
14
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ * SOFTWARE.
21
+ */
22
+ import { captureResult, fail, failWithDetail, succeed, succeedWithDetail, Success } from '../base';
23
+ import { Converters } from '../conversion';
24
+ import { Validators } from '../validation';
25
+ import { KeyValueConverters } from './keyValueConverters';
26
+ import { ValidatingResultMap } from './validatingResultMap';
27
+ /**
28
+ * A validator for weakly-typed access to an {@link AggregatedResultMap | aggregated result map}.
29
+ * @public
30
+ */
31
+ export class AggregatedResultMapValidator {
32
+ /**
33
+ * Constructs a new {@link AggregatedResultMapValidator | aggregated result map validator}.
34
+ * @param map - The {@link AggregatedResultMap | aggregated result map} being validated.
35
+ * @param converters - The key-value converters for weakly-typed access.
36
+ */
37
+ constructor(map, converters) {
38
+ this._map = map;
39
+ this.converters = converters;
40
+ }
41
+ /** {@inheritDoc Collections.ResultMapValidator.map} */
42
+ get map() {
43
+ return this._map;
44
+ }
45
+ /** {@inheritDoc Collections.ResultMapValidator.get} */
46
+ get(key) {
47
+ return this.converters.convertKey(key).onSuccess((k) => {
48
+ return this._map.get(k);
49
+ });
50
+ }
51
+ /** {@inheritDoc Collections.ResultMapValidator.has} */
52
+ has(key) {
53
+ const result = this.converters.convertKey(key);
54
+ if (result.isFailure()) {
55
+ return false;
56
+ }
57
+ return this._map.has(result.value);
58
+ }
59
+ /** {@inheritDoc Collections.ResultMapValidator.add} */
60
+ add(key, value) {
61
+ return this.converters.convertEntry([key, value]).onSuccess(([vk, vv]) => {
62
+ return this._map.add(vk, vv);
63
+ });
64
+ }
65
+ /** {@inheritDoc Collections.ResultMapValidator.set} */
66
+ set(key, value) {
67
+ return this.converters.convertEntry([key, value]).onSuccess(([vk, vv]) => {
68
+ return this._map.set(vk, vv);
69
+ });
70
+ }
71
+ /** {@inheritDoc Collections.ResultMapValidator.update} */
72
+ update(key, value) {
73
+ return this.converters.convertEntry([key, value]).onSuccess(([vk, vv]) => {
74
+ return this._map.update(vk, vv);
75
+ });
76
+ }
77
+ /** {@inheritDoc Collections.ResultMapValidator.delete} */
78
+ delete(key) {
79
+ return this.converters.convertKey(key).onSuccess((k) => {
80
+ return this._map.delete(k);
81
+ });
82
+ }
83
+ }
84
+ /**
85
+ * Type guard to check if an {@link AggregatedResultMapEntry | aggregated result map entry} is mutable.
86
+ */
87
+ function isMutableEntry(entry) {
88
+ return entry.isMutable === true;
89
+ }
90
+ /**
91
+ * Base class for an aggregated result map that wraps a collection of {@link ValidatingResultMap} instances,
92
+ * keyed by collection ID. Items are accessed via composite IDs that combine the collection ID
93
+ * and item ID with a delimiter.
94
+ * @remarks Consumers should inherit from this class or use {@link AggregatedResultMap | AggregatedResultMap}
95
+ * for fully generic.
96
+ * @public
97
+ */
98
+ export class AggregatedResultMapBase {
99
+ /**
100
+ * Protected constructor for derived classes.
101
+ * Use the {@link AggregatedResultMap.create | create static method} for safe construction with error handling.
102
+ * @param params - {@link IAggregatedResultMapConstructorParams | Parameters} for constructing the map.
103
+ * @throws If initialization fails (e.g., invalid collections).
104
+ * @public
105
+ */
106
+ constructor(params) {
107
+ var _a, _b, _c;
108
+ this._compositeIdValidator =
109
+ (_a = params.compositeIdValidator) !== null && _a !== void 0 ? _a : AggregatedResultMapBase._compositeIdValidator(params);
110
+ this._collectionIdConverter = params.collectionIdConverter;
111
+ this._itemIdConverter = params.itemIdConverter;
112
+ this._itemConverter = params.itemConverter;
113
+ this._metadataConverter = params.metadataConverter;
114
+ this._delimiter = (_b = params.separator) !== null && _b !== void 0 ? _b : '.';
115
+ // Build child key-value converters for item ID/value pairs
116
+ this._childKvConverters = new KeyValueConverters({
117
+ key: params.itemIdConverter,
118
+ value: params.itemConverter
119
+ });
120
+ // Build composite ID converter for parsing composite IDs
121
+ this._compositeIdConverter = Converters.compositeId(params.collectionIdConverter, this._delimiter, params.itemIdConverter);
122
+ // Build key-value converters for composite ID/item pairs
123
+ this._kvConverters = new KeyValueConverters({
124
+ key: this._compositeIdValidator,
125
+ value: params.itemConverter
126
+ });
127
+ // Build entry converter for collection entries
128
+ const entryConverter = AggregatedResultMapBase._entryConverter(params.collectionIdConverter, this._childKvConverters, params.metadataConverter);
129
+ // Build key-value converters for the collections map
130
+ this._collectionKvConverters = new KeyValueConverters({
131
+ key: params.collectionIdConverter,
132
+ value: entryConverter
133
+ });
134
+ // Convert all input collections to [id, entry] pairs for ValidatingResultMap
135
+ const initCollections = (_c = params.collections) !== null && _c !== void 0 ? _c : [];
136
+ const initEntries = initCollections.map((entry) => {
137
+ return [entry.id, entry];
138
+ });
139
+ // Build the inner ValidatingResultMap - throws on error
140
+ this._collections = new ValidatingResultMap({
141
+ converters: this._collectionKvConverters,
142
+ entries: initEntries
143
+ });
144
+ // Build the validator
145
+ this._validating = new AggregatedResultMapValidator(this, this._kvConverters);
146
+ }
147
+ // ==================== IReadOnlyValidatingResultMap implementation ====================
148
+ /**
149
+ * A validator for weakly-typed access to the map.
150
+ */
151
+ get validating() {
152
+ return this._validating;
153
+ }
154
+ /**
155
+ * The total number of items across all collections.
156
+ */
157
+ get size() {
158
+ let total = 0;
159
+ for (const entry of Array.from(this._collections.values())) {
160
+ total += entry.items.size;
161
+ }
162
+ return total;
163
+ }
164
+ /**
165
+ * Gets an item by its composite ID.
166
+ * @param key - The composite ID of the item.
167
+ * @returns `Success` with the item if found, `Failure` otherwise.
168
+ */
169
+ get(key) {
170
+ return this._splitAndLookup(key).onSuccess(({ entry, itemId }) => {
171
+ return entry.items.get(itemId);
172
+ });
173
+ }
174
+ /**
175
+ * Checks if an item exists by its composite ID.
176
+ * @param key - The composite ID of the item.
177
+ * @returns `true` if the item exists, `false` otherwise.
178
+ */
179
+ has(key) {
180
+ const result = this._splitAndLookup(key);
181
+ if (result.isFailure()) {
182
+ return false;
183
+ }
184
+ return result.value.entry.items.has(result.value.itemId);
185
+ }
186
+ /**
187
+ * Iterates over all entries in all collections.
188
+ */
189
+ *entries() {
190
+ for (const collectionEntry of Array.from(this._collections.values())) {
191
+ for (const [itemId, item] of Array.from(collectionEntry.items.entries())) {
192
+ // Validate the composite ID through the converter
193
+ const compositeId = this._compositeIdValidator
194
+ .convert(`${collectionEntry.id}${this._delimiter}${itemId}`)
195
+ .orThrow();
196
+ yield [compositeId, item];
197
+ }
198
+ }
199
+ }
200
+ /**
201
+ * Iterates over all composite keys.
202
+ */
203
+ *keys() {
204
+ for (const [key] of Array.from(this.entries())) {
205
+ yield key;
206
+ }
207
+ }
208
+ /**
209
+ * Iterates over all values.
210
+ */
211
+ *values() {
212
+ for (const collectionEntry of Array.from(this._collections.values())) {
213
+ for (const value of Array.from(collectionEntry.items.values())) {
214
+ yield value;
215
+ }
216
+ }
217
+ }
218
+ /**
219
+ * Calls a callback for each entry.
220
+ * @param cb - The callback to call.
221
+ * @param thisArg - Optional `this` argument for the callback.
222
+ */
223
+ forEach(cb, thisArg) {
224
+ for (const [key, value] of Array.from(this.entries())) {
225
+ cb.call(thisArg, value, key, this);
226
+ }
227
+ }
228
+ /**
229
+ * Gets an iterator over the map entries.
230
+ */
231
+ [Symbol.iterator]() {
232
+ return this.entries();
233
+ }
234
+ // ==================== Mutating methods ====================
235
+ /**
236
+ * Sets an item by its composite ID. Creates the collection if it doesn't exist and is mutable.
237
+ * @param key - The composite ID of the item.
238
+ * @param value - The value to set.
239
+ * @returns `Success` with the value if set, `Failure` otherwise.
240
+ */
241
+ set(key, value) {
242
+ return this._splitCompositeId(key).onSuccess(({ collectionId, itemId }) => {
243
+ return this._getOrCreateMutableCollection(collectionId).onSuccess((collection) => {
244
+ return collection.set(itemId, value);
245
+ });
246
+ });
247
+ }
248
+ /**
249
+ * Adds an item by its composite ID. Fails if the item already exists.
250
+ * @param key - The composite ID of the item.
251
+ * @param value - The value to add.
252
+ * @returns `Success` with the value if added, `Failure` otherwise.
253
+ */
254
+ add(key, value) {
255
+ return this._splitCompositeId(key).onSuccess(({ collectionId, itemId }) => {
256
+ return this._getOrCreateMutableCollection(collectionId).onSuccess((collection) => {
257
+ return collection.add(itemId, value);
258
+ });
259
+ });
260
+ }
261
+ /**
262
+ * Updates an existing item by its composite ID. Fails if the item doesn't exist.
263
+ * @param key - The composite ID of the item.
264
+ * @param value - The new value.
265
+ * @returns `Success` with the value if updated, `Failure` otherwise.
266
+ */
267
+ update(key, value) {
268
+ return this._splitAndLookupMutable(key).onSuccess(({ entry, itemId }) => {
269
+ return entry.items.update(itemId, value);
270
+ });
271
+ }
272
+ /**
273
+ * Deletes an item by its composite ID.
274
+ * @param key - The composite ID of the item.
275
+ * @returns `Success` with the deleted value, `Failure` otherwise.
276
+ */
277
+ delete(key) {
278
+ return this._splitAndLookupMutable(key).onSuccess(({ entry, itemId }) => {
279
+ return entry.items.delete(itemId);
280
+ });
281
+ }
282
+ getOrAdd(key, valueOrFactory) {
283
+ return this._splitCompositeId(key).onSuccess(({ collectionId, itemId }) => {
284
+ return this._getOrCreateMutableCollection(collectionId).onSuccess((collection) => {
285
+ // Use type guard to check if valueOrFactory is a factory function
286
+ if (this._isFactory(valueOrFactory)) {
287
+ return collection.getOrAdd(itemId, () => valueOrFactory(key));
288
+ }
289
+ return collection.getOrAdd(itemId, valueOrFactory);
290
+ });
291
+ });
292
+ }
293
+ /**
294
+ * Clears all items from all mutable collections.
295
+ * Immutable collections are not affected.
296
+ */
297
+ clear() {
298
+ for (const entry of Array.from(this._collections.values())) {
299
+ if (isMutableEntry(entry)) {
300
+ entry.items.clear();
301
+ }
302
+ }
303
+ }
304
+ /**
305
+ * Returns a read-only view of this map.
306
+ */
307
+ toReadOnly() {
308
+ return this;
309
+ }
310
+ // ==================== Convenience methods for split IDs ====================
311
+ /**
312
+ * Composes a collection ID and item ID into a composite ID.
313
+ * @param collectionId - The collection ID.
314
+ * @param itemId - The item ID.
315
+ * @returns `Success` with the composite ID if valid, `Failure` otherwise.
316
+ */
317
+ composeId(collectionId, itemId) {
318
+ const rawId = `${collectionId}${this._delimiter}${itemId}`;
319
+ return this._compositeIdValidator.convert(rawId);
320
+ }
321
+ /**
322
+ * Adds an item using separate collection and item IDs.
323
+ * @param collectionId - The collection ID.
324
+ * @param itemId - The item ID.
325
+ * @param value - The value to add.
326
+ * @returns `Success` with the composite ID if added, `Failure` otherwise.
327
+ */
328
+ addToCollection(collectionId, itemId, value) {
329
+ return this._getOrCreateMutableCollection(collectionId).onSuccess((collection) => {
330
+ return collection.add(itemId, value).onSuccess(() => {
331
+ return this.composeId(collectionId, itemId).withDetail('invalid-key', 'added');
332
+ });
333
+ });
334
+ }
335
+ /**
336
+ * Sets an item using separate collection and item IDs.
337
+ * @param collectionId - The collection ID.
338
+ * @param itemId - The item ID.
339
+ * @param value - The value to set.
340
+ * @returns `Success` with the composite ID if set, `Failure` otherwise.
341
+ */
342
+ setInCollection(collectionId, itemId, value) {
343
+ return this._getOrCreateMutableCollection(collectionId).onSuccess((collection) => {
344
+ return collection.set(itemId, value).onSuccess((_, detail) => {
345
+ return this.composeId(collectionId, itemId).withDetail('invalid-key', detail);
346
+ });
347
+ });
348
+ }
349
+ /**
350
+ * Updates an item using separate collection and item IDs.
351
+ * @param collectionId - The collection ID.
352
+ * @param itemId - The item ID.
353
+ * @param value - The new value.
354
+ * @returns `Success` with the composite ID if updated, `Failure` otherwise.
355
+ */
356
+ updateInCollection(collectionId, itemId, value) {
357
+ return this._getMutableCollection(collectionId).onSuccess((collection) => {
358
+ return collection.update(itemId, value).onSuccess(() => {
359
+ return this.composeId(collectionId, itemId).withDetail('invalid-key', 'updated');
360
+ });
361
+ });
362
+ }
363
+ /**
364
+ * Deletes an item using separate collection and item IDs.
365
+ * @param collectionId - The collection ID.
366
+ * @param itemId - The item ID.
367
+ * @returns `Success` with the composite ID if deleted, `Failure` otherwise.
368
+ */
369
+ deleteFromCollection(collectionId, itemId) {
370
+ return this._getMutableCollection(collectionId).onSuccess((collection) => {
371
+ return collection.delete(itemId).onSuccess(() => {
372
+ return this.composeId(collectionId, itemId).withDetail('invalid-key', 'deleted');
373
+ });
374
+ });
375
+ }
376
+ // ==================== Collection-level methods ====================
377
+ /**
378
+ * Provides read-only access to the underlying collections map.
379
+ * Use `collections.has(id)` and `collections.get(id)` to check existence and retrieve collections.
380
+ */
381
+ get collections() {
382
+ return this._collections;
383
+ }
384
+ /**
385
+ * The number of collections.
386
+ */
387
+ get collectionCount() {
388
+ return this._collections.size;
389
+ }
390
+ /**
391
+ * Gets the metadata for a specific collection.
392
+ * @param collectionId - The collection ID.
393
+ * @returns `Success` with the metadata if found, `Failure` otherwise.
394
+ */
395
+ getCollectionMetadata(collectionId) {
396
+ return this._collections.get(collectionId).asResult.onSuccess((entry) => {
397
+ return succeed(entry.metadata);
398
+ });
399
+ }
400
+ /**
401
+ * Sets the metadata for a mutable collection.
402
+ * @param collectionId - The collection ID.
403
+ * @param metadata - The metadata to set.
404
+ * @returns `Success` if set, `Failure` otherwise.
405
+ */
406
+ setCollectionMetadata(collectionId, metadata) {
407
+ return this._collections.get(collectionId).asResult.onSuccess((entry) => {
408
+ if (!isMutableEntry(entry)) {
409
+ return fail(`Cannot modify metadata of immutable collection '${collectionId}'`);
410
+ }
411
+ entry.metadata = metadata;
412
+ return succeed(metadata);
413
+ });
414
+ }
415
+ /**
416
+ * Adds a new collection from a pre-built entry object.
417
+ * @param entry - The collection entry to add (JSON with items/entries, or pre-instantiated).
418
+ * @returns `Success` with the entry if added, `Failure` otherwise.
419
+ */
420
+ addCollectionEntry(entry) {
421
+ return AggregatedResultMapBase._entryConverter(this._collectionIdConverter, this._childKvConverters, this._metadataConverter)
422
+ .convert(entry)
423
+ .withFailureDetail('invalid-value')
424
+ .onSuccess((c) => this._collections.add(c.id, c));
425
+ }
426
+ /**
427
+ * Adds a new collection with the specified ID and optional initial entries.
428
+ * @param collectionId - The collection ID as a string (will be validated).
429
+ * @param items - Optional initial entries for the collection.
430
+ * @param options - Optional settings (isImmutable defaults to false).
431
+ * @returns `Success` with the validated collection ID if added, `Failure` otherwise.
432
+ */
433
+ addCollectionWithItems(collectionId, items, options) {
434
+ const isMutable = (options === null || options === void 0 ? void 0 : options.isImmutable) !== true;
435
+ const entries = items ? Array.from(items) : [];
436
+ return this._collectionIdConverter
437
+ .convert(collectionId)
438
+ .onSuccess((id) => this.addCollectionEntry({ isMutable, id, entries, metadata: options === null || options === void 0 ? void 0 : options.metadata }).asResult.onSuccess(() => Success.with(id)));
439
+ }
440
+ // ==================== Protected helpers ====================
441
+ /**
442
+ * Deletes a collection from the internal collections map.
443
+ * This is a protected method for use by derived classes that need to implement
444
+ * collection deletion functionality.
445
+ *
446
+ * @param collectionId - The collection ID to delete.
447
+ * @returns `Success` with the deleted entry if found, `Failure` otherwise.
448
+ * @remarks
449
+ * - Does NOT check if the collection is mutable - callers should validate this first.
450
+ * - Use `collections.get(id)` to check if a collection exists and its mutability before calling.
451
+ */
452
+ _deleteCollection(collectionId) {
453
+ // ValidatingResultMap extends ResultMap which has delete method
454
+ return this._collections.delete(collectionId);
455
+ }
456
+ // ==================== Private helpers ====================
457
+ /**
458
+ * Type guard to check if a value is a factory function.
459
+ */
460
+ _isFactory(valueOrFactory) {
461
+ return typeof valueOrFactory === 'function';
462
+ }
463
+ _splitCompositeId(key) {
464
+ return this._compositeIdConverter.convert(key).withDetail('invalid-key', 'success');
465
+ }
466
+ _splitAndLookup(key) {
467
+ return this._splitCompositeId(key).onSuccess(({ collectionId, itemId }) => {
468
+ return this._collections.get(collectionId).onSuccess((entry) => {
469
+ return succeedWithDetail({ entry, itemId }, 'success');
470
+ });
471
+ });
472
+ }
473
+ _splitAndLookupMutable(key) {
474
+ return this._splitAndLookup(key).onSuccess(({ entry, itemId }) => {
475
+ if (!isMutableEntry(entry)) {
476
+ return failWithDetail(`Cannot modify immutable collection '${entry.id}'`, 'failure');
477
+ }
478
+ return succeedWithDetail({ entry, itemId }, 'success');
479
+ });
480
+ }
481
+ /**
482
+ * Gets an existing mutable collection by ID.
483
+ * Returns failure if the collection doesn't exist or is immutable.
484
+ */
485
+ _getMutableCollection(collectionId) {
486
+ return this._collections.get(collectionId).onSuccess((entry) => {
487
+ if (!isMutableEntry(entry)) {
488
+ return failWithDetail(`Cannot modify immutable collection '${collectionId}'`, 'failure');
489
+ }
490
+ return succeedWithDetail(entry.items, 'exists');
491
+ });
492
+ }
493
+ /**
494
+ * Gets an existing mutable collection or creates a new one.
495
+ */
496
+ _getOrCreateMutableCollection(collectionId) {
497
+ const existing = this._collections.get(collectionId);
498
+ if (existing.isSuccess()) {
499
+ if (!isMutableEntry(existing.value)) {
500
+ return failWithDetail(`Cannot modify immutable collection '${collectionId}'`, 'failure');
501
+ }
502
+ return succeedWithDetail(existing.value.items, 'exists');
503
+ }
504
+ // Create a new mutable collection
505
+ const newItems = new ValidatingResultMap({
506
+ converters: this._childKvConverters
507
+ });
508
+ const newEntry = {
509
+ isMutable: true,
510
+ id: collectionId,
511
+ items: newItems
512
+ };
513
+ return this._collections.add(collectionId, newEntry).onSuccess(() => {
514
+ return succeedWithDetail(newItems, 'added');
515
+ });
516
+ }
517
+ // ==================== Static converter factories ====================
518
+ /**
519
+ * Constructs a composite id validator using other converters/validators and separator supplied in params.
520
+ * @param params - Constructor params.
521
+ * @returns The composite ID validator.
522
+ */
523
+ static _compositeIdValidator(params) {
524
+ var _a;
525
+ return Validators.compositeId({
526
+ collectionId: Converters.asValidator(params.collectionIdConverter),
527
+ separator: (_a = params.separator) !== null && _a !== void 0 ? _a : '.',
528
+ itemId: Converters.asValidator(params.itemIdConverter)
529
+ });
530
+ }
531
+ /**
532
+ * Converts metadata from source, failing fast if metadata is present but no converter is configured.
533
+ * @param sourceMetadata - The metadata from the source (may be undefined).
534
+ * @param metadataConverter - Optional converter for metadata.
535
+ * @returns Success with converted metadata (or undefined), or Failure if metadata is present but disallowed.
536
+ */
537
+ static _convertMetadata(sourceMetadata, metadataConverter) {
538
+ if (sourceMetadata === undefined) {
539
+ return succeed(undefined);
540
+ }
541
+ if (!metadataConverter) {
542
+ return fail('metadata is not allowed when no metadataConverter is configured');
543
+ }
544
+ return metadataConverter.convert(sourceMetadata);
545
+ }
546
+ /**
547
+ * Extracts metadata from a source object if present.
548
+ */
549
+ static _extractSourceMetadata(from) {
550
+ return typeof from === 'object' && from !== null && 'metadata' in from ? from.metadata : undefined;
551
+ }
552
+ /**
553
+ * Creates a converter for pre-instantiated mutable entries.
554
+ * These are entries that already have a ValidatingResultMap as their items property.
555
+ */
556
+ static _instantiatedMutableConverter(collectionIdConverter, metadataConverter) {
557
+ const baseConverter = Converters.strictObject({
558
+ isMutable: Converters.literal(true),
559
+ id: collectionIdConverter,
560
+ items: Converters.isA('ValidatingResultMap', (t) => t instanceof ValidatingResultMap)
561
+ });
562
+ return Converters.generic((from) => {
563
+ return baseConverter.convert(from).onSuccess((base) => {
564
+ const sourceMetadata = AggregatedResultMapBase._extractSourceMetadata(from);
565
+ return AggregatedResultMapBase._convertMetadata(sourceMetadata, metadataConverter).onSuccess((metadata) => {
566
+ const entry = {
567
+ isMutable: true,
568
+ id: base.id,
569
+ items: base.items,
570
+ metadata
571
+ };
572
+ return succeed(entry);
573
+ });
574
+ });
575
+ });
576
+ }
577
+ /**
578
+ * Creates a converter for pre-instantiated read-only entries.
579
+ * These are entries that already have an IReadOnlyValidatingResultMap as their items property.
580
+ */
581
+ static _instantiatedReadonlyConverter(collectionIdConverter, metadataConverter) {
582
+ const baseConverter = Converters.strictObject({
583
+ isMutable: Converters.literal(false),
584
+ id: collectionIdConverter,
585
+ items: Converters.isA('IReadOnlyValidatingResultMap', (t) => t instanceof ValidatingResultMap)
586
+ });
587
+ return Converters.generic((from) => {
588
+ return baseConverter.convert(from).onSuccess((base) => {
589
+ const sourceMetadata = AggregatedResultMapBase._extractSourceMetadata(from);
590
+ return AggregatedResultMapBase._convertMetadata(sourceMetadata, metadataConverter).onSuccess((metadata) => {
591
+ const entry = {
592
+ isMutable: false,
593
+ id: base.id,
594
+ items: base.items,
595
+ metadata
596
+ };
597
+ return succeed(entry);
598
+ });
599
+ });
600
+ });
601
+ }
602
+ /**
603
+ * Creates a converter for JSON entries with an entries array.
604
+ * Converts the entries array to a ValidatingResultMap.
605
+ */
606
+ static _jsonWithEntriesConverter(collectionIdConverter, childKvConverters, metadataConverter) {
607
+ const entriesArrayConverter = Converters.isA('entries array', (t) => Array.isArray(t) && t.every((item) => Array.isArray(item) && item.length === 2));
608
+ const baseConverter = Converters.object({
609
+ isMutable: Converters.boolean,
610
+ id: collectionIdConverter,
611
+ entries: entriesArrayConverter
612
+ });
613
+ return Converters.generic((from) => {
614
+ return baseConverter.convert(from).onSuccess((json) => {
615
+ const sourceMetadata = AggregatedResultMapBase._extractSourceMetadata(from);
616
+ return childKvConverters.convertEntries(json.entries).onSuccess((convertedEntries) => {
617
+ const items = new ValidatingResultMap({
618
+ converters: childKvConverters,
619
+ entries: convertedEntries
620
+ });
621
+ return AggregatedResultMapBase._convertMetadata(sourceMetadata, metadataConverter).onSuccess((metadata) => {
622
+ if (json.isMutable) {
623
+ return succeed({
624
+ isMutable: true,
625
+ id: json.id,
626
+ items,
627
+ metadata
628
+ });
629
+ }
630
+ return succeed({
631
+ isMutable: false,
632
+ id: json.id,
633
+ items: items.toReadOnly(),
634
+ metadata
635
+ });
636
+ });
637
+ });
638
+ });
639
+ });
640
+ }
641
+ /**
642
+ * Creates a converter for JSON entries with an items object.
643
+ * Converts the items object to a ValidatingResultMap.
644
+ */
645
+ static _jsonWithItemsConverter(collectionIdConverter, childKvConverters, metadataConverter) {
646
+ const baseConverter = Converters.object({
647
+ isMutable: Converters.boolean,
648
+ id: collectionIdConverter,
649
+ items: Converters.generic((from) => {
650
+ if (typeof from === 'object' && from !== null && !Array.isArray(from)) {
651
+ return succeed(from);
652
+ }
653
+ return fail('expected an object');
654
+ })
655
+ });
656
+ return Converters.generic((from) => {
657
+ return baseConverter.convert(from).onSuccess((json) => {
658
+ const sourceMetadata = AggregatedResultMapBase._extractSourceMetadata(from);
659
+ const entries = Object.entries(json.items);
660
+ return childKvConverters.convertEntries(entries).onSuccess((convertedEntries) => {
661
+ const items = new ValidatingResultMap({
662
+ converters: childKvConverters,
663
+ entries: convertedEntries
664
+ });
665
+ return AggregatedResultMapBase._convertMetadata(sourceMetadata, metadataConverter).onSuccess((metadata) => {
666
+ if (json.isMutable) {
667
+ return succeed({
668
+ isMutable: true,
669
+ id: json.id,
670
+ items,
671
+ metadata
672
+ });
673
+ }
674
+ return succeed({
675
+ isMutable: false,
676
+ id: json.id,
677
+ items: items.toReadOnly(),
678
+ metadata
679
+ });
680
+ });
681
+ });
682
+ });
683
+ });
684
+ }
685
+ /**
686
+ * Creates a unified entry converter that handles all entry formats.
687
+ * Tries each format in order: instantiated mutable, instantiated readonly,
688
+ * JSON with entries array, JSON with items object.
689
+ */
690
+ static _entryConverter(collectionIdConverter, childKvConverters, metadataConverter) {
691
+ return Converters.oneOf([
692
+ AggregatedResultMapBase._instantiatedMutableConverter(collectionIdConverter, metadataConverter),
693
+ AggregatedResultMapBase._instantiatedReadonlyConverter(collectionIdConverter, metadataConverter),
694
+ AggregatedResultMapBase._jsonWithEntriesConverter(collectionIdConverter, childKvConverters, metadataConverter),
695
+ AggregatedResultMapBase._jsonWithItemsConverter(collectionIdConverter, childKvConverters, metadataConverter)
696
+ ]);
697
+ }
698
+ }
699
+ /**
700
+ * An aggregated result map that wraps a collection of {@link ValidatingResultMap | ValidatingResultMap} instances,
701
+ * keyed by collection ID. Items are accessed via composite IDs that combine the collection ID
702
+ * and item ID with a delimiter.
703
+ * @public
704
+ */
705
+ export class AggregatedResultMap extends AggregatedResultMapBase {
706
+ /**
707
+ * Constructs a new {@link AggregatedResultMap | AggregatedResultMap}.
708
+ * @param params -
709
+ */
710
+ constructor(params) {
711
+ super(params);
712
+ }
713
+ /**
714
+ * Creates a new {@link AggregatedResultMap | AggregatedResultMap}.
715
+ * @param params - Parameters for constructing the map.
716
+ * @returns `Success` with the new map if successful, `Failure` otherwise.
717
+ */
718
+ static create(params) {
719
+ return captureResult(() => new AggregatedResultMap(params));
720
+ }
721
+ }
722
+ //# sourceMappingURL=aggregatedResultMap.js.map