@twin.org/entity-storage-models 0.0.3-next.21 → 0.0.3-next.22

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.
@@ -0,0 +1,17 @@
1
+ // Copyright 2026 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { Factory } from "@twin.org/core";
4
+ /**
5
+ * Factory for optional per-step migration overrides.
6
+ *
7
+ * Only register an entry when a version step requires property renames or a custom
8
+ * transform hook. For purely structural changes (add/remove/type-change) the
9
+ * SchemaVersionService diffs the two versioned schema classes automatically
10
+ * without needing any factory entry.
11
+ *
12
+ * Keys follow the convention "<BaseSchemaName>_<fromVersion>_<toVersion>",
13
+ * for example "MyEntity_0_1" for the step that migrates MyEntity from version 0 to 1.
14
+ */
15
+ // eslint-disable-next-line @typescript-eslint/naming-convention
16
+ export const SchemaMigrationFactory = Factory.createFactory("schema-migration");
17
+ //# sourceMappingURL=schemaMigrationFactory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemaMigrationFactory.js","sourceRoot":"","sources":["../../../src/factories/schemaMigrationFactory.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAGzC;;;;;;;;;;GAUG;AACH,gEAAgE;AAChE,MAAM,CAAC,MAAM,sBAAsB,GAAG,OAAO,CAAC,aAAa,CAAmB,kBAAkB,CAAC,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { Factory } from \"@twin.org/core\";\nimport type { ISchemaMigration } from \"../models/ISchemaMigration.js\";\n\n/**\n * Factory for optional per-step migration overrides.\n *\n * Only register an entry when a version step requires property renames or a custom\n * transform hook. For purely structural changes (add/remove/type-change) the\n * SchemaVersionService diffs the two versioned schema classes automatically\n * without needing any factory entry.\n *\n * Keys follow the convention \"<BaseSchemaName>_<fromVersion>_<toVersion>\",\n * for example \"MyEntity_0_1\" for the step that migrates MyEntity from version 0 to 1.\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport const SchemaMigrationFactory = Factory.createFactory<ISchemaMigration>(\"schema-migration\");\n"]}
@@ -4,7 +4,9 @@ import { ContextIdStore } from "@twin.org/context";
4
4
  import { Coerce, GeneralError, Is, ObjectHelper } from "@twin.org/core";
5
5
  import { EntitySchemaDiffHelper, EntitySchemaPropertyType } from "@twin.org/entity";
6
6
  /**
7
- * Helper class for performing schema migrations between two connectors.
7
+ * Helper class for performing entity schema migrations between two connectors.
8
+ * The chain-based API (migrateWithChain / applyEntityChain) is the single migration
9
+ * path: a chain of one step covers the same case as a traditional single-step migration.
8
10
  */
