@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
105 lines (84 loc) • 3.92 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {type SchemaDefinition} from './schema-definition.js';
import {type SchemaMigration} from './schema-migration.js';
import {SemanticVersion} from '../../../../business/utils/semantic-version.js';
import {type ClassConstructor} from '../../../../business/utils/class-constructor.type.js';
import {type ObjectMapper} from '../../../mapper/api/object-mapper.js';
import {SchemaValidationError} from './schema-validation-error.js';
export abstract class SchemaDefinitionBase<T> implements SchemaDefinition<T> {
public abstract get classConstructor(): ClassConstructor<T>;
public abstract get migrations(): SchemaMigration[];
public abstract get name(): string;
public abstract get version(): SemanticVersion<number>;
protected constructor(protected readonly mapper: ObjectMapper) {}
public async transform(data: object, sourceVersion?: SemanticVersion<number>): Promise<T> {
if (data === undefined || data === null) {
return null;
}
const clone: any = structuredClone(data);
let dataVersion: number = clone.schemaVersion;
if (!dataVersion) {
dataVersion = sourceVersion ? sourceVersion.major : 0;
}
const migrated: object = await this.applyMigrations(clone, new SemanticVersion(dataVersion));
return this.mapper.fromObject(this.classConstructor, migrated);
}
public async validateMigrations(): Promise<void> {
if (this.migrations.length === 0) {
return;
}
// eslint-disable-next-line unicorn/no-array-sort
const versionJumps: number[] = this.migrations.map((value): number => value.version.major).sort();
for (let index: number = 1; index < versionJumps.length; index++) {
if (versionJumps[index] === versionJumps[index - 1]) {
throw new SchemaValidationError(`Duplicate migration version '${versionJumps[index]}'`);
}
}
let currentVersion: SemanticVersion<number> = this.nextVersionJump(new SemanticVersion(0));
for (const versionJump of versionJumps) {
const v: SemanticVersion<number> = new SemanticVersion(versionJump);
if (!v.equals(currentVersion)) {
throw new SchemaValidationError(
`Invalid migration version sequence detected; expected version '${v.major}' but got '${currentVersion.major}'`,
);
}
currentVersion = this.nextVersionJump(currentVersion);
}
return;
}
protected nextVersionJump(currentVersion: SemanticVersion<number>): SemanticVersion<number> {
const targetMigrations: SchemaMigration[] = this.findMigrations(currentVersion);
if (!targetMigrations || targetMigrations.length === 0) {
// No migration found for the current version - fail with an error
throw new SchemaValidationError(
`No migration found for version '${currentVersion.major}'; there is a gap in the migration sequence`,
);
}
return targetMigrations[0].version;
}
/*
* DV < version = 1 >
* M1 < range = [0, 6), version = 6 >
* M1.1 < range = [0, 4), version = 5 >
* M2 < range = [6, 7), version = 8 >
*/
protected async applyMigrations(data: object, dataVersion: SemanticVersion<number>): Promise<object> {
let migrations: SchemaMigration[] = this.findMigrations(dataVersion);
while (migrations.length > 0) {
const migration: SchemaMigration = migrations[0];
data = await migration.migrate(data);
dataVersion = migration.version;
migrations = this.findMigrations(dataVersion);
}
return data;
}
protected findMigrations(dataVersion: SemanticVersion<number>): SchemaMigration[] {
const eligibleMigrations: SchemaMigration[] = this.migrations.filter((value): boolean =>
value.range.contains(dataVersion),
);
if (eligibleMigrations.length > 0) {
eligibleMigrations.sort((l, r): number => l.version.compare(r.version));
}
return eligibleMigrations;
}
}