@twin.org/entity-storage-service 0.10.1-next.12 → 0.10.1-next.14

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.
@@ -81,6 +81,9 @@ export class SchemaVersionService {
81
81
  * When config.enabled is false the service runs in detect-only mode: it identifies schemas
82
82
  * that need migration and logs a warning for each one, but applies no changes.
83
83
  *
84
+ * A schema whose storage is missing columns fails with columnsMissingWithoutMigration when no
85
+ * rebuild will add them, including in detect-only mode.
86
+ *
84
87
  * Runs after all component bootstraps, so every managed table already exists.
85
88
  * @param nodeLoggingComponentType An optional logging component type.
86
89
  */
@@ -142,6 +145,7 @@ export class SchemaVersionService {
142
145
  // Detect-only: compare stored vs declared versions and warn about any lagging schemas.
143
146
  // Schemas with no stored version record are skipped — they are either fresh installs
144
147
  // or pre-tracking tables, neither of which can be diagnosed safely here.
148
+ // Missing columns still fail loudly: no migration will ever run to add them here.
145
149
  for (const [schemaName, schema] of currentSchemas) {
146
150
  const storedVersion = storedVersions.get(schemaName);
147
151
  const currentVersion = EntitySchemaHelper.getVersion(schema);
@@ -157,6 +161,16 @@ export class SchemaVersionService {
157
161
  }
158
162
  });
159
163
  }
164
+ const connector = schemaName === "SchemaVersion"
165
+ ? this._versionConnector
166
+ : this.findConnector(schemaName)?.connector;
167
+ const missingColumns = connector ? this.missingColumns(connector) : [];
168
+ if (missingColumns.length > 0) {
169
+ throw new GeneralError(SchemaVersionService.CLASS_NAME, "columnsMissingWithoutMigration", {
170
+ schemaName,
171
+ missingColumns: missingColumns.join(", ")
172
+ });
173
+ }
160
174
  }
161
175
  return;
162
176
  }
