@twin.org/entity-storage-service 0.9.2-next.1 → 0.9.2-next.10

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.
@@ -8,7 +8,7 @@ import { entity, property } from "@twin.org/entity";
8
8
  */
9
9
  let SchemaVersion = class SchemaVersion {
10
10
  /**
11
- * The entity schema type name primary key.
11
+ * The entity schema type name - primary key.
12
12
  */
13
13
  schemaName;
14
14
  /**
@@ -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,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"]}
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"]}
@@ -1 +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
+ {"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 * Whether schema migration is enabled. When false the service detects pending migrations\n\t * and logs a warning for each lagging schema but does not apply any changes.\n\t * @default true\n\t */\n\tenabled?: boolean;\n\n\t/**\n\t * The batch size for processing schema versions.\n\t */\n\tbatchSize?: number;\n}\n"]}
@@ -1,5 +1,3 @@
1
- // Copyright 2026 IOTA Stiftung.
2
- // SPDX-License-Identifier: Apache-2.0.
3
1
  import { ComponentFactory, GeneralError, Is } from "@twin.org/core";
4
2
  import { EntitySchemaFactory, EntitySchemaHelper } from "@twin.org/entity";
5
3
  import { EntityStorageConnectorFactory, MigrationHelper, SchemaMigrationFactory } from "@twin.org/entity-storage-models";
@@ -8,14 +6,14 @@ import { SchemaVersion } from "./entities/schemaVersion.js";
8
6
  * Service that checks and applies entity schema migrations at every node start-up.
9
7
  *
10
8
  * 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
9
+ * array in order to determine start sequence - there is no engine-level priority mechanism, so
12
10
  * registration position is the only guarantee that start() runs before any other service.
