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 })
|
@Schema({ timestamps: true })
|
||||||
export class Migration {
|
export class Migration {
|
||||||
@Prop({ required: true, unique: true })
|
@Prop({ type: String, required: true, unique: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Prop({ required: true, enum: MigrationAction })
|
@Prop({ type: String, required: true, enum: MigrationAction })
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import leanVirtuals from 'mongoose-lean-virtuals';
|
|||||||
|
|
||||||
import { config } from '@/config';
|
import { config } from '@/config';
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.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 {
|
||||||
@ -38,6 +39,7 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
private readonly logger: LoggerService,
|
private readonly logger: LoggerService,
|
||||||
|
private readonly metadataService: MetadataService,
|
||||||
@InjectModel(Migration.name)
|
@InjectModel(Migration.name)
|
||||||
private readonly migrationModel: MigrationModel,
|
private readonly migrationModel: MigrationModel,
|
||||||
) {
|
) {
|
||||||
@ -53,7 +55,13 @@ export class MigrationService implements OnApplicationBootstrap {
|
|||||||
const isProduction = config.env.toLowerCase().includes('prod');
|
const isProduction = config.env.toLowerCase().includes('prod');
|
||||||
if (!isProduction && config.mongo.autoMigrate) {
|
if (!isProduction && config.mongo.autoMigrate) {
|
||||||
this.logger.log('Executing migrations ...');
|
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) {
|
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 {
|
} else {
|
||||||
await this.runOne({ action, name });
|
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) {
|
private async runAll(action: MigrationAction) {
|
||||||
const files = await this.getDirFiles();
|
const files = await this.getDirFiles();
|
||||||
const migrationFiles = files
|
const migrationFiles = files
|
||||||
.filter((fileName) => fileName.includes('migration'))
|
.filter((fileName) => fileName.includes('migration'))
|
||||||
.map((fileName) => {
|
.map((fileName) => {
|
||||||
const [migrationFileName] = fileName.split('.');
|
const [migrationFileName] = fileName.split('.');
|
||||||
const [, ...migrationName] = migrationFileName.split('-');
|
const [, , ...migrationVersion] = migrationFileName.split('-');
|
||||||
return migrationName.join('-');
|
return `v${migrationVersion.join('.')}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const name of migrationFiles) {
|
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 {
|
interface MigrationRunParams {
|
||||||
name?: string;
|
name?: string;
|
||||||
action: MigrationAction;
|
action: MigrationAction;
|
||||||
|
version?: string;
|
||||||
|
isAutoMigrate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MigrationSuccessCallback extends MigrationRunParams {
|
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 { SettingController } from './controllers/setting.controller';
|
||||||
import { SettingRepository } from './repositories/setting.repository';
|
import { SettingRepository } from './repositories/setting.repository';
|
||||||
|
import { MetadataModel } from './schemas/metadata.schema';
|
||||||
import { SettingModel } from './schemas/setting.schema';
|
import { SettingModel } from './schemas/setting.schema';
|
||||||
import { SettingSeeder } from './seeds/setting.seed';
|
import { SettingSeeder } from './seeds/setting.seed';
|
||||||
|
import { MetadataService } from './services/metadata.service';
|
||||||
import { SettingService } from './services/setting.service';
|
import { SettingService } from './services/setting.service';
|
||||||
|
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
MongooseModule.forFeature([SettingModel]),
|
MongooseModule.forFeature([SettingModel, MetadataModel]),
|
||||||
PassportModule.register({
|
PassportModule.register({
|
||||||
session: true,
|
session: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [SettingRepository, SettingSeeder, SettingService],
|
providers: [
|
||||||
|
SettingRepository,
|
||||||
|
SettingSeeder,
|
||||||
|
SettingService,
|
||||||
|
MetadataService,
|
||||||
|
],
|
||||||
controllers: [SettingController],
|
controllers: [SettingController],
|
||||||
exports: [SettingService],
|
exports: [SettingService, MetadataService],
|
||||||
})
|
})
|
||||||
export class SettingModule {}
|
export class SettingModule {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user