@twin.org/entity-storage-models 0.0.3-next.21 → 0.0.3-next.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/factories/schemaMigrationFactory.js +17 -0
- package/dist/es/factories/schemaMigrationFactory.js.map +1 -0
- package/dist/es/helpers/entityStorageHelper.js +42 -1
- package/dist/es/helpers/entityStorageHelper.js.map +1 -1
- package/dist/es/helpers/migrationHelper.js +102 -129
- package/dist/es/helpers/migrationHelper.js.map +1 -1
- package/dist/es/index.js +4 -1
- package/dist/es/index.js.map +1 -1
- package/dist/es/models/IResolvedMigrationStep.js +2 -0
- package/dist/es/models/IResolvedMigrationStep.js.map +1 -0
- package/dist/es/models/ISchemaMigration.js +2 -0
- package/dist/es/models/ISchemaMigration.js.map +1 -0
- package/dist/types/factories/schemaMigrationFactory.d.ts +14 -0
- package/dist/types/helpers/entityStorageHelper.d.ts +20 -1
- package/dist/types/helpers/migrationHelper.d.ts +36 -43
- package/dist/types/index.d.ts +3 -0
- package/dist/types/models/IResolvedMigrationStep.d.ts +36 -0
- package/dist/types/models/ISchemaMigration.d.ts +27 -0
- package/docs/changelog.md +14 -0
- package/docs/reference/classes/EntityStorageHelper.md +74 -0
- package/docs/reference/classes/MigrationHelper.md +62 -152
- package/docs/reference/index.md +3 -0
- package/docs/reference/interfaces/IResolvedMigrationStep.md +82 -0
- package/docs/reference/interfaces/ISchemaMigration.md +65 -0
- package/docs/reference/variables/SchemaMigrationFactory.md +13 -0
- package/locales/en.json +4 -0
- package/package.json +1 -1
|
@@ -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"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Copyright 2026 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
-
import { Is, ObjectHelper } from "@twin.org/core";
|
|
3
|
+
import { GeneralError, Is, ObjectHelper } from "@twin.org/core";
|
|
4
4
|
import { ComparisonOperator, EntitySchemaHelper } from "@twin.org/entity";
|
|
5
5
|
/**
|
|
6
6
|
* Helper class for performing schema migrations between two connectors.
|
|
@@ -60,6 +60,47 @@ export class EntityStorageHelper {
|
|
|
60
60
|
}
|
|
61
61
|
return nonNullEntity;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate that every sort property in the list is indexed in the schema (isPrimary, isSecondary,
|
|
65
|
+
* or has a default sortDirection), throwing sortNotIndexed for the first violation found.
|
|
66
|
+
* @param schema The entity schema to validate against.
|
|
67
|
+
* @param sortProperties The sort properties to check.
|
|
68
|
+
* @throws GeneralError If a sort property is not indexed in the schema.
|
|
69
|
+
*/
|
|
70
|
+
static validateSortProperties(schema, sortProperties) {
|
|
71
|
+
if (Is.arrayValue(sortProperties)) {
|
|
72
|
+
for (const sortProperty of sortProperties) {
|
|
73
|
+
const propertySchema = schema.properties?.find(p => p.property === sortProperty.property);
|
|
74
|
+
if (Is.undefined(propertySchema) ||
|
|
75
|
+
(!propertySchema.isPrimary &&
|
|
76
|
+
!propertySchema.isSecondary &&
|
|
77
|
+
Is.empty(propertySchema.sortDirection))) {
|
|
78
|
+
throw new GeneralError(EntityStorageHelper.CLASS_NAME, "sortNotIndexed", {
|
|
79
|
+
property: sortProperty.property
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Validate that every property in the list exists in the schema, throwing propertyNotInSchema
|
|
87
|
+
* for the first property that is not found.
|
|
88
|
+
* @param schema The entity schema to validate against.
|
|
89
|
+
* @param properties The properties to check.
|
|
90
|
+
* @throws GeneralError If a property does not exist in the schema.
|
|
91
|
+
*/
|
|
92
|
+
static validateProperties(schema, properties) {
|
|
93
|
+
if (Is.arrayValue(properties)) {
|
|
94
|
+
for (const property of properties) {
|
|
95
|
+
const propertySchema = schema.properties?.find(p => p.property === property);
|
|
96
|
+
if (Is.undefined(propertySchema)) {
|
|
97
|
+
throw new GeneralError(EntityStorageHelper.CLASS_NAME, "propertyNotInSchema", {
|
|
98
|
+
property
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
63
104
|
/**
|
|
64
105
|
* Deep-clone condition tree and normalise null/undefined to undefined on Equals/NotEquals leaves
|
|
65
106
|
* so in-memory evaluation matches stored-absent semantics (optional absent props are omitted/undefined).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entityStorageHelper.js","sourceRoot":"","sources":["../../../src/helpers/entityStorageHelper.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"entityStorageHelper.js","sourceRoot":"","sources":["../../../src/helpers/entityStorageHelper.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { GeneralError, Is, ObjectHelper } from \"@twin.org/core\";\nimport {\n\tComparisonOperator,\n\ttype EntityCondition,\n\tEntitySchemaHelper,\n\ttype IEntitySchema,\n\ttype SortDirection\n} from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\n\n/**\n * Helper class for performing schema migrations between two connectors.\n */\nexport class EntityStorageHelper {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<EntityStorageHelper>();\n\n\t/**\n\t * Prepare the entity by handling undefined and null values and validating it against the schema.\n\t * @param entity The entity to handle undefined and null values for.\n\t * @param schema The schema to validate the entity against.\n\t * @param additionalProperties Optional list of additional properties to set on the entity.\n\t * @param options Options controlling how null/undefined optional properties are stored.\n\t * @param options.nullBehavior \"omit\" strips null/undefined optional properties before writing\n\t * (NoSQL — avoids index-key type errors). \"nullify\" converts undefined to null (SQL — the default).\n\t * @returns The entity with undefined and null values handled.\n\t */\n\tpublic static prepareEntity<T>(\n\t\tentity: T,\n\t\tschema: IEntitySchema<T>,\n\t\tadditionalProperties?: { property: string; value: unknown }[],\n\t\toptions?: { nullBehavior?: \"omit\" | \"nullify\" }\n\t): T {\n\t\tconst entityForValidation = ObjectHelper.clone(entity);\n\t\tEntitySchemaHelper.validateEntity(entityForValidation, schema);\n\n\t\tif (Is.arrayValue(schema.properties)) {\n\t\t\tfor (const property of schema.properties) {\n\t\t\t\tif (property.optional ?? false) {\n\t\t\t\t\tconst propValue = entityForValidation[property.property];\n\t\t\t\t\tif (options?.nullBehavior === \"omit\") {\n\t\t\t\t\t\tif (propValue === undefined || propValue === null) {\n\t\t\t\t\t\t\tObjectHelper.propertyDelete(entityForValidation, property.property as string);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (propValue === undefined) {\n\t\t\t\t\t\tObjectHelper.propertySet(entityForValidation, property.property as string, null);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (Is.arrayValue(additionalProperties)) {\n\t\t\tfor (const additionalProperty of additionalProperties) {\n\t\t\t\tObjectHelper.propertySet(\n\t\t\t\t\tentityForValidation,\n\t\t\t\t\tadditionalProperty.property,\n\t\t\t\t\tadditionalProperty.value\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn entityForValidation;\n\t}\n\n\t/**\n\t * Un-prepare the entity by removing null values.\n\t * @param entity The entity to handle undefined and null values for.\n\t * @param removeProperties Optional list of properties to remove from the entity.\n\t * @returns The entity with undefined and null values handled.\n\t */\n\tpublic static unPrepareEntity<T>(entity: Partial<T> | undefined, removeProperties?: string[]): T {\n\t\tconst nonNullEntity = ObjectHelper.removeEmptyProperties(entity, { removeNull: true });\n\n\t\tif (Is.arrayValue(removeProperties)) {\n\t\t\tfor (const property of removeProperties) {\n\t\t\t\tObjectHelper.propertyDelete(nonNullEntity, property);\n\t\t\t}\n\t\t}\n\n\t\treturn nonNullEntity as T;\n\t}\n\n\t/**\n\t * Validate that every sort property in the list is indexed in the schema (isPrimary, isSecondary,\n\t * or has a default sortDirection), throwing sortNotIndexed for the first violation found.\n\t * @param schema The entity schema to validate against.\n\t * @param sortProperties The sort properties to check.\n\t * @throws GeneralError If a sort property is not indexed in the schema.\n\t */\n\tpublic static validateSortProperties<T>(\n\t\tschema: IEntitySchema<T>,\n\t\tsortProperties?: { property: keyof T; sortDirection: SortDirection }[]\n\t): void {\n\t\tif (Is.arrayValue(sortProperties)) {\n\t\t\tfor (const sortProperty of sortProperties) {\n\t\t\t\tconst propertySchema = schema.properties?.find(p => p.property === sortProperty.property);\n\t\t\t\tif (\n\t\t\t\t\tIs.undefined(propertySchema) ||\n\t\t\t\t\t(!propertySchema.isPrimary &&\n\t\t\t\t\t\t!propertySchema.isSecondary &&\n\t\t\t\t\t\tIs.empty(propertySchema.sortDirection))\n\t\t\t\t) {\n\t\t\t\t\tthrow new GeneralError(EntityStorageHelper.CLASS_NAME, \"sortNotIndexed\", {\n\t\t\t\t\t\tproperty: sortProperty.property\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Validate that every property in the list exists in the schema, throwing propertyNotInSchema\n\t * for the first property that is not found.\n\t * @param schema The entity schema to validate against.\n\t * @param properties The properties to check.\n\t * @throws GeneralError If a property does not exist in the schema.\n\t */\n\tpublic static validateProperties<T>(schema: IEntitySchema<T>, properties?: (keyof T)[]): void {\n\t\tif (Is.arrayValue(properties)) {\n\t\t\tfor (const property of properties) {\n\t\t\t\tconst propertySchema = schema.properties?.find(p => p.property === property);\n\t\t\t\tif (Is.undefined(propertySchema)) {\n\t\t\t\t\tthrow new GeneralError(EntityStorageHelper.CLASS_NAME, \"propertyNotInSchema\", {\n\t\t\t\t\t\tproperty\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Deep-clone condition tree and normalise null/undefined to undefined on Equals/NotEquals leaves\n\t * so in-memory evaluation matches stored-absent semantics (optional absent props are omitted/undefined).\n\t * @param condition The user-supplied condition (not mutated).\n\t * @returns A clone safe to pass to check.\n\t */\n\tpublic static normalizeConditionValues<T>(condition: EntityCondition<T>): EntityCondition<T> {\n\t\tif (\"conditions\" in condition) {\n\t\t\treturn {\n\t\t\t\t...condition,\n\t\t\t\tconditions: condition.conditions.map(c => EntityStorageHelper.normalizeConditionValues(c))\n\t\t\t};\n\t\t}\n\n\t\tconst leaf = condition;\n\t\tif (\n\t\t\t(leaf.comparison === ComparisonOperator.Equals ||\n\t\t\t\tleaf.comparison === ComparisonOperator.NotEquals) &&\n\t\t\t(leaf.value === undefined || leaf.value === null)\n\t\t) {\n\t\t\treturn { ...leaf, value: undefined };\n\t\t}\n\t\treturn { ...leaf };\n\t}\n}\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
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
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 =
|
|
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
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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":"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","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
|
|
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
|
package/dist/es/index.js.map
CHANGED
|
@@ -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
|
|
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 @@
|
|
|
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 @@
|
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { type EntityCondition, type IEntitySchema } from "@twin.org/entity";
|
|
1
|
+
import { type EntityCondition, type IEntitySchema, type SortDirection } from "@twin.org/entity";
|
|
2
2
|
/**
|
|
3
3
|
* Helper class for performing schema migrations between two connectors.
|
|
4
4
|
*/
|
|
@@ -30,6 +30,25 @@ export declare class EntityStorageHelper {
|
|
|
30
30
|
* @returns The entity with undefined and null values handled.
|
|
31
31
|
*/
|
|
32
32
|
static unPrepareEntity<T>(entity: Partial<T> | undefined, removeProperties?: string[]): T;
|
|
33
|
+
/**
|
|
34
|
+
* Validate that every sort property in the list is indexed in the schema (isPrimary, isSecondary,
|
|
35
|
+
* or has a default sortDirection), throwing sortNotIndexed for the first violation found.
|
|
36
|
+
* @param schema The entity schema to validate against.
|
|
37
|
+
* @param sortProperties The sort properties to check.
|
|
38
|
+
* @throws GeneralError If a sort property is not indexed in the schema.
|
|
39
|
+
*/
|
|
40
|
+
static validateSortProperties<T>(schema: IEntitySchema<T>, sortProperties?: {
|
|
41
|
+
property: keyof T;
|
|
42
|
+
sortDirection: SortDirection;
|
|
43
|
+
}[]): void;
|
|
44
|
+
/**
|
|
45
|
+
* Validate that every property in the list exists in the schema, throwing propertyNotInSchema
|
|
46
|
+
* for the first property that is not found.
|
|
47
|
+
* @param schema The entity schema to validate against.
|
|
48
|
+
* @param properties The properties to check.
|
|
49
|
+
* @throws GeneralError If a property does not exist in the schema.
|
|
50
|
+
*/
|
|
51
|
+
static validateProperties<T>(schema: IEntitySchema<T>, properties?: (keyof T)[]): void;
|
|
33
52
|
/**
|
|
34
53
|
* Deep-clone condition tree and normalise null/undefined to undefined on Equals/NotEquals leaves
|
|
35
54
|
* so in-memory evaluation matches stored-absent semantics (optional absent props are omitted/undefined).
|