@@ -229,6 +243,17 @@ export class SchemaVersionService {
229
243
  else {
230
244
  resolvedStoredVersion = storedVersion;
231
245
  }
246
+ // Missing columns are only added by a rebuild, so fail at start when none is coming rather
247
+ // than on the first write.
248
+ const missingColumns = this.missingColumns(connector);
249
+ if (missingColumns.length > 0 &&
250
+ resolvedStoredVersion === currentVersion &&
251
+ !forceUpgradeAllSchemas) {
252
+ throw new GeneralError(SchemaVersionService.CLASS_NAME, "columnsMissingWithoutMigration", {
253
+ schemaName,
254
+ missingColumns: missingColumns.join(", ")
255
+ });
256
+ }
232
257
  // No-op: stored version already matches current.
233
258
  if (resolvedStoredVersion === currentVersion) {
234
259
  if (!forceUpgradeAllSchemas) {
@@ -407,6 +432,15 @@ export class SchemaVersionService {
407
432
  }
408
433
  return undefined;
409
434
  }
435
+ /**
436
+ * Get the schema columns bootstrap found missing from a connector's storage.
437
+ * @param connector The connector to inspect.
438
+ * @returns The missing column names, empty when none or the connector does not report them.
439
+ * @internal
440
+ */
441
+ missingColumns(connector) {
442
+ return connector.getMissingColumns?.() ?? [];
443
+ }
410
444
  /**
411
445
  * Logs migration progress using the provided logging component, if available.
412
446
  * @param logging The logging component to use for logging progress, if available.
@@ -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 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, and applyEntityTransform is not idempotent for structural changes.\n * A finalizing marker record is therefore written before finalizeMigration and removed after\n * the version write; a start-up that finds one refuses to run (migrationInterrupted).\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 * Suffix of the record written while a schema's migration is being finalized.\n\t * @internal\n\t */\n\tprivate static readonly _FINALIZING_SUFFIX = \":finalizing\";\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.endsWith(SchemaVersionService._FINALIZING_SUFFIX)) {\n\t\t\t\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"migrationInterrupted\", {\n\t\t\t\t\t\t\tschemaName: record.schemaName.slice(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t-SchemaVersionService._FINALIZING_SUFFIX.length\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tversion: record.version,\n\t\t\t\t\t\t\tmarker: record.schemaName\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (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\tif (storedVersion === undefined) {\n\t\t\t// No version record: treat the schema as v0, with or without data. The physical\n\t\t\t// shape cannot be verified, so for any current version above 0 the migration chain\n\t\t\t// rebuilds the storage; empty and legacy-partitioned tables included.\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\ttransformEntity: override?.transformEntity,\n\t\t\t\ttransformEntityProperty: override?.transformEntityProperty,\n\t\t\t\tremoveEntityProperty: override?.removeEntityProperty\n\t\t\t});\n\t\t}\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(loggingComponentType);\n\t\t}\n\t\tconst finalizingMarker = `${schemaName}${SchemaVersionService._FINALIZING_SUFFIX}`;\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\t{\n\t\t\t\t...migrationOptions,\n\t\t\t\tonFinalizing: async () => this.writeVersion(finalizingMarker, currentVersion)\n\t\t\t},\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\tawait this._versionConnector.remove(finalizingMarker);\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"]}
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, and applyEntityTransform is not idempotent for structural changes.\n * A finalizing marker record is therefore written before finalizeMigration and removed after\n * the version write; a start-up that finds one refuses to run (migrationInterrupted).\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 * Suffix of the record written while a schema's migration is being finalized.\n\t * @internal\n\t */\n\tprivate static readonly _FINALIZING_SUFFIX = \":finalizing\";\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 * A schema whose storage is missing columns fails with columnsMissingWithoutMigration when no\n\t * rebuild will add them, including in detect-only mode.\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.endsWith(SchemaVersionService._FINALIZING_SUFFIX)) {\n\t\t\t\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"migrationInterrupted\", {\n\t\t\t\t\t\t\tschemaName: record.schemaName.slice(\n\t\t\t\t\t\t\t\t0,\n\t\t\t\t\t\t\t\t-SchemaVersionService._FINALIZING_SUFFIX.length\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tversion: record.version,\n\t\t\t\t\t\t\tmarker: record.schemaName\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (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\t// Missing columns still fail loudly: no migration will ever run to add them 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\n\t\t\t\tconst connector =\n\t\t\t\t\tschemaName === nameof(SchemaVersion)\n\t\t\t\t\t\t? (this._versionConnector as IEntityStorageConnector)\n\t\t\t\t\t\t: this.findConnector(schemaName)?.connector;\n\t\t\t\tconst missingColumns = connector ? this.missingColumns(connector) : [];\n\t\t\t\tif (missingColumns.length > 0) {\n\t\t\t\t\tthrow new GeneralError(\n\t\t\t\t\t\tSchemaVersionService.CLASS_NAME,\n\t\t\t\t\t\t\"columnsMissingWithoutMigration\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tschemaName,\n\t\t\t\t\t\t\tmissingColumns: missingColumns.join(\", \")\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\tif (storedVersion === undefined) {\n\t\t\t// No version record: treat the schema as v0, with or without data. The physical\n\t\t\t// shape cannot be verified, so for any current version above 0 the migration chain\n\t\t\t// rebuilds the storage; empty and legacy-partitioned tables included.\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// Missing columns are only added by a rebuild, so fail at start when none is coming rather\n\t\t// than on the first write.\n\t\tconst missingColumns = this.missingColumns(connector);\n\t\tif (\n\t\t\tmissingColumns.length > 0 &&\n\t\t\tresolvedStoredVersion === currentVersion &&\n\t\t\t!forceUpgradeAllSchemas\n\t\t) {\n\t\t\tthrow new GeneralError(SchemaVersionService.CLASS_NAME, \"columnsMissingWithoutMigration\", {\n\t\t\t\tschemaName,\n\t\t\t\tmissingColumns: missingColumns.join(\", \")\n\t\t\t});\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\ttransformEntity: override?.transformEntity,\n\t\t\t\ttransformEntityProperty: override?.transformEntityProperty,\n\t\t\t\tremoveEntityProperty: override?.removeEntityProperty\n\t\t\t});\n\t\t}\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(loggingComponentType);\n\t\t}\n\t\tconst finalizingMarker = `${schemaName}${SchemaVersionService._FINALIZING_SUFFIX}`;\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\t{\n\t\t\t\t...migrationOptions,\n\t\t\t\tonFinalizing: async () => this.writeVersion(finalizingMarker, currentVersion)\n\t\t\t},\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\tawait this._versionConnector.remove(finalizingMarker);\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 * Get the schema columns bootstrap found missing from a connector's storage.\n\t * @param connector The connector to inspect.\n\t * @returns The missing column names, empty when none or the connector does not report them.\n\t * @internal\n\t */\n\tprivate missingColumns(connector: IEntityStorageConnector): string[] {\n\t\treturn (connector as IEntityStorageMigrationConnector).getMissingColumns?.() ?? [];\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"]}
@@ -47,6 +47,9 @@ export declare class SchemaVersionService implements IComponent {
47
47
  * When config.enabled is false the service runs in detect-only mode: it identifies schemas
48
48
  * that need migration and logs a warning for each one, but applies no changes.
49
49
  *
50
+ * A schema whose storage is missing columns fails with columnsMissingWithoutMigration when no
51
+ * rebuild will add them, including in detect-only mode.
52
+ *
50
53
  * Runs after all component bootstraps, so every managed table already exists.
51
54
  * @param nodeLoggingComponentType An optional logging component type.
52
55
  */
package/docs/changelog.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.1-next.14](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.10.1-next.13...entity-storage-service-v0.10.1-next.14) (2026-09-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * defer indexes over missing columns until the schema rebuild adds them ([#322](https://github.com/iotaledger/twin-entity-storage/issues/322)) ([9999639](https://github.com/iotaledger/twin-entity-storage/commit/99996399d22e23fa2587c86212f5b19ad45bad8f))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/entity-storage-models bumped from 0.10.1-next.13 to 0.10.1-next.14
16
+ * devDependencies
17
+ * @twin.org/entity-storage-connector-memory bumped from 0.10.1-next.13 to 0.10.1-next.14
18
+ * @twin.org/entity-storage-connector-mysql bumped from 0.10.1-next.13 to 0.10.1-next.14
19
+
20
+ ## [0.10.1-next.13](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.10.1-next.12...entity-storage-service-v0.10.1-next.13) (2026-09-24)
21
+
22
+
23
+ ### Miscellaneous Chores
24
+
25
+ * **entity-storage-service:** Synchronize repo versions
26
+
27
+
28
+ ### Dependencies
29
+
30
+ * The following workspace dependencies were updated
31
+ * dependencies
32
+ * @twin.org/entity-storage-models bumped from 0.10.1-next.12 to 0.10.1-next.13
33
+ * devDependencies
34
+ * @twin.org/entity-storage-connector-memory bumped from 0.10.1-next.12 to 0.10.1-next.13
35
+
3
36
  ## [0.10.1-next.12](https://github.com/iotaledger/twin-entity-storage/compare/entity-storage-service-v0.10.1-next.11...entity-storage-service-v0.10.1-next.12) (2026-09-23)
4
37
 
5
38
 
@@ -86,6 +86,9 @@ version records are written for other schemas.
86
86
  When config.enabled is false the service runs in detect-only mode: it identifies schemas
87
87
  that need migration and logs a warning for each one, but applies no changes.
88
88
 
89
+ A schema whose storage is missing columns fails with columnsMissingWithoutMigration when no
90
+ rebuild will add them, including in detect-only mode.
91
+
89
92
  Runs after all component bootstraps, so every managed table already exists.
90
93
 
91
94
  #### Parameters
package/locales/en.json CHANGED
@@ -5,7 +5,8 @@
5
5
  "noMigrationStep": "No migration step from version {missingFromVersion} to {missingToVersion} found for \"{schemaName}\". Cannot complete migration from {stored} to {current}. Register the versioned schema class \"{schemaName}V{missingFromVersion}\" in EntitySchemaFactory.",
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
- "migrationInterrupted": "A previous start-up died while finalizing the migration of schema \"{schemaName}\" to version {version}. Restore the data from the leftover migration copy, then delete the \"{marker}\" record from the schema version store."
8
+ "migrationInterrupted": "A previous start-up died while finalizing the migration of schema \"{schemaName}\" to version {version}. Restore the data from the leftover migration copy, then delete the \"{marker}\" record from the schema version store.",
9
+ "columnsMissingWithoutMigration": "Schema \"{schemaName}\" is missing columns {missingColumns} in its existing storage and no migration will rebuild it. Bump the entity version so the storage is rebuilt, or add the missing columns or drop the storage so it is recreated."
9
10
  }
10
11
  },
11
12
  "warn": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/entity-storage-service",
3
- "version": "0.10.1-next.12",
3
+ "version": "0.10.1-next.14",
4
4
  "description": "Service layer exposing storage contracts and REST endpoint definitions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  "@twin.org/context": "next",
19
19
  "@twin.org/core": "next",
20
20
  "@twin.org/entity": "next",
21
- "@twin.org/entity-storage-models": "0.10.1-next.12",
21
+ "@twin.org/entity-storage-models": "0.10.1-next.14",
22
22
  "@twin.org/logging-models": "next",
23
23
  "@twin.org/nameof": "next",
24
24
  "@twin.org/web": "next"