13
11
  * By the time start() is called, all component bootstraps have completed (every table already
14
12
  * exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every
15
13
  * registered schema and connector.
16
14
  *
17
15
  * Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming
18
- * convention current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
16
+ * convention - current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
19
17
  * The service groups schemas by base name (strips the trailing V number suffix) and resolves the
20
18
  * migration chain automatically by diffing consecutive versioned schemas. For steps that require
21
19
  * property renames or a custom transform hook, register an optional ISchemaMigration entry in
@@ -38,6 +36,11 @@ export class SchemaVersionService {
38
36
  * @internal
39
37
  */
40
38
  static _VERSION_SUFFIX_RE = /^(.+)V(\d+)$/;
39
+ /**
40
+ * Key used for the global connector-version record in the schema-version table.
41
+ * @internal
42
+ */
43
+ static _CONNECTOR_VERSION_KEY = "connectorVersion";
41
44
  /**
42
45
  * The connector used to read and write SchemaVersion records.
43
46
  * Not readonly because finalizeMigration may return a replacement connector object.
@@ -71,11 +74,15 @@ export class SchemaVersionService {
71
74
  * SchemaVersion itself is processed first so the version store is migrated before any
72
75
  * version records are written for other schemas.
73
76
  *
77
+ * When config.enabled is false the service runs in detect-only mode: it identifies schemas
78
+ * that need migration and logs a warning for each one, but applies no changes.
79
+ *
74
80
  * Runs after all component bootstraps, so every managed table already exists.
75
81
  * @param nodeLoggingComponentType An optional logging component type.
76
82
  */
77
83
  async start(nodeLoggingComponentType) {
78
84
  const logging = ComponentFactory.getIfExists(nodeLoggingComponentType);
85
+ const enabled = this._config?.enabled !== false;
79
86
  const migrationOptions = {
80
87
  batchSize: this._config?.batchSize,
81
88
  onProgress: async (progressItem, itemTotal, itemIndex) => {
@@ -104,27 +111,73 @@ export class SchemaVersionService {
104
111
  }
105
112
  // 2. Read ALL stored version records, paging through the full table.
106
113
  const storedVersions = new Map();
114
+ let storedConnectorVersion;
107
115
  let cursor;
108
116
  do {
109
117
  const queryResult = await this._versionConnector.query(undefined, undefined, undefined, cursor);
110
118
  for (const record of queryResult.entities ?? []) {
111
119
  if (Is.object(record)) {
112
- storedVersions.set(record.schemaName, record.version);
120
+ if (record.schemaName === SchemaVersionService._CONNECTOR_VERSION_KEY) {
121
+ storedConnectorVersion = record.version;
122
+ }
123
+ else {
124
+ storedVersions.set(record.schemaName, record.version);
125
+ }
113
126
  }
114
127
  }
115
128
  cursor = queryResult.cursor;
116
129
  } while (Is.stringValue(cursor));
130
+ if (!enabled) {
131
+ // Detect-only: compare stored vs declared versions and warn about any lagging schemas.
132
+ // Schemas with no stored version record are skipped — they are either fresh installs
133
+ // or pre-tracking tables, neither of which can be diagnosed safely here.
134
+ for (const [schemaName, schema] of currentSchemas) {
135
+ const storedVersion = storedVersions.get(schemaName);
136
+ const currentVersion = EntitySchemaHelper.getVersion(schema);
137
+ if (!Is.undefined(storedVersion) && storedVersion < currentVersion) {
138
+ await logging?.log({
139
+ source: SchemaVersionService.CLASS_NAME,
140
+ level: "warn",
141
+ message: "migrationDisabled",
142
+ data: {
143
+ schemaName,
144
+ from: storedVersion,
145
+ to: currentVersion
146
+ }
147
+ });
148
+ }
149
+ }
150
+ return;
151
+ }
152
+ const currentConnectorVersion = await this.calculateConnectorVersion();
153
+ const effectiveStoredConnectorVersion = storedConnectorVersion ?? 0;
154
+ const forceUpgradeAllSchemas = effectiveStoredConnectorVersion !== currentConnectorVersion;
155
+ if (forceUpgradeAllSchemas) {
156
+ await logging?.log({
157
+ source: SchemaVersionService.CLASS_NAME,
158
+ level: "info",
159
+ message: "connectorVersionUpdated",
160
+ data: {
161
+ from: effectiveStoredConnectorVersion,
162
+ to: currentConnectorVersion
163
+ }
164
+ });
165
+ }
117
166
  // 3. Process SchemaVersion first so the version store itself is fully migrated
118
167
  // before any version records are written for other schemas.
119
168
  const schemaVersionName = "SchemaVersion";
120
169
  const schemaVersionSchema = currentSchemas.get(schemaVersionName);
121
170
  if (schemaVersionSchema) {
122
171
  currentSchemas.delete(schemaVersionName);
123
- await this.processSchema(schemaVersionName, schemaVersionSchema, storedVersions, historicalByBase.get(schemaVersionName), migrationOptions, nodeLoggingComponentType, logging);
172
+ await this.processSchema(schemaVersionName, schemaVersionSchema, storedVersions.get(schemaVersionName), historicalByBase.get(schemaVersionName), forceUpgradeAllSchemas, migrationOptions, nodeLoggingComponentType, logging);
124
173
  }
125
174
  // 4. Process all remaining schemas.
126
175
  for (const [schemaName, schema] of currentSchemas) {
127
- await this.processSchema(schemaName, schema, storedVersions, historicalByBase.get(schemaName), migrationOptions, nodeLoggingComponentType, logging);
176
+ await this.processSchema(schemaName, schema, storedVersions.get(schemaName), historicalByBase.get(schemaName), forceUpgradeAllSchemas, migrationOptions, nodeLoggingComponentType, logging);
177
+ }
178
+ if (effectiveStoredConnectorVersion !== currentConnectorVersion ||
179
+ Is.undefined(storedConnectorVersion)) {
180
+ await this.writeConnectorVersion(currentConnectorVersion);
128
181
  }
129
182
  }
130
183
  /**
@@ -132,14 +185,15 @@ export class SchemaVersionService {
132
185
  * Extracted to avoid continue statements in the outer loop.
133
186
  * @param schemaName The base schema name.
134
187
  * @param schema The current schema definition.
135
- * @param storedVersions The full map of stored version records.
188
+ * @param storedVersion The full map of stored version records.
136
189
  * @param history The versioned-schema map for this schema (historicalByBase.get(schemaName)), or undefined if none exist.
190
+ * @param forceUpgradeAllSchemas True when any connector version changed and all schemas must re-run connector bootstrap.
137
191
  * @param migrationOptions The migration options to pass through to MigrationHelper.
138
192
  * @param loggingComponentType The optional component type to use for logging the migration progress.
139
193
  * @param logging An optional logging component to pass through to MigrationHelper for migration progress logging.
140
194
  * @internal
141
195
  */
142
- async processSchema(schemaName, schema, storedVersions, history, migrationOptions, loggingComponentType, logging) {
196
+ async processSchema(schemaName, schema, storedVersion, history, forceUpgradeAllSchemas, migrationOptions, loggingComponentType, logging) {
143
197
  const currentVersion = EntitySchemaHelper.getVersion(schema);
144
198
  // Find the entity-storage connector whose schema type matches this schema name.
145
199
  // For SchemaVersion itself, use the injected connector directly rather than re-discovering
@@ -148,39 +202,69 @@ export class SchemaVersionService {
148
202
  ? { connector: this._versionConnector, factoryKey: undefined }
149
203
  : this.findConnector(schemaName);
150
204
  if (!connectorEntry) {
151
- // No connector registered for this schema nothing to migrate.
205
+ // No connector registered for this schema - nothing to migrate.
152
206
  return;
153
207
  }
154
208
  const { connector, factoryKey } = connectorEntry;
155
209
  // Resolve the stored version, applying the backwards-compat baseline when no record exists.
156
- const stored = storedVersions.get(schemaName);
157
210
  let resolvedStoredVersion;
158
- if (stored === undefined) {
159
- // No version record: treat as v0 regardless of whether the table has data.
160
- // On SQL connectors the table may have a stale column structure even when empty;
161
- // running the chain over zero rows still calls finalizeMigration, which reconciles
162
- // the table shape via a connector swap.
163
- // Deployment precondition: any pre-existing data is genuinely at v0. A deployment
164
- // that hand-applied a later schema before this service was introduced would be
165
- // incorrectly replayed v0→…→current and should be seeded with an explicit record.
211
+ // Captured when the fresh-vs-legacy check below already resolved the connector's
212
+ // partitions, so migrateWithChain can reuse them instead of fetching them again.
213
+ let partitions;
214
+ const migrationConnector = connector;
215
+ const boundGetPartitionContextIds = migrationConnector.getPartitionContextIds?.bind(migrationConnector);
216
+ if (Is.function(boundGetPartitionContextIds)) {
217
+ partitions = await boundGetPartitionContextIds();
218
+ }
219
+ if (storedVersion === undefined) {
220
+ // No version record: check whether the table has any data.
221
+ // Empty table → this is a fresh bootstrap; seed at the current version so the
222
+ // migration chain never runs over an already-current-shape (or empty) table.
223
+ // Non-empty table → pre-existing data from before version tracking was introduced;
224
+ // treat as v0 and run the migration chain. applyEntityTransform preserves existing
225
+ // property values so current-shape rows are not degraded.
226
+ let hasExistingData;
227
+ if (Is.undefined(partitions)) {
228
+ // Connector does not have partitioning, so we can safely call count.
229
+ hasExistingData = (await connector.count()) > 0;
230
+ }
231
+ else {
232
+ // Connector has partitioning, if there are partition keys
233
+ // then it must have data, otherwise the table is empty.
234
+ hasExistingData = partitions.length > 0;
235
+ }
236
+ if (!hasExistingData) {
237
+ await this.writeVersion(schemaName, currentVersion);
238
+ return;
239
+ }
166
240
  resolvedStoredVersion = 0;
167
241
  await this.writeVersion(schemaName, 0);
168
242
  }
169
243
  else {
170
- resolvedStoredVersion = stored;
244
+ resolvedStoredVersion = storedVersion;
171
245
  }
172
246
  // No-op: stored version already matches current.
173
247
  if (resolvedStoredVersion === currentVersion) {
248
+ if (!forceUpgradeAllSchemas) {
249
+ await logging?.log({
250
+ source: SchemaVersionService.CLASS_NAME,
251
+ level: "info",
252
+ message: "noMigrationRequired",
253
+ data: {
254
+ schemaName,
255
+ version: resolvedStoredVersion
256
+ }
257
+ });
258
+ return;
259
+ }
174
260
  await logging?.log({
175
261
  source: SchemaVersionService.CLASS_NAME,
176
262
  level: "info",
177
- message: "noMigrationRequired",
263
+ message: "connectorVersionForceUpgrade",
178
264
  data: {
179
- schemaName,
180
- version: resolvedStoredVersion
265
+ schemaName
181
266
  }
182
267
  });
183
- return;
184
268
  }
185
269
  await logging?.log({
186
270
  source: SchemaVersionService.CLASS_NAME,
@@ -192,7 +276,7 @@ export class SchemaVersionService {
192
276
  to: currentVersion
193
277
  }
194
278
  });
195
- // Downgrade not supported.
279
+ // Downgrade - not supported.
196
280
  if (resolvedStoredVersion > currentVersion) {
197
281
  throw new GeneralError(SchemaVersionService.CLASS_NAME, "storedVersionNewer", {
198
282
  schemaName,
@@ -209,8 +293,7 @@ export class SchemaVersionService {
209
293
  current: currentVersion
210
294
  });
211
295
  }
212
- const migrationConnector = connector;
213
- // Upgrade — resolve and run the chain.
296
+ // Upgrade - resolve and run the chain.
214
297
  const steps = [];
215
298
  for (let v = resolvedStoredVersion; v < currentVersion; v++) {
216
299
  const fromSchema = history?.get(v);
@@ -242,7 +325,7 @@ export class SchemaVersionService {
242
325
  transformEntityProperty: override?.transformEntityProperty
243
326
  });
244
327
  }
245
- const { finalConnector } = await MigrationHelper.migrateWithChain(migrationConnector, schemaName, steps, migrationOptions, loggingComponentType);
328
+ const { finalConnector } = await MigrationHelper.migrateWithChain(migrationConnector, schemaName, partitions, steps, migrationOptions, loggingComponentType);
246
329
  // Some connectors (e.g. in-memory) return a brand-new object from finalizeMigration
247
330
  // rather than mutating the source in place. Re-register the factory entry so that any
248
331
  // subsequent EntityStorageConnectorFactory.get() call returns the migrated instance.
@@ -273,6 +356,35 @@ export class SchemaVersionService {
273
356
  updatedAt: new Date().toISOString()
274
357
  });
275
358
  }
359
+ /**
360
+ * Upserts the global connector-version record.
361
+ * @param connectorVersion The connector implementation version.
362
+ * @internal
363
+ */
364
+ async writeConnectorVersion(connectorVersion) {
365
+ await this._versionConnector.set({
366
+ schemaName: SchemaVersionService._CONNECTOR_VERSION_KEY,
367
+ version: connectorVersion,
368
+ updatedAt: new Date().toISOString()
369
+ });
370
+ }
371
+ /**
372
+ * Get the global connector version from a migration-capable connector.
373
+ * Missing connectorVersion support is treated as version 0.
374
+ * @returns The connector version.
375
+ * @internal
376
+ */
377
+ async calculateConnectorVersion() {
378
+ if (!this._versionConnector || !("connectorVersion" in this._versionConnector)) {
379
+ return 0;
380
+ }
381
+ const connector = this._versionConnector;
382
+ const boundConnectorVersion = connector.connectorVersion.bind(connector);
383
+ if (Is.function(boundConnectorVersion)) {
384
+ return boundConnectorVersion();
385
+ }
386
+ return 0;
387
+ }
276
388
  /**
277
389
  * Searches EntityStorageConnectorFactory for the connector whose registered schema type
278
390
  * matches the given schema name.
@@ -289,7 +401,7 @@ export class SchemaVersionService {
289
401
  }
290
402
  }
291
403
  catch {
292
- // Connector not yet created or registration issue skip.
404
+ // Connector not yet created or registration issue - skip.
293
405
  }
294
406
  }
295
407
  return undefined;
@@ -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 { 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: resolvedStoredVersion,\n\t\t\t\tto: currentVersion\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
+ {"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 type { IContextIds } from \"@twin.org/context\";\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 * Key used for the global connector-version record in the schema-version table.\n\t * @internal\n\t */\n\tprivate static readonly _CONNECTOR_VERSION_KEY = \"connectorVersion\";\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 * When config.enabled is false the service runs in detect-only mode: it identifies schemas\n\t * that need migration and logs a warning for each one, but applies no changes.\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\t\tconst enabled = this._config?.enabled !== false;\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 storedConnectorVersion: number | undefined;\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\tif (record.schemaName === SchemaVersionService._CONNECTOR_VERSION_KEY) {\n\t\t\t\t\t\tstoredConnectorVersion = record.version;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstoredVersions.set(record.schemaName, record.version);\n\t\t\t\t\t}\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\tif (!enabled) {\n\t\t\t// Detect-only: compare stored vs declared versions and warn about any lagging schemas.\n\t\t\t// Schemas with no stored version record are skipped — they are either fresh installs\n\t\t\t// or pre-tracking tables, neither of which can be diagnosed safely here.\n\t\t\tfor (const [schemaName, schema] of currentSchemas) {\n\t\t\t\tconst storedVersion = storedVersions.get(schemaName);\n\t\t\t\tconst currentVersion = EntitySchemaHelper.getVersion(schema);\n\t\t\t\tif (!Is.undefined(storedVersion) && storedVersion < currentVersion) {\n\t\t\t\t\tawait logging?.log({\n\t\t\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\t\t\tlevel: \"warn\",\n\t\t\t\t\t\tmessage: \"migrationDisabled\",\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\tschemaName,\n\t\t\t\t\t\t\tfrom: storedVersion,\n\t\t\t\t\t\t\tto: currentVersion\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentConnectorVersion = await this.calculateConnectorVersion();\n\t\tconst effectiveStoredConnectorVersion = storedConnectorVersion ?? 0;\n\t\tconst forceUpgradeAllSchemas = effectiveStoredConnectorVersion !== currentConnectorVersion;\n\n\t\tif (forceUpgradeAllSchemas) {\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: \"connectorVersionUpdated\",\n\t\t\t\tdata: {\n\t\t\t\t\tfrom: effectiveStoredConnectorVersion,\n\t\t\t\t\tto: currentConnectorVersion\n\t\t\t\t}\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.get(schemaVersionName),\n\t\t\t\thistoricalByBase.get(schemaVersionName),\n\t\t\t\tforceUpgradeAllSchemas,\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.get(schemaName),\n\t\t\t\thistoricalByBase.get(schemaName),\n\t\t\t\tforceUpgradeAllSchemas,\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\tif (\n\t\t\teffectiveStoredConnectorVersion !== currentConnectorVersion ||\n\t\t\tIs.undefined(storedConnectorVersion)\n\t\t) {\n\t\t\tawait this.writeConnectorVersion(currentConnectorVersion);\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 storedVersion 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 forceUpgradeAllSchemas True when any connector version changed and all schemas must re-run connector bootstrap.\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\tstoredVersion: number | undefined,\n\t\thistory: Map<number, IEntitySchema> | undefined,\n\t\tforceUpgradeAllSchemas: boolean,\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\tlet resolvedStoredVersion: number;\n\n\t\t// Captured when the fresh-vs-legacy check below already resolved the connector's\n\t\t// partitions, so migrateWithChain can reuse them instead of fetching them again.\n\t\tlet partitions: IContextIds[] | undefined;\n\n\t\tconst migrationConnector = connector as IEntityStorageMigrationConnector;\n\n\t\tconst boundGetPartitionContextIds =\n\t\t\tmigrationConnector.getPartitionContextIds?.bind(migrationConnector);\n\t\tif (Is.function(boundGetPartitionContextIds)) {\n\t\t\tpartitions = await boundGetPartitionContextIds();\n\t\t}\n\n\t\tif (storedVersion === undefined) {\n\t\t\t// No version record: check whether the table has any data.\n\t\t\t// Empty table → this is a fresh bootstrap; seed at the current version so the\n\t\t\t// migration chain never runs over an already-current-shape (or empty) table.\n\t\t\t// Non-empty table → pre-existing data from before version tracking was introduced;\n\t\t\t// treat as v0 and run the migration chain. applyEntityTransform preserves existing\n\t\t\t// property values so current-shape rows are not degraded.\n\t\t\tlet hasExistingData: boolean;\n\t\t\tif (Is.undefined(partitions)) {\n\t\t\t\t// Connector does not have partitioning, so we can safely call count.\n\t\t\t\thasExistingData = (await connector.count()) > 0;\n\t\t\t} else {\n\t\t\t\t// Connector has partitioning, if there are partition keys\n\t\t\t\t// then it must have data, otherwise the table is empty.\n\t\t\t\thasExistingData = partitions.length > 0;\n\t\t\t}\n\n\t\t\tif (!hasExistingData) {\n\t\t\t\tawait this.writeVersion(schemaName, currentVersion);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolvedStoredVersion = 0;\n\t\t\tawait this.writeVersion(schemaName, 0);\n\t\t} else {\n\t\t\tresolvedStoredVersion = storedVersion;\n\t\t}\n\n\t\t// No-op: stored version already matches current.\n\t\tif (resolvedStoredVersion === currentVersion) {\n\t\t\tif (!forceUpgradeAllSchemas) {\n\t\t\t\tawait logging?.log({\n\t\t\t\t\tsource: SchemaVersionService.CLASS_NAME,\n\t\t\t\t\tlevel: \"info\",\n\t\t\t\t\tmessage: \"noMigrationRequired\",\n\t\t\t\t\tdata: {\n\t\t\t\t\t\tschemaName,\n\t\t\t\t\t\tversion: resolvedStoredVersion\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\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: \"connectorVersionForceUpgrade\",\n\t\t\t\tdata: {\n\t\t\t\t\tschemaName\n\t\t\t\t}\n\t\t\t});\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: resolvedStoredVersion,\n\t\t\t\tto: currentVersion\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\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\tpartitions,\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 * Upserts the global connector-version record.\n\t * @param connectorVersion The connector implementation version.\n\t * @internal\n\t */\n\tprivate async writeConnectorVersion(connectorVersion: number): Promise<void> {\n\t\tawait this._versionConnector.set({\n\t\t\tschemaName: SchemaVersionService._CONNECTOR_VERSION_KEY,\n\t\t\tversion: connectorVersion,\n\t\t\tupdatedAt: new Date().toISOString()\n\t\t});\n\t}\n\n\t/**\n\t * Get the global connector version from a migration-capable connector.\n\t * Missing connectorVersion support is treated as version 0.\n\t * @returns The connector version.\n\t * @internal\n\t */\n\tprivate async calculateConnectorVersion(): Promise<number> {\n\t\tif (!this._versionConnector || !(\"connectorVersion\" in this._versionConnector)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst connector = this._versionConnector as IEntityStorageMigrationConnector;\n\t\tconst boundConnectorVersion = connector.connectorVersion.bind(connector);\n\t\tif (Is.function(boundConnectorVersion)) {\n\t\t\treturn boundConnectorVersion();\n\t\t}\n\n\t\treturn 0;\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"]}
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export declare class SchemaVersion {
7
7
  /**
8
- * The entity schema type name primary key.
8
+ * The entity schema type name - primary key.
9
9
  */
10
10
  schemaName: string;
11
11
  /**
@@ -2,6 +2,12 @@
2
2
  * Constructor options config for SchemaVersionService.
3
3
  */
4
4
  export interface ISchemaVersionServiceConfig {
5
+ /**
6
+ * Whether schema migration is enabled. When false the service detects pending migrations
7
+ * and logs a warning for each lagging schema but does not apply any changes.
8
+ * @default true
9
+ */
10
+ enabled?: boolean;
5
11
  /**
6
12
  * The batch size for processing schema versions.
7
13
  */
@@ -4,14 +4,14 @@ import type { ISchemaVersionServiceConstructorOptions } from "./models/ISchemaVe
4
4
  * Service that checks and applies entity schema migrations at every node start-up.
5
5
  *
6
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
7
+ * array in order to determine start sequence - there is no engine-level priority mechanism, so
8
8
  * registration position is the only guarantee that start() runs before any other service.
9
9
  * By the time start() is called, all component bootstraps have completed (every table already
10
10
  * exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every
11
11
  * registered schema and connector.
12
12
  *
13
13
  * Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming
14
- * convention current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
14
+ * convention - current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
15
15
  * The service groups schemas by base name (strips the trailing V number suffix) and resolves the
16
16
  * migration chain automatically by diffing consecutive versioned schemas. For steps that require
17
17
  * property renames or a custom transform hook, register an optional ISchemaMigration entry in
@@ -45,6 +45,9 @@ export declare class SchemaVersionService implements IComponent {
45
45
  * SchemaVersion itself is processed first so the version store is migrated before any
46
46
  * version records are written for other schemas.
47
47
  *
48
+ * When config.enabled is false the service runs in detect-only mode: it identifies schemas
49
+ * that need migration and logs a warning for each one, but applies no changes.
50
+ *
48
51
  * Runs after all component bootstraps, so every managed table already exists.
49
52
  * @param nodeLoggingComponentType An optional logging component type.
50
53
  */
package/docs/changelog.md CHANGED
@@ -1,5 +1,149 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.2-next.10](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.9...entity-storage-service-v0.9.2-next.10) (2026-08-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * schema service warning ([#215](https://github.com/iotaledger/twin-entity-storage/issues/215)) ([a69c9ae](https://github.com/iotaledger/twin-entity-storage/commit/a69c9ae54506a1f5dafd0e073678a105cd9ec9b9))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.9 to 0.9.2-next.10
16
+ * devDependencies
17
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.9 to 0.9.2-next.10
18
+
19
+ ## [0.9.2-next.9](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.8...entity-storage-service-v0.9.2-next.9) (2026-08-12)
20
+
21
+
22
+ ### Miscellaneous Chores
23
+
24
+ * **entity-storage-service:** Synchronize repo versions
25
+
26
+
27
+ ### Dependencies
28
+
29
+ * The following workspace dependencies were updated
30
+ * dependencies
31
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.8 to 0.9.2-next.9
32
+ * devDependencies
33
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.8 to 0.9.2-next.9
34
+
35
+ ## [0.9.2-next.8](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.7...entity-storage-service-v0.9.2-next.8) (2026-08-12)
36
+
37
+
38
+ ### Features
39
+
40
+ * indexing ([#207](https://github.com/iotaledger/twin-entity-storage/issues/207)) ([2fd1f0d](https://github.com/iotaledger/twin-entity-storage/commit/2fd1f0d992344905c9dd1a44addfc4f9d5b5c168))
41
+
42
+
43
+ ### Dependencies
44
+
45
+ * The following workspace dependencies were updated
46
+ * dependencies
47
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.7 to 0.9.2-next.8
48
+ * devDependencies
49
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.7 to 0.9.2-next.8
50
+
51
+ ## [0.9.2-next.7](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.6...entity-storage-service-v0.9.2-next.7) (2026-08-11)
52
+
53
+
54
+ ### Miscellaneous Chores
55
+
56
+ * **entity-storage-service:** Synchronize repo versions
57
+
58
+
59
+ ### Dependencies
60
+
61
+ * The following workspace dependencies were updated
62
+ * dependencies
63
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.6 to 0.9.2-next.7
64
+ * devDependencies
65
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.6 to 0.9.2-next.7
66
+
67
+ ## [0.9.2-next.6](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.5...entity-storage-service-v0.9.2-next.6) (2026-08-07)
68
+
69
+
70
+ ### Miscellaneous Chores
71
+
72
+ * **entity-storage-service:** Synchronize repo versions
73
+
74
+
75
+ ### Dependencies
76
+
77
+ * The following workspace dependencies were updated
78
+ * dependencies
79
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.5 to 0.9.2-next.6
80
+ * devDependencies
81
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.5 to 0.9.2-next.6
82
+
83
+ ## [0.9.2-next.5](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.4...entity-storage-service-v0.9.2-next.5) (2026-08-07)
84
+
85
+
86
+ ### Features
87
+
88
+ * linting and dependency update ([c307b60](https://github.com/iotaledger/twin-entity-storage/commit/c307b606d03ea436b7c43d4e1764b5c08f415555))
89
+
90
+
91
+ ### Dependencies
92
+
93
+ * The following workspace dependencies were updated
94
+ * dependencies
95
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.4 to 0.9.2-next.5
96
+ * devDependencies
97
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.4 to 0.9.2-next.5
98
+
99
+ ## [0.9.2-next.4](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.3...entity-storage-service-v0.9.2-next.4) (2026-08-04)
100
+
101
+
102
+ ### Bug Fixes
103
+
104
+ * schema version check crashes in multi-tenant mode ([#192](https://github.com/iotaledger/twin-entity-storage/issues/192)) ([a816341](https://github.com/iotaledger/twin-entity-storage/commit/a8163415ce116582f3c3c23294f7f4062099d8f3))
105
+
106
+
107
+ ### Dependencies
108
+
109
+ * The following workspace dependencies were updated
110
+ * dependencies
111
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.3 to 0.9.2-next.4
112
+ * devDependencies
113
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.3 to 0.9.2-next.4
114
+
115
+ ## [0.9.2-next.3](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.2...entity-storage-service-v0.9.2-next.3) (2026-08-03)
116
+
117
+
118
+ ### Miscellaneous Chores
119
+
120
+ * **entity-storage-service:** Synchronize repo versions
121
+
122
+
123
+ ### Dependencies
124
+
125
+ * The following workspace dependencies were updated
126
+ * dependencies
127
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.2 to 0.9.2-next.3
128
+ * devDependencies
129
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.2 to 0.9.2-next.3
130
+
131
+ ## [0.9.2-next.2](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.1...entity-storage-service-v0.9.2-next.2) (2026-07-29)
132
+
133
+
134
+ ### Bug Fixes
135
+
136
+ * use existing data during migration ([#186](https://github.com/iotaledger/twin-entity-storage/issues/186)) ([c6e91f5](https://github.com/iotaledger/twin-entity-storage/commit/c6e91f54198d4ab1cc242d0825f63082980d4f9c))
137
+
138
+
139
+ ### Dependencies
140
+
141
+ * The following workspace dependencies were updated
142
+ * dependencies
143
+ * @twin.org/entity-storage-models bumped from 0.9.2-next.1 to 0.9.2-next.2
144
+ * devDependencies
145
+ * @twin.org/entity-storage-connector-memory bumped from 0.9.2-next.1 to 0.9.2-next.2
146
+
3
147
  ## [0.9.2-next.1](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.9.2-next.0...entity-storage-service-v0.9.2-next.1) (2026-07-29)
4
148
 
5
149
 
@@ -20,7 +20,7 @@ each successful migration.
20
20
 
21
21
  > **schemaName**: `string`
22
22
 
23
- The entity schema type name primary key.
23
+ The entity schema type name - primary key.
24
24
 
25
25
  ***
26
26
 
@@ -3,14 +3,14 @@
3
3
  Service that checks and applies entity schema migrations at every node start-up.
4
4
 
5
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
6
+ array in order to determine start sequence - there is no engine-level priority mechanism, so
7
7
  registration position is the only guarantee that start() runs before any other service.
8
8
  By the time start() is called, all component bootstraps have completed (every table already
9
9
  exists) and EntitySchemaFactory / EntityStorageConnectorFactory are fully populated with every
10
10
  registered schema and connector.
11
11
 
12
12
  Migration mechanics: old schema versions are registered in EntitySchemaFactory by naming
13
- convention current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
13
+ convention - current schema = "MyEntity", first history = "MyEntityV0", second = "MyEntityV1".
14
14
  The service groups schemas by base name (strips the trailing V number suffix) and resolves the
15
15
  migration chain automatically by diffing consecutive versioned schemas. For steps that require
16
16
  property renames or a custom transform hook, register an optional ISchemaMigration entry in
@@ -84,6 +84,9 @@ whose stored version is behind the current version declared in EntitySchemaFacto
84
84
  SchemaVersion itself is processed first so the version store is migrated before any
85
85
  version records are written for other schemas.
86
86
 
87
+ When config.enabled is false the service runs in detect-only mode: it identifies schemas
88
+ that need migration and logs a warning for each one, but applies no changes.
89
+
87
90
  Runs after all component bootstraps, so every managed table already exists.
88
91
 
89
92
  #### Parameters
@@ -4,6 +4,21 @@ Constructor options config for SchemaVersionService.
4
4
 
5
5
  ## Properties
6
6
 
7
+ ### enabled? {#enabled}
8
+
9
+ > `optional` **enabled?**: `boolean`
10
+
11
+ Whether schema migration is enabled. When false the service detects pending migrations
12
+ and logs a warning for each lagging schema but does not apply any changes.
13
+
14
+ #### Default
15
+
16
+ ```ts
17
+ true
18
+ ```
19
+
20
+ ***
21
+
7
22
  ### batchSize? {#batchsize}
8
23
 
9
24
  > `optional` **batchSize?**: `number`
package/locales/en.json CHANGED
@@ -7,10 +7,17 @@
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
9
  },
10
+ "warn": {
11
+ "schemaVersionService": {
12
+ "migrationDisabled": "Schema migration is disabled. Schema \"{schemaName}\" requires migration from version {from} to {to}. Continuing without migration may cause errors."
13
+ }
14
+ },
10
15
  "info": {
11
16
  "schemaVersionService": {
12
17
  "noMigrationRequired": "No migration required for schema \"{schemaName}\", version {version}.",
13
18
  "migrationRequired": "Migration required for schema \"{schemaName}\" from version {from} to {to}.",
19
+ "connectorVersionUpdated": "Connector version changed from {from} to {to}; forcing schema migrations.",
20
+ "connectorVersionForceUpgrade": "Forcing migration for schema \"{schemaName}\" because connector version changed.",
14
21
  "partitionStart": "Starting migration of {itemTotal} partitions.",
15
22
  "partitionProgress": "Migrating partition {itemIndex} of {itemTotal}.",
16
23
  "partitionEnd": "Completed migration of {itemTotal} partitions.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/entity-storage-service",
3
- "version": "0.9.2-next.1",
3
+ "version": "0.9.2-next.10",
4
4
  "description": "Service layer exposing storage contracts and REST endpoint definitions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,9 +15,10 @@
15
15
  },
16
16
  "dependencies": {
17
17
  "@twin.org/api-models": "next",
18
+ "@twin.org/context": "next",
18
19
  "@twin.org/core": "next",
19
20
  "@twin.org/entity": "next",
20
- "@twin.org/entity-storage-models": "0.9.2-next.1",
21
+ "@twin.org/entity-storage-models": "0.9.2-next.10",
21
22
  "@twin.org/logging-models": "next",
22
23
  "@twin.org/nameof": "next",
23
24
  "@twin.org/web": "next"