@twin.org/entity-storage-service 0.0.3-next.23 → 0.0.3-next.25

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.
@@ -1,21 +1,18 @@
1
1
  // Copyright 2026 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
- import { entity, property, EntitySchemaPropertyFormat } from "@twin.org/entity";
3
+ import { entity, property } from "@twin.org/entity";
4
4
  /**
5
- * Entity that records the currently applied schema version for a managed entity schema.
6
- * Persisted through a normal entity-storage connector, giving every backend a schemaVersion
7
- * table/collection for free.
8
- *
9
- * SchemaVersionService processes this schema first before all others so that the version
10
- * store is fully migrated before any version records are written for other schemas.
5
+ * Tracks the currently applied schema version for each managed entity schema.
6
+ * One record per schema name. Written once on first boot, then updated after
7
+ * each successful migration.
11
8
  */
12
9
  let SchemaVersion = class SchemaVersion {
13
10
  /**
14
- * The schema type name (matches the key used in EntitySchemaFactory).
11
+ * The entity schema type name primary key.
15
12
  */
16
13
  schemaName;
17
14
  /**
18
- * The version currently applied in storage for this schema.
15
+ * The currently deployed version of this schema.
19
16
  */
20
17
  version;
21
18
  /**
@@ -32,7 +29,7 @@ __decorate([
32
29
  __metadata("design:type", Number)
33
30
  ], SchemaVersion.prototype, "version", void 0);
34
31
  __decorate([
35
- property({ type: "string", format: EntitySchemaPropertyFormat.DateTime }),
32
+ property({ type: "string", format: "date-time" }),
36
33
  __metadata("design:type", String)
37
34
  ], SchemaVersion.prototype, "updatedAt", void 0);
38
35
  SchemaVersion = __decorate([
@@ -1 +1 @@
1
- {"version":3,"file":"schemaVersion.js","sourceRoot":"","sources":["../../../src/entities/schemaVersion.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,0BAA0B,EAAE,MAAM,kBAAkB,CAAC;AAEhF;;;;;;;GAOG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAa;IACzB;;OAEG;IAEI,UAAU,CAAU;IAE3B;;OAEG;IAEI,OAAO,CAAU;IAExB;;OAEG;IAEI,SAAS,CAAU;CAC1B,CAAA;AAbO;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;;iDACnB;AAMpB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;8CACN;AAMjB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,0BAA0B,CAAC,QAAQ,EAAE,CAAC;;gDAChD;AAjBd,aAAa;IADzB,MAAM,EAAE;GACI,aAAa,CAkBzB","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { entity, property, EntitySchemaPropertyFormat } from \"@twin.org/entity\";\n\n/**\n * Entity that records the currently applied schema version for a managed entity schema.\n * Persisted through a normal entity-storage connector, giving every backend a schemaVersion\n * table/collection for free.\n *\n * SchemaVersionService processes this schema first before all others so that the version\n * store is fully migrated before any version records are written for other schemas.\n */\n@entity()\nexport class SchemaVersion {\n\t/**\n\t * The schema type name (matches the key used in EntitySchemaFactory).\n\t */\n\t@property({ type: \"string\", isPrimary: true })\n\tpublic schemaName!: string;\n\n\t/**\n\t * The version currently applied in storage for this schema.\n\t */\n\t@property({ type: \"integer\" })\n\tpublic version!: number;\n\n\t/**\n\t * ISO 8601 timestamp of the last version write.\n\t */\n\t@property({ type: \"string\", format: EntitySchemaPropertyFormat.DateTime })\n\tpublic updatedAt!: string;\n}\n"]}
1
+ {"version":3,"file":"schemaVersion.js","sourceRoot":"","sources":["../../../src/entities/schemaVersion.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;GAIG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAa;IACzB;;OAEG;IAEI,UAAU,CAAU;IAE3B;;OAEG;IAEI,OAAO,CAAU;IAExB;;OAEG;IAEI,SAAS,CAAU;CAC1B,CAAA;AAbO;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;;iDACnB;AAMpB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;8CACN;AAMjB;IADN,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;;gDACxB;AAjBd,aAAa;IADzB,MAAM,EAAE;GACI,aAAa,CAkBzB","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { entity, property } from \"@twin.org/entity\";\n\n/**\n * Tracks the currently applied schema version for each managed entity schema.\n * One record per schema name. Written once on first boot, then updated after\n * each successful migration.\n */\n@entity()\nexport class SchemaVersion {\n\t/**\n\t * The entity schema type name primary key.\n\t */\n\t@property({ type: \"string\", isPrimary: true })\n\tpublic schemaName!: string;\n\n\t/**\n\t * The currently deployed version of this schema.\n\t */\n\t@property({ type: \"integer\" })\n\tpublic version!: number;\n\n\t/**\n\t * ISO 8601 timestamp of the last version write.\n\t */\n\t@property({ type: \"string\", format: \"date-time\" })\n\tpublic updatedAt!: string;\n}\n"]}
package/dist/es/index.js CHANGED
@@ -6,8 +6,9 @@ export * from "./entityStorageService.js";
6
6
  export * from "./models/IEntityStorageRoutesExamples.js";
7
7
  export * from "./models/IEntityStorageServiceConfig.js";
8
8
  export * from "./models/IEntityStorageServiceConstructorOptions.js";
9
+ export * from "./models/ISchemaVersionServiceConfig.js";
9
10
  export * from "./models/ISchemaVersionServiceConstructorOptions.js";
10
11
  export * from "./restEntryPoints.js";
11
- export * from "./schemaVersionService.js";
12
12
  export * from "./schema.js";
13
+ export * from "./schemaVersionService.js";
13
14
  //# 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,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qDAAqD,CAAC;AACpE,cAAc,qDAAqD,CAAC;AACpE,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,aAAa,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./entities/schemaVersion.js\";\nexport * from \"./entityStorageRoutes.js\";\nexport * from \"./entityStorageService.js\";\nexport * from \"./models/IEntityStorageRoutesExamples.js\";\nexport * from \"./models/IEntityStorageServiceConfig.js\";\nexport * from \"./models/IEntityStorageServiceConstructorOptions.js\";\nexport * from \"./models/ISchemaVersionServiceConstructorOptions.js\";\nexport * from \"./restEntryPoints.js\";\nexport * from \"./schemaVersionService.js\";\nexport * from \"./schema.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,0BAA0B,CAAC;AACzC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,0CAA0C,CAAC;AACzD,cAAc,yCAAyC,CAAC;AACxD,cAAc,qDAAqD,CAAC;AACpE,cAAc,yCAAyC,CAAC;AACxD,cAAc,qDAAqD,CAAC;AACpE,cAAc,sBAAsB,CAAC;AACrC,cAAc,aAAa,CAAC;AAC5B,cAAc,2BAA2B,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./entities/schemaVersion.js\";\nexport * from \"./entityStorageRoutes.js\";\nexport * from \"./entityStorageService.js\";\nexport * from \"./models/IEntityStorageRoutesExamples.js\";\nexport * from \"./models/IEntityStorageServiceConfig.js\";\nexport * from \"./models/IEntityStorageServiceConstructorOptions.js\";\nexport * from \"./models/ISchemaVersionServiceConfig.js\";\nexport * from \"./models/ISchemaVersionServiceConstructorOptions.js\";\nexport * from \"./restEntryPoints.js\";\nexport * from \"./schema.js\";\nexport * from \"./schemaVersionService.js\";\n"]}
@@ -0,0 +1,4 @@
1
+ // Copyright 2026 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ export {};
4
+ //# sourceMappingURL=ISchemaVersionServiceConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ISchemaVersionServiceConfig.js","sourceRoot":"","sources":["../../../src/models/ISchemaVersionServiceConfig.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Constructor options config for SchemaVersionService.\n */\nexport interface ISchemaVersionServiceConfig {\n\t/**\n\t * The batch size for processing schema versions.\n\t */\n\tbatchSize?: number;\n}\n"]}
@@ -1,4 +1,2 @@
1
- // Copyright 2026 IOTA Stiftung.
2
- // SPDX-License-Identifier: Apache-2.0.
3
1
  export {};
4
2
  //# sourceMappingURL=ISchemaVersionServiceConstructorOptions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ISchemaVersionServiceConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/ISchemaVersionServiceConstructorOptions.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Constructor options for SchemaVersionService.\n */\nexport interface ISchemaVersionServiceConstructorOptions {\n\t/**\n\t * The version storage type.\n\t * @default schema-version\n\t */\n\tschemaVersionStorageType?: string;\n}\n"]}
1
+ {"version":3,"file":"ISchemaVersionServiceConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/ISchemaVersionServiceConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { ISchemaVersionServiceConfig } from \"./ISchemaVersionServiceConfig.js\";\n\n/**\n * Constructor options for SchemaVersionService.\n */\nexport interface ISchemaVersionServiceConstructorOptions {\n\t/**\n\t * The version storage type.\n\t * @default schema-version\n\t */\n\tschemaVersionStorageType?: string;\n\n\t/**\n\t * Optional config.\n\t */\n\tconfig?: ISchemaVersionServiceConfig;\n}\n"]}
package/dist/es/schema.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { EntitySchemaFactory, EntitySchemaHelper } from "@twin.org/entity";
4
4
  import { SchemaVersion } from "./entities/schemaVersion.js";
5
5
  /**
6
- * Initialize the schema for the entity storage.
6
+ * Initialize the schema for the entity storage models.
7
7
  */
8
8
  export function initSchema() {
9
9
  EntitySchemaFactory.register("SchemaVersion", () => EntitySchemaHelper.getSchema(SchemaVersion));
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/schema.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D;;GAEG;AACH,MAAM,UAAU,UAAU;IACzB,mBAAmB,CAAC,QAAQ,kBAA0B,GAAG,EAAE,CAC1D,kBAAkB,CAAC,SAAS,CAAC,aAAa,CAAC,CAC3C,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { EntitySchemaFactory, EntitySchemaHelper } from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { SchemaVersion } from \"./entities/schemaVersion.js\";\n\n/**\n * Initialize the schema for the entity storage.\n */\nexport function initSchema(): void {\n\tEntitySchemaFactory.register(nameof<SchemaVersion>(), () =>\n\t\tEntitySchemaHelper.getSchema(SchemaVersion)\n\t);\n}\n"]}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/schema.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D;;GAEG;AACH,MAAM,UAAU,UAAU;IACzB,mBAAmB,CAAC,QAAQ,kBAA0B,GAAG,EAAE,CAC1D,kBAAkB,CAAC,SAAS,CAAC,aAAa,CAAC,CAC3C,CAAC;AACH,CAAC","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { EntitySchemaFactory, EntitySchemaHelper } from \"@twin.org/entity\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { SchemaVersion } from \"./entities/schemaVersion.js\";\n\n/**\n * Initialize the schema for the entity storage models.\n */\nexport function initSchema(): void {\n\tEntitySchemaFactory.register(nameof<SchemaVersion>(), () =>\n\t\tEntitySchemaHelper.getSchema(SchemaVersion)\n\t);\n}\n"]}
@@ -1,16 +1,18 @@
1
1
  // Copyright 2026 IOTA Stiftung.
