feat: initial commit

This commit is contained in:
Mohamed Marrouchi
2024-09-10 10:50:11 +01:00
commit 30e5766487
879 changed files with 122820 additions and 0 deletions

View 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' },
],
});
});
});
});

View 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);
}
}