mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix: migration and db version logic
This commit is contained in:
parent
c94ec95599
commit
a1765b647b
@ -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],
|
||||||
|
@ -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>;
|
||||||
|
@ -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,
|
||||||
|
@ -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
23
api/src/utils/test/fixtures/metadata.ts
vendored
Normal 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);
|
||||||
|
};
|
28
api/src/utils/test/fixtures/migration.ts
vendored
Normal file
28
api/src/utils/test/fixtures/migration.ts
vendored
Normal 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);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user