mirror of
https://github.com/hexastack/hexabot
synced 2025-05-08 14:54:45 +00:00
feat: add compodoc + enforce typing
This commit is contained in:
parent
652ca78120
commit
252857fbf7
@ -11,6 +11,7 @@ will apply migrations automatically but only if it's a dev environement and `con
|
|||||||
- Track migration execution status in a MongoDB collection (`migrations`).
|
- Track migration execution status in a MongoDB collection (`migrations`).
|
||||||
- Run individual or all migrations with ease.
|
- Run individual or all migrations with ease.
|
||||||
- Built-in support for rollback logic.
|
- Built-in support for rollback logic.
|
||||||
|
- Keeps track of the database schema version in the metadata collection (SettingModule).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import { Command, CommandRunner } from 'nest-commander';
|
|||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
|
|
||||||
import { MigrationService } from './migration.service';
|
import { MigrationService } from './migration.service';
|
||||||
import { MigrationAction } from './types';
|
import { MigrationAction, MigrationVersion } from './types';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'migration',
|
name: 'migration',
|
||||||
@ -28,22 +28,34 @@ export class MigrationCommand extends CommandRunner {
|
|||||||
async run(passedParam: string[]): Promise<void> {
|
async run(passedParam: string[]): Promise<void> {
|
||||||
const [subcommand] = passedParam;
|
const [subcommand] = passedParam;
|
||||||
switch (subcommand) {
|
switch (subcommand) {
|
||||||
case 'create':
|
case 'create': {
|
||||||
const [, filename] = passedParam;
|
const [, version] = passedParam;
|
||||||
return await this.migrationService.create(filename);
|
|
||||||
case 'migrate':
|
if (!this.isValidVersion(version)) {
|
||||||
const [, action, name] = passedParam;
|
throw new TypeError('Invalid version value.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.migrationService.create(version);
|
||||||
|
}
|
||||||
|
case 'migrate': {
|
||||||
|
const [, action, version] = passedParam;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!Object.values(MigrationAction).includes(action as MigrationAction)
|
!Object.values(MigrationAction).includes(action as MigrationAction)
|
||||||
) {
|
) {
|
||||||
this.logger.error('Invalid Operation');
|
this.logger.error('Invalid Operation');
|
||||||
this.exit();
|
this.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isValidVersion(version)) {
|
||||||
|
throw new TypeError('Invalid version value.');
|
||||||
|
}
|
||||||
|
|
||||||
return await this.migrationService.run({
|
return await this.migrationService.run({
|
||||||
action: action as MigrationAction,
|
action: action as MigrationAction,
|
||||||
name,
|
version,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
this.logger.error('No valid command provided');
|
this.logger.error('No valid command provided');
|
||||||
this.exit();
|
this.exit();
|
||||||
@ -55,4 +67,14 @@ export class MigrationCommand extends CommandRunner {
|
|||||||
this.logger.log('Exiting migration process.');
|
this.logger.log('Exiting migration process.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the migration version is in valid format
|
||||||
|
* @param version migration version name
|
||||||
|
* @returns True, if the migration version name is valid
|
||||||
|
*/
|
||||||
|
public isValidVersion(version: string): version is MigrationVersion {
|
||||||
|
const regex = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
||||||
|
return regex.test(version);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
|
||||||
@ -18,7 +19,11 @@ import { MigrationModel } from './migration.schema';
|
|||||||
import { MigrationService } from './migration.service';
|
import { MigrationService } from './migration.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MongooseModule.forFeature([MigrationModel]), LoggerModule],
|
imports: [
|
||||||
|
MongooseModule.forFeature([MigrationModel]),
|
||||||
|
LoggerModule,
|
||||||
|
HttpModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
MigrationService,
|
MigrationService,
|
||||||
MigrationCommand,
|
MigrationCommand,
|
||||||
|
@ -11,12 +11,12 @@ import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|||||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||||
import { THydratedDocument } from '@/utils/types/filter.types';
|
import { THydratedDocument } from '@/utils/types/filter.types';
|
||||||
|
|
||||||
import { MigrationAction } from './types';
|
import { MigrationAction, MigrationVersion } from './types';
|
||||||
|
|
||||||
@Schema({ timestamps: true })
|
@Schema({ timestamps: true })
|
||||||
export class Migration {
|
export class Migration {
|
||||||
@Prop({ type: String, required: true, unique: true })
|
@Prop({ type: String, required: true, unique: true })
|
||||||
name: string;
|
version: MigrationVersion;
|
||||||
|
|
||||||
@Prop({ type: String, required: true, enum: Object.values(MigrationAction) })
|
@Prop({ type: String, required: true, enum: Object.values(MigrationAction) })
|
||||||
status: MigrationAction;
|
status: MigrationAction;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import { existsSync, readdirSync, writeFileSync } from 'fs';
|
import { existsSync, readdirSync, writeFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { InjectModel } from '@nestjs/mongoose';
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
@ -26,8 +27,10 @@ import idPlugin from '@/utils/schema-plugin/id.plugin';
|
|||||||
import { Migration, MigrationDocument } from './migration.schema';
|
import { Migration, MigrationDocument } from './migration.schema';
|
||||||
import {
|
import {
|
||||||
MigrationAction,
|
MigrationAction,
|
||||||
|
MigrationName,
|
||||||
MigrationRunParams,
|
MigrationRunParams,
|
||||||
MigrationSuccessCallback,
|
MigrationSuccessCallback,
|
||||||
|
MigrationVersion,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -36,6 +39,7 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
private readonly logger: LoggerService,
|
private readonly logger: LoggerService,
|
||||||
private readonly metadataService: MetadataService,
|
private readonly metadataService: MetadataService,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
@InjectModel(Migration.name)
|
@InjectModel(Migration.name)
|
||||||
private readonly migrationModel: Model<Migration>,
|
private readonly migrationModel: Model<Migration>,
|
||||||
) {
|
) {
|
||||||
@ -65,11 +69,17 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CREATE
|
/**
|
||||||
|
* Get The migrations dir path configured in migration.module.ts
|
||||||
|
* @returns The migrations dir path
|
||||||
|
*/
|
||||||
public get migrationFilePath() {
|
public get migrationFilePath() {
|
||||||
return this.moduleRef.get('MONGO_MIGRATION_DIR');
|
return this.moduleRef.get('MONGO_MIGRATION_DIR');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the migration path is well set and exists
|
||||||
|
*/
|
||||||
public validateMigrationPath() {
|
public validateMigrationPath() {
|
||||||
if (!existsSync(this.migrationFilePath)) {
|
if (!existsSync(this.migrationFilePath)) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@ -79,18 +89,29 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async create(name: string) {
|
/**
|
||||||
const fileName: string = kebabCase(name) + '.migration.ts';
|
* Creates a new migration file with the specified name.
|
||||||
|
*
|
||||||
|
* The file name is generated in kebab-case format, prefixed with a timestamp.
|
||||||
|
* If a migration file with the same name already exists, an error is logged, and the process exits.
|
||||||
|
*
|
||||||
|
* @param version - The name of the migration to create.
|
||||||
|
* @returns Resolves when the migration file is successfully created.
|
||||||
|
*
|
||||||
|
* @throws If there is an issue writing the migration file.
|
||||||
|
*/
|
||||||
|
public create(version: MigrationVersion) {
|
||||||
|
const fileName: string = kebabCase(version) + '.migration.ts';
|
||||||
|
|
||||||
// check if file already exists
|
// check if file already exists
|
||||||
const files = await this.getDirFiles();
|
const files = this.getMigrationFiles();
|
||||||
const exist = files.some((file) => {
|
const exist = files.some((file) => {
|
||||||
const migrationName = this.getMigrationName(file);
|
const migrationName = this.getMigrationName(file);
|
||||||
return migrationName === fileName;
|
return migrationName === fileName;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
this.logger.error(`Migration file for "${name}" already exists`);
|
this.logger.error(`Migration file for "${version}" already exists`);
|
||||||
this.exit();
|
this.exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +121,7 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
try {
|
try {
|
||||||
writeFileSync(filePath, template);
|
writeFileSync(filePath, template);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Migration file for "${name}" created: ${migrationFileName}`,
|
`Migration file for "${version}" created: ${migrationFileName}`,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(e.stack);
|
this.logger.error(e.stack);
|
||||||
@ -109,21 +130,30 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a migration template to be used while creating a new migration
|
||||||
|
* @returns A migration template
|
||||||
|
*/
|
||||||
private getMigrationTemplate() {
|
private getMigrationTemplate() {
|
||||||
return `import mongoose from 'mongoose';
|
return `import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
import { MigrationServices } from '../types';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
async up() {
|
async up(services: MigrationServices) {
|
||||||
// Migration logic
|
// Migration logic
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
async down() {
|
async down(services: MigrationServices) {
|
||||||
// Rollback logic
|
// Rollback logic
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
};`;
|
};`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Establishes a MongoDB connection
|
||||||
|
*/
|
||||||
private async connect() {
|
private async connect() {
|
||||||
try {
|
try {
|
||||||
const connection = await mongoose.connect(config.mongo.uri, {
|
const connection = await mongoose.connect(config.mongo.uri, {
|
||||||
@ -140,13 +170,21 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async run({
|
/**
|
||||||
action,
|
* Executes migration operations based on the provided parameters.
|
||||||
name,
|
*
|
||||||
version,
|
* Determines the migration operation to perform (run all, run a specific migration, or run upgrades)
|
||||||
isAutoMigrate,
|
* based on the input parameters. The process may exit after completing the operation.
|
||||||
}: MigrationRunParams) {
|
*
|
||||||
if (!name) {
|
* @param action - The migration action to perform (e.g., 'up' or 'down').
|
||||||
|
* @param name - The specific migration name to execute. If not provided, all migrations are considered.
|
||||||
|
* @param version - The target version for automatic migration upgrades.
|
||||||
|
* @param isAutoMigrate - A flag indicating whether to perform automatic migration upgrades.
|
||||||
|
*
|
||||||
|
* @returns Resolves when the migration operation is successfully completed.
|
||||||
|
*/
|
||||||
|
public async run({ action, version, isAutoMigrate }: MigrationRunParams) {
|
||||||
|
if (!version) {
|
||||||
if (isAutoMigrate) {
|
if (isAutoMigrate) {
|
||||||
await this.runUpgrades(action, version);
|
await this.runUpgrades(action, version);
|
||||||
} else {
|
} else {
|
||||||
@ -154,46 +192,67 @@ module.exports = {
|
|||||||
this.exit();
|
this.exit();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.runOne({ action, name });
|
await this.runOne({ action, version });
|
||||||
this.exit();
|
this.exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runOne({ name, action }: MigrationRunParams) {
|
/**
|
||||||
// verify DB status
|
* Executes a specific migration action for a given version.
|
||||||
|
*
|
||||||
|
* Verifies the migration status in the database before attempting to execute the action.
|
||||||
|
* If the migration has already been executed, the process stops. Otherwise, it loads the
|
||||||
|
* migration file, performs the action, and handles success or failure through callbacks.
|
||||||
|
*
|
||||||
|
* @param version - The version of the migration to run.
|
||||||
|
* @param action - The action to perform (e.g., 'up' or 'down').
|
||||||
|
*
|
||||||
|
* @returns Resolves when the migration action is successfully executed or stops if the migration already exists.
|
||||||
|
*/
|
||||||
|
private async runOne({ version, action }: MigrationRunParams) {
|
||||||
|
// Verify DB status
|
||||||
const { exist, migrationDocument } = await this.verifyStatus({
|
const { exist, migrationDocument } = await this.verifyStatus({
|
||||||
name,
|
version,
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
return true; // stop exec;
|
return true; // stop exec;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const migration = await this.loadMigrationFile(name);
|
const migration = await this.loadMigrationFile(version);
|
||||||
const result = await migration[action]();
|
const result = await migration[action]({
|
||||||
|
logger: this.logger,
|
||||||
|
http: this.httpService,
|
||||||
|
});
|
||||||
if (result) {
|
if (result) {
|
||||||
await this.successCallback({
|
await this.successCallback({
|
||||||
name,
|
version,
|
||||||
action,
|
action,
|
||||||
migrationDocument,
|
migrationDocument,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.failureCallback({
|
this.failureCallback({
|
||||||
name,
|
version,
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
this.logger.log(e.stack);
|
this.logger.log(e.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isNewerVersion(version1: string, version2: string): boolean {
|
/**
|
||||||
const regex = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
* Compares two version strings to determine if the first version is newer than the second.
|
||||||
if (!regex.test(version1) || !regex.test(version2)) {
|
*
|
||||||
throw new TypeError('Invalid version number!');
|
* @param version1 - The first version string (e.g., 'v1.2.3').
|
||||||
}
|
* @param version2 - The second version string (e.g., 'v1.2.2').
|
||||||
|
* @returns `true` if the first version is newer than the second, otherwise `false`.
|
||||||
|
*/
|
||||||
|
private isNewerVersion(
|
||||||
|
version1: MigrationVersion,
|
||||||
|
version2: MigrationVersion,
|
||||||
|
): boolean {
|
||||||
// Split both versions into their numeric components
|
// Split both versions into their numeric components
|
||||||
const v1Parts = version1.replace('v', '').split('.').map(Number);
|
const v1Parts = version1.replace('v', '').split('.').map(Number);
|
||||||
const v2Parts = version2.replace('v', '').split('.').map(Number);
|
const v2Parts = version2.replace('v', '').split('.').map(Number);
|
||||||
@ -214,39 +273,65 @@ module.exports = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runUpgrades(action: MigrationAction, version: string) {
|
/**
|
||||||
const versions = await this.getAvailableUpgradeVersions();
|
* Executes migration upgrades for all available versions newer than the specified version.
|
||||||
|
*
|
||||||
|
* @param action - The migration action to perform (e.g., 'up').
|
||||||
|
* @param version - The current version to compare against for upgrades.
|
||||||
|
*
|
||||||
|
* @returns The last successfully upgraded version.
|
||||||
|
*/
|
||||||
|
private async runUpgrades(
|
||||||
|
action: MigrationAction,
|
||||||
|
version: MigrationVersion,
|
||||||
|
) {
|
||||||
|
const versions = this.getAvailableUpgradeVersions();
|
||||||
const filteredVersions = versions.filter((v) =>
|
const filteredVersions = versions.filter((v) =>
|
||||||
this.isNewerVersion(v, version),
|
this.isNewerVersion(v, version),
|
||||||
);
|
);
|
||||||
let lastVersion = version;
|
let lastVersion = version;
|
||||||
|
|
||||||
for (const version of filteredVersions) {
|
for (const version of filteredVersions) {
|
||||||
await this.runOne({ name: version, action });
|
await this.runOne({ version, action });
|
||||||
lastVersion = version;
|
lastVersion = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lastVersion;
|
return lastVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the specified migration action for all available versions.
|
||||||
|
*
|
||||||
|
* @param action - The migration action to perform (e.g., 'up' or 'down').
|
||||||
|
*
|
||||||
|
* @returns Resolves when all migration actions are successfully completed.
|
||||||
|
*/
|
||||||
private async runAll(action: MigrationAction) {
|
private async runAll(action: MigrationAction) {
|
||||||
const versions = await this.getAvailableUpgradeVersions();
|
const versions = this.getAvailableUpgradeVersions();
|
||||||
|
|
||||||
for (const version of versions) {
|
for (const version of versions) {
|
||||||
await this.runOne({ name: version, action });
|
await this.runOne({ version, action });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDirFiles() {
|
/**
|
||||||
return readdirSync(this.migrationFilePath);
|
* Verifies the migration status for a specific version and action.
|
||||||
}
|
*
|
||||||
|
* @param version - The version of the migration to verify.
|
||||||
private async verifyStatus({ name, action }: MigrationRunParams): Promise<{
|
* @param action - The migration action to verify (e.g., 'up' or 'down').
|
||||||
|
*
|
||||||
|
* @returns A promise resolving to an object containing:
|
||||||
|
* - `exist`: A boolean indicating if the migration already exists in the specified state.
|
||||||
|
* - `migrationDocument`: The existing migration document, or `null` if not found.
|
||||||
|
*/
|
||||||
|
private async verifyStatus({ version, action }: MigrationRunParams): Promise<{
|
||||||
exist: boolean;
|
exist: boolean;
|
||||||
migrationDocument: MigrationDocument | null;
|
migrationDocument: MigrationDocument | null;
|
||||||
}> {
|
}> {
|
||||||
let exist = false;
|
let exist = false;
|
||||||
const migrationDocument = await this.migrationModel.findOne({ name });
|
const migrationDocument = await this.migrationModel.findOne({
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
|
||||||
if (migrationDocument) {
|
if (migrationDocument) {
|
||||||
exist = Boolean(migrationDocument.status === action);
|
exist = Boolean(migrationDocument.status === action);
|
||||||
@ -260,47 +345,79 @@ module.exports = {
|
|||||||
return { exist, migrationDocument };
|
return { exist, migrationDocument };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMigrationFiles() {
|
/**
|
||||||
const files = await this.getDirFiles();
|
* Retrieves all migration files from the migration directory.
|
||||||
return files.filter((file) => /\.migration\.(js|ts)/.test(file));
|
*
|
||||||
|
* Reads the files in the migration directory and filters for those matching
|
||||||
|
* the `.migration.js` or `.migration.ts` file extensions.
|
||||||
|
*
|
||||||
|
* @returns A promise resolving to an array of migration file names.
|
||||||
|
*/
|
||||||
|
getMigrationFiles() {
|
||||||
|
const files = readdirSync(this.migrationFilePath);
|
||||||
|
return files.filter((file) => /\.migration\.(js|ts)$/.test(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getMigrationName(filename: string) {
|
/**
|
||||||
|
* Extracts the migration name from a given filename.
|
||||||
|
*
|
||||||
|
* @param filename - The migration file name to process (e.g., '1234567890-my-migration.migration.ts').
|
||||||
|
* @returns The extracted migration name (e.g., 'my-migration').
|
||||||
|
*/
|
||||||
|
private getMigrationName(filename: string): MigrationName {
|
||||||
const [, ...migrationNameParts] = filename.split('-');
|
const [, ...migrationNameParts] = filename.split('-');
|
||||||
const migrationName = migrationNameParts.join('-');
|
const migrationName = migrationNameParts.join('-');
|
||||||
|
|
||||||
return migrationName;
|
return migrationName.replace(/\.migration\.(js|ts)/, '') as MigrationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAvailableUpgradeVersions() {
|
/**
|
||||||
const filenames = await this.getMigrationFiles();
|
* Retrieves a list of available migration upgrade versions.
|
||||||
|
*
|
||||||
|
* Processes all migration files to extract and format their version identifiers.
|
||||||
|
*
|
||||||
|
* @returns An array of formatted migration versions (e.g., ['v1.0.0', 'v1.1.0']).
|
||||||
|
*/
|
||||||
|
private getAvailableUpgradeVersions() {
|
||||||
|
const filenames = this.getMigrationFiles();
|
||||||
|
|
||||||
return filenames.map((filename: string) => {
|
return filenames
|
||||||
const [migrationFileName] = filename.split('.');
|
.map((filename) => this.getMigrationName(filename))
|
||||||
const [, , ...migrationVersion] = migrationFileName.split('-');
|
.map((name) => {
|
||||||
return `v${migrationVersion.join('.')}`;
|
const [, ...migrationVersion] = name.split('-');
|
||||||
});
|
return `v${migrationVersion.join('.')}` as MigrationVersion;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMigrationFileByName(version: string): Promise<string | null> {
|
/**
|
||||||
const files = await this.getMigrationFiles();
|
* Finds the migration file corresponding to a specific version.
|
||||||
|
*
|
||||||
|
* @param version - The migration version to search for (e.g., 'v1.0.0').
|
||||||
|
* @returns The file name of the matching migration, or `null` if no match is found.
|
||||||
|
*/
|
||||||
|
findMigrationFileByVersion(version: MigrationVersion): string | null {
|
||||||
|
const files = this.getMigrationFiles();
|
||||||
|
const migrationName = kebabCase(version) as MigrationName;
|
||||||
return (
|
return (
|
||||||
files.find((file) => {
|
files
|
||||||
const migrationName = this.getMigrationName(file).replace(
|
.map((file) => this.getMigrationName(file))
|
||||||
/\.migration\.(js|ts)/,
|
.find((name) => migrationName === name) || null
|
||||||
'',
|
|
||||||
);
|
|
||||||
return migrationName === kebabCase(version);
|
|
||||||
}) || null
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadMigrationFile(name: string) {
|
/**
|
||||||
|
* Loads a migration file for a specific version.
|
||||||
|
*
|
||||||
|
* @param version - The migration version to load.
|
||||||
|
*
|
||||||
|
* @returns The loaded migration object containing `up` and `down` methods.
|
||||||
|
*/
|
||||||
|
private async loadMigrationFile(version: MigrationVersion) {
|
||||||
try {
|
try {
|
||||||
// Map the provided name to the actual file with timestamp
|
// Map the provided name to the actual file with timestamp
|
||||||
const fileName = await this.findMigrationFileByName(name);
|
const fileName = this.findMigrationFileByVersion(version);
|
||||||
if (!fileName) {
|
if (!fileName) {
|
||||||
this.logger.error(`Migration file for "${name}" not found.`);
|
this.logger.error(`Migration file for "${version}" not found.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,48 +429,72 @@ module.exports = {
|
|||||||
typeof migration.down !== 'function'
|
typeof migration.down !== 'function'
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Migration file "${name}" must export an object with "up" and "down" methods.`,
|
`Migration file "${version}" must export an object with "up" and "down" methods.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return migration;
|
return migration;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`Failed to load migration "${name}".\n${e.message}`);
|
throw new Error(`Failed to load migration "${version}".\n${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the status of a migration in the database.
|
||||||
|
*
|
||||||
|
* @param version - The version of the migration to update.
|
||||||
|
* @param action - The action performed on the migration (e.g., 'up' or 'down').
|
||||||
|
* @param migrationDocument - An optional existing migration document to update. If not provided, a new document is created.
|
||||||
|
*
|
||||||
|
* @returns Resolves when the migration status is successfully updated.
|
||||||
|
*/
|
||||||
async updateStatus({
|
async updateStatus({
|
||||||
name,
|
version,
|
||||||
action,
|
action,
|
||||||
migrationDocument,
|
migrationDocument,
|
||||||
}: Omit<MigrationSuccessCallback, 'terminal'>) {
|
}: Omit<MigrationSuccessCallback, 'terminal'>) {
|
||||||
const document =
|
const document =
|
||||||
migrationDocument ||
|
migrationDocument ||
|
||||||
new this.migrationModel({
|
new this.migrationModel({
|
||||||
name,
|
version,
|
||||||
});
|
});
|
||||||
document.status = action;
|
document.status = action;
|
||||||
await document.save();
|
await document.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles successful completion of a migration operation.
|
||||||
|
*
|
||||||
|
* @param version - The version of the successfully completed migration.
|
||||||
|
* @param action - The action performed (e.g., 'up' or 'down').
|
||||||
|
* @param migrationDocument - The migration document to update.
|
||||||
|
*
|
||||||
|
* @returns Resolves when all success-related operations are completed.
|
||||||
|
*/
|
||||||
private async successCallback({
|
private async successCallback({
|
||||||
name,
|
version,
|
||||||
action,
|
action,
|
||||||
migrationDocument,
|
migrationDocument,
|
||||||
}: MigrationSuccessCallback) {
|
}: MigrationSuccessCallback) {
|
||||||
await this.updateStatus({ name, action, migrationDocument });
|
await this.updateStatus({ version, action, migrationDocument });
|
||||||
const migrationDisplayName = `${name} [${action}]`;
|
const migrationDisplayName = `${version} [${action}]`;
|
||||||
this.logger.log(`"${migrationDisplayName}" migration done`);
|
this.logger.log(`"${migrationDisplayName}" migration done`);
|
||||||
// Update DB version
|
// Update DB version
|
||||||
const result = await this.metadataService.createOrUpdate({
|
const result = await this.metadataService.createOrUpdate({
|
||||||
name: 'db-version',
|
name: 'db-version',
|
||||||
value: name,
|
value: version,
|
||||||
});
|
});
|
||||||
const operation = result ? 'updated' : 'created';
|
const operation = result ? 'updated' : 'created';
|
||||||
this.logger.log(`db-version metadata ${operation} "${name}"`);
|
this.logger.log(`db-version metadata ${operation} "${name}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private failureCallback({ name, action }: MigrationRunParams) {
|
/**
|
||||||
const migrationDisplayName = `${name} [${action}]`;
|
* Handles the failure of a migration operation.
|
||||||
|
*
|
||||||
|
* @param version - The version of the migration that failed.
|
||||||
|
* @param action - The action that failed (e.g., 'up' or 'down').
|
||||||
|
*/
|
||||||
|
private failureCallback({ version, action }: MigrationRunParams) {
|
||||||
|
const migrationDisplayName = `${version} [${action}]`;
|
||||||
this.logger.error(`"${migrationDisplayName}" migration failed`);
|
this.logger.error(`"${migrationDisplayName}" migration failed`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,20 +8,21 @@
|
|||||||
|
|
||||||
import { MigrationDocument } from './migration.schema';
|
import { MigrationDocument } from './migration.schema';
|
||||||
|
|
||||||
enum MigrationAction {
|
export enum MigrationAction {
|
||||||
UP = 'up',
|
UP = 'up',
|
||||||
DOWN = 'down',
|
DOWN = 'down',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MigrationRunParams {
|
export type MigrationVersion = `v${number}.${number}.${number}`;
|
||||||
name?: string;
|
|
||||||
|
export type MigrationName = `v-${number}-${number}-${number}`;
|
||||||
|
|
||||||
|
export interface MigrationRunParams {
|
||||||
action: MigrationAction;
|
action: MigrationAction;
|
||||||
version?: string;
|
version?: MigrationVersion;
|
||||||
isAutoMigrate?: boolean;
|
isAutoMigrate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MigrationSuccessCallback extends MigrationRunParams {
|
export interface MigrationSuccessCallback extends MigrationRunParams {
|
||||||
migrationDocument: MigrationDocument;
|
migrationDocument: MigrationDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MigrationAction, MigrationRunParams, MigrationSuccessCallback };
|
|
||||||
|
4
api/src/utils/test/fixtures/migration.ts
vendored
4
api/src/utils/test/fixtures/migration.ts
vendored
@ -13,11 +13,11 @@ import { MigrationAction } from '@/migration/types';
|
|||||||
|
|
||||||
const migrationFixtures: Migration[] = [
|
const migrationFixtures: Migration[] = [
|
||||||
{
|
{
|
||||||
name: 'v2.1.2',
|
version: 'v2.1.2',
|
||||||
status: MigrationAction.UP,
|
status: MigrationAction.UP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'v2.1.1',
|
version: 'v2.1.1',
|
||||||
status: MigrationAction.DOWN,
|
status: MigrationAction.DOWN,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user