9
11
  export class MigrationHelper {
10
12
  /**
@@ -12,126 +14,16 @@ export class MigrationHelper {
12
14
  */
13
15
  static CLASS_NAME = "MigrationHelper";
14
16
  /**
15
- * Performs a migration between two connectors, using the provided options and schema diff to control the migration behaviour.
16
- * @param sourceConnector The connector to migrate from to allow the migration helper to create the new connector and finalize the migration.
17
- * @param targetEntitySchemaName The name of the new entity schema.
18
- * @param renames An optional list of property renames to apply during migration.
19
- * @param options Options controlling migration behaviour.
20
- * @param loggingComponentType An optional logging component type to use for bootstrapping and starting connectors if necessary.
21
- * @returns The connector for the new schema and the number of entities successfully migrated, the sourceConnector will no longer be usable, finalConnector will be undefined if no migration was necessary.
22
- */
23
- static async migrate(sourceConnector, targetEntitySchemaName, renames, options, loggingComponentType) {
24
- let targetConnector;
25
- try {
26
- // We use the migration method to create the new connector as it will use a temporary storage location if necessary
27
- targetConnector = await sourceConnector.createTargetConnector(targetEntitySchemaName);
28
- // Startup both connectors to ensure they are ready for the migration, this will call bootstrap and start if they are defined.
29
- await MigrationHelper.startupConnector(sourceConnector, loggingComponentType);
30
- await MigrationHelper.startupConnector(targetConnector, loggingComponentType);
31
- // Get the unique partition context ids to run the migration for each partition.
32
- let partitionContextIds = await sourceConnector.getPartitionContextIds();
33
- // If there are no partitions, we still want to run the migration once to handle the schema changes,
34
- // so we create a single empty context for the migration to run in.
35
- if (!Is.arrayValue(partitionContextIds)) {
36
- partitionContextIds ??= [];
37
- partitionContextIds.push({});
38
- }
39
- // Get the schemas
40
- const sourceSchema = sourceConnector.getSchema();
41
- const targetSchema = targetConnector.getSchema();
42
- // Get the schema diff between the source and target schemas.
43
- const schemaDiff = EntitySchemaDiffHelper.diff(sourceSchema.properties ?? [], targetSchema.properties ?? [], renames);
44
- // Only perform a migration if the schemas have changed
45
- if (EntitySchemaDiffHelper.hasChanges(schemaDiff)) {
46
- // Perform the migration, which will read from the current store and write to the target connector's store.
47
- const migrated = await MigrationHelper.migrateEntities(sourceConnector, targetConnector, partitionContextIds, schemaDiff, options);
48
- // The migration is now complete, finalize the migration which could entail
49
- // renaming of resources etc, determined by the implementation of finalizeMigration.
50
- // it also needs to stop and teardown any resources no longer in use after the migration,
51
- // such as the old connector and its underlying storage.
52
- const finalConnector = await sourceConnector.finalizeMigration(targetConnector, options, loggingComponentType);
53
- return {
54
- finalConnector,
55
- migrated
56
- };
57
- }
58
- return {
59
- finalConnector: undefined,
60
- migrated: 0
61
- };
62
- }
63
- catch (error) {
64
- await sourceConnector.cleanupMigration(targetConnector, options, loggingComponentType);
65
- throw new GeneralError(MigrationHelper.CLASS_NAME, "migrationFailed", undefined, error);
66
- }
67
- }
68
- /**
69
- * Generic per-partition migration loop.
70
- * @param source Connector to read from (current schema, already bootstrapped).
71
- * @param target Connector to write to (new schema, already bootstrapped).
72
- * @param partitionContextIds The context ids to use for the migration, used for partitioning and can be used in the transform function when `options.transformEntityProperty` is provided.
73
- * @param schemaDiff The schema diff.
74
- * @param options Optional migration controls (batchSize, transformEntity, onProgress).
75
- * @returns The number of entities successfully migrated.
76
- */
77
- static async migrateEntities(source, target, partitionContextIds, schemaDiff, options) {
78
- let migrated = 0;
79
- await options?.onStepProgress?.("migrationStart", 0, 0);
80
- const effectivePartitions = partitionContextIds.length > 0 ? partitionContextIds : [{}];
81
- for (let i = 0; i < effectivePartitions.length; i++) {
82
- await ContextIdStore.run(effectivePartitions[i], async () => {
83
- await options?.onStepProgress?.("partitionStart", effectivePartitions.length, i);
84
- migrated += await MigrationHelper.migratePartition(source, target, effectivePartitions.length, i, schemaDiff, options);
85
- await options?.onStepProgress?.("partitionEnd", effectivePartitions.length, i);
86
- });
87
- }
88
- await options?.onStepProgress?.("migrationEnd", 0, 0);
89
- return migrated;
90
- }
91
- /**
92
- * Generic per-partition migration loop.
93
- * @param source Connector to read from (current schema, already bootstrapped).
94
- * @param target Connector to write to (new schema, already bootstrapped).
95
- * @param partitionTotal The total number of partitions to migrate, used for progress reporting.
96
- * @param partitionIndex The index of the current partition being migrated, used for progress reporting.
97
- * @param schemaDiff Schema diff used to add nullable defaults and drop removed fields when `options.transformEntity` is not provided.
98
- * @param options Optional migration controls (batchSize, transformEntity, onProgress).
99
- * @returns The number of entities successfully migrated.
100
- */
101
- static async migratePartition(source, target, partitionTotal, partitionIndex, schemaDiff, options) {
102
- const batchSize = options?.batchSize ?? 100;
103
- let migrated = 0;
104
- let cursor;
105
- const totalEntities = await source.count();
106
- await options?.onPartitionProgress?.(totalEntities, 0);
107
- if (totalEntities > 0) {
108
- do {
109
- const page = await source.query(undefined, undefined, undefined, cursor, batchSize);
110
- cursor = page.cursor;
111
- if (Is.arrayValue(page.entities)) {
112
- const transformedBatch = [];
113
- for (const entity of page.entities) {
114
- const transformedEntity = MigrationHelper.applyEntityTransform(entity, schemaDiff, options);
115
- transformedBatch.push(transformedEntity);
116
- }
117
- await target.setBatch(transformedBatch);
118
- migrated += transformedBatch.length;
119
- }
120
- await options?.onPartitionProgress?.(totalEntities, migrated);
121
- } while (Is.stringValue(cursor));
122
- }
123
- return migrated;
124
- }
125
- /**
126
- * Applies the entity transformation for migration, using the provided options and schema diff.
17
+ * Applies the entity transformation for a single diff, handling added, removed, and
18
+ * modified properties according to the provided schema diff and optional transform hook.
127
19
  * @param entity The entity to transform.
128
20
  * @param schemaDiff The schema diff between the old and new schemas.
129
- * @param options The migration options.
21
+ * @param transformEntityProperty Optional per-property transform hook for object/array properties.
130
22
  * @returns The transformed entity ready to be written to the new schema.
131
- * @throws GeneralError if a transformation is required for an object or array property but no `options.transformEntityProperty` function is provided.
23
+ * @throws GeneralError if a transformation is required for an object or array property but no transformEntityProperty function is provided.
132
24
  * @throws GeneralError if coercion of a modified property results in undefined for a non-optional target property.
133
25
  */
134
- static applyEntityTransform(entity, schemaDiff, options) {
26
+ static applyEntityTransform(entity, schemaDiff, transformEntityProperty) {
135
27
  const newEntity = {};
136
28
  for (const property of schemaDiff.unchanged) {
137
29
  ObjectHelper.propertySet(newEntity, property.property, ObjectHelper.propertyGet(entity, property.property));
@@ -140,20 +32,20 @@ export class MigrationHelper {
140
32
  let defValue;
141
33
  if (!(change.optional ?? false)) {
142
34
  if (change.type === EntitySchemaPropertyType.Boolean) {
143
- defValue = false;
35
+ defValue = change.defaultValue ?? false;
144
36
  }
145
37
  else if (change.type === EntitySchemaPropertyType.Number ||
146
38
  change.type === EntitySchemaPropertyType.Integer) {
147
- defValue = 0;
39
+ defValue = change.defaultValue ?? 0;
148
40
  }
149
41
  else if (change.type === EntitySchemaPropertyType.String) {
150
- defValue = "";
42
+ defValue = change.defaultValue ?? "";
151
43
  }
152
44
  else if (change.type === EntitySchemaPropertyType.Array) {
153
- defValue = [];
45
+ defValue = change.defaultValue ?? [];
154
46
  }
155
47
  else if (change.type === EntitySchemaPropertyType.Object) {
156
- defValue = {};
48
+ defValue = change.defaultValue ?? {};
157
49
  }
158
50
  }
159
51
  if (Is.notEmpty(defValue)) {
@@ -175,14 +67,14 @@ export class MigrationHelper {
175
67
  }
176
68
  else if (change.to.type === EntitySchemaPropertyType.Array ||
177
69
  change.to.type === EntitySchemaPropertyType.Object) {
178
- if (!Is.function(options?.transformEntityProperty)) {
70
+ if (!Is.function(transformEntityProperty)) {
179
71
  throw new GeneralError(MigrationHelper.CLASS_NAME, "transformRequiredForProperty", {
180
72
  from: change.from.property,
181
73
  to: change.to.property,
182
74
  type: change.from.type
183
75
  });
184
76
  }
185
- newValue = options.transformEntityProperty(change.from, change.to, currentValue);
77
+ newValue = transformEntityProperty(change.from, change.to, currentValue);
186
78
  }
187
79
  if (newValue === undefined && !(change.to.optional ?? false)) {
188
80
  throw new GeneralError(MigrationHelper.CLASS_NAME, "coercionProducedUndefined", {
@@ -195,15 +87,96 @@ export class MigrationHelper {
195
87
  }
196
88
  }
197
89
  // Removed properties are simply dropped.
198
- // for (const change of schemaDiff.removed) {
199
- // }
200
90
  return newEntity;
201
91
  }
202
92
  /**
203
- * Startup the connector by calling bootstrap and start if they are defined.
204
- * @param connector The connector to startup.
205
- * @param loggingComponentType An optional logging component type to use for bootstrapping and starting the connector.
206
- * @returns Nothing.
93
+ * Transforms a single entity through an ordered chain of fully-resolved migration steps.
94
+ * For each step the method diffs fromProperties against toProperties, then applies
95
+ * applyEntityTransform. Each step's output feeds the next step's input so that
96
+ * per-step transformEntityProperty hooks are honoured throughout the chain.
97
+ * @param entity The entity to transform (at the shape described by steps[0].fromProperties).
98
+ * @param steps Ordered, fully-resolved migration steps from stored version to current version.
99
+ * Each step's fromProperties and toProperties are resolved by the caller before invocation.
100
+ * @returns The entity transformed to the shape described by steps[last].toProperties.
101
+ */
102
+ static applyEntityChain(entity, steps) {
103
+ let current = entity;
104
+ for (const step of steps) {
105
+ const diff = EntitySchemaDiffHelper.diff(step.fromProperties, step.toProperties, step.renames);
106
+ current = MigrationHelper.applyEntityTransform(current, diff, step.transformEntityProperty);
107
+ }
108
+ return current;
109
+ }
110
+ /**
111
+ * Performs a chain migration in a single connector swap, regardless of how many version
112
+ * steps the chain spans. Creates one target connector, reads all source entities, applies
113
+ * applyEntityChain to each, writes them to the target, then finalizes the migration.
114
+ * A chain of one step is equivalent to a traditional single-step migration.
115
+ * @param sourceConnector The connector holding data at the stored schema version.
116
+ * @param targetSchemaName The schema name for the current version (used to create the target connector).
117
+ * @param steps Ordered, fully-resolved migration steps from stored to current version.
118
+ * @param loggingComponentType An optional logging component type for connector startup.
119
+ * @param batchSize Number of entities to read and write per batch. Defaults to 100.
120
+ * @returns The finalized connector and the count of migrated entities.
121
+ */
122
+ static async migrateWithChain(sourceConnector, targetSchemaName, steps, loggingComponentType, batchSize = 100) {
123
+ let targetConnector;
124
+ try {
125
+ targetConnector = await sourceConnector.createTargetConnector(targetSchemaName);
126
+ await MigrationHelper.startupConnector(sourceConnector, loggingComponentType);
127
+ await MigrationHelper.startupConnector(targetConnector, loggingComponentType);
128
+ let partitionContextIds = await sourceConnector.getPartitionContextIds();
129
+ if (!Is.arrayValue(partitionContextIds)) {
130
+ partitionContextIds ??= [];
131
+ partitionContextIds.push({});
132
+ }
133
+ let migrated = 0;
134
+ const effectivePartitions = partitionContextIds.length > 0 ? partitionContextIds : [{}];
135
+ const resolvedTarget = targetConnector;
136
+ for (const contextIds of effectivePartitions) {
137
+ await ContextIdStore.run(contextIds, async () => {
138
+ migrated += await MigrationHelper.migratePartitionWithChain(sourceConnector, resolvedTarget, steps, batchSize);
139
+ });
140
+ }
141
+ const finalConnector = await sourceConnector.finalizeMigration(targetConnector, undefined, loggingComponentType);
142
+ return { finalConnector, migrated };
143
+ }
144
+ catch (error) {
145
+ await sourceConnector.cleanupMigration(targetConnector, undefined, loggingComponentType);
146
+ throw new GeneralError(MigrationHelper.CLASS_NAME, "migrationFailed", undefined, error);
147
+ }
148
+ }
149
+ /**
150
+ * Reads all entities from one partition of the source connector, applies the migration
151
+ * chain to each entity, and writes the results to the target connector.
152
+ * @param source The connector to read from (already bootstrapped).
153
+ * @param target The connector to write to (already bootstrapped).
154
+ * @param steps Ordered, fully-resolved migration steps.
155
+ * @param batchSize Number of entities to read and write per batch.
156
+ * @returns The number of entities migrated.
157
+ * @internal
158
+ */
159
+ static async migratePartitionWithChain(source, target, steps, batchSize) {
160
+ let migrated = 0;
161
+ let cursor;
162
+ const totalEntities = await source.count();
163
+ if (totalEntities > 0) {
164
+ do {
165
+ const page = await source.query(undefined, undefined, undefined, cursor, batchSize);
166
+ cursor = page.cursor;
167
+ if (Is.arrayValue(page.entities)) {
168
+ const transformedBatch = page.entities.map(entity => MigrationHelper.applyEntityChain(entity, steps));
169
+ await target.setBatch(transformedBatch);
170
+ migrated += transformedBatch.length;
171
+ }
172
+ } while (Is.stringValue(cursor));
173
+ }
174
+ return migrated;
175
+ }
176
+ /**
177
+ * Starts the connector by calling bootstrap and start if they are defined.
178
+ * @param connector The connector to start.
179
+ * @param loggingComponentType An optional logging component type.
207
180
  * @internal
208
181
  */
209
182
  static async startupConnector(connector, loggingComponentType) {
@@ -1 +1 @@
1
- {"version":3,"file":"migrationHelper.js","sourceRoot":"","sources":["../../../src/helpers/migrationHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,cAAc,EAAoB,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACN,sBAAsB,EACtB,wBAAwB,EAExB,MAAM,kBAAkB,CAAC;AAM1B;;GAEG;AACH,MAAM,OAAO,eAAe;IAC3B;;OAEG;IACI,MAAM,CAAU,UAAU,qBAAqC;IAEtE;;;;;;;;OAQG;IACI,MAAM,CAAC,KAAK,CAAC,OAAO,CAC1B,eAAoD,EACpD,sBAA8B,EAC9B,OAAwC,EACxC,OAAiC,EACjC,oBAA6B;QAK7B,IAAI,eAAuD,CAAC;QAC5D,IAAI,CAAC;YACJ,mHAAmH;YACnH,eAAe,GAAG,MAAM,eAAe,CAAC,qBAAqB,CAAI,sBAAsB,CAAC,CAAC;YAEzF,8HAA8H;YAC9H,MAAM,eAAe,CAAC,gBAAgB,CAAI,eAAe,EAAE,oBAAoB,CAAC,CAAC;YACjF,MAAM,eAAe,CAAC,gBAAgB,CAAI,eAAe,EAAE,oBAAoB,CAAC,CAAC;YAEjF,gFAAgF;YAChF,IAAI,mBAAmB,GAAG,MAAM,eAAe,CAAC,sBAAsB,EAAE,CAAC;YAEzE,oGAAoG;YACpG,mEAAmE;YACnE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,mBAAmB,CAAC,EAAE,CAAC;gBACzC,mBAAmB,KAAK,EAAE,CAAC;gBAC3B,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC9B,CAAC;YAED,kBAAkB;YAClB,MAAM,YAAY,GAAG,eAAe,CAAC,SAAS,EAAE,CAAC;YACjD,MAAM,YAAY,GAAG,eAAe,CAAC,SAAS,EAAE,CAAC;YAEjD,6DAA6D;YAC7D,MAAM,UAAU,GAAG,sBAAsB,CAAC,IAAI,CAC7C,YAAY,CAAC,UAAU,IAAI,EAAE,EAC7B,YAAY,CAAC,UAAU,IAAI,EAAE,EAC7B,OAAO,CACP,CAAC;YAEF,uDAAuD;YACvD,IAAI,sBAAsB,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;gBACnD,2GAA2G;gBAC3G,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,eAAe,CACrD,eAAe,EACf,eAAe,EACf,mBAAmB,EACnB,UAAU,EACV,OAAO,CACP,CAAC;gBAEF,2EAA2E;gBAC3E,oFAAoF;gBACpF,yFAAyF;gBACzF,wDAAwD;gBACxD,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC,iBAAiB,CAC7D,eAAe,EACf,OAAO,EACP,oBAAoB,CACpB,CAAC;gBAEF,OAAO;oBACN,cAAc;oBACd,QAAQ;iBACR,CAAC;YACH,CAAC;YAED,OAAO;gBACN,cAAc,EAAE,SAAS;gBACzB,QAAQ,EAAE,CAAC;aACX,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,eAAe,CAAC,gBAAgB,CAAC,eAAe,EAAE,OAAO,EAAE,oBAAoB,CAAC,CAAC;YAEvF,MAAM,IAAI,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QACzF,CAAC;IACF,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,KAAK,CAAC,eAAe,CAClC,MAA2C,EAC3C,MAAkC,EAClC,mBAAkC,EAClC,UAAmC,EACnC,OAAiC;QAEjC,IAAI,QAAQ,GAAG,CAAC,CAAC;QAEjB,MAAM,OAAO,EAAE,cAAc,EAAE,CAAC,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAExD,MAAM,mBAAmB,GACxB,mBAAmB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAE7D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,mBAAmB,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrD,MAAM,cAAc,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE;gBAC3D,MAAM,OAAO,EAAE,cAAc,EAAE,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;gBAEjF,QAAQ,IAAI,MAAM,eAAe,CAAC,gBAAgB,CACjD,MAAM,EACN,MAAM,EACN,mBAAmB,CAAC,MAAM,EAC1B,CAAC,EACD,UAAU,EACV,OAAO,CACP,CAAC;gBAEF,MAAM,OAAO,EAAE,cAAc,EAAE,CAAC,cAAc,EAAE,mBAAmB,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YAChF,CAAC,CAAC,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,EAAE,cAAc,EAAE,CAAC,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAEtD,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED;;;;;;;;;OASG;IACI,MAAM,CAAC,KAAK,CAAC,gBAAgB,CACnC,MAAkC,EAClC,MAAkC,EAClC,cAAsB,EACtB,cAAsB,EACtB,UAAmC,EACnC,OAAiC;QAEjC,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,GAAG,CAAC;QAC5C,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,MAA0B,CAAC;QAE/B,MAAM,aAAa,GAAG,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QAE3C,MAAM,OAAO,EAAE,mBAAmB,EAAE,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QAEvD,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;YACvB,GAAG,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;gBACpF,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;gBAErB,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;oBAClC,MAAM,gBAAgB,GAAQ,EAAE,CAAC;oBAEjC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACpC,MAAM,iBAAiB,GAAG,eAAe,CAAC,oBAAoB,CAC7D,MAAM,EACN,UAAU,EACV,OAAO,CACP,CAAC;wBACF,gBAAgB,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;oBAC1C,CAAC;oBAED,MAAM,MAAM,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAC;oBACxC,QAAQ,IAAI,gBAAgB,CAAC,MAAM,CAAC;gBACrC,CAAC;gBAED,MAAM,OAAO,EAAE,mBAAmB,EAAE,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;YAC/D,CAAC,QAAQ,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE;QAClC,CAAC;QAED,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,oBAAoB,CACjC,MAAkB,EAClB,UAAmC,EACnC,OAAiC;QAEjC,MAAM,SAAS,GAAG,EAAO,CAAC;QAE1B,KAAK,MAAM,QAAQ,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;YAC7C,YAAY,CAAC,WAAW,CACvB,SAAS,EACT,QAAQ,CAAC,QAAkB,EAC3B,YAAY,CAAC,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAkB,CAAC,CAC7D,CAAC;QACH,CAAC;QAED,KAAK,MAAM,MAAM,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;YACvC,IAAI,QAAQ,CAAC;YAEb,IAAI,CAAC,CAAC,MAAM,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,CAAC;gBACjC,IAAI,MAAM,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,EAAE,CAAC;oBACtD,QAAQ,GAAG,KAAK,CAAC;gBAClB,CAAC;qBAAM,IACN,MAAM,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM;oBAC/C,MAAM,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,EAC/C,CAAC;oBACF,QAAQ,GAAG,CAAC,CAAC;gBACd,CAAC;qBAAM,IAAI,MAAM,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM,EAAE,CAAC;oBAC5D,QAAQ,GAAG,EAAE,CAAC;gBACf,CAAC;qBAAM,IAAI,MAAM,CAAC,IAAI,KAAK,wBAAwB,CAAC,KAAK,EAAE,CAAC;oBAC3D,QAAQ,GAAG,EAAE,CAAC;gBACf,CAAC;qBAAM,IAAI,MAAM,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM,EAAE,CAAC;oBAC5D,QAAQ,GAAG,EAAE,CAAC;gBACf,CAAC;YACF,CAAC;YAED,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC3B,YAAY,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,QAAkB,EAAE,QAAQ,CAAC,CAAC;YAC1E,CAAC;QACF,CAAC;QAED,KAAK,MAAM,MAAM,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC;YAC1C,MAAM,YAAY,GAAG,YAAY,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,QAAkB,CAAC,CAAC;YACtF,IAAI,QAAQ,CAAC;YAEb,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,EAAE,CAAC;gBACzD,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YACzC,CAAC;iBAAM,IACN,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM;gBAClD,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,wBAAwB,CAAC,OAAO,EAClD,CAAC;gBACF,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YACxC,CAAC;iBAAM,IAAI,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM,EAAE,CAAC;gBAC/D,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YACxC,CAAC;iBAAM,IACN,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,wBAAwB,CAAC,KAAK;gBACjD,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,wBAAwB,CAAC,MAAM,EACjD,CAAC;gBACF,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,uBAAuB,CAAC,EAAE,CAAC;oBACpD,MAAM,IAAI,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,8BAA8B,EAAE;wBAClF,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ;wBAC1B,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ;wBACtB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;qBACtB,CAAC,CAAC;gBACJ,CAAC;gBAED,QAAQ,GAAG,OAAO,CAAC,uBAAuB,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;YAClF,CAAC;YAED,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC9D,MAAM,IAAI,YAAY,CAAC,eAAe,CAAC,UAAU,EAAE,2BAA2B,EAAE;oBAC/E,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,QAAQ;oBAC5B,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,IAAI;iBACpB,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC3B,YAAY,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,CAAC,QAAkB,EAAE,QAAQ,CAAC,CAAC;YAC7E,CAAC;QACF,CAAC;QAED,yCAAyC;QACzC,6CAA6C;QAC7C,IAAI;QAEJ,OAAO,SAAS,CAAC;IAClB,CAAC;IAED;;;;;;OAMG;IACK,MAAM,CAAC,KAAK,CAAC,gBAAgB,CACpC,SAAqC,EACrC,oBAAwC;QAExC,MAAM,SAAS,GAAG,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACvD,IAAI,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC5B,MAAM,SAAS,CAAC,oBAAoB,CAAC,CAAC;QACvC,CAAC;QACD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACnC,CAAC;IACF,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { ContextIdStore, type IContextIds } from \"@twin.org/context\";\nimport { Coerce, GeneralError, Is, ObjectHelper } from \"@twin.org/core\";\nimport {\n\tEntitySchemaDiffHelper,\n\tEntitySchemaPropertyType,\n\ttype IEntitySchemaDiff\n} from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IEntityStorageConnector } from \"../models/IEntityStorageConnector.js\";\nimport type { IEntityStorageMigrationConnector } from \"../models/IEntityStorageMigrationConnector.js\";\nimport type { IMigrationOptions } from \"../models/IMigrationOptions.js\";\n\n/**\n * Helper class for performing schema migrations between two connectors.\n */\nexport class MigrationHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<MigrationHelper>();\n\n\t/**\n\t * Performs a migration between two connectors, using the provided options and schema diff to control the migration behaviour.\n\t * @param sourceConnector The connector to migrate from to allow the migration helper to create the new connector and finalize the migration.\n\t * @param targetEntitySchemaName The name of the new entity schema.\n\t * @param renames An optional list of property renames to apply during migration.\n\t * @param options Options controlling migration behaviour.\n\t * @param loggingComponentType An optional logging component type to use for bootstrapping and starting connectors if necessary.\n\t * @returns The connector for the new schema and the number of entities successfully migrated, the sourceConnector will no longer be usable, finalConnector will be undefined if no migration was necessary.\n\t */\n\tpublic static async migrate<T, U>(\n\t\tsourceConnector: IEntityStorageMigrationConnector<T>,\n\t\ttargetEntitySchemaName: string,\n\t\trenames?: { from: string; to: string }[],\n\t\toptions?: IMigrationOptions<T, U>,\n\t\tloggingComponentType?: string\n\t): Promise<{\n\t\tfinalConnector?: IEntityStorageConnector<U>;\n\t\tmigrated: number;\n\t}> {\n\t\tlet targetConnector: IEntityStorageConnector<U> | undefined;\n\t\ttry {\n\t\t\t// We use the migration method to create the new connector as it will use a temporary storage location if necessary\n\t\t\ttargetConnector = await sourceConnector.createTargetConnector<U>(targetEntitySchemaName);\n\n\t\t\t// Startup both connectors to ensure they are ready for the migration, this will call bootstrap and start if they are defined.\n\t\t\tawait MigrationHelper.startupConnector<T>(sourceConnector, loggingComponentType);\n\t\t\tawait MigrationHelper.startupConnector<U>(targetConnector, loggingComponentType);\n\n\t\t\t// Get the unique partition context ids to run the migration for each partition.\n\t\t\tlet partitionContextIds = await sourceConnector.getPartitionContextIds();\n\n\t\t\t// If there are no partitions, we still want to run the migration once to handle the schema changes,\n\t\t\t// so we create a single empty context for the migration to run in.\n\t\t\tif (!Is.arrayValue(partitionContextIds)) {\n\t\t\t\tpartitionContextIds ??= [];\n\t\t\t\tpartitionContextIds.push({});\n\t\t\t}\n\n\t\t\t// Get the schemas\n\t\t\tconst sourceSchema = sourceConnector.getSchema();\n\t\t\tconst targetSchema = targetConnector.getSchema();\n\n\t\t\t// Get the schema diff between the source and target schemas.\n\t\t\tconst schemaDiff = EntitySchemaDiffHelper.diff<T, U>(\n\t\t\t\tsourceSchema.properties ?? [],\n\t\t\t\ttargetSchema.properties ?? [],\n\t\t\t\trenames\n\t\t\t);\n\n\t\t\t// Only perform a migration if the schemas have changed\n\t\t\tif (EntitySchemaDiffHelper.hasChanges(schemaDiff)) {\n\t\t\t\t// Perform the migration, which will read from the current store and write to the target connector's store.\n\t\t\t\tconst migrated = await MigrationHelper.migrateEntities<T, U>(\n\t\t\t\t\tsourceConnector,\n\t\t\t\t\ttargetConnector,\n\t\t\t\t\tpartitionContextIds,\n\t\t\t\t\tschemaDiff,\n\t\t\t\t\toptions\n\t\t\t\t);\n\n\t\t\t\t// The migration is now complete, finalize the migration which could entail\n\t\t\t\t// renaming of resources etc, determined by the implementation of finalizeMigration.\n\t\t\t\t// it also needs to stop and teardown any resources no longer in use after the migration,\n\t\t\t\t// such as the old connector and its underlying storage.\n\t\t\t\tconst finalConnector = await sourceConnector.finalizeMigration(\n\t\t\t\t\ttargetConnector,\n\t\t\t\t\toptions,\n\t\t\t\t\tloggingComponentType\n\t\t\t\t);\n\n\t\t\t\treturn {\n\t\t\t\t\tfinalConnector,\n\t\t\t\t\tmigrated\n\t\t\t\t};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tfinalConnector: undefined,\n\t\t\t\tmigrated: 0\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tawait sourceConnector.cleanupMigration(targetConnector, options, loggingComponentType);\n\n\t\t\tthrow new GeneralError(MigrationHelper.CLASS_NAME, \"migrationFailed\", undefined, error);\n\t\t}\n\t}\n\n\t/**\n\t * Generic per-partition migration loop.\n\t * @param source Connector to read from (current schema, already bootstrapped).\n\t * @param target Connector to write to (new schema, already bootstrapped).\n\t * @param partitionContextIds The context ids to use for the migration, used for partitioning and can be used in the transform function when `options.transformEntityProperty` is provided.\n\t * @param schemaDiff The schema diff.\n\t * @param options Optional migration controls (batchSize, transformEntity, onProgress).\n\t * @returns The number of entities successfully migrated.\n\t */\n\tpublic static async migrateEntities<T = unknown, U = T>(\n\t\tsource: IEntityStorageMigrationConnector<T>,\n\t\ttarget: IEntityStorageConnector<U>,\n\t\tpartitionContextIds: IContextIds[],\n\t\tschemaDiff: IEntitySchemaDiff<T, U>,\n\t\toptions?: IMigrationOptions<T, U>\n\t): Promise<number> {\n\t\tlet migrated = 0;\n\n\t\tawait options?.onStepProgress?.(\"migrationStart\", 0, 0);\n\n\t\tconst effectivePartitions: IContextIds[] =\n\t\t\tpartitionContextIds.length > 0 ? partitionContextIds : [{}];\n\n\t\tfor (let i = 0; i < effectivePartitions.length; i++) {\n\t\t\tawait ContextIdStore.run(effectivePartitions[i], async () => {\n\t\t\t\tawait options?.onStepProgress?.(\"partitionStart\", effectivePartitions.length, i);\n\n\t\t\t\tmigrated += await MigrationHelper.migratePartition(\n\t\t\t\t\tsource,\n\t\t\t\t\ttarget,\n\t\t\t\t\teffectivePartitions.length,\n\t\t\t\t\ti,\n\t\t\t\t\tschemaDiff,\n\t\t\t\t\toptions\n\t\t\t\t);\n\n\t\t\t\tawait options?.onStepProgress?.(\"partitionEnd\", effectivePartitions.length, i);\n\t\t\t});\n\t\t}\n\n\t\tawait options?.onStepProgress?.(\"migrationEnd\", 0, 0);\n\n\t\treturn migrated;\n\t}\n\n\t/**\n\t * Generic per-partition migration loop.\n\t * @param source Connector to read from (current schema, already bootstrapped).\n\t * @param target Connector to write to (new schema, already bootstrapped).\n\t * @param partitionTotal The total number of partitions to migrate, used for progress reporting.\n\t * @param partitionIndex The index of the current partition being migrated, used for progress reporting.\n\t * @param schemaDiff Schema diff used to add nullable defaults and drop removed fields when `options.transformEntity` is not provided.\n\t * @param options Optional migration controls (batchSize, transformEntity, onProgress).\n\t * @returns The number of entities successfully migrated.\n\t */\n\tpublic static async migratePartition<T = unknown, U = unknown>(\n\t\tsource: IEntityStorageConnector<T>,\n\t\ttarget: IEntityStorageConnector<U>,\n\t\tpartitionTotal: number,\n\t\tpartitionIndex: number,\n\t\tschemaDiff: IEntitySchemaDiff<T, U>,\n\t\toptions?: IMigrationOptions<T, U>\n\t): Promise<number> {\n\t\tconst batchSize = options?.batchSize ?? 100;\n\t\tlet migrated = 0;\n\t\tlet cursor: string | undefined;\n\n\t\tconst totalEntities = await source.count();\n\n\t\tawait options?.onPartitionProgress?.(totalEntities, 0);\n\n\t\tif (totalEntities > 0) {\n\t\t\tdo {\n\t\t\t\tconst page = await source.query(undefined, undefined, undefined, cursor, batchSize);\n\t\t\t\tcursor = page.cursor;\n\n\t\t\t\tif (Is.arrayValue(page.entities)) {\n\t\t\t\t\tconst transformedBatch: U[] = [];\n\n\t\t\t\t\tfor (const entity of page.entities) {\n\t\t\t\t\t\tconst transformedEntity = MigrationHelper.applyEntityTransform<T, U>(\n\t\t\t\t\t\t\tentity,\n\t\t\t\t\t\t\tschemaDiff,\n\t\t\t\t\t\t\toptions\n\t\t\t\t\t\t);\n\t\t\t\t\t\ttransformedBatch.push(transformedEntity);\n\t\t\t\t\t}\n\n\t\t\t\t\tawait target.setBatch(transformedBatch);\n\t\t\t\t\tmigrated += transformedBatch.length;\n\t\t\t\t}\n\n\t\t\t\tawait options?.onPartitionProgress?.(totalEntities, migrated);\n\t\t\t} while (Is.stringValue(cursor));\n\t\t}\n\n\t\treturn migrated;\n\t}\n\n\t/**\n\t * Applies the entity transformation for migration, using the provided options and schema diff.\n\t * @param entity The entity to transform.\n\t * @param schemaDiff The schema diff between the old and new schemas.\n\t * @param options The migration options.\n\t * @returns The transformed entity ready to be written to the new schema.\n\t * @throws GeneralError if a transformation is required for an object or array property but no `options.transformEntityProperty` function is provided.\n\t * @throws GeneralError if coercion of a modified property results in undefined for a non-optional target property.\n\t */\n\tpublic static applyEntityTransform<T = unknown, U = unknown>(\n\t\tentity: Partial<T>,\n\t\tschemaDiff: IEntitySchemaDiff<T, U>,\n\t\toptions?: IMigrationOptions<T, U>\n\t): U {\n\t\tconst newEntity = {} as U;\n\n\t\tfor (const property of schemaDiff.unchanged) {\n\t\t\tObjectHelper.propertySet(\n\t\t\t\tnewEntity,\n\t\t\t\tproperty.property as string,\n\t\t\t\tObjectHelper.propertyGet(entity, property.property as string)\n\t\t\t);\n\t\t}\n\n\t\tfor (const change of schemaDiff.added) {\n\t\t\tlet defValue;\n\n\t\t\tif (!(change.optional ?? false)) {\n\t\t\t\tif (change.type === EntitySchemaPropertyType.Boolean) {\n\t\t\t\t\tdefValue = false;\n\t\t\t\t} else if (\n\t\t\t\t\tchange.type === EntitySchemaPropertyType.Number ||\n\t\t\t\t\tchange.type === EntitySchemaPropertyType.Integer\n\t\t\t\t) {\n\t\t\t\t\tdefValue = 0;\n\t\t\t\t} else if (change.type === EntitySchemaPropertyType.String) {\n\t\t\t\t\tdefValue = \"\";\n\t\t\t\t} else if (change.type === EntitySchemaPropertyType.Array) {\n\t\t\t\t\tdefValue = [];\n\t\t\t\t} else if (change.type === EntitySchemaPropertyType.Object) {\n\t\t\t\t\tdefValue = {};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (Is.notEmpty(defValue)) {\n\t\t\t\tObjectHelper.propertySet(newEntity, change.property as string, defValue);\n\t\t\t}\n\t\t}\n\n\t\tfor (const change of schemaDiff.modified) {\n\t\t\tconst currentValue = ObjectHelper.propertyGet(entity, change.from.property as string);\n\t\t\tlet newValue;\n\n\t\t\tif (change.to.type === EntitySchemaPropertyType.Boolean) {\n\t\t\t\tnewValue = Coerce.boolean(currentValue);\n\t\t\t} else if (\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Number ||\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Integer\n\t\t\t) {\n\t\t\t\tnewValue = Coerce.number(currentValue);\n\t\t\t} else if (change.to.type === EntitySchemaPropertyType.String) {\n\t\t\t\tnewValue = Coerce.string(currentValue);\n\t\t\t} else if (\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Array ||\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Object\n\t\t\t) {\n\t\t\t\tif (!Is.function(options?.transformEntityProperty)) {\n\t\t\t\t\tthrow new GeneralError(MigrationHelper.CLASS_NAME, \"transformRequiredForProperty\", {\n\t\t\t\t\t\tfrom: change.from.property,\n\t\t\t\t\t\tto: change.to.property,\n\t\t\t\t\t\ttype: change.from.type\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tnewValue = options.transformEntityProperty(change.from, change.to, currentValue);\n\t\t\t}\n\n\t\t\tif (newValue === undefined && !(change.to.optional ?? false)) {\n\t\t\t\tthrow new GeneralError(MigrationHelper.CLASS_NAME, \"coercionProducedUndefined\", {\n\t\t\t\t\tproperty: change.to.property,\n\t\t\t\t\ttype: change.to.type\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (Is.notEmpty(newValue)) {\n\t\t\t\tObjectHelper.propertySet(newEntity, change.to.property as string, newValue);\n\t\t\t}\n\t\t}\n\n\t\t// Removed properties are simply dropped.\n\t\t// for (const change of schemaDiff.removed) {\n\t\t// }\n\n\t\treturn newEntity;\n\t}\n\n\t/**\n\t * Startup the connector by calling bootstrap and start if they are defined.\n\t * @param connector The connector to startup.\n\t * @param loggingComponentType An optional logging component type to use for bootstrapping and starting the connector.\n\t * @returns Nothing.\n\t * @internal\n\t */\n\tprivate static async startupConnector<T>(\n\t\tconnector: IEntityStorageConnector<T>,\n\t\tloggingComponentType: string | undefined\n\t): Promise<void> {\n\t\tconst bootstrap = connector.bootstrap?.bind(connector);\n\t\tif (Is.function(bootstrap)) {\n\t\t\tawait bootstrap(loggingComponentType);\n\t\t}\n\t\tconst start = connector.start?.bind(connector);\n\t\tif (Is.function(start)) {\n\t\t\tawait start(loggingComponentType);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"migrationHelper.js","sourceRoot":"","sources":["../../../src/helpers/migrationHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { ContextIdStore } from \"@twin.org/context\";\nimport { Coerce, GeneralError, Is, ObjectHelper } from \"@twin.org/core\";\nimport {\n\tEntitySchemaDiffHelper,\n\tEntitySchemaPropertyType,\n\ttype IEntitySchemaDiff\n} from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\nimport type { IEntityStorageConnector } from \"../models/IEntityStorageConnector.js\";\nimport type { IEntityStorageMigrationConnector } from \"../models/IEntityStorageMigrationConnector.js\";\nimport type { IMigrationOptions } from \"../models/IMigrationOptions.js\";\nimport type { IResolvedMigrationStep } from \"../models/IResolvedMigrationStep.js\";\n\n/**\n * Helper class for performing entity schema migrations between two connectors.\n * The chain-based API (migrateWithChain / applyEntityChain) is the single migration\n * path: a chain of one step covers the same case as a traditional single-step migration.\n */\nexport class MigrationHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<MigrationHelper>();\n\n\t/**\n\t * Applies the entity transformation for a single diff, handling added, removed, and\n\t * modified properties according to the provided schema diff and optional transform hook.\n\t * @param entity The entity to transform.\n\t * @param schemaDiff The schema diff between the old and new schemas.\n\t * @param transformEntityProperty Optional per-property transform hook for object/array properties.\n\t * @returns The transformed entity ready to be written to the new schema.\n\t * @throws GeneralError if a transformation is required for an object or array property but no transformEntityProperty function is provided.\n\t * @throws GeneralError if coercion of a modified property results in undefined for a non-optional target property.\n\t */\n\tpublic static applyEntityTransform<T = unknown, U = unknown>(\n\t\tentity: Partial<T>,\n\t\tschemaDiff: IEntitySchemaDiff<T, U>,\n\t\ttransformEntityProperty?: IMigrationOptions<T, U>[\"transformEntityProperty\"]\n\t): U {\n\t\tconst newEntity = {} as U;\n\n\t\tfor (const property of schemaDiff.unchanged) {\n\t\t\tObjectHelper.propertySet(\n\t\t\t\tnewEntity,\n\t\t\t\tproperty.property as string,\n\t\t\t\tObjectHelper.propertyGet(entity, property.property as string)\n\t\t\t);\n\t\t}\n\n\t\tfor (const change of schemaDiff.added) {\n\t\t\tlet defValue;\n\n\t\t\tif (!(change.optional ?? false)) {\n\t\t\t\tif (change.type === EntitySchemaPropertyType.Boolean) {\n\t\t\t\t\tdefValue = change.defaultValue ?? false;\n\t\t\t\t} else if (\n\t\t\t\t\tchange.type === EntitySchemaPropertyType.Number ||\n\t\t\t\t\tchange.type === EntitySchemaPropertyType.Integer\n\t\t\t\t) {\n\t\t\t\t\tdefValue = change.defaultValue ?? 0;\n\t\t\t\t} else if (change.type === EntitySchemaPropertyType.String) {\n\t\t\t\t\tdefValue = change.defaultValue ?? \"\";\n\t\t\t\t} else if (change.type === EntitySchemaPropertyType.Array) {\n\t\t\t\t\tdefValue = change.defaultValue ?? [];\n\t\t\t\t} else if (change.type === EntitySchemaPropertyType.Object) {\n\t\t\t\t\tdefValue = change.defaultValue ?? {};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (Is.notEmpty(defValue)) {\n\t\t\t\tObjectHelper.propertySet(newEntity, change.property as string, defValue);\n\t\t\t}\n\t\t}\n\n\t\tfor (const change of schemaDiff.modified) {\n\t\t\tconst currentValue = ObjectHelper.propertyGet(entity, change.from.property as string);\n\t\t\tlet newValue;\n\n\t\t\tif (change.to.type === EntitySchemaPropertyType.Boolean) {\n\t\t\t\tnewValue = Coerce.boolean(currentValue);\n\t\t\t} else if (\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Number ||\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Integer\n\t\t\t) {\n\t\t\t\tnewValue = Coerce.number(currentValue);\n\t\t\t} else if (change.to.type === EntitySchemaPropertyType.String) {\n\t\t\t\tnewValue = Coerce.string(currentValue);\n\t\t\t} else if (\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Array ||\n\t\t\t\tchange.to.type === EntitySchemaPropertyType.Object\n\t\t\t) {\n\t\t\t\tif (!Is.function(transformEntityProperty)) {\n\t\t\t\t\tthrow new GeneralError(MigrationHelper.CLASS_NAME, \"transformRequiredForProperty\", {\n\t\t\t\t\t\tfrom: change.from.property,\n\t\t\t\t\t\tto: change.to.property,\n\t\t\t\t\t\ttype: change.from.type\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tnewValue = transformEntityProperty(change.from, change.to, currentValue);\n\t\t\t}\n\n\t\t\tif (newValue === undefined && !(change.to.optional ?? false)) {\n\t\t\t\tthrow new GeneralError(MigrationHelper.CLASS_NAME, \"coercionProducedUndefined\", {\n\t\t\t\t\tproperty: change.to.property,\n\t\t\t\t\ttype: change.to.type\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif (Is.notEmpty(newValue)) {\n\t\t\t\tObjectHelper.propertySet(newEntity, change.to.property as string, newValue);\n\t\t\t}\n\t\t}\n\n\t\t// Removed properties are simply dropped.\n\n\t\treturn newEntity;\n\t}\n\n\t/**\n\t * Transforms a single entity through an ordered chain of fully-resolved migration steps.\n\t * For each step the method diffs fromProperties against toProperties, then applies\n\t * applyEntityTransform. Each step's output feeds the next step's input so that\n\t * per-step transformEntityProperty hooks are honoured throughout the chain.\n\t * @param entity The entity to transform (at the shape described by steps[0].fromProperties).\n\t * @param steps Ordered, fully-resolved migration steps from stored version to current version.\n\t * Each step's fromProperties and toProperties are resolved by the caller before invocation.\n\t * @returns The entity transformed to the shape described by steps[last].toProperties.\n\t */\n\tpublic static applyEntityChain(entity: unknown, steps: IResolvedMigrationStep[]): unknown {\n\t\tlet current: unknown = entity;\n\t\tfor (const step of steps) {\n\t\t\tconst diff = EntitySchemaDiffHelper.diff(\n\t\t\t\tstep.fromProperties,\n\t\t\t\tstep.toProperties,\n\t\t\t\tstep.renames\n\t\t\t);\n\t\t\tcurrent = MigrationHelper.applyEntityTransform(\n\t\t\t\tcurrent as Partial<unknown>,\n\t\t\t\tdiff,\n\t\t\t\tstep.transformEntityProperty\n\t\t\t);\n\t\t}\n\t\treturn current;\n\t}\n\n\t/**\n\t * Performs a chain migration in a single connector swap, regardless of how many version\n\t * steps the chain spans. Creates one target connector, reads all source entities, applies\n\t * applyEntityChain to each, writes them to the target, then finalizes the migration.\n\t * A chain of one step is equivalent to a traditional single-step migration.\n\t * @param sourceConnector The connector holding data at the stored schema version.\n\t * @param targetSchemaName The schema name for the current version (used to create the target connector).\n\t * @param steps Ordered, fully-resolved migration steps from stored to current version.\n\t * @param loggingComponentType An optional logging component type for connector startup.\n\t * @param batchSize Number of entities to read and write per batch. Defaults to 100.\n\t * @returns The finalized connector and the count of migrated entities.\n\t */\n\tpublic static async migrateWithChain(\n\t\tsourceConnector: IEntityStorageMigrationConnector,\n\t\ttargetSchemaName: string,\n\t\tsteps: IResolvedMigrationStep[],\n\t\tloggingComponentType?: string,\n\t\tbatchSize = 100\n\t): Promise<{\n\t\tfinalConnector: IEntityStorageConnector;\n\t\tmigrated: number;\n\t}> {\n\t\tlet targetConnector: IEntityStorageConnector | undefined;\n\t\ttry {\n\t\t\ttargetConnector = await sourceConnector.createTargetConnector(targetSchemaName);\n\n\t\t\tawait MigrationHelper.startupConnector(sourceConnector, loggingComponentType);\n\t\t\tawait MigrationHelper.startupConnector(targetConnector, loggingComponentType);\n\n\t\t\tlet partitionContextIds = await sourceConnector.getPartitionContextIds();\n\t\t\tif (!Is.arrayValue(partitionContextIds)) {\n\t\t\t\tpartitionContextIds ??= [];\n\t\t\t\tpartitionContextIds.push({});\n\t\t\t}\n\n\t\t\tlet migrated = 0;\n\t\t\tconst effectivePartitions = partitionContextIds.length > 0 ? partitionContextIds : [{}];\n\n\t\t\tconst resolvedTarget = targetConnector;\n\t\t\tfor (const contextIds of effectivePartitions) {\n\t\t\t\tawait ContextIdStore.run(contextIds, async () => {\n\t\t\t\t\tmigrated += await MigrationHelper.migratePartitionWithChain(\n\t\t\t\t\t\tsourceConnector,\n\t\t\t\t\t\tresolvedTarget,\n\t\t\t\t\t\tsteps,\n\t\t\t\t\t\tbatchSize\n\t\t\t\t\t);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst finalConnector = await sourceConnector.finalizeMigration(\n\t\t\t\ttargetConnector,\n\t\t\t\tundefined,\n\t\t\t\tloggingComponentType\n\t\t\t);\n\n\t\t\treturn { finalConnector, migrated };\n\t\t} catch (error) {\n\t\t\tawait sourceConnector.cleanupMigration(targetConnector, undefined, loggingComponentType);\n\t\t\tthrow new GeneralError(MigrationHelper.CLASS_NAME, \"migrationFailed\", undefined, error);\n\t\t}\n\t}\n\n\t/**\n\t * Reads all entities from one partition of the source connector, applies the migration\n\t * chain to each entity, and writes the results to the target connector.\n\t * @param source The connector to read from (already bootstrapped).\n\t * @param target The connector to write to (already bootstrapped).\n\t * @param steps Ordered, fully-resolved migration steps.\n\t * @param batchSize Number of entities to read and write per batch.\n\t * @returns The number of entities migrated.\n\t * @internal\n\t */\n\tprivate static async migratePartitionWithChain(\n\t\tsource: IEntityStorageMigrationConnector,\n\t\ttarget: IEntityStorageConnector,\n\t\tsteps: IResolvedMigrationStep[],\n\t\tbatchSize: number\n\t): Promise<number> {\n\t\tlet migrated = 0;\n\t\tlet cursor: string | undefined;\n\t\tconst totalEntities = await source.count();\n\n\t\tif (totalEntities > 0) {\n\t\t\tdo {\n\t\t\t\tconst page = await source.query(undefined, undefined, undefined, cursor, batchSize);\n\t\t\t\tcursor = page.cursor;\n\n\t\t\t\tif (Is.arrayValue(page.entities)) {\n\t\t\t\t\tconst transformedBatch: unknown[] = page.entities.map(entity =>\n\t\t\t\t\t\tMigrationHelper.applyEntityChain(entity, steps)\n\t\t\t\t\t);\n\t\t\t\t\tawait target.setBatch(transformedBatch);\n\t\t\t\t\tmigrated += transformedBatch.length;\n\t\t\t\t}\n\t\t\t} while (Is.stringValue(cursor));\n\t\t}\n\n\t\treturn migrated;\n\t}\n\n\t/**\n\t * Starts the connector by calling bootstrap and start if they are defined.\n\t * @param connector The connector to start.\n\t * @param loggingComponentType An optional logging component type.\n\t * @internal\n\t */\n\tprivate static async startupConnector<T>(\n\t\tconnector: IEntityStorageConnector<T>,\n\t\tloggingComponentType: string | undefined\n\t): Promise<void> {\n\t\tconst bootstrap = connector.bootstrap?.bind(connector);\n\t\tif (Is.function(bootstrap)) {\n\t\t\tawait bootstrap(loggingComponentType);\n\t\t}\n\t\tconst start = connector.start?.bind(connector);\n\t\tif (Is.function(start)) {\n\t\t\tawait start(loggingComponentType);\n\t\t}\n\t}\n}\n"]}
package/dist/es/index.js CHANGED
@@ -1,6 +1,7 @@
1
- // Copyright 2024 IOTA Stiftung.
1
+ // Copyright 2026 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
3
  export * from "./factories/entityStorageConnectorFactory.js";
4
+ export * from "./factories/schemaMigrationFactory.js";
4
5
  export * from "./helpers/entityStorageHelper.js";
5
6
  export * from "./helpers/migrationHelper.js";
6
7
  export * from "./models/api/IEntityStorageCountRequest.js";
@@ -18,4 +19,6 @@ export * from "./models/IEntityStorageComponent.js";
18
19
  export * from "./models/IEntityStorageConnector.js";
19
20
  export * from "./models/IEntityStorageMigrationConnector.js";
20
21
  export * from "./models/IMigrationOptions.js";
22
+ export * from "./models/IResolvedMigrationStep.js";
23
+ export * from "./models/ISchemaMigration.js";
21
24
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,8CAA8C,CAAC;AAC7D,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4CAA4C,CAAC;AAC3D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,0CAA0C,CAAC;AACzD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,kDAAkD,CAAC;AACjE,cAAc,6CAA6C,CAAC;AAC5D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,0CAA0C,CAAC;AACzD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,+BAA+B,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./factories/entityStorageConnectorFactory.js\";\nexport * from \"./helpers/entityStorageHelper.js\";\nexport * from \"./helpers/migrationHelper.js\";\nexport * from \"./models/api/IEntityStorageCountRequest.js\";\nexport * from \"./models/api/IEntityStorageCountResponse.js\";\nexport * from \"./models/api/IEntityStorageEmptyRequest.js\";\nexport * from \"./models/api/IEntityStorageGetRequest.js\";\nexport * from \"./models/api/IEntityStorageGetResponse.js\";\nexport * from \"./models/api/IEntityStorageListRequest.js\";\nexport * from \"./models/api/IEntityStorageListResponse.js\";\nexport * from \"./models/api/IEntityStorageRemoveBatchRequest.js\";\nexport * from \"./models/api/IEntityStorageRemoveRequest.js\";\nexport * from \"./models/api/IEntityStorageSetBatchRequest.js\";\nexport * from \"./models/api/IEntityStorageSetRequest.js\";\nexport * from \"./models/IEntityStorageComponent.js\";\nexport * from \"./models/IEntityStorageConnector.js\";\nexport * from \"./models/IEntityStorageMigrationConnector.js\";\nexport * from \"./models/IMigrationOptions.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,8CAA8C,CAAC;AAC7D,cAAc,uCAAuC,CAAC;AACtD,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4CAA4C,CAAC;AAC3D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,0CAA0C,CAAC;AACzD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,kDAAkD,CAAC;AACjE,cAAc,6CAA6C,CAAC;AAC5D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,0CAA0C,CAAC;AACzD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,8CAA8C,CAAC;AAC7D,cAAc,+BAA+B,CAAC;AAC9C,cAAc,oCAAoC,CAAC;AACnD,cAAc,8BAA8B,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./factories/entityStorageConnectorFactory.js\";\nexport * from \"./factories/schemaMigrationFactory.js\";\nexport * from \"./helpers/entityStorageHelper.js\";\nexport * from \"./helpers/migrationHelper.js\";\nexport * from \"./models/api/IEntityStorageCountRequest.js\";\nexport * from \"./models/api/IEntityStorageCountResponse.js\";\nexport * from \"./models/api/IEntityStorageEmptyRequest.js\";\nexport * from \"./models/api/IEntityStorageGetRequest.js\";\nexport * from \"./models/api/IEntityStorageGetResponse.js\";\nexport * from \"./models/api/IEntityStorageListRequest.js\";\nexport * from \"./models/api/IEntityStorageListResponse.js\";\nexport * from \"./models/api/IEntityStorageRemoveBatchRequest.js\";\nexport * from \"./models/api/IEntityStorageRemoveRequest.js\";\nexport * from \"./models/api/IEntityStorageSetBatchRequest.js\";\nexport * from \"./models/api/IEntityStorageSetRequest.js\";\nexport * from \"./models/IEntityStorageComponent.js\";\nexport * from \"./models/IEntityStorageConnector.js\";\nexport * from \"./models/IEntityStorageMigrationConnector.js\";\nexport * from \"./models/IMigrationOptions.js\";\nexport * from \"./models/IResolvedMigrationStep.js\";\nexport * from \"./models/ISchemaMigration.js\";\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=IResolvedMigrationStep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IResolvedMigrationStep.js","sourceRoot":"","sources":["../../../src/models/IResolvedMigrationStep.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IEntitySchemaProperty } from \"@twin.org/entity\";\nimport type { IMigrationOptions } from \"./IMigrationOptions.js\";\n\n/**\n * A fully-resolved single migration step used by MigrationHelper.\n * The SchemaVersionService builds these by looking up versioned schema classes\n * from EntitySchemaFactory (e.g. MyEntityV0, MyEntityV1) before invoking the helper,\n * keeping factory knowledge out of the helper itself.\n * @template T The entity type. Defaults to `unknown`. Use a concrete entity type\n * when the step's source and target schemas are known at the call site.\n */\nexport interface IResolvedMigrationStep<T = unknown, U = unknown> {\n\t/**\n\t * The property list of the entity at the start of this step (the \"old\" shape).\n\t * Sourced from the versioned schema class registered in EntitySchemaFactory,\n\t * e.g. EntitySchemaFactory.get(\"MyEntityV0\").properties.\n\t */\n\tfromProperties: IEntitySchemaProperty<T>[];\n\n\t/**\n\t * The property list of the entity at the end of this step (the \"new\" shape).\n\t * For the final step this is the live current schema's properties.\n\t */\n\ttoProperties: IEntitySchemaProperty<U>[];\n\n\t/**\n\t * Optional property renames for this step, forwarded to EntitySchemaDiffHelper.diff.\n\t */\n\trenames?: { from: string; to: string }[];\n\n\t/**\n\t * Optional per-property transformer for object/array properties that the structural\n\t * diff cannot handle automatically. Sourced from an ISchemaMigration override when\n\t * one is registered in SchemaMigrationFactory for this step.\n\t */\n\ttransformEntityProperty?: IMigrationOptions<T, U>[\"transformEntityProperty\"];\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ISchemaMigration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ISchemaMigration.js","sourceRoot":"","sources":["../../../src/models/ISchemaMigration.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IMigrationOptions } from \"./IMigrationOptions.js\";\n\n/**\n * Optional per-step override for a single version-to-version migration.\n * Only register an entry in SchemaMigrationFactory when a step requires property\n * renames or a custom object/array transform. For purely structural changes\n * (add/remove/type-change fields) no entry is needed — the runner diffs the two\n * versioned schema classes (e.g. MyEntityV0 vs MyEntityV1) from EntitySchemaFactory\n * automatically.\n *\n * Register under the key \"<BaseSchemaName>_<fromVersion>_<toVersion>\"\n * e.g. \"MyEntity_0_1\" for the step that migrates from version 0 to version 1.\n * The key itself encodes the version pair; no version field is needed on the object.\n */\nexport interface ISchemaMigration<T = unknown, U = unknown> {\n\t/**\n\t * Optional property renames to apply during this step.\n\t */\n\trenames?: { from: string; to: string }[];\n\n\t/**\n\t * Optional per-property transformer for object/array properties that cannot be\n\t * automatically coerced. T is the source entity type, U is the target entity type.\n\t */\n\ttransformEntityProperty?: IMigrationOptions<T, U>[\"transformEntityProperty\"];\n}\n"]}
@@ -0,0 +1,14 @@
1
+ import { Factory } from "@twin.org/core";
2
+ import type { ISchemaMigration } from "../models/ISchemaMigration.js";
3
+ /**
4
+ * Factory for optional per-step migration overrides.
5
+ *
6
+ * Only register an entry when a version step requires property renames or a custom
7
+ * transform hook. For purely structural changes (add/remove/type-change) the
8
+ * SchemaVersionService diffs the two versioned schema classes automatically
9
+ * without needing any factory entry.
10
+ *
11
+ * Keys follow the convention "<BaseSchemaName>_<fromVersion>_<toVersion>",
12
+ * for example "MyEntity_0_1" for the step that migrates MyEntity from version 0 to 1.
13
+ */
14
+ export declare const SchemaMigrationFactory: Factory<ISchemaMigration<unknown, unknown>>;
@@ -1,10 +1,12 @@
1
- import { type IContextIds } from "@twin.org/context";
2
1
  import { type IEntitySchemaDiff } from "@twin.org/entity";
3
2
  import type { IEntityStorageConnector } from "../models/IEntityStorageConnector.js";
4
3
  import type { IEntityStorageMigrationConnector } from "../models/IEntityStorageMigrationConnector.js";
5
4
  import type { IMigrationOptions } from "../models/IMigrationOptions.js";
5
+ import type { IResolvedMigrationStep } from "../models/IResolvedMigrationStep.js";
6
6
  /**
7
- * Helper class for performing schema migrations between two connectors.
7
+ * Helper class for performing entity schema migrations between two connectors.
8
+ * The chain-based API (migrateWithChain / applyEntityChain) is the single migration
9
+ * path: a chain of one step covers the same case as a traditional single-step migration.
8
10
  */
9
11
  export declare class MigrationHelper {
10
12
  /**
@@ -12,50 +14,41 @@ export declare class MigrationHelper {
12
14
  */
13
15
  static readonly CLASS_NAME: string;
14
16
  /**
15
- * Performs a migration between two connectors, using the provided options and schema diff to control the migration behaviour.
16
- * @param sourceConnector The connector to migrate from to allow the migration helper to create the new connector and finalize the migration.
17
- * @param targetEntitySchemaName The name of the new entity schema.
18
- * @param renames An optional list of property renames to apply during migration.
19
- * @param options Options controlling migration behaviour.
20
- * @param loggingComponentType An optional logging component type to use for bootstrapping and starting connectors if necessary.
21
- * @returns The connector for the new schema and the number of entities successfully migrated, the sourceConnector will no longer be usable, finalConnector will be undefined if no migration was necessary.
22
- */
23
- static migrate<T, U>(sourceConnector: IEntityStorageMigrationConnector<T>, targetEntitySchemaName: string, renames?: {
24
- from: string;
25
- to: string;
26
- }[], options?: IMigrationOptions<T, U>, loggingComponentType?: string): Promise<{
27
- finalConnector?: IEntityStorageConnector<U>;
28
- migrated: number;
29
- }>;
30
- /**
31
- * Generic per-partition migration loop.
32
- * @param source Connector to read from (current schema, already bootstrapped).
33
- * @param target Connector to write to (new schema, already bootstrapped).
34
- * @param partitionContextIds The context ids to use for the migration, used for partitioning and can be used in the transform function when `options.transformEntityProperty` is provided.
35
- * @param schemaDiff The schema diff.
36
- * @param options Optional migration controls (batchSize, transformEntity, onProgress).
37
- * @returns The number of entities successfully migrated.
38
- */
39
- static migrateEntities<T = unknown, U = T>(source: IEntityStorageMigrationConnector<T>, target: IEntityStorageConnector<U>, partitionContextIds: IContextIds[], schemaDiff: IEntitySchemaDiff<T, U>, options?: IMigrationOptions<T, U>): Promise<number>;
40
- /**
41
- * Generic per-partition migration loop.
42
- * @param source Connector to read from (current schema, already bootstrapped).
43
- * @param target Connector to write to (new schema, already bootstrapped).
44
- * @param partitionTotal The total number of partitions to migrate, used for progress reporting.
45
- * @param partitionIndex The index of the current partition being migrated, used for progress reporting.
46
- * @param schemaDiff Schema diff used to add nullable defaults and drop removed fields when `options.transformEntity` is not provided.
47
- * @param options Optional migration controls (batchSize, transformEntity, onProgress).
48
- * @returns The number of entities successfully migrated.
49
- */
50
- static migratePartition<T = unknown, U = unknown>(source: IEntityStorageConnector<T>, target: IEntityStorageConnector<U>, partitionTotal: number, partitionIndex: number, schemaDiff: IEntitySchemaDiff<T, U>, options?: IMigrationOptions<T, U>): Promise<number>;
51
- /**
52
- * Applies the entity transformation for migration, using the provided options and schema diff.
17
+ * Applies the entity transformation for a single diff, handling added, removed, and
18
+ * modified properties according to the provided schema diff and optional transform hook.
53
19
  * @param entity The entity to transform.
54
20
  * @param schemaDiff The schema diff between the old and new schemas.
55
- * @param options The migration options.
21
+ * @param transformEntityProperty Optional per-property transform hook for object/array properties.
56
22
  * @returns The transformed entity ready to be written to the new schema.
57
- * @throws GeneralError if a transformation is required for an object or array property but no `options.transformEntityProperty` function is provided.
23
+ * @throws GeneralError if a transformation is required for an object or array property but no transformEntityProperty function is provided.
58
24
  * @throws GeneralError if coercion of a modified property results in undefined for a non-optional target property.
59
25
  */
60
- static applyEntityTransform<T = unknown, U = unknown>(entity: Partial<T>, schemaDiff: IEntitySchemaDiff<T, U>, options?: IMigrationOptions<T, U>): U;
26
+ static applyEntityTransform<T = unknown, U = unknown>(entity: Partial<T>, schemaDiff: IEntitySchemaDiff<T, U>, transformEntityProperty?: IMigrationOptions<T, U>["transformEntityProperty"]): U;
27
+ /**
28
+ * Transforms a single entity through an ordered chain of fully-resolved migration steps.
29
+ * For each step the method diffs fromProperties against toProperties, then applies
30
+ * applyEntityTransform. Each step's output feeds the next step's input so that
31
+ * per-step transformEntityProperty hooks are honoured throughout the chain.
32
+ * @param entity The entity to transform (at the shape described by steps[0].fromProperties).
33
+ * @param steps Ordered, fully-resolved migration steps from stored version to current version.
34
+ * Each step's fromProperties and toProperties are resolved by the caller before invocation.
35
+ * @returns The entity transformed to the shape described by steps[last].toProperties.
36
+ */
37
+ static applyEntityChain(entity: unknown, steps: IResolvedMigrationStep[]): unknown;
38
+ /**
39
+ * Performs a chain migration in a single connector swap, regardless of how many version
40
+ * steps the chain spans. Creates one target connector, reads all source entities, applies
41
+ * applyEntityChain to each, writes them to the target, then finalizes the migration.
42
+ * A chain of one step is equivalent to a traditional single-step migration.
43
+ * @param sourceConnector The connector holding data at the stored schema version.
44
+ * @param targetSchemaName The schema name for the current version (used to create the target connector).
45
+ * @param steps Ordered, fully-resolved migration steps from stored to current version.
46
+ * @param loggingComponentType An optional logging component type for connector startup.
47
+ * @param batchSize Number of entities to read and write per batch. Defaults to 100.
48
+ * @returns The finalized connector and the count of migrated entities.
49
+ */
50
+ static migrateWithChain(sourceConnector: IEntityStorageMigrationConnector, targetSchemaName: string, steps: IResolvedMigrationStep[], loggingComponentType?: string, batchSize?: number): Promise<{
51
+ finalConnector: IEntityStorageConnector;
52
+ migrated: number;
53
+ }>;
61
54
  }
@@ -1,4 +1,5 @@
1
1
  export * from "./factories/entityStorageConnectorFactory.js";
2
+ export * from "./factories/schemaMigrationFactory.js";
2
3
  export * from "./helpers/entityStorageHelper.js";
3
4
  export * from "./helpers/migrationHelper.js";
4
5
  export * from "./models/api/IEntityStorageCountRequest.js";
@@ -16,3 +17,5 @@ export * from "./models/IEntityStorageComponent.js";
16
17
  export * from "./models/IEntityStorageConnector.js";
17
18
  export * from "./models/IEntityStorageMigrationConnector.js";
18
19
  export * from "./models/IMigrationOptions.js";
20
+ export * from "./models/IResolvedMigrationStep.js";
21
+ export * from "./models/ISchemaMigration.js";
@@ -0,0 +1,36 @@
1
+ import type { IEntitySchemaProperty } from "@twin.org/entity";
2
+ import type { IMigrationOptions } from "./IMigrationOptions.js";
3
+ /**
4
+ * A fully-resolved single migration step used by MigrationHelper.
5
+ * The SchemaVersionService builds these by looking up versioned schema classes
6
+ * from EntitySchemaFactory (e.g. MyEntityV0, MyEntityV1) before invoking the helper,
7
+ * keeping factory knowledge out of the helper itself.
8
+ * @template T The entity type. Defaults to `unknown`. Use a concrete entity type
9
+ * when the step's source and target schemas are known at the call site.
10
+ */
11
+ export interface IResolvedMigrationStep<T = unknown, U = unknown> {
12
+ /**
13
+ * The property list of the entity at the start of this step (the "old" shape).
14
+ * Sourced from the versioned schema class registered in EntitySchemaFactory,
15
+ * e.g. EntitySchemaFactory.get("MyEntityV0").properties.
16
+ */
17
+ fromProperties: IEntitySchemaProperty<T>[];
18
+ /**
19
+ * The property list of the entity at the end of this step (the "new" shape).
20
+ * For the final step this is the live current schema's properties.
21
+ */
22
+ toProperties: IEntitySchemaProperty<U>[];
23
+ /**
24
+ * Optional property renames for this step, forwarded to EntitySchemaDiffHelper.diff.
25
+ */
26
+ renames?: {
27
+ from: string;
28
+ to: string;
29
+ }[];
30
+ /**
31
+ * Optional per-property transformer for object/array properties that the structural
32
+ * diff cannot handle automatically. Sourced from an ISchemaMigration override when
33
+ * one is registered in SchemaMigrationFactory for this step.
34
+ */
35
+ transformEntityProperty?: IMigrationOptions<T, U>["transformEntityProperty"];
36
+ }
@@ -0,0 +1,27 @@
1
+ import type { IMigrationOptions } from "./IMigrationOptions.js";
2
+ /**
3
+ * Optional per-step override for a single version-to-version migration.
4
+ * Only register an entry in SchemaMigrationFactory when a step requires property
5
+ * renames or a custom object/array transform. For purely structural changes
6
+ * (add/remove/type-change fields) no entry is needed — the runner diffs the two
7
+ * versioned schema classes (e.g. MyEntityV0 vs MyEntityV1) from EntitySchemaFactory
8
+ * automatically.
9
+ *
10
+ * Register under the key "<BaseSchemaName>_<fromVersion>_<toVersion>"
11
+ * e.g. "MyEntity_0_1" for the step that migrates from version 0 to version 1.
12
+ * The key itself encodes the version pair; no version field is needed on the object.
13
+ */
14
+ export interface ISchemaMigration<T = unknown, U = unknown> {
15
+ /**
16
+ * Optional property renames to apply during this step.
17
+ */
18
+ renames?: {
19
+ from: string;
20
+ to: string;
21
+ }[];
22
+ /**
23
+ * Optional per-property transformer for object/array properties that cannot be
24
+ * automatically coerced. T is the source entity type, U is the target entity type.
25
+ */
26
+ transformEntityProperty?: IMigrationOptions<T, U>["transformEntityProperty"];
27
+ }
package/docs/changelog.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.3-next.22](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-models-v0.0.3-next.21...entity-storage-models-v0.0.3-next.22) (2026-06-08)
4
+
5
+
6
+ ### Features
7
+
8
+ * add ISchemaMigration chain, SchemaVersionMigrator runner and version store ([#110](https://github.com/iotaledger/twin-entity-storage/issues/110)) ([2dac924](https://github.com/iotaledger/twin-entity-storage/commit/2dac9244a752cb58304d1649ff03c3a2469783dd))
9
+
3
10
  ## [0.0.3-next.21](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-models-v0.0.3-next.20...entity-storage-models-v0.0.3-next.21) (2026-06-01)
4
11
 
5
12
 
@@ -1,6 +1,8 @@
1
1
  # Class: MigrationHelper
2
2
 
3
- Helper class for performing schema migrations between two connectors.
3
+ Helper class for performing entity schema migrations between two connectors.
4
+ The chain-based API (migrateWithChain / applyEntityChain) is the single migration
5
+ path: a chain of one step covers the same case as a traditional single-step migration.
4
6
 
5
7
  ## Constructors
6
8
 
@@ -22,67 +24,12 @@ Runtime name for the class.
22
24
 
23
25
  ## Methods
24
26
 
25
- ### migrate() {#migrate}
26
-
27
- > `static` **migrate**\<`T`, `U`\>(`sourceConnector`, `targetEntitySchemaName`, `renames?`, `options?`, `loggingComponentType?`): `Promise`\<\{ `finalConnector?`: [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md)\<`U`\>; `migrated`: `number`; \}\>
28
-
29
- Performs a migration between two connectors, using the provided options and schema diff to control the migration behaviour.
30
-
31
- #### Type Parameters
32
-
33
- ##### T
34
-
35
- `T`
36
-
37
- ##### U
38
-
39
- `U`
40
-
41
- #### Parameters
42
-
43
- ##### sourceConnector
44
-
45
- [`IEntityStorageMigrationConnector`](../interfaces/IEntityStorageMigrationConnector.md)\<`T`\>
46
-
47
- The connector to migrate from to allow the migration helper to create the new connector and finalize the migration.
48
-
49
- ##### targetEntitySchemaName
50
-
51
- `string`
52
-
53
- The name of the new entity schema.
54
-
55
- ##### renames?
56
-
57
- `object`[]
58
-
59
- An optional list of property renames to apply during migration.
60
-
61
- ##### options?
62
-
63
- [`IMigrationOptions`](../interfaces/IMigrationOptions.md)\<`T`, `U`\>
64
-
65
- Options controlling migration behaviour.
66
-
67
- ##### loggingComponentType?
68
-
69
- `string`
70
-
71
- An optional logging component type to use for bootstrapping and starting connectors if necessary.
72
-
73
- #### Returns
74
-
75
- `Promise`\<\{ `finalConnector?`: [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md)\<`U`\>; `migrated`: `number`; \}\>
76
-
77
- The connector for the new schema and the number of entities successfully migrated, the sourceConnector will no longer be usable, finalConnector will be undefined if no migration was necessary.
78
-
79
- ***
80
-
81
- ### migrateEntities() {#migrateentities}
27
+ ### applyEntityTransform() {#applyentitytransform}
82
28
 
83
- > `static` **migrateEntities**\<`T`, `U`\>(`source`, `target`, `partitionContextIds`, `schemaDiff`, `options?`): `Promise`\<`number`\>
29
+ > `static` **applyEntityTransform**\<`T`, `U`\>(`entity`, `schemaDiff`, `transformEntityProperty?`): `U`
84
30
 
85
- Generic per-partition migration loop.
31
+ Applies the entity transformation for a single diff, handling added, removed, and
32
+ modified properties according to the provided schema diff and optional transform hook.
86
33
 
87
34
  #### Type Parameters
88
35
 
@@ -92,156 +39,119 @@ Generic per-partition migration loop.
92
39
 
93
40
  ##### U
94
41
 
95
- `U` = `T`
42
+ `U` = `unknown`
96
43
 
97
44
  #### Parameters
98
45
 
99
- ##### source
100
-
101
- [`IEntityStorageMigrationConnector`](../interfaces/IEntityStorageMigrationConnector.md)\<`T`\>
102
-
103
- Connector to read from (current schema, already bootstrapped).
104
-
105
- ##### target
106
-
107
- [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md)\<`U`\>
108
-
109
- Connector to write to (new schema, already bootstrapped).
110
-
111
- ##### partitionContextIds
46
+ ##### entity
112
47
 
113
- `IContextIds`[]
48
+ `Partial`\<`T`\>
114
49
 
115
- The context ids to use for the migration, used for partitioning and can be used in the transform function when `options.transformEntityProperty` is provided.
50
+ The entity to transform.
116
51
 
117
52
  ##### schemaDiff
118
53
 
119
54
  `IEntitySchemaDiff`\<`T`, `U`\>
120
55
 
121
- The schema diff.
56
+ The schema diff between the old and new schemas.
122
57
 
123
- ##### options?
58
+ ##### transformEntityProperty?
124
59
 
125
- [`IMigrationOptions`](../interfaces/IMigrationOptions.md)\<`T`, `U`\>
60
+ (`schema1Property`, `schemaProperty2`, `value`) => `unknown`
126
61
 
127
- Optional migration controls (batchSize, transformEntity, onProgress).
62
+ Optional per-property transform hook for object/array properties.
128
63
 
129
64
  #### Returns
130
65
 
131
- `Promise`\<`number`\>
132
-
133
- The number of entities successfully migrated.
66
+ `U`
134
67
 
135
- ***
68
+ The transformed entity ready to be written to the new schema.
136
69
 
137
- ### migratePartition() {#migratepartition}
70
+ #### Throws
138
71
 
139
- > `static` **migratePartition**\<`T`, `U`\>(`source`, `target`, `partitionTotal`, `partitionIndex`, `schemaDiff`, `options?`): `Promise`\<`number`\>
72
+ GeneralError if a transformation is required for an object or array property but no transformEntityProperty function is provided.
140
73
 
141
- Generic per-partition migration loop.
74
+ #### Throws
142
75
 
143
- #### Type Parameters
76
+ GeneralError if coercion of a modified property results in undefined for a non-optional target property.
144
77
 
145
- ##### T
78
+ ***
146
79
 
147
- `T` = `unknown`
80
+ ### applyEntityChain() {#applyentitychain}
148
81
 
149
- ##### U
82
+ > `static` **applyEntityChain**(`entity`, `steps`): `unknown`
150
83
 
151
- `U` = `unknown`
84
+ Transforms a single entity through an ordered chain of fully-resolved migration steps.
85
+ For each step the method diffs fromProperties against toProperties, then applies
86
+ applyEntityTransform. Each step's output feeds the next step's input so that
87
+ per-step transformEntityProperty hooks are honoured throughout the chain.
152
88
 
153
89
  #### Parameters
154
90
 
155
- ##### source
156
-
157
- [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md)\<`T`\>
158
-
159
- Connector to read from (current schema, already bootstrapped).
160
-
161
- ##### target
162
-
163
- [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md)\<`U`\>
164
-
165
- Connector to write to (new schema, already bootstrapped).
166
-
167
- ##### partitionTotal
168
-
169
- `number`
170
-
171
- The total number of partitions to migrate, used for progress reporting.
172
-
173
- ##### partitionIndex
174
-
175
- `number`
176
-
177
- The index of the current partition being migrated, used for progress reporting.
178
-
179
- ##### schemaDiff
91
+ ##### entity
180
92
 
181
- `IEntitySchemaDiff`\<`T`, `U`\>
93
+ `unknown`
182
94
 
183
- Schema diff used to add nullable defaults and drop removed fields when `options.transformEntity` is not provided.
95
+ The entity to transform (at the shape described by steps[0].fromProperties).
184
96
 
185
- ##### options?
97
+ ##### steps
186
98
 
187
- [`IMigrationOptions`](../interfaces/IMigrationOptions.md)\<`T`, `U`\>
99
+ [`IResolvedMigrationStep`](../interfaces/IResolvedMigrationStep.md)\<`unknown`, `unknown`\>[]
188
100
 
189
- Optional migration controls (batchSize, transformEntity, onProgress).
101
+ Ordered, fully-resolved migration steps from stored version to current version.
102
+ Each step's fromProperties and toProperties are resolved by the caller before invocation.
190
103
 
191
104
  #### Returns
192
105
 
193
- `Promise`\<`number`\>
106
+ `unknown`
194
107
 
195
- The number of entities successfully migrated.
108
+ The entity transformed to the shape described by steps[last].toProperties.
196
109
 
197
110
  ***
198
111
 
199
- ### applyEntityTransform() {#applyentitytransform}
112
+ ### migrateWithChain() {#migratewithchain}
200
113
 
201
- > `static` **applyEntityTransform**\<`T`, `U`\>(`entity`, `schemaDiff`, `options?`): `U`
114
+ > `static` **migrateWithChain**(`sourceConnector`, `targetSchemaName`, `steps`, `loggingComponentType?`, `batchSize?`): `Promise`\<\{ `finalConnector`: [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md); `migrated`: `number`; \}\>
202
115
 
203
- Applies the entity transformation for migration, using the provided options and schema diff.
116
+ Performs a chain migration in a single connector swap, regardless of how many version
117
+ steps the chain spans. Creates one target connector, reads all source entities, applies
118
+ applyEntityChain to each, writes them to the target, then finalizes the migration.
119
+ A chain of one step is equivalent to a traditional single-step migration.
204
120
 
205
- #### Type Parameters
206
-
207
- ##### T
208
-
209
- `T` = `unknown`
210
-
211
- ##### U
121
+ #### Parameters
212
122
 
213
- `U` = `unknown`
123
+ ##### sourceConnector
214
124
 
215
- #### Parameters
125
+ [`IEntityStorageMigrationConnector`](../interfaces/IEntityStorageMigrationConnector.md)
216
126
 
217
- ##### entity
127
+ The connector holding data at the stored schema version.
218
128
 
219
- `Partial`\<`T`\>
129
+ ##### targetSchemaName
220
130
 
221
- The entity to transform.
131
+ `string`
222
132
 
223
- ##### schemaDiff
133
+ The schema name for the current version (used to create the target connector).
224
134
 
225
- `IEntitySchemaDiff`\<`T`, `U`\>
135
+ ##### steps
226
136
 
227
- The schema diff between the old and new schemas.
137
+ [`IResolvedMigrationStep`](../interfaces/IResolvedMigrationStep.md)\<`unknown`, `unknown`\>[]
228
138
 
229
- ##### options?
139
+ Ordered, fully-resolved migration steps from stored to current version.
230
140
 
231
- [`IMigrationOptions`](../interfaces/IMigrationOptions.md)\<`T`, `U`\>
141
+ ##### loggingComponentType?
232
142
 
233
- The migration options.
143
+ `string`
234
144
 
235
- #### Returns
145
+ An optional logging component type for connector startup.
236
146
 
237
- `U`
147
+ ##### batchSize?
238
148
 
239
- The transformed entity ready to be written to the new schema.
149
+ `number` = `100`
240
150
 
241
- #### Throws
151
+ Number of entities to read and write per batch. Defaults to 100.
242
152
 
243
- GeneralError if a transformation is required for an object or array property but no `options.transformEntityProperty` function is provided.
153
+ #### Returns
244
154
 
245
- #### Throws
155
+ `Promise`\<\{ `finalConnector`: [`IEntityStorageConnector`](../interfaces/IEntityStorageConnector.md); `migrated`: `number`; \}\>
246
156
 
247
- GeneralError if coercion of a modified property results in undefined for a non-optional target property.
157
+ The finalized connector and the count of migrated entities.
@@ -11,6 +11,8 @@
11
11
  - [IEntityStorageConnector](interfaces/IEntityStorageConnector.md)
12
12
  - [IEntityStorageMigrationConnector](interfaces/IEntityStorageMigrationConnector.md)
13
13
  - [IMigrationOptions](interfaces/IMigrationOptions.md)
14
+ - [IResolvedMigrationStep](interfaces/IResolvedMigrationStep.md)
15
+ - [ISchemaMigration](interfaces/ISchemaMigration.md)
14
16
  - [IEntityStorageCountRequest](interfaces/IEntityStorageCountRequest.md)
15
17
  - [IEntityStorageCountResponse](interfaces/IEntityStorageCountResponse.md)
16
18
  - [IEntityStorageEmptyRequest](interfaces/IEntityStorageEmptyRequest.md)
@@ -26,3 +28,4 @@
26
28
  ## Variables
27
29
 
28
30
  - [EntityStorageConnectorFactory](variables/EntityStorageConnectorFactory.md)
31
+ - [SchemaMigrationFactory](variables/SchemaMigrationFactory.md)
@@ -0,0 +1,82 @@
1
+ # Interface: IResolvedMigrationStep\<T, U\>
2
+
3
+ A fully-resolved single migration step used by MigrationHelper.
4
+ The SchemaVersionService builds these by looking up versioned schema classes
5
+ from EntitySchemaFactory (e.g. MyEntityV0, MyEntityV1) before invoking the helper,
6
+ keeping factory knowledge out of the helper itself.
7
+
8
+ ## Type Parameters
9
+
10
+ ### T
11
+
12
+ `T` = `unknown`
13
+
14
+ The entity type. Defaults to `unknown`. Use a concrete entity type
15
+ when the step's source and target schemas are known at the call site.
16
+
17
+ ### U
18
+
19
+ `U` = `unknown`
20
+
21
+ ## Properties
22
+
23
+ ### fromProperties {#fromproperties}
24
+
25
+ > **fromProperties**: `IEntitySchemaProperty`\<`T`\>[]
26
+
27
+ The property list of the entity at the start of this step (the "old" shape).
28
+ Sourced from the versioned schema class registered in EntitySchemaFactory,
29
+ e.g. EntitySchemaFactory.get("MyEntityV0").properties.
30
+
31
+ ***
32
+
33
+ ### toProperties {#toproperties}
34
+
35
+ > **toProperties**: `IEntitySchemaProperty`\<`U`\>[]
36
+
37
+ The property list of the entity at the end of this step (the "new" shape).
38
+ For the final step this is the live current schema's properties.
39
+
40
+ ***
41
+
42
+ ### renames? {#renames}
43
+
44
+ > `optional` **renames?**: `object`[]
45
+
46
+ Optional property renames for this step, forwarded to EntitySchemaDiffHelper.diff.
47
+
48
+ #### from
49
+
50
+ > **from**: `string`
51
+
52
+ #### to
53
+
54
+ > **to**: `string`
55
+
56
+ ***
57
+
58
+ ### transformEntityProperty? {#transformentityproperty}
59
+
60
+ > `optional` **transformEntityProperty?**: (`schema1Property`, `schemaProperty2`, `value`) => `unknown`
61
+
62
+ Optional per-property transformer for object/array properties that the structural
63
+ diff cannot handle automatically. Sourced from an ISchemaMigration override when
64
+ one is registered in SchemaMigrationFactory for this step.
65
+
66
+ #### Parameters
67
+
68
+ ##### schema1Property
69
+
70
+ `IEntitySchemaProperty`\<`T`\>
71
+
72
+ ##### schemaProperty2
73
+
74
+ `IEntitySchemaProperty`\<`U`\>
75
+
76
+ ##### value
77
+
78
+ `unknown`
79
+
80
+ #### Returns
81
+
82
+ `unknown`
@@ -0,0 +1,65 @@
1
+ # Interface: ISchemaMigration\<T, U\>
2
+
3
+ Optional per-step override for a single version-to-version migration.
4
+ Only register an entry in SchemaMigrationFactory when a step requires property
5
+ renames or a custom object/array transform. For purely structural changes
6
+ (add/remove/type-change fields) no entry is needed — the runner diffs the two
7
+ versioned schema classes (e.g. MyEntityV0 vs MyEntityV1) from EntitySchemaFactory
8
+ automatically.
9
+
10
+ Register under the key "<BaseSchemaName>_<fromVersion>_<toVersion>"
11
+ e.g. "MyEntity_0_1" for the step that migrates from version 0 to version 1.
12
+ The key itself encodes the version pair; no version field is needed on the object.
13
+
14
+ ## Type Parameters
15
+
16
+ ### T
17
+
18
+ `T` = `unknown`
19
+
20
+ ### U
21
+
22
+ `U` = `unknown`
23
+
24
+ ## Properties
25
+
26
+ ### renames? {#renames}
27
+
28
+ > `optional` **renames?**: `object`[]
29
+
30
+ Optional property renames to apply during this step.
31
+
32
+ #### from
33
+
34
+ > **from**: `string`
35
+
36
+ #### to
37
+
38
+ > **to**: `string`
39
+
40
+ ***
41
+
42
+ ### transformEntityProperty? {#transformentityproperty}
43
+
44
+ > `optional` **transformEntityProperty?**: (`schema1Property`, `schemaProperty2`, `value`) => `unknown`
45
+
46
+ Optional per-property transformer for object/array properties that cannot be
47
+ automatically coerced. T is the source entity type, U is the target entity type.
48
+
49
+ #### Parameters
50
+
51
+ ##### schema1Property
52
+
53
+ `IEntitySchemaProperty`\<`T`\>
54
+
55
+ ##### schemaProperty2
56
+
57
+ `IEntitySchemaProperty`\<`U`\>
58
+
59
+ ##### value
60
+
61
+ `unknown`
62
+
63
+ #### Returns
64
+
65
+ `unknown`
@@ -0,0 +1,13 @@
1
+ # Variable: SchemaMigrationFactory
2
+
3
+ > `const` **SchemaMigrationFactory**: `Factory`\<[`ISchemaMigration`](../interfaces/ISchemaMigration.md)\<`unknown`, `unknown`\>\>
4
+
5
+ Factory for optional per-step migration overrides.
6
+
7
+ Only register an entry when a version step requires property renames or a custom
8
+ transform hook. For purely structural changes (add/remove/type-change) the
9
+ SchemaVersionService diffs the two versioned schema classes automatically
10
+ without needing any factory entry.
11
+
12
+ Keys follow the convention "<BaseSchemaName>_<fromVersion>_<toVersion>",
13
+ for example "MyEntity_0_1" for the step that migrates MyEntity from version 0 to 1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/entity-storage-models",
3
- "version": "0.0.3-next.21",
3
+ "version": "0.0.3-next.22",
4
4
  "description": "Shared models for storage contracts, requests, responses and connector capabilities.",
5
5
  "repository": {
6
6
  "type": "git",