fix: migration and db version logic

This commit is contained in:
yassinedorbozgithub 2025-01-03 16:39:55 +01:00
parent c94ec95599
commit a1765b647b
6 changed files with 108 additions and 37 deletions

View File

@ -14,22 +14,17 @@ import { MongooseModule } from '@nestjs/mongoose';
import { LoggerModule } from '@/logger/logger.module'; import { LoggerModule } from '@/logger/logger.module';
import { MigrationCommand } from './migration.command'; import { MigrationCommand } from './migration.command';
import { Migration, MigrationSchema } from './migration.schema'; import { MigrationModel } from './migration.schema';
import { MigrationService } from './migration.service'; import { MigrationService } from './migration.service';
@Module({ @Module({
imports: [ imports: [MongooseModule.forFeature([MigrationModel]), LoggerModule],
MongooseModule.forFeature([
{ name: Migration.name, schema: MigrationSchema },
]),
LoggerModule,
],
providers: [ providers: [
MigrationService, MigrationService,
MigrationCommand, MigrationCommand,
{ {
provide: 'MONGO_MIGRATION_DIR', provide: 'MONGO_MIGRATION_DIR',
useValue: join(process.cwd(), 'src', 'migration', 'migrations'), useValue: join(__dirname, 'migrations'),
}, },
], ],
exports: [MigrationService], exports: [MigrationService],

View File

@ -6,8 +6,10 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/ */
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Model } from 'mongoose';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
import { THydratedDocument } from '@/utils/types/filter.types';
import { MigrationAction } from './types'; import { MigrationAction } from './types';
@ -17,11 +19,14 @@ export class Migration {
name: string; name: string;
@Prop({ type: String, required: true, enum: MigrationAction }) @Prop({ type: String, required: true, enum: MigrationAction })
status: string; status: MigrationAction;
} }
export const MigrationSchema = SchemaFactory.createForClass(Migration); export const MigrationModel: ModelDefinition = LifecycleHookManager.attach({
name: Migration.name,
schema: SchemaFactory.createForClass(Migration),
});
export type MigrationDocument = Migration & Document; export default MigrationModel.schema;
export type MigrationModel = Model<MigrationDocument>; export type MigrationDocument = THydratedDocument<Migration>;

View File

@ -13,7 +13,7 @@ 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';
import { kebabCase } from 'lodash'; import { kebabCase } from 'lodash';
import mongoose from 'mongoose'; import mongoose, { Model } from 'mongoose';
import leanDefaults from 'mongoose-lean-defaults'; import leanDefaults from 'mongoose-lean-defaults';
import leanGetters from 'mongoose-lean-getters'; import leanGetters from 'mongoose-lean-getters';
import leanVirtuals from 'mongoose-lean-virtuals'; import leanVirtuals from 'mongoose-lean-virtuals';
@ -23,11 +23,7 @@ import { LoggerService } from '@/logger/logger.service';
import { MetadataService } from '@/setting/services/metadata.service'; import { MetadataService } from '@/setting/services/metadata.service';
import idPlugin from '@/utils/schema-plugin/id.plugin'; import idPlugin from '@/utils/schema-plugin/id.plugin';
import { import { Migration, MigrationDocument } from './migration.schema';
Migration,
MigrationDocument,
MigrationModel,
} from './migration.schema';
import { import {
MigrationAction, MigrationAction,
MigrationRunParams, MigrationRunParams,
@ -41,7 +37,7 @@ export class MigrationService implements OnApplicationBootstrap {
private readonly logger: LoggerService, private readonly logger: LoggerService,
private readonly metadataService: MetadataService, private readonly metadataService: MetadataService,
@InjectModel(Migration.name) @InjectModel(Migration.name)
private readonly migrationModel: MigrationModel, private readonly migrationModel: Model<Migration>,
) { ) {
this.validateMigrationPath(); this.validateMigrationPath();
} }
@ -55,8 +51,8 @@ export class MigrationService implements OnApplicationBootstrap {
const isCLI = Boolean(process.env.HEXABOT_CLI); const isCLI = Boolean(process.env.HEXABOT_CLI);
if (!isCLI && config.mongo.autoMigrate) { if (!isCLI && config.mongo.autoMigrate) {
this.logger.log('Executing migrations ...'); this.logger.log('Executing migrations ...');
const { value: version = '2.1.9' } = const metadata = await this.metadataService.getMetadata('db-version');
await this.metadataService.getMetadata('db-version'); const version = metadata ? metadata.value : 'v2.1.9';
await this.run({ await this.run({
action: MigrationAction.UP, action: MigrationAction.UP,
version, version,
@ -89,8 +85,7 @@ export class MigrationService implements OnApplicationBootstrap {
// check if file already exists // check if file already exists
const files = await this.getDirFiles(); const files = await this.getDirFiles();
const exist = files.some((file) => { const exist = files.some((file) => {
const [, ...actualFileName] = file.split('-'); const migrationName = this.getMigrationName(file);
const migrationName = actualFileName.join('-');
return migrationName === fileName; return migrationName === fileName;
}); });
@ -152,14 +147,19 @@ module.exports = {
if (!name) { if (!name) {
if (isAutoMigrate) { if (isAutoMigrate) {
const newVersion = await this.runFromVersion(action, version); const newVersion = await this.runFromVersion(action, version);
await this.metadataService.setMetadata('db-version', newVersion);
await this.metadataService.findOrCreate({
name: 'db-version',
value: newVersion,
});
} else { } else {
await this.runAll(action); await this.runAll(action);
this.exit();
} }
} else { } else {
await this.runOne({ action, name }); await this.runOne({ action, name });
this.exit();
} }
this.exit();
} }
private async runOne({ name, action }: MigrationRunParams) { private async runOne({ name, action }: MigrationRunParams) {
@ -218,7 +218,7 @@ module.exports = {
private async runFromVersion(action: MigrationAction, version: string) { private async runFromVersion(action: MigrationAction, version: string) {
const files = await this.getDirFiles(); const files = await this.getDirFiles();
const migrationFiles = files const migrationFiles = files
.filter((fileName) => fileName.includes('migration')) .filter((fileName) => fileName.endsWith('.migration.js'))
.map((fileName) => { .map((fileName) => {
const [migrationFileName] = fileName.split('.'); const [migrationFileName] = fileName.split('.');
const [, , ...migrationVersion] = migrationFileName.split('-'); const [, , ...migrationVersion] = migrationFileName.split('-');
@ -273,20 +273,26 @@ module.exports = {
return { exist, migrationDocument }; return { exist, migrationDocument };
} }
private async getMigrationFiles() { async getMigrationFiles() {
const files = await this.getDirFiles(); const files = await this.getDirFiles();
return files.filter((file) => /\.migration\.(js|ts)/.test(file)); return files.filter((file) => /\.migration\.(js|ts)/.test(file));
} }
private async findMigrationFileByName(name: string): Promise<string | null> { private getMigrationName(filename: string) {
const [, ...migrationNameParts] = filename.split('-');
const migrationName = migrationNameParts.join('-');
return migrationName;
}
async findMigrationFileByName(name: string): Promise<string | null> {
const files = await this.getMigrationFiles(); const files = await this.getMigrationFiles();
return ( return (
files.find((file) => { files.find((file) => {
const [, ...migrationNameParts] = file.split('-'); const migrationName = this.getMigrationName(file).replace(
const migrationName = migrationNameParts /\.migration\.(js|ts)/,
.join('-') '',
.replace(/\.migration\.(js|ts)/, ''); );
return migrationName === kebabCase(name); return migrationName === kebabCase(name);
}) || null }) || null
); );
@ -318,7 +324,7 @@ module.exports = {
} }
} }
private async updateStatus({ async updateStatus({
name, name,
action, action,
migrationDocument, migrationDocument,

View File

@ -1,5 +1,5 @@
/* /*
* Copyright © 2024 Hexastack. All rights reserved. * Copyright © 2025 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
@ -19,6 +19,20 @@ export class MetadataService {
private readonly metadataModel: Model<Metadata>, private readonly metadataModel: Model<Metadata>,
) {} ) {}
async createMetadata(dto: Partial<Metadata>) {
return await this.metadataModel.create(dto);
}
async findOrCreate(dto: Partial<Metadata>) {
const metadata = await this.metadataModel.findOne({ name: dto.name });
if (metadata) {
await this.setMetadata(dto.name, dto.value);
} else {
await this.createMetadata(dto);
}
}
async getMetadata(name: string) { async getMetadata(name: string) {
return await this.metadataModel.findOne({ name }); return await this.metadataModel.findOne({ name });
} }

23
api/src/utils/test/fixtures/metadata.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import mongoose from 'mongoose';
import { Metadata, MetadataModel } from '@/setting/schemas/metadata.schema';
const metadataFixtures: Metadata[] = [
{
name: 'app-version',
value: '2.2.0',
},
];
export const installMetadataFixtures = async () => {
const Metadata = mongoose.model(MetadataModel.name, MetadataModel.schema);
return await Metadata.insertMany(metadataFixtures);
};

View File

@ -0,0 +1,28 @@
/*
* Copyright © 2025 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import mongoose from 'mongoose';
import { Migration, MigrationModel } from '@/migration/migration.schema';
import { MigrationAction } from '@/migration/types';
const migrationFixtures: Migration[] = [
{
name: 'v2.1.2',
status: MigrationAction.UP,
},
{
name: 'v2.1.1',
status: MigrationAction.DOWN,
},
];
export const installMigrationFixtures = async () => {
const Migration = mongoose.model(MigrationModel.name, MigrationModel.schema);
return await Migration.insertMany(migrationFixtures);
};