2
2
  // SPDX-License-Identifier: Apache-2.0.
3
- import { GeneralError, Is } from "@twin.org/core";
3
+ import { ComponentFactory, GeneralError, Is } from "@twin.org/core";
4
4
  import { EntitySchemaFactory, EntitySchemaHelper } from "@twin.org/entity";
5
5
  import { EntityStorageConnectorFactory, MigrationHelper, SchemaMigrationFactory } from "@twin.org/entity-storage-models";
6
6
  import { SchemaVersion } from "./entities/schemaVersion.js";
7
7
  /**
8
8
  * Service that checks and applies entity schema migrations at every node start-up.
9
9
  *
10
- * This service should be registered as the first component so that its start() runs before
11
- * any other service. By the time start() is called, all component bootstraps have completed
12
- * (every table already exists) and EntitySchemaFactory / EntityStorageConnectorFactory are
13
- * fully populated with every registered schema and connector.
10
+ * This service must be the first entry in coreTypeInitialisers.json. The engine iterates that
11
+ * array in order to determine start sequence — there is no engine-level priority mechanism, so
12
+ * registration position is the only guarantee that start() runs before any other service.
13
+ * By the time start() is called, all component bootstraps have completed (every table already
14
+ * exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every
15
+ * registered schema and connector.
14
16
  *
15
17
  * Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming
16
18
  * convention — current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
@@ -38,15 +40,22 @@ export class SchemaVersionService {
38
40
  static _VERSION_SUFFIX_RE = /^(.+)V(\d+)$/;
39
41
  /**
40
42
  * The connector used to read and write SchemaVersion records.
43
+ * Not readonly because finalizeMigration may return a replacement connector object.
41
44
  * @internal
42
45
  */
43
- _schemaVersionConnector;
46
+ _versionConnector;
47
+ /**
48
+ * Optional config passed through constructor options.
49
+ * @internal
50
+ */
51
+ _config;
44
52
  /**
45
53
  * Create a new SchemaVersionService.
46
- * @param options The constructor options.
54
+ * @param options Optional constructor options.
47
55
  */
48
56
  constructor(options) {
49
- this._schemaVersionConnector = EntityStorageConnectorFactory.get(options.schemaVersionStorageType ?? "schema-version");
57
+ this._versionConnector = EntityStorageConnectorFactory.get(options?.schemaVersionStorageType ?? "schema-version");
58
+ this._config = options?.config;
50
59
  }
51
60
  /**
52
61
  * Returns the class name.
@@ -66,6 +75,13 @@ export class SchemaVersionService {
66
75
  * @param nodeLoggingComponentType An optional logging component type.
67
76
  */
68
77
  async start(nodeLoggingComponentType) {
78
+ const logging = ComponentFactory.getIfExists(nodeLoggingComponentType);
79
+ const migrationOptions = {
80
+ batchSize: this._config?.batchSize,
81
+ onProgress: async (progressItem, itemTotal, itemIndex) => {
82
+ await this.logProgress(logging, progressItem, itemTotal, itemIndex);
83
+ }
84
+ };
69
85
  // 1. Collect all registered schema names and partition into current vs historical.
70
86
  const allNames = EntitySchemaFactory.names();
71
87
  const historicalByBase = new Map();
@@ -86,25 +102,29 @@ export class SchemaVersionService {
86
102
  currentSchemas.set(name, EntitySchemaFactory.get(name));
87
103
  }
88
104
  }
89
- // 2. Read ALL stored version records in one query.
90
- const queryResult = await this._schemaVersionConnector.query();
105
+ // 2. Read ALL stored version records, paging through the full table.
91
106
  const storedVersions = new Map();
92
- for (const record of queryResult.entities ?? []) {
93
- if (Is.object(record)) {
94
- storedVersions.set(record.schemaName, record.version);
107
+ let cursor;
108
+ do {
109
+ const queryResult = await this._versionConnector.query(undefined, undefined, undefined, cursor);
110
+ for (const record of queryResult.entities ?? []) {
111
+ if (Is.object(record)) {
112
+ storedVersions.set(record.schemaName, record.version);
113
+ }
95
114
  }
96
- }
115
+ cursor = queryResult.cursor;
116
+ } while (Is.stringValue(cursor));
97
117
  // 3. Process SchemaVersion first so the version store itself is fully migrated
98
118
  // before any version records are written for other schemas.
99
119
  const schemaVersionName = "SchemaVersion";
100
120
  const schemaVersionSchema = currentSchemas.get(schemaVersionName);
101
121
  if (schemaVersionSchema) {
102
122
  currentSchemas.delete(schemaVersionName);
103
- await this.processSchema(schemaVersionName, schemaVersionSchema, storedVersions, historicalByBase.get(schemaVersionName), nodeLoggingComponentType);
123
+ await this.processSchema(schemaVersionName, schemaVersionSchema, storedVersions, historicalByBase.get(schemaVersionName), migrationOptions, nodeLoggingComponentType, logging);
104
124
  }
105
125
  // 4. Process all remaining schemas.
106
126
  for (const [schemaName, schema] of currentSchemas) {
107
- await this.processSchema(schemaName, schema, storedVersions, historicalByBase.get(schemaName), nodeLoggingComponentType);
127
+ await this.processSchema(schemaName, schema, storedVersions, historicalByBase.get(schemaName), migrationOptions, nodeLoggingComponentType, logging);
108
128
  }
109
129
  }
110
130
  /**
@@ -114,20 +134,27 @@ export class SchemaVersionService {
114
134
  * @param schema The current schema definition.
115
135
  * @param storedVersions The full map of stored version records.
116
136
  * @param history The versioned-schema map for this schema (historicalByBase.get(schemaName)), or undefined if none exist.
117
- * @param nodeLoggingComponentType An optional logging component type.
137
+ * @param migrationOptions The migration options to pass through to MigrationHelper.
138
+ * @param loggingComponentType The optional component type to use for logging the migration progress.
139
+ * @param logging An optional logging component to pass through to MigrationHelper for migration progress logging.
118
140
  * @internal
119
141
  */
