@zelgadis87/utils-core 5.4.3 → 5.4.5

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/.rollup/index.cjs CHANGED
@@ -1276,6 +1276,7 @@ function wrapWithString(str, start, end = start) {
1276
1276
  }
1277
1277
 
1278
1278
  /**
1279
+
1279
1280
  * An error class that aggregates multiple errors from a combined operation.
1280
1281
  * Used by `Operation.combine` to represent failures from multiple operations.
1281
1282
  *
@@ -1295,6 +1296,31 @@ class OperationAggregateError extends Error {
1295
1296
  return this.errors.length;
1296
1297
  }
1297
1298
  }
1299
+ /**
1300
+ * Combines multiple operation results into a single result.
1301
+ * - If all operations succeed, returns success with an array of all data values.
1302
+ * - If any operation fails, returns failure with an `OperationAggregateError` containing all errors.
1303
+ *
1304
+ * When combining operations whose error type is already `OperationAggregateError<E>`,
1305
+ * the errors are automatically flattened to avoid nesting (e.g., combining results from
1306
+ * previous `combine` calls).
1307
+ *
1308
+ * @template T - The success data type of individual operations
1309
+ * @template E - The error type of individual operations (defaults to Error)
1310
+ * @param results - Array of operation results to combine
1311
+ * @returns A single operation with either all data or all aggregated errors (flattened)
1312
+ */
1313
+ function combine$1(results) {
1314
+ const [successes, failures] = partition(results, r => r.success);
1315
+ if (failures.length === 0) {
1316
+ return { success: true, data: successes.map(r => r.data) };
1317
+ }
1318
+ const allErrors = failures.flatMap(r => {
1319
+ const error = r.error;
1320
+ return error instanceof OperationAggregateError ? error.errors : [error];
1321
+ });
1322
+ return { success: false, error: new OperationAggregateError(allErrors) };
1323
+ }
1298
1324
  const Operation = {
1299
1325
  ok: (data) => {
1300
1326
  return { success: true, data };
@@ -1336,19 +1362,31 @@ const Operation = {
1336
1362
  return partition(results, r => r.success);
1337
1363
  },
1338
1364
  /**
1339
- * Combines multiple operation results into a single result.
1340
- * - If all operations succeed, returns success with an array of all data values.
1341
- * - If any operation fails, returns failure with an `OperationAggregateError` containing all errors.
1365
+ * Transforms the success value of an operation using the provided function.
1366
+ * If the operation is a failure, it is returned unchanged (same reference).
1367
+ * The transformation function is never called on a failure.
1342
1368
  *
1343
- * @template T - The success data type of individual operations
1344
- * @template E - The error type of individual operations (defaults to Error)
1345
- * @param results - Array of operation results to combine
1346
- * @returns A single operation with either all data or all aggregated errors
1369
+ * @template T - The input success data type
1370
+ * @template U - The output success data type
1371
+ * @template E - The error type (preserved exactly)
1372
+ * @param op - The operation result to transform
1373
+ * @param fn - Pure function applied to the success value
1374
+ * @returns A new success with the transformed value, or the original failure
1375
+ *
1376
+ * @example
1377
+ * Operation.map(Operation.ok(1), x => x * 2);
1378
+ * // => { success: true, data: 2 }
1379
+ *
1380
+ * Operation.map(Operation.ko('err'), x => x * 2);
1381
+ * // => { success: false, error: 'err' }
1347
1382
  */
1348
- combine: (results) => {
1349
- const [successes, failures] = Operation.partition(results);
1350
- return failures.length === 0 ? Operation.ok(successes.map(r => r.data)) : Operation.ko(new OperationAggregateError(failures.map(r => r.error)));
1383
+ map: (op, fn) => {
1384
+ if (op.success) {
1385
+ return { success: true, data: fn(op.data) };
1386
+ }
1387
+ return op;
1351
1388
  },
1389
+ combine: combine$1,
1352
1390
  };
1353
1391
 
1354
1392
  function asPromise(promisable) {
@@ -3678,11 +3716,70 @@ function printTransitions(transitions) {
3678
3716
  }
3679
3717
 
3680
3718
  const VERSION_FIELD = "$version";
3719
+ /**
3720
+ * Versioned data migration system for transparently upgrading serialized data to the latest version.
3721
+ *
3722
+ * DataUpgrader manages the migration of data structures across multiple versions, automatically
3723
+ * finding the shortest upgrade path and applying transformations. This is particularly useful for
3724
+ * handling persisted data that may be in any historical format.
3725
+ *
3726
+ * **Key Use Case:** Reading serialized data of unknown version and migrating it transparently.
3727
+ *
3728
+ * @example
3729
+ * ```typescript
3730
+ * // Step 1: Define all historical versions with $version discriminator
3731
+ * type TSerializedData_V1 = { name: string; $version: 1 };
3732
+ * type TSerializedData_V2 = { name: string; age: number; $version: 2 };
3733
+ * type TSerializedData_V3 = { name: string; age: number; email: string; $version: 3 };
3734
+ *
3735
+ * // Step 2: Create union type and current version type
3736
+ * type TSerializedData_Vstar = TSerializedData_V1 | TSerializedData_V2 | TSerializedData_V3;
3737
+ * type TSerializedData = TSerializedData_V3;
3738
+ *
3739
+ * // Step 3: Create upgrader with transition functions
3740
+ * const upgrader = DataUpgrader.create<TSerializedData_Vstar, TSerializedData>(3)
3741
+ * .addTransition(1, 2, async (data: TSerializedData_V1) => ({
3742
+ * ...data,
3743
+ * age: 0,
3744
+ * $version: 2
3745
+ * }))
3746
+ * .addTransition(2, 3, async (data: TSerializedData_V2) => ({
3747
+ * ...data,
3748
+ * email: "",
3749
+ * $version: 3
3750
+ * }))
3751
+ * .addTransition(1, 3, async (data: TSerializedData_V1) => ({
3752
+ * // Optional: Direct path for efficiency
3753
+ * ...data,
3754
+ * age: 0,
3755
+ * email: "",
3756
+ * $version: 3
3757
+ * }));
3758
+ *
3759
+ * // Step 4: Use in data loading - handles any version transparently
3760
+ * async function loadData(json: string): Promise<TSerializedData> {
3761
+ * const data = parseJson<TSerializedData_Vstar>(json);
3762
+ * return await upgrader.upgrade(data);
3763
+ * }
3764
+ *
3765
+ * // Works with any version:
3766
+ * const v1 = await loadData('{"name":"Alice","$version":1}'); // → V3
3767
+ * const v2 = await loadData('{"name":"Bob","age":25,"$version":2}'); // → V3
3768
+ * const v3 = await loadData('{"name":"Carol","age":30,"email":"c@example.com","$version":3}'); // → V3
3769
+ * ```
3770
+ *
3771
+ * @typeParam X - Union type of all possible data versions (must extend TUpgradable)
3772
+ * @typeParam XLatest - The latest version type (must extend X)
3773
+ *
3774
+ * @see {@link isUpgradable | Type guard for checking if data is upgradable}
3775
+ * @see {@link DataUpgrader.Errors | Error types for upgrade failures}
3776
+ */
3681
3777
  class DataUpgrader {
3682
3778
  latestVersion;
3683
- transitions = {};
3684
- constructor(latestVersion) {
3779
+ transitions;
3780
+ constructor(latestVersion, transitions = {}) {
3685
3781
  this.latestVersion = latestVersion;
3782
+ this.transitions = transitions;
3686
3783
  }
3687
3784
  addTransition(from, to, apply) {
3688
3785
  if (from === undefined || from < 0)
@@ -3729,6 +3826,17 @@ class DataUpgrader {
3729
3826
  static create(latest) {
3730
3827
  return new DataUpgrader(latest);
3731
3828
  }
3829
+ /**
3830
+ * Creates a DataUpgrader from a DataUpgraderBuilder instance.
3831
+ * @internal
3832
+ */
3833
+ static fromBuilder(builder, latestVersion) {
3834
+ // Extract transitions from builder and convert to strongly-typed TTransitionMatrix
3835
+ // This type assertion is safe because the builder has validated all transitions
3836
+ // at compile-time via its type parameters
3837
+ const transitions = builder.getTransitions();
3838
+ return new DataUpgrader(latestVersion, transitions);
3839
+ }
3732
3840
  static Errors = {
3733
3841
  EmptyUpgrade: EmptyUpgradeError,
3734
3842
  UnavailableUpgrade: UnavailableUpgradeError
@@ -3737,9 +3845,145 @@ class DataUpgrader {
3737
3845
  function isUpgradable(obj) {
3738
3846
  return isDefined(obj) && typeof obj === "object" && VERSION_FIELD in obj && isNumber(obj.$version) && isPositiveNumber(obj.$version);
3739
3847
  }
3848
+ /**
3849
+ * The current system is unable to catch the following problems at compile-time:
3850
+ * type A_V1 = { a: number, $version: 1 }
3851
+ * type A_V2 = { a: number, b: number, $version: 2 }
3852
+ * type A_V3 = { a: number, b: number, c: string, $version: 3 }
3853
+ * type A_Vstar = A_V1 | A_V2 | A_V3;
3854
+ * type A = A_V3;
3855
+ *
3856
+ * const DU1 = DataUpgrader.create<A_Vstar, A>( 3 );
3857
+ *
3858
+ * const DU2 = DataUpgrader.create<A_Vstar, A>( 3 )
3859
+ * .addTransition( 1, 3, x => ( { ...x, b: 2, c: "foo", $version: 3 } ) )
3860
+ * .addTransition( 1, 3, x => ( { ...x, b: 3, c: "bar", $version: 3 } ) )
3861
+ * ;
3862
+ *
3863
+ * const DU3 = DataUpgrader.create<A_Vstar, A>( 3 )
3864
+ * .addTransition( 1, 2, x => ( { ...x, b: 2, $version: 2 } ) )
3865
+ * .addTransition( 2, 1, x => ( { ...x, $version: 1 } ) )
3866
+ * ;
3867
+ */
3868
+
3869
+ /**
3870
+ * Builder for creating type-safe DataUpgrader instances with compile-time safety.
3871
+ *
3872
+ * Prevents at compile-time:
3873
+ * - Missing upgrade paths (incomplete version coverage)
3874
+ * - Duplicate transition definitions
3875
+ * - Backward transitions (e.g., version 2 → 1)
3876
+ *
3877
+ * @example
3878
+ * ```typescript
3879
+ * // Step 1: Define all historical versions with $version discriminator
3880
+ * type V1 = { name: string; $version: 1 };
3881
+ * type V2 = { name: string; age: number; $version: 2 };
3882
+ * type V3 = { name: string; age: number; email: string; $version: 3 };
3883
+ *
3884
+ * // Step 2: Build the upgrader with type-safe transitions
3885
+ * const upgrader = DataUpgraderBuilder.start<V1>()
3886
+ * .addTransition<V1, V2>(1, 2, async (data) => ({
3887
+ * ...data,
3888
+ * age: 0,
3889
+ * $version: 2
3890
+ * }))
3891
+ * .addTransition<V2, V3>(2, 3, async (data) => ({
3892
+ * ...data,
3893
+ * email: "",
3894
+ * $version: 3
3895
+ * }))
3896
+ * .addShortcut<V1, V3>(1, 3, async (data) => ({
3897
+ * // Optional: Direct path for efficiency
3898
+ * ...data,
3899
+ * age: 0,
3900
+ * email: "",
3901
+ * $version: 3
3902
+ * }))
3903
+ * .build<V3>(3);
3904
+ *
3905
+ * // Step 3: Use the upgrader
3906
+ * async function loadData(json: string): Promise<V3> {
3907
+ * const data = JSON.parse(json);
3908
+ * return await upgrader.upgrade(data);
3909
+ * }
3910
+ *
3911
+ * // Works with any version:
3912
+ * const v1 = await loadData('{"name":"Alice","$version":1}'); // → V3
3913
+ * const v2 = await loadData('{"name":"Bob","age":25,"$version":2}'); // → V3
3914
+ * const v3 = await loadData('{"name":"Carol","age":30,"email":"c@example.com","$version":3}'); // → V3
3915
+ * ```
3916
+ */
3917
+ class DataUpgraderBuilder {
3918
+ transitions;
3919
+ constructor(transitions = {}) {
3920
+ this.transitions = transitions;
3921
+ }
3922
+ /** Starts building a new DataUpgrader from the lowest version. */
3923
+ static start() {
3924
+ return new DataUpgraderBuilder();
3925
+ }
3926
+ /**
3927
+ * Adds a sequential transition from the current version to the next version.
3928
+ * Prevents backward transitions and duplicates at compile-time.
3929
+ *
3930
+ * @example
3931
+ * ```typescript
3932
+ * builder.addTransition<V1, V2>(1, 2, async (d) => ({ ...d, extra: 0, $version: 2 }))
3933
+ * ```
3934
+ */
3935
+ addTransition(fromVersion, toVersion, apply) {
3936
+ const newTransitions = {
3937
+ ...this.transitions,
3938
+ [toVersion]: {
3939
+ ...(this.transitions[toVersion] ?? {}),
3940
+ [fromVersion]: { from: fromVersion, to: toVersion, apply: async (d) => apply(d) }
3941
+ }
3942
+ };
3943
+ const builder = new DataUpgraderBuilder(newTransitions);
3944
+ return builder;
3945
+ }
3946
+ /**
3947
+ * Adds a shortcut transition between arbitrary versions.
3948
+ * Both versions must already be in the accumulated union type.
3949
+ *
3950
+ * @example
3951
+ * ```typescript
3952
+ * builder.addShortcut<V1, V3>(1, 3, async (d) => ({ ...d, extra: 0, flag: false, $version: 3 }))
3953
+ * ```
3954
+ */
3955
+ addShortcut(fromVersion, toVersion, apply) {
3956
+ const newTransitions = {
3957
+ ...this.transitions,
3958
+ [toVersion]: {
3959
+ ...(this.transitions[toVersion] ?? {}),
3960
+ [fromVersion]: { from: fromVersion, to: toVersion, apply: async (d) => apply(d) }
3961
+ }
3962
+ };
3963
+ const builder = new DataUpgraderBuilder(newTransitions);
3964
+ return builder;
3965
+ }
3966
+ /**
3967
+ * Builds the DataUpgrader with the specified latest version.
3968
+ * The latest version must be in the accumulated union type.
3969
+ *
3970
+ * @example
3971
+ * ```typescript
3972
+ * const upgrader = builder.build<V3>(3);
3973
+ * ```
3974
+ */
3975
+ build(latestVersion) {
3976
+ return DataUpgrader.fromBuilder(this, latestVersion);
3977
+ }
3978
+ /** @internal */
3979
+ getTransitions() {
3980
+ return this.transitions;
3981
+ }
3982
+ }
3740
3983
 
3741
3984
  exports.Cached = Cached;
3742
3985
  exports.DataUpgrader = DataUpgrader;
3986
+ exports.DataUpgraderBuilder = DataUpgraderBuilder;
3743
3987
  exports.Deferred = Deferred;
3744
3988
  exports.DeferredCanceledError = DeferredCanceledError;
3745
3989
  exports.ErrorCannotInstantiatePresentOptionalWithEmptyValue = ErrorCannotInstantiatePresentOptionalWithEmptyValue;