mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: add metadata collection to store the db version
This commit is contained in:
parent
ad45a70743
commit
92bec65862
@ -13,10 +13,10 @@ import { MigrationAction } from './types';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Migration {
|
||||
@Prop({ required: true, unique: true })
|
||||
@Prop({ type: String, required: true, unique: true })
|
||||
name: string;
|
||||
|
||||
@Prop({ required: true, enum: MigrationAction })
|
||||
@Prop({ type: String, required: true, enum: MigrationAction })
|
||||
status: string;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import leanVirtuals from 'mongoose-lean-virtuals';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { MetadataService } from '@/setting/services/metadata.service';
|
||||
import idPlugin from '@/utils/schema-plugin/id.plugin';
|
||||
|
||||
import {
|
||||
@ -38,6 +39,7 @@ export class MigrationService implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly metadataService: MetadataService,
|
||||
@InjectModel(Migration.name)
|
||||
private readonly migrationModel: MigrationModel,
|
||||
) {
|
||||
@ -53,7 +55,13 @@ export class MigrationService implements OnApplicationBootstrap {
|
||||
const isProduction = config.env.toLowerCase().includes('prod');
|
||||
if (!isProduction && config.mongo.autoMigrate) {
|
||||
this.logger.log('Executing migrations ...');
|
||||
await this.run({ action: MigrationAction.UP });
|
||||
const { value: version = '2.1.9' } =
|
||||
await this.metadataService.getMetadata('db-version');
|
||||
await this.run({
|
||||
action: MigrationAction.UP,
|
||||
version,
|
||||
isAutoMigrate: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,9 +143,19 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
public async run({ action, name }: MigrationRunParams) {
|
||||
public async run({
|
||||
action,
|
||||
name,
|
||||
version,
|
||||
isAutoMigrate,
|
||||
}: MigrationRunParams) {
|
||||
if (!name) {
|
||||
await this.runAll(action);
|
||||
if (isAutoMigrate) {
|
||||
const newVersion = await this.runFromVersion(action, version);
|
||||
await this.metadataService.setMetadata('db-version', newVersion);
|
||||
} else {
|
||||
await this.runAll(action);
|
||||
}
|
||||
} else {
|
||||
await this.runOne({ action, name });
|
||||
}
|
||||
@ -171,14 +189,60 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
isNewerVersion(version1: string, version2: string): boolean {
|
||||
const regex = /^v?(\d+)\.(\d+)\.(\d+)$/;
|
||||
if (!regex.test(version1) || !regex.test(version2)) {
|
||||
throw new TypeError('Invalid version number!');
|
||||
}
|
||||
|
||||
// Split both versions into their numeric components
|
||||
const v1Parts = version1.replace('v', '').split('.').map(Number);
|
||||
const v2Parts = version2.replace('v', '').split('.').map(Number);
|
||||
|
||||
// Compare each part of the version number
|
||||
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||
const v1Part = v1Parts[i] || 0; // Default to 0 if undefined
|
||||
const v2Part = v2Parts[i] || 0; // Default to 0 if undefined
|
||||
|
||||
if (v1Part > v2Part) {
|
||||
return true;
|
||||
} else if (v1Part < v2Part) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If all parts are equal, the versions are the same
|
||||
return false;
|
||||
}
|
||||
|
||||
private async runFromVersion(action: MigrationAction, version: string) {
|
||||
const files = await this.getDirFiles();
|
||||
const migrationFiles = files
|
||||
.filter((fileName) => fileName.includes('migration'))
|
||||
.map((fileName) => {
|
||||
const [migrationFileName] = fileName.split('.');
|
||||
const [, , ...migrationVersion] = migrationFileName.split('-');
|
||||
return `v${migrationVersion.join('.')}`;
|
||||
})
|
||||
.filter((v) => this.isNewerVersion(v, version));
|
||||
|
||||
let lastVersion = version;
|
||||
for (const name of migrationFiles) {
|
||||
await this.runOne({ name, action });
|
||||
lastVersion = name;
|
||||
}
|
||||
|
||||
return lastVersion;
|
||||
}
|
||||
|
||||
private async runAll(action: MigrationAction) {
|
||||
const files = await this.getDirFiles();
|
||||
const migrationFiles = files
|
||||
.filter((fileName) => fileName.includes('migration'))
|
||||
.map((fileName) => {
|
||||
const [migrationFileName] = fileName.split('.');
|
||||
const [, ...migrationName] = migrationFileName.split('-');
|
||||
return migrationName.join('-');
|
||||
const [, , ...migrationVersion] = migrationFileName.split('-');
|
||||
return `v${migrationVersion.join('.')}`;
|
||||
});
|
||||
|
||||
for (const name of migrationFiles) {
|
||||
|
@ -0,0 +1,10 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
module.exports = {
|
||||
async up() {
|
||||
// Migration logic
|
||||
},
|
||||
async down() {
|
||||
// Rollback logic
|
||||
},
|
||||
};
|
@ -16,6 +16,8 @@ enum MigrationAction {
|
||||
interface MigrationRunParams {
|
||||
name?: string;
|
||||
action: MigrationAction;
|
||||
version?: string;
|
||||
isAutoMigrate?: boolean;
|
||||
}
|
||||
|
||||
interface MigrationSuccessCallback extends MigrationRunParams {
|
||||
|
30
api/src/setting/schemas/metadata.schema.ts
Normal file
30
api/src/setting/schemas/metadata.schema.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Metadata {
|
||||
@Prop({ type: String, required: true, unique: true })
|
||||
name: string;
|
||||
|
||||
@Prop({ type: JSON, required: true })
|
||||
value: any;
|
||||
}
|
||||
|
||||
export const MetadataSchema = SchemaFactory.createForClass(Metadata);
|
||||
|
||||
export const MetadataModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Metadata.name,
|
||||
schema: SchemaFactory.createForClass(Metadata),
|
||||
});
|
||||
|
||||
export type MetadataDocument = Metadata & Document;
|
29
api/src/setting/services/metadata.service.ts
Normal file
29
api/src/setting/services/metadata.service.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright © 2024 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 { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
|
||||
import { Metadata } from '../schemas/metadata.schema';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataService {
|
||||
constructor(
|
||||
@InjectModel(Metadata.name)
|
||||
private readonly metadataModel: Model<Metadata>,
|
||||
) {}
|
||||
|
||||
async getMetadata(name: string) {
|
||||
return await this.metadataModel.findOne({ name });
|
||||
}
|
||||
|
||||
async setMetadata(name: string, value: any) {
|
||||
return await this.metadataModel.updateOne({ name }, { $set: { value } });
|
||||
}
|
||||
}
|
@ -12,20 +12,27 @@ import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { SettingController } from './controllers/setting.controller';
|
||||
import { SettingRepository } from './repositories/setting.repository';
|
||||
import { MetadataModel } from './schemas/metadata.schema';
|
||||
import { SettingModel } from './schemas/setting.schema';
|
||||
import { SettingSeeder } from './seeds/setting.seed';
|
||||
import { MetadataService } from './services/metadata.service';
|
||||
import { SettingService } from './services/setting.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([SettingModel]),
|
||||
MongooseModule.forFeature([SettingModel, MetadataModel]),
|
||||
PassportModule.register({
|
||||
session: true,
|
||||
}),
|
||||
],
|
||||
providers: [SettingRepository, SettingSeeder, SettingService],
|
||||
providers: [
|
||||
SettingRepository,
|
||||
SettingSeeder,
|
||||
SettingService,
|
||||
MetadataService,
|
||||
],
|
||||
controllers: [SettingController],
|
||||
exports: [SettingService],
|
||||
exports: [SettingService, MetadataService],
|
||||
})
|
||||
export class SettingModule {}
|
||||
|
Loading…
Reference in New Issue
Block a user