mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: add metadata repo + seeder
This commit is contained in:
parent
923a34c3e4
commit
ef788591bf
@ -6,35 +6,43 @@
|
||||
* 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 { existsSync, writeFileSync } from 'fs';
|
||||
import fs from 'fs';
|
||||
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { getModelToken } from '@nestjs/mongoose';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { getModelToken, MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { MetadataRepository } from '@/setting/repositories/metadata.repository';
|
||||
import { Metadata, MetadataModel } from '@/setting/schemas/metadata.schema';
|
||||
import { MetadataService } from '@/setting/services/metadata.service';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { Migration } from './migration.schema';
|
||||
import { Migration, MigrationModel } from './migration.schema';
|
||||
import { MigrationService } from './migration.service';
|
||||
import { MigrationAction } from './types';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
describe('MigrationService', () => {
|
||||
let service: MigrationService;
|
||||
let loggerService: LoggerService;
|
||||
let metadataService: MetadataService;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
// imports: [
|
||||
// rootMongooseTestModule(() => Promise.resolve()),
|
||||
// MongooseModule.forFeature([MigrationModel]),
|
||||
// ],
|
||||
imports: [
|
||||
rootMongooseTestModule(async () => await Promise.resolve()),
|
||||
MongooseModule.forFeature([MetadataModel, MigrationModel]),
|
||||
],
|
||||
providers: [
|
||||
MetadataRepository,
|
||||
MetadataService,
|
||||
MigrationService,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: LoggerService,
|
||||
useValue: {
|
||||
@ -42,12 +50,6 @@ describe('MigrationService', () => {
|
||||
error: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MetadataService,
|
||||
useValue: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: HttpService,
|
||||
useValue: {},
|
||||
@ -72,35 +74,18 @@ describe('MigrationService', () => {
|
||||
service = module.get<MigrationService>(MigrationService);
|
||||
loggerService = module.get<LoggerService>(LoggerService);
|
||||
metadataService = module.get<MetadataService>(MetadataService);
|
||||
|
||||
jest.spyOn(service, 'exit').mockImplementation(); // Mock exit to avoid Jest process termination
|
||||
});
|
||||
|
||||
describe('validateMigrationPath', () => {
|
||||
it('should log an error and exit if the migration path does not exist', () => {
|
||||
(existsSync as jest.Mock).mockReturnValue(false);
|
||||
const exitSpy = jest.spyOn(service, 'exit').mockImplementation();
|
||||
|
||||
service.validateMigrationPath();
|
||||
|
||||
expect(loggerService.error).toHaveBeenCalledWith(
|
||||
'Migration directory "/migrations" not exists.',
|
||||
);
|
||||
expect(exitSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not log an error or exit if the migration path exists', () => {
|
||||
(existsSync as jest.Mock).mockReturnValue(true);
|
||||
const exitSpy = jest.spyOn(service, 'exit').mockImplementation();
|
||||
|
||||
service.validateMigrationPath();
|
||||
|
||||
expect(loggerService.error).not.toHaveBeenCalled();
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
afterEach(jest.clearAllMocks);
|
||||
afterAll(closeInMongodConnection);
|
||||
|
||||
describe('create', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(service, 'exit').mockImplementation(); // Mock exit to avoid Jest process termination
|
||||
});
|
||||
|
||||
afterEach(jest.restoreAllMocks);
|
||||
|
||||
it('should create a migration file and log success', () => {
|
||||
const mockFiles = ['12345-some-migration.migration.ts'];
|
||||
jest.spyOn(service, 'getMigrationFiles').mockReturnValue(mockFiles);
|
||||
@ -112,15 +97,18 @@ describe('MigrationService', () => {
|
||||
.mockImplementation((file) => file);
|
||||
const exitSpy = jest.spyOn(service, 'exit').mockImplementation();
|
||||
|
||||
(existsSync as jest.Mock).mockReturnValue(true);
|
||||
(writeFileSync as jest.Mock).mockImplementation();
|
||||
jest.spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
jest.spyOn(fs, 'writeFileSync').mockReturnValue();
|
||||
|
||||
service.create('v2.2.0');
|
||||
|
||||
const expectedFilePath = expect.stringMatching(
|
||||
/\/migrations\/\d+-v-2-2-0.migration.ts$/,
|
||||
);
|
||||
expect(writeFileSync).toHaveBeenCalledWith(expectedFilePath, 'template');
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
expectedFilePath,
|
||||
'template',
|
||||
);
|
||||
expect(loggerService.log).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/Migration file for "v2.2.0" created/),
|
||||
);
|
||||
@ -142,11 +130,17 @@ describe('MigrationService', () => {
|
||||
});
|
||||
|
||||
describe('onApplicationBootstrap', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(service, 'exit').mockImplementation(); // Mock exit to avoid Jest process termination
|
||||
});
|
||||
|
||||
afterEach(jest.restoreAllMocks);
|
||||
|
||||
it('should log a message and execute migrations when autoMigrate is true', async () => {
|
||||
process.env.HEXABOT_CLI = '';
|
||||
jest
|
||||
.spyOn(metadataService, 'get')
|
||||
.mockResolvedValue({ name: 'db-version', value: 'v2.1.9' });
|
||||
.spyOn(metadataService, 'findOne')
|
||||
.mockResolvedValue({ name: 'db-version', value: 'v2.1.9' } as Metadata);
|
||||
jest.spyOn(service, 'run').mockResolvedValue();
|
||||
|
||||
await service.onApplicationBootstrap();
|
||||
@ -156,13 +150,18 @@ describe('MigrationService', () => {
|
||||
);
|
||||
expect(service.run).toHaveBeenCalledWith({
|
||||
action: 'up',
|
||||
version: 'v2.1.9',
|
||||
isAutoMigrate: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(service, 'exit').mockImplementation(); // Mock exit to avoid Jest process termination
|
||||
});
|
||||
|
||||
afterEach(jest.restoreAllMocks);
|
||||
|
||||
it('should call runUpgrades when version is not provided and isAutoMigrate is true', async () => {
|
||||
const runUpgradesSpy = jest
|
||||
.spyOn(service as any, 'runUpgrades')
|
||||
@ -170,11 +169,10 @@ describe('MigrationService', () => {
|
||||
|
||||
await service.run({
|
||||
action: MigrationAction.UP,
|
||||
version: null,
|
||||
isAutoMigrate: true,
|
||||
});
|
||||
|
||||
expect(runUpgradesSpy).toHaveBeenCalledWith('up', null);
|
||||
expect(runUpgradesSpy).toHaveBeenCalledWith('up', 'v2.1.9');
|
||||
expect(service.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -219,6 +217,8 @@ describe('MigrationService', () => {
|
||||
let failureCallbackSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(service, 'exit').mockImplementation(); // Mock exit to avoid Jest process termination
|
||||
|
||||
verifyStatusSpy = jest
|
||||
.spyOn(service as any, 'verifyStatus')
|
||||
.mockResolvedValue({ exist: false, migrationDocument: {} });
|
||||
@ -236,12 +236,7 @@ describe('MigrationService', () => {
|
||||
.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
verifyStatusSpy.mockClear();
|
||||
loadMigrationFileSpy.mockClear();
|
||||
successCallbackSpy.mockClear();
|
||||
failureCallbackSpy.mockClear();
|
||||
});
|
||||
afterEach(jest.restoreAllMocks);
|
||||
|
||||
it('should return false and not execute if migration already exists', async () => {
|
||||
verifyStatusSpy.mockResolvedValue({ exist: true, migrationDocument: {} });
|
||||
@ -353,6 +348,8 @@ describe('MigrationService', () => {
|
||||
let runOneSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(service, 'exit').mockImplementation(); // Mock exit to avoid Jest process termination
|
||||
|
||||
getAvailableUpgradeVersionsSpy = jest
|
||||
.spyOn(service as any, 'getAvailableUpgradeVersions')
|
||||
.mockReturnValue(['v2.2.0', 'v2.3.0', 'v2.4.0']); // Mock available versions
|
||||
@ -366,6 +363,8 @@ describe('MigrationService', () => {
|
||||
runOneSpy = jest.spyOn(service as any, 'runOne').mockResolvedValue(true);
|
||||
});
|
||||
|
||||
afterEach(jest.restoreAllMocks);
|
||||
|
||||
it('should filter versions and call runOne for each newer version', async () => {
|
||||
const result = await (service as any).runUpgrades('up', 'v2.2.0');
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
* 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 { existsSync, readdirSync, writeFileSync } from 'fs';
|
||||
import fs from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
@ -45,13 +45,11 @@ export class MigrationService implements OnApplicationBootstrap {
|
||||
private readonly httpService: HttpService,
|
||||
@InjectModel(Migration.name)
|
||||
private readonly migrationModel: Model<Migration>,
|
||||
) {
|
||||
if (config.env !== 'test') {
|
||||
this.validateMigrationPath();
|
||||
}
|
||||
}
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
await this.ensureMigrationPathExists();
|
||||
|
||||
if (mongoose.connection.readyState !== 1) {
|
||||
await this.connect();
|
||||
}
|
||||
@ -60,11 +58,8 @@ export class MigrationService implements OnApplicationBootstrap {
|
||||
const isCLI = Boolean(process.env.HEXABOT_CLI);
|
||||
if (!isCLI && config.mongo.autoMigrate) {
|
||||
this.logger.log('Executing migrations ...');
|
||||
const metadata = await this.metadataService.get('db-version');
|
||||
const version = metadata ? metadata.value : INITIAL_DB_VERSION;
|
||||
await this.run({
|
||||
action: MigrationAction.UP,
|
||||
version,
|
||||
isAutoMigrate: true,
|
||||
});
|
||||
}
|
||||
@ -85,12 +80,11 @@ export class MigrationService implements OnApplicationBootstrap {
|
||||
/**
|
||||
* Checks if the migration path is well set and exists
|
||||
*/
|
||||
public validateMigrationPath() {
|
||||
if (!existsSync(this.migrationFilePath)) {
|
||||
this.logger.error(
|
||||
`Migration directory "${this.migrationFilePath}" not exists.`,
|
||||
);
|
||||
this.exit();
|
||||
private async ensureMigrationPathExists() {
|
||||
if (config.env !== 'test' && !fs.existsSync(this.migrationFilePath)) {
|
||||
await fs.promises.mkdir(this.migrationFilePath, {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,7 +117,7 @@ export class MigrationService implements OnApplicationBootstrap {
|
||||
const filePath = join(this.migrationFilePath, migrationFileName);
|
||||
const template = this.getMigrationTemplate();
|
||||
try {
|
||||
writeFileSync(filePath, template);
|
||||
fs.writeFileSync(filePath, template);
|
||||
this.logger.log(
|
||||
`Migration file for "${version}" created: ${migrationFileName}`,
|
||||
);
|
||||
@ -195,6 +189,10 @@ module.exports = {
|
||||
public async run({ action, version, isAutoMigrate }: MigrationRunParams) {
|
||||
if (!version) {
|
||||
if (isAutoMigrate) {
|
||||
const metadata = await this.metadataService.findOne({
|
||||
name: 'db-version',
|
||||
});
|
||||
const version = metadata ? metadata.value : INITIAL_DB_VERSION;
|
||||
await this.runUpgrades(action, version);
|
||||
} else {
|
||||
await this.runAll(action);
|
||||
@ -302,6 +300,12 @@ module.exports = {
|
||||
const filteredVersions = versions.filter((v) =>
|
||||
this.isNewerVersion(v, version),
|
||||
);
|
||||
|
||||
if (!filteredVersions.length) {
|
||||
this.logger.log('No migrations to execute ...');
|
||||
return version;
|
||||
}
|
||||
|
||||
let lastVersion = version;
|
||||
|
||||
for (const version of filteredVersions) {
|
||||
@ -371,7 +375,7 @@ module.exports = {
|
||||
* @returns A promise resolving to an array of migration file names.
|
||||
*/
|
||||
getMigrationFiles() {
|
||||
const files = readdirSync(this.migrationFilePath);
|
||||
const files = fs.readdirSync(this.migrationFilePath);
|
||||
return files.filter((file) => /\.migration\.(js|ts)$/.test(file));
|
||||
}
|
||||
|
||||
@ -496,13 +500,19 @@ module.exports = {
|
||||
await this.updateStatus({ version, action, migrationDocument });
|
||||
const migrationDisplayName = `${version} [${action}]`;
|
||||
this.logger.log(`"${migrationDisplayName}" migration done`);
|
||||
// Update DB version
|
||||
const result = await this.metadataService.createOrUpdate({
|
||||
name: 'db-version',
|
||||
value: version,
|
||||
});
|
||||
const operation = result ? 'updated' : 'created';
|
||||
this.logger.log(`db-version metadata ${operation} "${name}"`);
|
||||
// Create or Update DB version
|
||||
await this.metadataService.updateOne(
|
||||
{ name: 'db-version' },
|
||||
{
|
||||
value: version,
|
||||
},
|
||||
{
|
||||
// Create or update
|
||||
upsert: true,
|
||||
new: true,
|
||||
},
|
||||
);
|
||||
this.logger.log(`db-version metadata "${version}"`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -6,6 +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 { HttpService } from '@nestjs/axios';
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
|
||||
import { MigrationDocument } from './migration.schema';
|
||||
|
||||
export enum MigrationAction {
|
||||
@ -26,3 +30,8 @@ export interface MigrationRunParams {
|
||||
export interface MigrationSuccessCallback extends MigrationRunParams {
|
||||
migrationDocument: MigrationDocument;
|
||||
}
|
||||
|
||||
export type MigrationServices = {
|
||||
logger: LoggerService;
|
||||
http: HttpService;
|
||||
};
|
||||
|
@ -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.
|
||||
@ -21,6 +21,8 @@ import { NlpEntitySeeder } from './nlp/seeds/nlp-entity.seed';
|
||||
import { nlpEntityModels } from './nlp/seeds/nlp-entity.seed-model';
|
||||
import { NlpValueSeeder } from './nlp/seeds/nlp-value.seed';
|
||||
import { nlpValueModels } from './nlp/seeds/nlp-value.seed-model';
|
||||
import { MetadataSeeder } from './setting/seeds/metadata.seed';
|
||||
import { DEFAULT_METADATA } from './setting/seeds/metadata.seed-model';
|
||||
import { SettingSeeder } from './setting/seeds/setting.seed';
|
||||
import { DEFAULT_SETTINGS } from './setting/seeds/setting.seed-model';
|
||||
import { PermissionCreateDto } from './user/dto/permission.dto';
|
||||
@ -41,6 +43,7 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
const contextVarSeeder = app.get(ContextVarSeeder);
|
||||
const roleSeeder = app.get(RoleSeeder);
|
||||
const settingSeeder = app.get(SettingSeeder);
|
||||
const metadataSeeder = app.get(MetadataSeeder);
|
||||
const permissionSeeder = app.get(PermissionSeeder);
|
||||
const userSeeder = app.get(UserSeeder);
|
||||
const languageSeeder = app.get(LanguageSeeder);
|
||||
@ -106,13 +109,15 @@ export async function seedDatabase(app: INestApplicationContext) {
|
||||
logger.error('Unable to seed the database with users!');
|
||||
throw e;
|
||||
}
|
||||
// Seed users
|
||||
try {
|
||||
await settingSeeder.seed(DEFAULT_SETTINGS);
|
||||
} catch (e) {
|
||||
logger.error('Unable to seed the database with settings!');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Seed settings and metadata
|
||||
try {
|
||||
await settingSeeder.seed(DEFAULT_SETTINGS);
|
||||
await metadataSeeder.seed(DEFAULT_METADATA);
|
||||
} catch (e) {
|
||||
logger.error('Unable to seed the database with settings and metadata!');
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Seed categories
|
||||
|
23
api/src/setting/dto/metadata.dto.ts
Normal file
23
api/src/setting/dto/metadata.dto.ts
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 { ApiProperty, PartialType } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class MetadataCreateDto {
|
||||
@ApiProperty({ description: 'Metadata name', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Metadata value' })
|
||||
@IsNotEmpty()
|
||||
value: any;
|
||||
}
|
||||
|
||||
export class MetadataUpdateDto extends PartialType(MetadataCreateDto) {}
|
26
api/src/setting/repositories/metadata.repository.ts
Normal file
26
api/src/setting/repositories/metadata.repository.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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 { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
|
||||
import { BaseRepository } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Metadata } from '../schemas/metadata.schema';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataRepository extends BaseRepository<Metadata> {
|
||||
constructor(
|
||||
readonly eventEmitter: EventEmitter2,
|
||||
@InjectModel(Metadata.name) readonly model: Model<Metadata>,
|
||||
) {
|
||||
super(eventEmitter, model, Metadata);
|
||||
}
|
||||
}
|
@ -9,10 +9,11 @@
|
||||
import { ModelDefinition, Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Metadata {
|
||||
export class Metadata extends BaseSchema {
|
||||
@Prop({ type: String, required: true, unique: true })
|
||||
name: string;
|
||||
|
||||
|
16
api/src/setting/seeds/metadata.seed-model.ts
Normal file
16
api/src/setting/seeds/metadata.seed-model.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* 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 { MetadataCreateDto } from '../dto/metadata.dto';
|
||||
|
||||
export const DEFAULT_METADATA = [
|
||||
{
|
||||
name: 'db-version',
|
||||
value: process.env.npm_package_version,
|
||||
},
|
||||
] as const satisfies MetadataCreateDto[];
|
21
api/src/setting/seeds/metadata.seed.ts
Normal file
21
api/src/setting/seeds/metadata.seed.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BaseSeeder } from '@/utils/generics/base-seeder';
|
||||
|
||||
import { MetadataRepository } from '../repositories/metadata.repository';
|
||||
import { Metadata } from '../schemas/metadata.schema';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataSeeder extends BaseSeeder<Metadata> {
|
||||
constructor(private readonly metadataRepository: MetadataRepository) {
|
||||
super(metadataRepository);
|
||||
}
|
||||
}
|
@ -7,48 +7,15 @@
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { HydratedDocument, Model } from 'mongoose';
|
||||
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { MetadataRepository } from '../repositories/metadata.repository';
|
||||
import { Metadata } from '../schemas/metadata.schema';
|
||||
|
||||
@Injectable()
|
||||
export class MetadataService {
|
||||
constructor(
|
||||
@InjectModel(Metadata.name)
|
||||
private readonly metadataModel: Model<Metadata>,
|
||||
) {}
|
||||
|
||||
private toClassObject(metadata: HydratedDocument<Metadata>) {
|
||||
return plainToClass(
|
||||
Metadata,
|
||||
metadata.toObject({ virtuals: true, getters: true }),
|
||||
{ excludePrefixes: ['_'] },
|
||||
);
|
||||
}
|
||||
|
||||
async createOrUpdate(dto: Metadata) {
|
||||
const metadata = await this.metadataModel.findOneAndUpdate(
|
||||
{ name: dto.name },
|
||||
dto,
|
||||
{
|
||||
upsert: true,
|
||||
},
|
||||
);
|
||||
return this.toClassObject(metadata);
|
||||
}
|
||||
|
||||
async get(name: string) {
|
||||
const metadata = await this.metadataModel.findOne({ name });
|
||||
return this.toClassObject(metadata);
|
||||
}
|
||||
|
||||
async set(name: string, value: any) {
|
||||
const metadata = await this.metadataModel.findOneAndUpdate(
|
||||
{ name },
|
||||
{ $set: { value } },
|
||||
);
|
||||
return this.toClassObject(metadata);
|
||||
export class MetadataService extends BaseService<Metadata> {
|
||||
constructor(readonly repository: MetadataRepository) {
|
||||
super(repository);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
@ -11,9 +11,11 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { SettingController } from './controllers/setting.controller';
|
||||
import { MetadataRepository } from './repositories/metadata.repository';
|
||||
import { SettingRepository } from './repositories/setting.repository';
|
||||
import { MetadataModel } from './schemas/metadata.schema';
|
||||
import { SettingModel } from './schemas/setting.schema';
|
||||
import { MetadataSeeder } from './seeds/metadata.seed';
|
||||
import { SettingSeeder } from './seeds/setting.seed';
|
||||
import { MetadataService } from './services/metadata.service';
|
||||
import { SettingService } from './services/setting.service';
|
||||
@ -28,7 +30,9 @@ import { SettingService } from './services/setting.service';
|
||||
],
|
||||
providers: [
|
||||
SettingRepository,
|
||||
MetadataRepository,
|
||||
SettingSeeder,
|
||||
MetadataSeeder,
|
||||
SettingService,
|
||||
MetadataService,
|
||||
],
|
||||
|
@ -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.
|
||||
@ -20,6 +20,7 @@ import {
|
||||
Model,
|
||||
ProjectionType,
|
||||
Query,
|
||||
QueryOptions,
|
||||
SortOrder,
|
||||
UpdateQuery,
|
||||
UpdateWithAggregationPipeline,
|
||||
@ -474,6 +475,9 @@ export abstract class BaseRepository<
|
||||
async updateOne<D extends Partial<U>>(
|
||||
criteria: string | TFilterQuery<T>,
|
||||
dto: UpdateQuery<D>,
|
||||
options: QueryOptions<D> | null = {
|
||||
new: true,
|
||||
},
|
||||
): Promise<T | null> {
|
||||
const query = this.model.findOneAndUpdate<T>(
|
||||
{
|
||||
@ -482,9 +486,7 @@ export abstract class BaseRepository<
|
||||
{
|
||||
$set: dto,
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
},
|
||||
options,
|
||||
);
|
||||
const filterCriteria = query.getFilter();
|
||||
const queryUpdates = query.getUpdate();
|
||||
|
@ -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.
|
||||
@ -84,6 +84,7 @@ describe('BaseService', () => {
|
||||
expect(dummyRepository.updateOne).toHaveBeenCalledWith(
|
||||
createdId,
|
||||
updatedPayload,
|
||||
undefined,
|
||||
);
|
||||
expect(result).toEqualPayload(updatedPayload);
|
||||
});
|
||||
@ -98,6 +99,7 @@ describe('BaseService', () => {
|
||||
expect(dummyRepository.updateOne).toHaveBeenCalledWith(
|
||||
updatedPayload,
|
||||
updatedCriteriaPayload,
|
||||
undefined,
|
||||
);
|
||||
expect(result).toEqualPayload(updatedCriteriaPayload);
|
||||
});
|
||||
|
@ -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.
|
||||
@ -9,7 +9,7 @@
|
||||
import { ConflictException } from '@nestjs/common';
|
||||
import { ClassTransformOptions } from 'class-transformer';
|
||||
import { MongoError } from 'mongodb';
|
||||
import { ProjectionType } from 'mongoose';
|
||||
import { ProjectionType, QueryOptions } from 'mongoose';
|
||||
|
||||
import { TFilterQuery } from '@/utils/types/filter.types';
|
||||
|
||||
@ -173,8 +173,9 @@ export abstract class BaseService<
|
||||
async updateOne<D extends Partial<Omit<T, keyof BaseSchema>>>(
|
||||
criteria: string | TFilterQuery<T>,
|
||||
dto: D,
|
||||
options?: QueryOptions<D> | null,
|
||||
): Promise<T | null> {
|
||||
return await this.repository.updateOne(criteria, dto);
|
||||
return await this.repository.updateOne(criteria, dto, options);
|
||||
}
|
||||
|
||||
async updateMany<D extends Partial<Omit<T, keyof BaseSchema>>>(
|
||||
|
5
api/src/utils/test/fixtures/metadata.ts
vendored
5
api/src/utils/test/fixtures/metadata.ts
vendored
@ -8,9 +8,10 @@
|
||||
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import { Metadata, MetadataModel } from '@/setting/schemas/metadata.schema';
|
||||
import { MetadataCreateDto } from '@/setting/dto/metadata.dto';
|
||||
import { MetadataModel } from '@/setting/schemas/metadata.schema';
|
||||
|
||||
const metadataFixtures: Metadata[] = [
|
||||
const metadataFixtures: MetadataCreateDto[] = [
|
||||
{
|
||||
name: 'app-version',
|
||||
value: '2.2.0',
|
||||
|
Loading…
Reference in New Issue
Block a user