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