120
- async processSchema(schemaName, schema, storedVersions, history, nodeLoggingComponentType) {
142
+ async processSchema(schemaName, schema, storedVersions, history, migrationOptions, loggingComponentType, logging) {
121
143
  const currentVersion = EntitySchemaHelper.getVersion(schema);
122
144
  // Find the entity-storage connector whose schema type matches this schema name.
123
- const connector = this.findConnector(schemaName);
124
- if (!connector) {
145
+ // For SchemaVersion itself, use the injected connector directly rather than re-discovering
146
+ // it through the factory, which could resolve a different instance than _versionConnector.
147
+ const connectorEntry = schemaName === "SchemaVersion"
148
+ ? { connector: this._versionConnector, factoryKey: undefined }
149
+ : this.findConnector(schemaName);
150
+ if (!connectorEntry) {
125
151
  // No connector registered for this schema — nothing to migrate.
126
152
  return;
127
153
  }
154
+ const { connector, factoryKey } = connectorEntry;
128
155
  // Resolve the stored version, applying the backwards-compat baseline when no record exists.
129
156
  const stored = storedVersions.get(schemaName);
130
- let resolvedStored;
157
+ let resolvedStoredVersion;
131
158
  if (stored === undefined) {
132
159
  // No version record: treat as v0 regardless of whether the table has data.
133
160
  // On SQL connectors the table may have a stale column structure even when empty;
@@ -136,21 +163,40 @@ export class SchemaVersionService {
136
163
  // Deployment precondition: any pre-existing data is genuinely at v0. A deployment
137
164
  // that hand-applied a later schema before this service was introduced would be
138
165
  // incorrectly replayed v0→…→current and should be seeded with an explicit record.
139
- resolvedStored = 0;
166
+ resolvedStoredVersion = 0;
140
167
  await this.writeVersion(schemaName, 0);
141
168
  }
142
169
  else {
143
- resolvedStored = stored;
170
+ resolvedStoredVersion = stored;
144
171
  }
145
172
  // No-op: stored version already matches current.
146
- if (resolvedStored === currentVersion) {
173
+ if (resolvedStoredVersion === currentVersion) {
174
+ await logging?.log({
175
+ source: SchemaVersionService.CLASS_NAME,
176
+ level: "info",
177
+ message: "noMigrationRequired",
178
+ data: {
179
+ schemaName,
180
+ version: resolvedStoredVersion
181
+ }
182
+ });
147
183
  return;
148
184
  }
185
+ await logging?.log({
186
+ source: SchemaVersionService.CLASS_NAME,
187
+ level: "info",
188
+ message: "migrationRequired",
189
+ data: {
190
+ schemaName,
191
+ from: currentVersion,
192
+ to: resolvedStoredVersion
193
+ }
194
+ });
149
195
  // Downgrade — not supported.
150
- if (resolvedStored > currentVersion) {
196
+ if (resolvedStoredVersion > currentVersion) {
151
197
  throw new GeneralError(SchemaVersionService.CLASS_NAME, "storedVersionNewer", {
152
198
  schemaName,
153
- stored: resolvedStored,
199
+ stored: resolvedStoredVersion,
154
200
  current: currentVersion
155
201
  });
156
202
  }
@@ -159,19 +205,19 @@ export class SchemaVersionService {
159
205
  if (!("createTargetConnector" in connector)) {
160
206
  throw new GeneralError(SchemaVersionService.CLASS_NAME, "connectorNotMigrationCapable", {
161
207
  schemaName,
162
- stored: resolvedStored,
208
+ stored: resolvedStoredVersion,
163
209
  current: currentVersion
164
210
  });
165
211
  }
166
212
  const migrationConnector = connector;
167
213
  // Upgrade — resolve and run the chain.
168
214
  const steps = [];
169
- for (let v = resolvedStored; v < currentVersion; v++) {
215
+ for (let v = resolvedStoredVersion; v < currentVersion; v++) {
170
216
  const fromSchema = history?.get(v);
171
217
  if (!fromSchema) {
172
218
  throw new GeneralError(SchemaVersionService.CLASS_NAME, "noMigrationStep", {
173
219
  schemaName,
174
- stored: resolvedStored,
220
+ stored: resolvedStoredVersion,
175
221
  current: currentVersion,
176
222
  missingFromVersion: v,
177
223
  missingToVersion: v + 1
@@ -181,7 +227,7 @@ export class SchemaVersionService {
181
227
  if (!toSchema) {
182
228
  throw new GeneralError(SchemaVersionService.CLASS_NAME, "noMigrationStepTarget", {
183
229
  schemaName,
184
- stored: resolvedStored,
230
+ stored: resolvedStoredVersion,
185
231
  current: currentVersion,
186
232
  missingFromVersion: v,
187
233
  missingToVersion: v + 1
@@ -196,7 +242,20 @@ export class SchemaVersionService {
196
242
  transformEntityProperty: override?.transformEntityProperty
197
243
  });
198
244
  }
199
- await MigrationHelper.migrateWithChain(migrationConnector, schemaName, steps, nodeLoggingComponentType);
245
+ const { finalConnector } = await MigrationHelper.migrateWithChain(migrationConnector, schemaName, steps, migrationOptions, loggingComponentType);
246
+ // Some connectors (e.g. in-memory) return a brand-new object from finalizeMigration
247
+ // rather than mutating the source in place. Re-register the factory entry so that any
248
+ // subsequent EntityStorageConnectorFactory.get() call returns the migrated instance.
249
+ if (finalConnector !== connector) {
250
+ if (factoryKey) {
251
+ EntityStorageConnectorFactory.register(factoryKey, () => finalConnector);
252
+ }
253
+ // For SchemaVersion keep _versionConnector in sync so writeVersion below uses
254
+ // the migrated instance.
255
+ if (schemaName === "SchemaVersion") {
256
+ this._versionConnector = finalConnector;
257
+ }
258
+ }
200
259
  // Advance the stored version only after finalizeMigration has succeeded.
201
260
  // See crash-window note in the class comment.
202
261
  await this.writeVersion(schemaName, currentVersion);
@@ -208,7 +267,7 @@ export class SchemaVersionService {
208
267
  * @internal
209
268
  */
210
269
  async writeVersion(schemaName, version) {
211
- await this._schemaVersionConnector.set({
270
+ await this._versionConnector.set({
212
271
  schemaName,
213
272
  version,
214
273
  updatedAt: new Date().toISOString()
@@ -218,7 +277,7 @@ export class SchemaVersionService {
218
277
  * Searches EntityStorageConnectorFactory for the connector whose registered schema type
219
278
  * matches the given schema name.
220
279
  * @param schemaName The entity type name to look up.
221
- * @returns The matching connector, or undefined if none is registered.
280
+ * @returns The matching connector and its factory key, or undefined if none is registered.
222
281
  * @internal
223
282
  */
224
283
  findConnector(schemaName) {
@@ -226,7 +285,7 @@ export class SchemaVersionService {
226
285
  try {
227
286
  const connector = EntityStorageConnectorFactory.get(name);
228
287
  if (connector.getSchema?.().type === schemaName) {
229
- return connector;
288
+ return { connector, factoryKey: name };
230
289
  }
231
290
  }
232
291
  catch {
@@ -235,5 +294,63 @@ export class SchemaVersionService {
235
294
  }
236
295
  return undefined;
237
296
  }
297
+ /**
298
+ * Logs migration progress using the provided logging component, if available.
299
+ * @param logging The logging component to use for logging progress, if available.
300
+ * @param progressItem The progress item being updated.
301
+ * @param itemTotal The total number of items to process for this progress item.
302
+ * @param itemIndex The index of the current item being processed for this progress item.
303
+ * @internal
304
+ */
305
+ async logProgress(logging, progressItem, itemTotal, itemIndex) {
306
+ if (progressItem === "partitionStart") {
307
+ await logging?.log({
308
+ source: SchemaVersionService.CLASS_NAME,
309
+ level: "info",
310
+ message: "partitionStart",
311
+ data: { progressItem, itemTotal, itemIndex }
312
+ });
313
+ }
314
+ else if (progressItem === "partitionProgress") {
315
+ await logging?.log({
316
+ source: SchemaVersionService.CLASS_NAME,
317
+ level: "info",
318
+ message: "partitionProgress",
319
+ data: { progressItem, itemTotal, itemIndex }
320
+ });
321
+ }
322
+ else if (progressItem === "partitionEnd") {
323
+ await logging?.log({
324
+ source: SchemaVersionService.CLASS_NAME,
325
+ level: "info",
326
+ message: "partitionEnd",
327
+ data: { progressItem, itemTotal, itemIndex }
328
+ });
329
+ }
330
+ else if (progressItem === "partitionItemsStart") {
331
+ await logging?.log({
332
+ source: SchemaVersionService.CLASS_NAME,
333
+ level: "info",
334
+ message: "partitionItemsStart",
335
+ data: { progressItem, itemTotal, itemIndex }
336
+ });
337
+ }
338
+ else if (progressItem === "partitionItemsProgress") {
339
+ await logging?.log({
340
+ source: SchemaVersionService.CLASS_NAME,
341
+ level: "info",
342
+ message: "partitionItemsProgress",
343
+ data: { progressItem, itemTotal, itemIndex }
344
+ });
345
+ }
346
+ else if (progressItem === "partitionItemsEnd") {
347
+ await logging?.log({
348
+ source: SchemaVersionService.CLASS_NAME,
349
+ level: "info",
350
+ message: "partitionItemsEnd",
351
+ data: { progressItem, itemTotal, itemIndex }
352
+ });
353
+ }
354
+ }
238
355
  }
239
356
  //# sourceMappingURL=schemaVersionService.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemaVersionService.js","sourceRoot":"","sources":["../../src/schemaVersionService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { GeneralError, Is, type IComponent } from \"@twin.org/core\";\nimport { EntitySchemaFactory, EntitySchemaHelper, type IEntitySchema } from \"@twin.org/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\tMigrationHelper,\n\tSchemaMigrationFactory,\n\ttype IEntityStorageConnector,\n\ttype IEntityStorageMigrationConnector,\n\ttype IResolvedMigrationStep\n} from \"@twin.org/entity-storage-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { SchemaVersion } from \"./entities/schemaVersion.js\";\nimport type { ISchemaVersionServiceConstructorOptions } from \"./models/ISchemaVersionServiceConstructorOptions.js\";\n\n/**\n * Service that checks and applies entity schema migrations at every node start-up.\n *\n * This service should be registered as the first component so that its start() runs before\n * any other service. By the time start() is called, all component bootstraps have completed\n * (every table already exists) and EntitySchemaFactory / EntityStorageConnectorFactory are\n * fully populated with every registered schema and connector.\n *\n * Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming\n * convention — current schema = \"MyEntity\", first history = \"MyEntityV0\", second = \"MyEntityV1\".\n * The service groups schemas by base name (strips the trailing V number suffix) and resolves the\n * migration chain automatically by diffing consecutive versioned schemas. For steps that require\n * property renames or a custom transform hook, register an optional ISchemaMigration entry in\n * SchemaMigrationFactory under the key \"Base_from_to\" (e.g. \"MyEntity_0_1\").\n *\n * Crash-window note: finalizeMigration and the subsequent version-record write are two\n * separate operations. If the process dies between them the next boot re-runs the chain\n * over already-migrated data. applyEntityTransform is NOT idempotent for structural changes\n * (newly-added optional fields would be dropped on re-run). A transaction spanning both\n * writes is a precondition for production; track this in the concurrency follow-up.\n */\nexport class SchemaVersionService implements IComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<SchemaVersionService>();\n\n\t/**\n\t * Regex to detect a versioned schema name and extract the base name and version number.\n\t * Matches names like \"MyEntityV0\", \"AuditableItemGraphV2\", etc.\n\t * @internal\n\t */\n\tprivate static readonly _VERSION_SUFFIX_RE = /^(.+)V(\\d+)$/;\n\n\t/**\n\t * The connector used to read and write SchemaVersion records.\n\t * @internal\n\t */\n\tprivate readonly _schemaVersionConnector: IEntityStorageConnector<SchemaVersion>;\n\n\t/**\n\t * Create a new SchemaVersionService.\n\t * @param options The constructor options.\n\t */\n\tconstructor(options: ISchemaVersionServiceConstructorOptions) {\n\t\tthis._schemaVersionConnector = EntityStorageConnectorFactory.get(\n\t\t\toptions.schemaVersionStorageType ?? \"schema-version\"\n\t\t);\n\t}\n\n\t/**\n\t * Returns the class name.\n\t * @returns The class name.\n\t */\n\tpublic className(): string {\n\t\treturn SchemaVersionService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Reads all registered entity schemas, groups versioned schemas by base name, reads the\n\t * full schemaVersion table in one pass, then orchestrates chain migrations for any schema\n\t * whose stored version is behind the current version declared in EntitySchemaFactory.\n\t * SchemaVersion itself is processed first so the version store is migrated before any\n\t * version records are written for other schemas.\n\t *\n\t * Runs after all component bootstraps, so every managed table already exists.\n\t * @param nodeLoggingComponentType An optional logging component type.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\t// 1. Collect all registered schema names and partition into current vs historical.\n\t\tconst allNames = EntitySchemaFactory.names();\n\n\t\tconst historicalByBase = new Map<string, Map<number, IEntitySchema>>();\n\t\tconst currentSchemas = new Map<string, IEntitySchema>();\n\n\t\tfor (const name of allNames) {\n\t\t\tconst match = SchemaVersionService._VERSION_SUFFIX_RE.exec(name);\n\t\t\tif (match) {\n\t\t\t\tconst baseName = match[1];\n\t\t\t\tconst version = Number.parseInt(match[2], 10);\n\t\t\t\tlet versions = historicalByBase.get(baseName);\n\t\t\t\tif (!versions) {\n\t\t\t\t\tversions = new Map();\n\t\t\t\t\thistoricalByBase.set(baseName, versions);\n\t\t\t\t}\n\t\t\t\tversions.set(version, EntitySchemaFactory.get(name));\n\t\t\t} else {\n\t\t\t\tcurrentSchemas.set(name, EntitySchemaFactory.get(name));\n\t\t\t}\n\t\t}\n\n\t\t// 2. Read ALL stored version records in one query.\n\t\tconst queryResult = await this._schemaVersionConnector.query();\n\t\tconst storedVersions = new Map<string, number>();\n\t\tfor (const record of queryResult.entities ?? []) {\n\t\t\tif (Is.object<SchemaVersion>(record)) {\n\t\t\t\tstoredVersions.set(record.schemaName, record.version);\n\t\t\t}\n\t\t}\n\n\t\t// 3. Process SchemaVersion first so the version store itself is fully migrated\n\t\t// before any version records are written for other schemas.\n\t\tconst schemaVersionName = nameof(SchemaVersion);\n\t\tconst schemaVersionSchema = currentSchemas.get(schemaVersionName);\n\t\tif (schemaVersionSchema) {\n\t\t\tcurrentSchemas.delete(schemaVersionName);\n\t\t\tawait this.processSchema(\n\t\t\t\tschemaVersionName,\n\t\t\t\tschemaVersionSchema,\n\t\t\t\tstoredVersions,\n\t\t\t\thistoricalByBase.get(schemaVersionName),\n\t\t\t\tnodeLoggingComponentType\n\t\t\t);\n\t\t}\n\n\t\t// 4. Process all remaining schemas.\n\t\tfor (const [schemaName, schema] of currentSchemas) {\n\t\t\tawait this.processSchema(\n\t\t\t\tschemaName,\n\t\t\t\tschema,\n\t\t\t\tstoredVersions,\n\t\t\t\thistoricalByBase.get(schemaName),\n\t\t\t\tnodeLoggingComponentType\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Checks and applies any pending migration for a single entity schema.\n\t * Extracted to avoid continue statements in the outer loop.\n\t * @param schemaName The base schema name.\n\t * @param schema The current schema definition.\n\t * @param storedVersions The full map of stored version records.\n\t * @param history The versioned-schema map for this schema (historicalByBase.get(schemaName)), or undefined if none exist.\n\t * @param nodeLoggingComponentType An optional logging component type.\n\t * @internal\n\t */\n\tprivate async processSchema(\n\t\tschemaName: string,\n\t\tschema: IEntitySchema,\n\t\tstoredVersions: Map<string, number>,\n\t\thistory: Map<number, IEntitySchema> | undefined,\n\t\tnodeLoggingComponentType: string | undefined\n\t): Promise<void> {\n\t\tconst currentVersion = EntitySchemaHelper.getVersion(schema);\n\n\t\t// Find the entity-storage connector whose schema type matches this schema name.\n\t\tconst connector = this.findConnector(schemaName);\n\t\tif (!connector) {\n\t\t\t// No connector registered for this schema — nothing to migrate.\n\t\t\treturn;\n\t\t}\n\n\t\t// Resolve the stored version, applying the backwards-compat baseline when no record exists.\n\t\tconst stored = storedVersions.get(schemaName);\n\t\tlet resolvedStored: number;\n\n\t\tif (stored === undefined) {\n\t\t\t// No version record: treat as v0 regardless of whether the table has data.\n\t\t\t// On SQL connectors the table may have a stale column structure even when empty;\n\t\t\t// running the chain over zero rows still calls finalizeMigration, which reconciles\n\t\t\t// the table shape via a connector swap.\n\t\t\t// Deployment precondition: any pre-existing data is genuinely at v0. A deployment\n\t\t\t// that hand-applied a later schema before this service was introduced would be\n\t\t\t// incorrectly replayed v0→…→current and should be seeded with an explicit record.\n\t\t\tresolvedStored = 0;\n\t\t\tawait this.writeVersion(schemaName, 0);\n\t\t} else {\n\t\t\tresolvedStored = stored;\n\t\t}\n\n\t\t// No-op: stored version already matches current.\n\t\tif (resolvedStored === currentVersion) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Downgrade — not supported.\n\t\tif (resolvedStored > currentVersion) {\n\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"storedVersionNewer\", {\n\t\t\t\tschemaName,\n\t\t\t\tstored: resolvedStored,\n\t\t\t\tcurrent: currentVersion\n\t\t\t});\n\t\t}\n\n\t\t// Migration is needed. If the connector does not support it, throw immediately so\n\t\t// the problem surfaces at boot rather than at runtime when writes hit the wrong table shape.\n\t\tif (!(\"createTargetConnector\" in connector)) {\n\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"connectorNotMigrationCapable\", {\n\t\t\t\tschemaName,\n\t\t\t\tstored: resolvedStored,\n\t\t\t\tcurrent: currentVersion\n\t\t\t});\n\t\t}\n\n\t\tconst migrationConnector = connector as IEntityStorageMigrationConnector;\n\n\t\t// Upgrade — resolve and run the chain.\n\t\tconst steps: IResolvedMigrationStep[] = [];\n\n\t\tfor (let v = resolvedStored; v < currentVersion; v++) {\n\t\t\tconst fromSchema = history?.get(v);\n\t\t\tif (!fromSchema) {\n\t\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"noMigrationStep\", {\n\t\t\t\t\tschemaName,\n\t\t\t\t\tstored: resolvedStored,\n\t\t\t\t\tcurrent: currentVersion,\n\t\t\t\t\tmissingFromVersion: v,\n\t\t\t\t\tmissingToVersion: v + 1\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst toSchema = v + 1 < currentVersion ? history?.get(v + 1) : schema;\n\t\t\tif (!toSchema) {\n\t\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"noMigrationStepTarget\", {\n\t\t\t\t\tschemaName,\n\t\t\t\t\tstored: resolvedStored,\n\t\t\t\t\tcurrent: currentVersion,\n\t\t\t\t\tmissingFromVersion: v,\n\t\t\t\t\tmissingToVersion: v + 1\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst overrideKey = `${schemaName}_${v}_${v + 1}`;\n\t\t\tconst override = SchemaMigrationFactory.getIfExists(overrideKey);\n\n\t\t\tsteps.push({\n\t\t\t\tfromProperties: fromSchema.properties ?? [],\n\t\t\t\ttoProperties: toSchema.properties ?? [],\n\t\t\t\trenames: override?.renames,\n\t\t\t\ttransformEntityProperty: override?.transformEntityProperty\n\t\t\t});\n\t\t}\n\n\t\tawait MigrationHelper.migrateWithChain(\n\t\t\tmigrationConnector,\n\t\t\tschemaName,\n\t\t\tsteps,\n\t\t\tnodeLoggingComponentType\n\t\t);\n\n\t\t// Advance the stored version only after finalizeMigration has succeeded.\n\t\t// See crash-window note in the class comment.\n\t\tawait this.writeVersion(schemaName, currentVersion);\n\t}\n\n\t/**\n\t * Upserts a SchemaVersion record for the given schema name.\n\t * @param schemaName The schema type name.\n\t * @param version The version to record.\n\t * @internal\n\t */\n\tprivate async writeVersion(schemaName: string, version: number): Promise<void> {\n\t\tawait this._schemaVersionConnector.set({\n\t\t\tschemaName,\n\t\t\tversion,\n\t\t\tupdatedAt: new Date().toISOString()\n\t\t});\n\t}\n\n\t/**\n\t * Searches EntityStorageConnectorFactory for the connector whose registered schema type\n\t * matches the given schema name.\n\t * @param schemaName The entity type name to look up.\n\t * @returns The matching connector, or undefined if none is registered.\n\t * @internal\n\t */\n\tprivate findConnector(schemaName: string): IEntityStorageConnector | undefined {\n\t\tfor (const name of EntityStorageConnectorFactory.names()) {\n\t\t\ttry {\n\t\t\t\tconst connector = EntityStorageConnectorFactory.get(name);\n\t\t\t\tif (connector.getSchema?.().type === schemaName) {\n\t\t\t\t\treturn connector;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Connector not yet created or registration issue — skip.\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n}\n"]}
1
+ {"version":3,"file":"schemaVersionService.js","sourceRoot":"","sources":["../../src/schemaVersionService.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright 2026 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport { ComponentFactory, GeneralError, Is, type IComponent } from \"@twin.org/core\";\nimport { EntitySchemaFactory, EntitySchemaHelper, type IEntitySchema } from \"@twin.org/entity\";\nimport {\n\tEntityStorageConnectorFactory,\n\ttype IEntityStorageConnector,\n\ttype IEntityStorageMigrationConnector,\n\ttype IMigrationOptions,\n\ttype IResolvedMigrationStep,\n\tMigrationHelper,\n\tSchemaMigrationFactory\n} from \"@twin.org/entity-storage-models\";\nimport type { ILoggingComponent } from \"@twin.org/logging-models\";\nimport { nameof } from \"@twin.org/nameof\";\nimport { SchemaVersion } from \"./entities/schemaVersion.js\";\nimport type { ISchemaVersionServiceConfig } from \"./models/ISchemaVersionServiceConfig.js\";\nimport type { ISchemaVersionServiceConstructorOptions } from \"./models/ISchemaVersionServiceConstructorOptions.js\";\n\n/**\n * Service that checks and applies entity schema migrations at every node start-up.\n *\n * This service must be the first entry in coreTypeInitialisers.json. The engine iterates that\n * array in order to determine start sequence — there is no engine-level priority mechanism, so\n * registration position is the only guarantee that start() runs before any other service.\n * By the time start() is called, all component bootstraps have completed (every table already\n * exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every\n * registered schema and connector.\n *\n * Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming\n * convention — current schema = \"MyEntity\", first history = \"MyEntityV0\", second = \"MyEntityV1\".\n * The service groups schemas by base name (strips the trailing V number suffix) and resolves the\n * migration chain automatically by diffing consecutive versioned schemas. For steps that require\n * property renames or a custom transform hook, register an optional ISchemaMigration entry in\n * SchemaMigrationFactory under the key \"Base_from_to\" (e.g. \"MyEntity_0_1\").\n *\n * Crash-window note: finalizeMigration and the subsequent version-record write are two\n * separate operations. If the process dies between them the next boot re-runs the chain\n * over already-migrated data. applyEntityTransform is NOT idempotent for structural changes\n * (newly-added optional fields would be dropped on re-run). A transaction spanning both\n * writes is a precondition for production; track this in the concurrency follow-up.\n */\nexport class SchemaVersionService implements IComponent {\n\t/**\n\t * Runtime name for the class.\n\t */\n\tpublic static readonly CLASS_NAME: string = nameof<SchemaVersionService>();\n\n\t/**\n\t * Regex to detect a versioned schema name and extract the base name and version number.\n\t * Matches names like \"MyEntityV0\", \"AuditableItemGraphV2\", etc.\n\t * @internal\n\t */\n\tprivate static readonly _VERSION_SUFFIX_RE = /^(.+)V(\\d+)$/;\n\n\t/**\n\t * The connector used to read and write SchemaVersion records.\n\t * Not readonly because finalizeMigration may return a replacement connector object.\n\t * @internal\n\t */\n\tprivate _versionConnector: IEntityStorageConnector<SchemaVersion>;\n\n\t/**\n\t * Optional config passed through constructor options.\n\t * @internal\n\t */\n\tprivate readonly _config?: ISchemaVersionServiceConfig;\n\n\t/**\n\t * Create a new SchemaVersionService.\n\t * @param options Optional constructor options.\n\t */\n\tconstructor(options?: ISchemaVersionServiceConstructorOptions) {\n\t\tthis._versionConnector = EntityStorageConnectorFactory.get(\n\t\t\toptions?.schemaVersionStorageType ?? \"schema-version\"\n\t\t);\n\t\tthis._config = options?.config;\n\t}\n\n\t/**\n\t * Returns the class name.\n\t * @returns The class name.\n\t */\n\tpublic className(): string {\n\t\treturn SchemaVersionService.CLASS_NAME;\n\t}\n\n\t/**\n\t * Reads all registered entity schemas, groups versioned schemas by base name, reads the\n\t * full schemaVersion table in one pass, then orchestrates chain migrations for any schema\n\t * whose stored version is behind the current version declared in EntitySchemaFactory.\n\t * SchemaVersion itself is processed first so the version store is migrated before any\n\t * version records are written for other schemas.\n\t *\n\t * Runs after all component bootstraps, so every managed table already exists.\n\t * @param nodeLoggingComponentType An optional logging component type.\n\t */\n\tpublic async start(nodeLoggingComponentType?: string): Promise<void> {\n\t\tconst logging = ComponentFactory.getIfExists<ILoggingComponent>(nodeLoggingComponentType);\n\n\t\tconst migrationOptions: IMigrationOptions = {\n\t\t\tbatchSize: this._config?.batchSize,\n\t\t\tonProgress: async (progressItem, itemTotal, itemIndex) => {\n\t\t\t\tawait this.logProgress(logging, progressItem, itemTotal, itemIndex);\n\t\t\t}\n\t\t};\n\n\t\t// 1. Collect all registered schema names and partition into current vs historical.\n\t\tconst allNames = EntitySchemaFactory.names();\n\n\t\tconst historicalByBase = new Map<string, Map<number, IEntitySchema>>();\n\t\tconst currentSchemas = new Map<string, IEntitySchema>();\n\n\t\tfor (const name of allNames) {\n\t\t\tconst match = SchemaVersionService._VERSION_SUFFIX_RE.exec(name);\n\t\t\tif (match) {\n\t\t\t\tconst baseName = match[1];\n\t\t\t\tconst version = Number.parseInt(match[2], 10);\n\t\t\t\tlet versions = historicalByBase.get(baseName);\n\t\t\t\tif (!versions) {\n\t\t\t\t\tversions = new Map();\n\t\t\t\t\thistoricalByBase.set(baseName, versions);\n\t\t\t\t}\n\t\t\t\tversions.set(version, EntitySchemaFactory.get(name));\n\t\t\t} else {\n\t\t\t\tcurrentSchemas.set(name, EntitySchemaFactory.get(name));\n\t\t\t}\n\t\t}\n\n\t\t// 2. Read ALL stored version records, paging through the full table.\n\t\tconst storedVersions = new Map<string, number>();\n\t\tlet cursor: string | undefined;\n\t\tdo {\n\t\t\tconst queryResult = await this._versionConnector.query(\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\tundefined,\n\t\t\t\tcursor\n\t\t\t);\n\t\t\tfor (const record of queryResult.entities ?? []) {\n\t\t\t\tif (Is.object<SchemaVersion>(record)) {\n\t\t\t\t\tstoredVersions.set(record.schemaName, record.version);\n\t\t\t\t}\n\t\t\t}\n\t\t\tcursor = queryResult.cursor;\n\t\t} while (Is.stringValue(cursor));\n\n\t\t// 3. Process SchemaVersion first so the version store itself is fully migrated\n\t\t// before any version records are written for other schemas.\n\t\tconst schemaVersionName = nameof(SchemaVersion);\n\t\tconst schemaVersionSchema = currentSchemas.get(schemaVersionName);\n\t\tif (schemaVersionSchema) {\n\t\t\tcurrentSchemas.delete(schemaVersionName);\n\t\t\tawait this.processSchema(\n\t\t\t\tschemaVersionName,\n\t\t\t\tschemaVersionSchema,\n\t\t\t\tstoredVersions,\n\t\t\t\thistoricalByBase.get(schemaVersionName),\n\t\t\t\tmigrationOptions,\n\t\t\t\tnodeLoggingComponentType,\n\t\t\t\tlogging\n\t\t\t);\n\t\t}\n\n\t\t// 4. Process all remaining schemas.\n\t\tfor (const [schemaName, schema] of currentSchemas) {\n\t\t\tawait this.processSchema(\n\t\t\t\tschemaName,\n\t\t\t\tschema,\n\t\t\t\tstoredVersions,\n\t\t\t\thistoricalByBase.get(schemaName),\n\t\t\t\tmigrationOptions,\n\t\t\t\tnodeLoggingComponentType,\n\t\t\t\tlogging\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Checks and applies any pending migration for a single entity schema.\n\t * Extracted to avoid continue statements in the outer loop.\n\t * @param schemaName The base schema name.\n\t * @param schema The current schema definition.\n\t * @param storedVersions The full map of stored version records.\n\t * @param history The versioned-schema map for this schema (historicalByBase.get(schemaName)), or undefined if none exist.\n\t * @param migrationOptions The migration options to pass through to MigrationHelper.\n\t * @param loggingComponentType The optional component type to use for logging the migration progress.\n\t * @param logging An optional logging component to pass through to MigrationHelper for migration progress logging.\n\t * @internal\n\t */\n\tprivate async processSchema(\n\t\tschemaName: string,\n\t\tschema: IEntitySchema,\n\t\tstoredVersions: Map<string, number>,\n\t\thistory: Map<number, IEntitySchema> | undefined,\n\t\tmigrationOptions: IMigrationOptions,\n\t\tloggingComponentType: string | undefined,\n\t\tlogging: ILoggingComponent | undefined\n\t): Promise<void> {\n\t\tconst currentVersion = EntitySchemaHelper.getVersion(schema);\n\n\t\t// Find the entity-storage connector whose schema type matches this schema name.\n\t\t// For SchemaVersion itself, use the injected connector directly rather than re-discovering\n\t\t// it through the factory, which could resolve a different instance than _versionConnector.\n\t\tconst connectorEntry =\n\t\t\tschemaName === nameof(SchemaVersion)\n\t\t\t\t? { connector: this._versionConnector as IEntityStorageConnector, factoryKey: undefined }\n\t\t\t\t: this.findConnector(schemaName);\n\t\tif (!connectorEntry) {\n\t\t\t// No connector registered for this schema — nothing to migrate.\n\t\t\treturn;\n\t\t}\n\t\tconst { connector, factoryKey } = connectorEntry;\n\n\t\t// Resolve the stored version, applying the backwards-compat baseline when no record exists.\n\t\tconst stored = storedVersions.get(schemaName);\n\t\tlet resolvedStoredVersion: number;\n\n\t\tif (stored === undefined) {\n\t\t\t// No version record: treat as v0 regardless of whether the table has data.\n\t\t\t// On SQL connectors the table may have a stale column structure even when empty;\n\t\t\t// running the chain over zero rows still calls finalizeMigration, which reconciles\n\t\t\t// the table shape via a connector swap.\n\t\t\t// Deployment precondition: any pre-existing data is genuinely at v0. A deployment\n\t\t\t// that hand-applied a later schema before this service was introduced would be\n\t\t\t// incorrectly replayed v0→…→current and should be seeded with an explicit record.\n\t\t\tresolvedStoredVersion = 0;\n\t\t\tawait this.writeVersion(schemaName, 0);\n\t\t} else {\n\t\t\tresolvedStoredVersion = stored;\n\t\t}\n\n\t\t// No-op: stored version already matches current.\n\t\tif (resolvedStoredVersion === currentVersion) {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"noMigrationRequired\",\n\t\t\t\tdata: {\n\t\t\t\t\tschemaName,\n\t\t\t\t\tversion: resolvedStoredVersion\n\t\t\t\t}\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tawait logging?.log({\n\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\tlevel: \"info\",\n\t\t\tmessage: \"migrationRequired\",\n\t\t\tdata: {\n\t\t\t\tschemaName,\n\t\t\t\tfrom: currentVersion,\n\t\t\t\tto: resolvedStoredVersion\n\t\t\t}\n\t\t});\n\n\t\t// Downgrade — not supported.\n\t\tif (resolvedStoredVersion > currentVersion) {\n\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"storedVersionNewer\", {\n\t\t\t\tschemaName,\n\t\t\t\tstored: resolvedStoredVersion,\n\t\t\t\tcurrent: currentVersion\n\t\t\t});\n\t\t}\n\n\t\t// Migration is needed. If the connector does not support it, throw immediately so\n\t\t// the problem surfaces at boot rather than at runtime when writes hit the wrong table shape.\n\t\tif (!(\"createTargetConnector\" in connector)) {\n\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"connectorNotMigrationCapable\", {\n\t\t\t\tschemaName,\n\t\t\t\tstored: resolvedStoredVersion,\n\t\t\t\tcurrent: currentVersion\n\t\t\t});\n\t\t}\n\n\t\tconst migrationConnector = connector as IEntityStorageMigrationConnector;\n\n\t\t// Upgrade — resolve and run the chain.\n\t\tconst steps: IResolvedMigrationStep[] = [];\n\n\t\tfor (let v = resolvedStoredVersion; v < currentVersion; v++) {\n\t\t\tconst fromSchema = history?.get(v);\n\t\t\tif (!fromSchema) {\n\t\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"noMigrationStep\", {\n\t\t\t\t\tschemaName,\n\t\t\t\t\tstored: resolvedStoredVersion,\n\t\t\t\t\tcurrent: currentVersion,\n\t\t\t\t\tmissingFromVersion: v,\n\t\t\t\t\tmissingToVersion: v + 1\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst toSchema = v + 1 < currentVersion ? history?.get(v + 1) : schema;\n\t\t\tif (!toSchema) {\n\t\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"noMigrationStepTarget\", {\n\t\t\t\t\tschemaName,\n\t\t\t\t\tstored: resolvedStoredVersion,\n\t\t\t\t\tcurrent: currentVersion,\n\t\t\t\t\tmissingFromVersion: v,\n\t\t\t\t\tmissingToVersion: v + 1\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst overrideKey = `${schemaName}_${v}_${v + 1}`;\n\t\t\tconst override = SchemaMigrationFactory.getIfExists(overrideKey);\n\n\t\t\tsteps.push({\n\t\t\t\tfromProperties: fromSchema.properties ?? [],\n\t\t\t\ttoProperties: toSchema.properties ?? [],\n\t\t\t\trenames: override?.renames,\n\t\t\t\ttransformEntityProperty: override?.transformEntityProperty\n\t\t\t});\n\t\t}\n\n\t\tconst { finalConnector } = await MigrationHelper.migrateWithChain(\n\t\t\tmigrationConnector,\n\t\t\tschemaName,\n\t\t\tsteps,\n\t\t\tmigrationOptions,\n\t\t\tloggingComponentType\n\t\t);\n\n\t\t// Some connectors (e.g. in-memory) return a brand-new object from finalizeMigration\n\t\t// rather than mutating the source in place. Re-register the factory entry so that any\n\t\t// subsequent EntityStorageConnectorFactory.get() call returns the migrated instance.\n\t\tif (finalConnector !== connector) {\n\t\t\tif (factoryKey) {\n\t\t\t\tEntityStorageConnectorFactory.register(factoryKey, () => finalConnector);\n\t\t\t}\n\t\t\t// For SchemaVersion keep _versionConnector in sync so writeVersion below uses\n\t\t\t// the migrated instance.\n\t\t\tif (schemaName === nameof(SchemaVersion)) {\n\t\t\t\tthis._versionConnector = finalConnector as IEntityStorageConnector<SchemaVersion>;\n\t\t\t}\n\t\t}\n\n\t\t// Advance the stored version only after finalizeMigration has succeeded.\n\t\t// See crash-window note in the class comment.\n\t\tawait this.writeVersion(schemaName, currentVersion);\n\t}\n\n\t/**\n\t * Upserts a SchemaVersion record for the given schema name.\n\t * @param schemaName The schema type name.\n\t * @param version The version to record.\n\t * @internal\n\t */\n\tprivate async writeVersion(schemaName: string, version: number): Promise<void> {\n\t\tawait this._versionConnector.set({\n\t\t\tschemaName,\n\t\t\tversion,\n\t\t\tupdatedAt: new Date().toISOString()\n\t\t});\n\t}\n\n\t/**\n\t * Searches EntityStorageConnectorFactory for the connector whose registered schema type\n\t * matches the given schema name.\n\t * @param schemaName The entity type name to look up.\n\t * @returns The matching connector and its factory key, or undefined if none is registered.\n\t * @internal\n\t */\n\tprivate findConnector(\n\t\tschemaName: string\n\t): { connector: IEntityStorageConnector; factoryKey: string } | undefined {\n\t\tfor (const name of EntityStorageConnectorFactory.names()) {\n\t\t\ttry {\n\t\t\t\tconst connector = EntityStorageConnectorFactory.get(name);\n\t\t\t\tif (connector.getSchema?.().type === schemaName) {\n\t\t\t\t\treturn { connector, factoryKey: name };\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Connector not yet created or registration issue — skip.\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Logs migration progress using the provided logging component, if available.\n\t * @param logging The logging component to use for logging progress, if available.\n\t * @param progressItem The progress item being updated.\n\t * @param itemTotal The total number of items to process for this progress item.\n\t * @param itemIndex The index of the current item being processed for this progress item.\n\t * @internal\n\t */\n\tprivate async logProgress(\n\t\tlogging: ILoggingComponent | undefined,\n\t\tprogressItem: string,\n\t\titemTotal: number,\n\t\titemIndex: number\n\t): Promise<void> {\n\t\tif (progressItem === \"partitionStart\") {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"partitionStart\",\n\t\t\t\tdata: { progressItem, itemTotal, itemIndex }\n\t\t\t});\n\t\t} else if (progressItem === \"partitionProgress\") {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"partitionProgress\",\n\t\t\t\tdata: { progressItem, itemTotal, itemIndex }\n\t\t\t});\n\t\t} else if (progressItem === \"partitionEnd\") {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"partitionEnd\",\n\t\t\t\tdata: { progressItem, itemTotal, itemIndex }\n\t\t\t});\n\t\t} else if (progressItem === \"partitionItemsStart\") {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"partitionItemsStart\",\n\t\t\t\tdata: { progressItem, itemTotal, itemIndex }\n\t\t\t});\n\t\t} else if (progressItem === \"partitionItemsProgress\") {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"partitionItemsProgress\",\n\t\t\t\tdata: { progressItem, itemTotal, itemIndex }\n\t\t\t});\n\t\t} else if (progressItem === \"partitionItemsEnd\") {\n\t\t\tawait logging?.log({\n\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\tlevel: \"info\",\n\t\t\t\tmessage: \"partitionItemsEnd\",\n\t\t\t\tdata: { progressItem, itemTotal, itemIndex }\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
@@ -1,18 +1,15 @@
1
1
  /**
2
- * Entity that records the currently applied schema version for a managed entity schema.
3
- * Persisted through a normal entity-storage connector, giving every backend a schemaVersion
4
- * table/collection for free.
5
- *
6
- * SchemaVersionService processes this schema first before all others so that the version
7
- * store is fully migrated before any version records are written for other schemas.
2
+ * Tracks the currently applied schema version for each managed entity schema.
3
+ * One record per schema name. Written once on first boot, then updated after
4
+ * each successful migration.
8
5
  */
9
6
  export declare class SchemaVersion {
10
7
  /**
11
- * The schema type name (matches the key used in EntitySchemaFactory).
8
+ * The entity schema type name primary key.
12
9
  */
13
10
  schemaName: string;
14
11
  /**
15
- * The version currently applied in storage for this schema.
12
+ * The currently deployed version of this schema.
16
13
  */
17
14
  version: number;
18
15
  /**
@@ -4,7 +4,8 @@ export * from "./entityStorageService.js";
4
4
  export * from "./models/IEntityStorageRoutesExamples.js";
5
5
  export * from "./models/IEntityStorageServiceConfig.js";
6
6
  export * from "./models/IEntityStorageServiceConstructorOptions.js";
7
+ export * from "./models/ISchemaVersionServiceConfig.js";
7
8
  export * from "./models/ISchemaVersionServiceConstructorOptions.js";
8
9
  export * from "./restEntryPoints.js";
9
- export * from "./schemaVersionService.js";
10
10
  export * from "./schema.js";
11
+ export * from "./schemaVersionService.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Constructor options config for SchemaVersionService.
3
+ */
4
+ export interface ISchemaVersionServiceConfig {
5
+ /**
6
+ * The batch size for processing schema versions.
7
+ */
8
+ batchSize?: number;
9
+ }
@@ -1,3 +1,4 @@
1
+ import type { ISchemaVersionServiceConfig } from "./ISchemaVersionServiceConfig.js";
1
2
  /**
2
3
  * Constructor options for SchemaVersionService.
3
4
  */
@@ -7,4 +8,8 @@ export interface ISchemaVersionServiceConstructorOptions {
7
8
  * @default schema-version
8
9
  */
9
10
  schemaVersionStorageType?: string;
11
+ /**
12
+ * Optional config.
13
+ */
14
+ config?: ISchemaVersionServiceConfig;
10
15
  }
@@ -1,4 +1,4 @@
1
1
  /**
2
- * Initialize the schema for the entity storage.
2
+ * Initialize the schema for the entity storage models.
3
3
  */
4
4
  export declare function initSchema(): void;
@@ -3,10 +3,12 @@ import type { ISchemaVersionServiceConstructorOptions } from "./models/ISchemaVe
3
3
  /**
4
4
  * Service that checks and applies entity schema migrations at every node start-up.
5
5
  *
6
- * This service should be registered as the first component so that its start() runs before
7
- * any other service. By the time start() is called, all component bootstraps have completed
8
- * (every table already exists) and EntitySchemaFactory / EntityStorageConnectorFactory are
9
- * fully populated with every registered schema and connector.
6
+ * This service must be the first entry in coreTypeInitialisers.json. The engine iterates that
7
+ * array in order to determine start sequence — there is no engine-level priority mechanism, so
8
+ * registration position is the only guarantee that start() runs before any other service.
9
+ * By the time start() is called, all component bootstraps have completed (every table already
10
+ * exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every
11
+ * registered schema and connector.
10
12
  *
11
13
  * Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming
12
14
  * convention — current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
@@ -28,9 +30,9 @@ export declare class SchemaVersionService implements IComponent {
28
30
  static readonly CLASS_NAME: string;
29
31
  /**
30
32
  * Create a new SchemaVersionService.
31
- * @param options The constructor options.
33
+ * @param options Optional constructor options.
32
34
  */
33
- constructor(options: ISchemaVersionServiceConstructorOptions);
35
+ constructor(options?: ISchemaVersionServiceConstructorOptions);
34
36
  /**
35
37
  * Returns the class name.
36
38
  * @returns The class name.
package/docs/changelog.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.3-next.25](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.0.3-next.24...entity-storage-service-v0.0.3-next.25) (2026-06-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * migration progress ([#121](https://github.com/iotaledger/twin-entity-storage/issues/121)) ([d032162](https://github.com/iotaledger/twin-entity-storage/commit/d032162768b6b7d4ccca7e39b80f8bc3ba46440e))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/entity-storage-models bumped from 0.0.3-next.24 to 0.0.3-next.25
16
+ * devDependencies
17
+ * @twin.org/entity-storage-connector-memory bumped from 0.0.3-next.24 to 0.0.3-next.25
18
+
19
+ ## [0.0.3-next.24](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.0.3-next.23...entity-storage-service-v0.0.3-next.24) (2026-06-08)
20
+
21
+
22
+ ### Features
23
+
24
+ * add SchemaVersionService for automatic schema migrations ([#118](https://github.com/iotaledger/twin-entity-storage/issues/118)) ([b2ad843](https://github.com/iotaledger/twin-entity-storage/commit/b2ad8435185c53304aca99eb4d98582009b3902d))
25
+
26
+
27
+ ### Dependencies
28
+
29
+ * The following workspace dependencies were updated
30
+ * dependencies
31
+ * @twin.org/entity-storage-models bumped from 0.0.3-next.23 to 0.0.3-next.24
32
+ * devDependencies
33
+ * @twin.org/entity-storage-connector-memory bumped from 0.0.3-next.23 to 0.0.3-next.24
34
+
3
35
  ## [0.0.3-next.23](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.0.3-next.22...entity-storage-service-v0.0.3-next.23) (2026-06-08)
4
36
 
5
37
 
@@ -1,11 +1,8 @@
1
1
  # Class: SchemaVersion
2
2
 
3
- Entity that records the currently applied schema version for a managed entity schema.
4
- Persisted through a normal entity-storage connector, giving every backend a schemaVersion
5
- table/collection for free.
6
-
7
- SchemaVersionService processes this schema first before all others so that the version
8
- store is fully migrated before any version records are written for other schemas.
3
+ Tracks the currently applied schema version for each managed entity schema.
4
+ One record per schema name. Written once on first boot, then updated after
5
+ each successful migration.
9
6
 
10
7
  ## Constructors
11
8
 
@@ -23,7 +20,7 @@ store is fully migrated before any version records are written for other schemas
23
20
 
24
21
  > **schemaName**: `string`
25
22
 
26
- The schema type name (matches the key used in EntitySchemaFactory).
23
+ The entity schema type name primary key.
27
24
 
28
25
  ***
29
26
 
@@ -31,7 +28,7 @@ The schema type name (matches the key used in EntitySchemaFactory).
31
28
 
32
29
  > **version**: `number`
33
30
 
34
- The version currently applied in storage for this schema.
31
+ The currently deployed version of this schema.
35
32
 
36
33
  ***
37
34
 
@@ -2,10 +2,12 @@
2
2
 
3
3
  Service that checks and applies entity schema migrations at every node start-up.
4
4
 
5
- This service should be registered as the first component so that its start() runs before
6
- any other service. By the time start() is called, all component bootstraps have completed
7
- (every table already exists) and EntitySchemaFactory / EntityStorageConnectorFactory are
8
- fully populated with every registered schema and connector.
5
+ This service must be the first entry in coreTypeInitialisers.json. The engine iterates that
6
+ array in order to determine start sequence — there is no engine-level priority mechanism, so
7
+ registration position is the only guarantee that start() runs before any other service.
8
+ By the time start() is called, all component bootstraps have completed (every table already
9
+ exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every
10
+ registered schema and connector.
9
11
 
10
12
  Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming
11
13
  convention — current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
@@ -28,17 +30,17 @@ writes is a precondition for production; track this in the concurrency follow-up
28
30
 
29
31
  ### Constructor
30
32
 
31
- > **new SchemaVersionService**(`options`): `SchemaVersionService`
33
+ > **new SchemaVersionService**(`options?`): `SchemaVersionService`
32
34
 
33
35
  Create a new SchemaVersionService.
34
36
 
35
37
  #### Parameters
36
38
 
37
- ##### options
39
+ ##### options?
38
40
 
39
41
  [`ISchemaVersionServiceConstructorOptions`](../interfaces/ISchemaVersionServiceConstructorOptions.md)
40
42
 
41
- The constructor options.
43
+ Optional constructor options.
42
44
 
43
45
  #### Returns
44
46
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **initSchema**(): `void`
4
4
 
5
- Initialize the schema for the entity storage.
5
+ Initialize the schema for the entity storage models.
6
6
 
7
7
  ## Returns
8
8
 
@@ -11,6 +11,7 @@
11
11
  - [IEntityStorageRoutesExamples](interfaces/IEntityStorageRoutesExamples.md)
12
12
  - [IEntityStorageServiceConfig](interfaces/IEntityStorageServiceConfig.md)
13
13
  - [IEntityStorageServiceConstructorOptions](interfaces/IEntityStorageServiceConstructorOptions.md)
14
+ - [ISchemaVersionServiceConfig](interfaces/ISchemaVersionServiceConfig.md)
14
15
  - [ISchemaVersionServiceConstructorOptions](interfaces/ISchemaVersionServiceConstructorOptions.md)
15
16
 
16
17
  ## Variables
@@ -0,0 +1,11 @@
1
+ # Interface: ISchemaVersionServiceConfig
2
+
3
+ Constructor options config for SchemaVersionService.
4
+
5
+ ## Properties
6
+
7
+ ### batchSize? {#batchsize}
8
+
9
+ > `optional` **batchSize?**: `number`
10
+
11
+ The batch size for processing schema versions.
@@ -15,3 +15,11 @@ The version storage type.
15
15
  ```ts
16
16
  schema-version
17
17
  ```
18
+
19
+ ***
20
+
21
+ ### config? {#config}
22
+
23
+ > `optional` **config?**: [`ISchemaVersionServiceConfig`](ISchemaVersionServiceConfig.md)
24
+
25
+ Optional config.
package/locales/en.json CHANGED
@@ -6,5 +6,17 @@
6
6
  "noMigrationStepTarget": "No target schema for migration step {missingFromVersion} to {missingToVersion} found for \"{schemaName}\". Cannot complete migration from {stored} to {current}. Register the versioned schema class \"{schemaName}V{missingToVersion}\" in EntitySchemaFactory.",
7
7
  "connectorNotMigrationCapable": "Schema \"{schemaName}\" needs migration from version {stored} to {current} but its connector does not support automatic migration. Please migrate the schema manually before starting the node."
8
8
  }
9
+ },
10
+ "info": {
11
+ "schemaVersionService": {
12
+ "noMigrationRequired": "No migration required for schema \"{schemaName}\", version {version}.",
13
+ "migrationRequired": "Migration required for schema \"{schemaName}\" from version {from} to {to}.",
14
+ "partitionStart": "Starting migration of {itemTotal} partitions.",
15
+ "partitionProgress": "Migrating partition {itemIndex} of {itemTotal}.",
16
+ "partitionEnd": "Completed migration of {itemTotal} partitions.",
17
+ "partitionItemsStart": "Starting migration of {itemTotal} items in current partition.",
18
+ "partitionItemsProgress": "Migrating items in current partition: {itemIndex} of {itemTotal} processed.",
19
+ "partitionItemsEnd": "Completed migration of {itemTotal} items in current partition."
20
+ }
9
21
  }
10
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/entity-storage-service",
3
- "version": "0.0.3-next.23",
3
+ "version": "0.0.3-next.25",
4
4
  "description": "Service layer exposing storage contracts and REST endpoint definitions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,8 @@
17
17
  "@twin.org/api-models": "next",
18
18
  "@twin.org/core": "next",
19
19
  "@twin.org/entity": "next",
20
- "@twin.org/entity-storage-models": "0.0.3-next.23",
20
+ "@twin.org/entity-storage-models": "0.0.3-next.25",
21
+ "@twin.org/logging-models": "next",
21
22
  "@twin.org/nameof": "next",
22
23
  "@twin.org/web": "next"
23
24
  },