mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: initial commit
This commit is contained in:
145
api/src/setting/services/setting.service.spec.ts
Normal file
145
api/src/setting/services/setting.service.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import {
|
||||
installSettingFixtures,
|
||||
settingFixtures,
|
||||
} from '@/utils/test/fixtures/setting';
|
||||
import {
|
||||
closeInMongodConnection,
|
||||
rootMongooseTestModule,
|
||||
} from '@/utils/test/test';
|
||||
|
||||
import { SettingService } from './setting.service';
|
||||
import { SettingRepository } from '../repositories/setting.repository';
|
||||
import { Setting, SettingModel } from '../schemas/setting.schema';
|
||||
import { SettingType } from '../schemas/types';
|
||||
import { SettingSeeder } from '../seeds/setting.seed';
|
||||
|
||||
describe('SettingService', () => {
|
||||
let settingService: SettingService;
|
||||
let settingRepository: SettingRepository;
|
||||
const commonAttributes = {
|
||||
type: SettingType.text,
|
||||
id: '',
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [
|
||||
rootMongooseTestModule(installSettingFixtures),
|
||||
MongooseModule.forFeature([SettingModel]),
|
||||
],
|
||||
providers: [
|
||||
SettingService,
|
||||
SettingRepository,
|
||||
SettingSeeder,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
LoggerService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
settingRepository = module.get<SettingRepository>(SettingRepository);
|
||||
});
|
||||
|
||||
afterAll(closeInMongodConnection);
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('load', () => {
|
||||
it('Should return loaded settings', async () => {
|
||||
jest.spyOn(settingRepository, 'findAll');
|
||||
const result = await settingService.load();
|
||||
|
||||
expect(settingRepository.findAll).toHaveBeenCalled();
|
||||
expect(result).toEqualPayload(
|
||||
settingService.group(settingFixtures as Setting[]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildTree', () => {
|
||||
it('should return an empty object when settings are empty', () => {
|
||||
expect(settingService.buildTree([])).toEqual({});
|
||||
});
|
||||
|
||||
it('should categorize settings by group and map labels to values', () => {
|
||||
const settings: Setting[] = [
|
||||
{ group: 'group1', label: 'setting1', value: 'value1' },
|
||||
{ group: 'group1', label: 'setting2', value: 'value2' },
|
||||
{ group: 'group2', label: 'setting1', value: 'value3' },
|
||||
].map((s) => ({ ...commonAttributes, ...s }));
|
||||
expect(settingService.buildTree(settings)).toEqualPayload({
|
||||
group1: { setting1: 'value1', setting2: 'value2' },
|
||||
group2: { setting1: 'value3' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined group as "undefinedGroup"', () => {
|
||||
const settings: Setting[] = [
|
||||
{
|
||||
...commonAttributes,
|
||||
label: 'setting1',
|
||||
value: 'value1',
|
||||
group: '',
|
||||
},
|
||||
];
|
||||
expect(settingService.buildTree(settings)).toEqualPayload({
|
||||
undefinedGroup: { setting1: 'value1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('group', () => {
|
||||
it('should return an empty object when settings are empty', () => {
|
||||
expect(settingService.group([])).toEqual({});
|
||||
});
|
||||
|
||||
it('should group settings by their group property', () => {
|
||||
const settings = [
|
||||
{ group: 'group1', label: 'setting1', value: 'value1' },
|
||||
{ group: 'group1', label: 'setting2', value: 'value2' },
|
||||
{ group: 'group2', label: 'setting1', value: 'value3' },
|
||||
].map((s) => ({ ...commonAttributes, ...s }));
|
||||
expect(settingService.group(settings)).toEqualPayload({
|
||||
group1: [
|
||||
{ group: 'group1', label: 'setting1', type: 'text', value: 'value1' },
|
||||
{ group: 'group1', label: 'setting2', type: 'text', value: 'value2' },
|
||||
],
|
||||
group2: [
|
||||
{ group: 'group2', label: 'setting1', type: 'text', value: 'value3' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
130
api/src/setting/services/setting.service.ts
Normal file
130
api/src/setting/services/setting.service.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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).
|
||||
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
|
||||
*/
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Cache } from 'cache-manager';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { Config } from '@/config/types';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SETTING_CACHE_KEY } from '@/utils/constants/cache';
|
||||
import { Cacheable } from '@/utils/decorators/cacheable.decorator';
|
||||
import { BaseService } from '@/utils/generics/base-service';
|
||||
|
||||
import { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingRepository } from '../repositories/setting.repository';
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
import { Settings } from '../schemas/types';
|
||||
import { SettingSeeder } from '../seeds/setting.seed';
|
||||
|
||||
@Injectable()
|
||||
export class SettingService extends BaseService<Setting> {
|
||||
constructor(
|
||||
readonly repository: SettingRepository,
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
private readonly logger: LoggerService,
|
||||
private readonly seeder: SettingSeeder,
|
||||
) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the settings if they don't already exist for the provided group.
|
||||
*
|
||||
* @param group - The group of settings to check.
|
||||
* @param data - The array of settings to seed if none exist.
|
||||
*/
|
||||
async seedIfNotExist(group: string, data: SettingCreateDto[]) {
|
||||
const count = await this.count({ group });
|
||||
if (count === 0) {
|
||||
await this.seeder.seed(data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all settings and returns them grouped in ascending order by weight.
|
||||
*
|
||||
* @returns A grouped object of settings.
|
||||
*/
|
||||
async load() {
|
||||
const settings = await this.findAll(['weight', 'asc']);
|
||||
return this.group(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a tree structure from the settings array.
|
||||
*
|
||||
* Each setting is grouped by its `group` and returned as a structured object.
|
||||
*
|
||||
* @param settings - An array of settings to build into a tree structure.
|
||||
*
|
||||
* @returns A `Settings` object organized by group.
|
||||
*/
|
||||
public buildTree(settings: Setting[]): Settings {
|
||||
return settings.reduce((acc: Settings, s: Setting) => {
|
||||
const groupKey = s.group || 'undefinedGroup';
|
||||
|
||||
acc[groupKey] = acc[groupKey] || {};
|
||||
acc[groupKey][s.label] = s.value;
|
||||
|
||||
return acc;
|
||||
}, {} as Settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the settings into a record where the key is the setting group and
|
||||
* the value is an array of settings in that group.
|
||||
*
|
||||
* @param settings - An array of settings to group.
|
||||
*
|
||||
* @returns A record where each key is a group and each value is an array of settings.
|
||||
*/
|
||||
public group(settings: Setting[]): Record<string, Setting[]> {
|
||||
return (
|
||||
settings?.reduce((acc, curr) => {
|
||||
const group = acc[curr.group] || [];
|
||||
group.push(curr);
|
||||
acc[curr.group] = group;
|
||||
return acc;
|
||||
}, {}) || {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the application configuration object.
|
||||
*
|
||||
* @returns The global configuration object.
|
||||
*/
|
||||
getConfig(): Config {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for setting updates. Listens to 'hook:settings:*:*' events
|
||||
* and invalidates the cache for settings when triggered.
|
||||
*/
|
||||
@OnEvent('hook:settings:*:*')
|
||||
async handleSettingUpdateEvent() {
|
||||
this.cacheManager.del(SETTING_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves settings from the cache if available, or loads them from the
|
||||
* repository and caches the result.
|
||||
*
|
||||
* @returns A promise that resolves to a `Settings` object.
|
||||
*/
|
||||
@Cacheable(SETTING_CACHE_KEY)
|
||||
async getSettings(): Promise<Settings> {
|
||||
const settings = await this.findAll();
|
||||
return this.buildTree(settings);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user