mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: initial commit
This commit is contained in:
108
api/src/setting/controllers/setting.controller.spec.ts
Normal file
108
api/src/setting/controllers/setting.controller.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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 { SettingController } from './setting.controller';
|
||||
import { SettingRepository } from '../repositories/setting.repository';
|
||||
import { SettingModel } from '../schemas/setting.schema';
|
||||
import { SettingSeeder } from '../seeds/setting.seed';
|
||||
import { SettingService } from '../services/setting.service';
|
||||
|
||||
describe('SettingController', () => {
|
||||
let settingController: SettingController;
|
||||
let settingService: SettingService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
controllers: [SettingController],
|
||||
imports: [
|
||||
rootMongooseTestModule(installSettingFixtures),
|
||||
MongooseModule.forFeature([SettingModel]),
|
||||
],
|
||||
providers: [
|
||||
SettingService,
|
||||
SettingRepository,
|
||||
SettingSeeder,
|
||||
LoggerService,
|
||||
EventEmitter2,
|
||||
{
|
||||
provide: ExtendedI18nService,
|
||||
useValue: {
|
||||
t: jest.fn().mockImplementation((t) => t),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CACHE_MANAGER,
|
||||
useValue: {
|
||||
del: jest.fn(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
settingController = module.get<SettingController>(SettingController);
|
||||
settingService = module.get<SettingService>(SettingService);
|
||||
});
|
||||
|
||||
afterAll(closeInMongodConnection);
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
describe('find', () => {
|
||||
it('Should return an array of ordered by group Settings', async () => {
|
||||
jest.spyOn(settingService, 'find');
|
||||
const result = await settingController.find(
|
||||
{},
|
||||
{
|
||||
sort: ['weight', 'asc'],
|
||||
},
|
||||
);
|
||||
|
||||
expect(settingService.find).toHaveBeenCalled();
|
||||
expect(result).toEqualPayload(settingFixtures);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOne', () => {
|
||||
it('Should update and return a specific Setting by id', async () => {
|
||||
jest.spyOn(settingService, 'updateOne');
|
||||
const payload = {
|
||||
value: 'updated setting value',
|
||||
};
|
||||
const id = (await settingService.findOne({ value: 'admin@example.com' }))
|
||||
.id;
|
||||
const result = await settingController.updateOne(id, payload);
|
||||
|
||||
expect(settingService.updateOne).toHaveBeenCalledWith(id, payload);
|
||||
expect(result).toEqualPayload({
|
||||
...settingFixtures.find(
|
||||
(settingFixture) => settingFixture.value === 'admin@example.com',
|
||||
),
|
||||
value: payload.value,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
92
api/src/setting/controllers/setting.controller.ts
Normal file
92
api/src/setting/controllers/setting.controller.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Patch,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
||||
import { TFilterQuery } from 'mongoose';
|
||||
|
||||
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { QuerySortDto } from '@/utils/pagination/pagination-query.dto';
|
||||
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
||||
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
||||
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
import { SettingService } from '../services/setting.service';
|
||||
|
||||
@UseInterceptors(CsrfInterceptor)
|
||||
@Controller('setting')
|
||||
export class SettingController {
|
||||
constructor(
|
||||
private readonly settingService: SettingService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Finds settings that match the provided filters and sorting options.
|
||||
*
|
||||
* @param filters - Filters to apply when querying settings, limited to the `group` field.
|
||||
* @param sort - Sorting options for the query.
|
||||
*
|
||||
* @returns A list of settings that match the criteria.
|
||||
*/
|
||||
@Get()
|
||||
async find(
|
||||
@Query(
|
||||
new SearchFilterPipe<Setting>({
|
||||
allowedFields: ['group'],
|
||||
}),
|
||||
)
|
||||
filters: TFilterQuery<Setting>,
|
||||
@Query(PageQueryPipe) { sort }: { sort: QuerySortDto<Setting> },
|
||||
) {
|
||||
return await this.settingService.find(filters, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all settings available in the system.
|
||||
*
|
||||
* @returns A list of all settings.
|
||||
*/
|
||||
@Get('load')
|
||||
async load() {
|
||||
return await this.settingService.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a setting by its ID. If the setting does not exist, throws a `NotFoundException`.
|
||||
*
|
||||
* @param id - The ID of the setting to update.
|
||||
* @param settingUpdateDto - The new value of the setting.
|
||||
*
|
||||
* @returns The updated setting.
|
||||
*/
|
||||
@CsrfCheck(true)
|
||||
@Patch(':id')
|
||||
async updateOne(
|
||||
@Param('id') id: string,
|
||||
@Body() settingUpdateDto: { value: any },
|
||||
): Promise<Setting> {
|
||||
const result = await this.settingService.updateOne(id, settingUpdateDto);
|
||||
if (!result) {
|
||||
this.logger.warn(`Unable to update setting by id ${id}`);
|
||||
throw new NotFoundException(`Setting with ID ${id} not found`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
71
api/src/setting/dto/setting.dto.ts
Normal file
71
api/src/setting/dto/setting.dto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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 { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import {
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
IsOptional,
|
||||
} from 'class-validator';
|
||||
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
export class SettingCreateDto {
|
||||
@ApiProperty({ description: 'Setting group of setting', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
group: string;
|
||||
|
||||
@ApiProperty({ description: 'Setting label of setting', type: String })
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Setting type of the setting',
|
||||
enum: [
|
||||
'text',
|
||||
'multiple_text',
|
||||
'checkbox',
|
||||
'select',
|
||||
'number',
|
||||
'attachment',
|
||||
'multiple_attachment',
|
||||
],
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsIn(Object.values(SettingType))
|
||||
type: SettingType;
|
||||
|
||||
@ApiProperty({ description: 'Setting value of the setting' })
|
||||
@IsNotEmpty()
|
||||
value: any;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Setting options',
|
||||
isArray: true,
|
||||
type: Array,
|
||||
})
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
options?: string[];
|
||||
|
||||
//TODO: adding swagger decorators
|
||||
config?: Record<string, any>;
|
||||
|
||||
//TODO: adding swagger decorators
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export class SettingUpdateDto {
|
||||
@ApiProperty({ description: 'value of the setting' })
|
||||
value: null | string | number | boolean | string[] | Record<string, any>;
|
||||
}
|
||||
118
api/src/setting/repositories/setting.repository.ts
Normal file
118
api/src/setting/repositories/setting.repository.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Document, Model, Query, Types } from 'mongoose';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { ExtendedI18nService } from '@/extended-i18n.service';
|
||||
import { BaseRepository } from '@/utils/generics/base-repository';
|
||||
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
|
||||
@Injectable()
|
||||
export class SettingRepository extends BaseRepository<Setting> {
|
||||
constructor(
|
||||
@InjectModel(Setting.name) readonly model: Model<Setting>,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly i18n: ExtendedI18nService,
|
||||
) {
|
||||
super(model, Setting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the `Setting` document after it has been retrieved.
|
||||
*
|
||||
* Checks the `type` of the setting and validates the `value` field according to the type:
|
||||
* - `text` expects a string.
|
||||
* - `multiple_text` expects an array of strings.
|
||||
* - `checkbox` expects a boolean.
|
||||
*
|
||||
* @param setting The `Setting` document to be validated.
|
||||
*/
|
||||
async postValidate(
|
||||
setting: Document<unknown, unknown, Setting> &
|
||||
Setting & { _id: Types.ObjectId },
|
||||
) {
|
||||
if (setting.type === 'text' && typeof setting.value !== 'string') {
|
||||
throw new Error('Setting Model : Value must be a string!');
|
||||
} else if (setting.type === 'multiple_text') {
|
||||
const isStringArray =
|
||||
Array.isArray(setting.value) &&
|
||||
setting.value.every((v) => {
|
||||
return typeof v === 'string';
|
||||
});
|
||||
if (!isStringArray) {
|
||||
throw new Error('Setting Model : Value must be a string array!');
|
||||
}
|
||||
} else if (
|
||||
setting.type === 'checkbox' &&
|
||||
typeof setting.value !== 'boolean'
|
||||
) {
|
||||
throw new Error('Setting Model : Value must be a boolean!');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event after a `Setting` has been updated.
|
||||
*
|
||||
* This method is used to synchronize global settings by emitting an event
|
||||
* based on the `group` and `label` of the `Setting`. It also updates the i18n
|
||||
* default language setting when the `default_lang` label is updated.
|
||||
*
|
||||
* @param _query The Mongoose query object used to find and update the document.
|
||||
* @param setting The updated `Setting` object.
|
||||
*/
|
||||
async postUpdate(
|
||||
_query: Query<
|
||||
Document<Setting, any, any>,
|
||||
Document<Setting, any, any>,
|
||||
unknown,
|
||||
Setting,
|
||||
'findOneAndUpdate'
|
||||
>,
|
||||
setting: Setting,
|
||||
) {
|
||||
// Sync global settings var
|
||||
this.eventEmitter.emit(
|
||||
'hook:settings:' + setting.group + ':' + setting.label,
|
||||
setting,
|
||||
);
|
||||
|
||||
if (setting.label === 'default_lang') {
|
||||
// @todo : check if this actually updates the default lang
|
||||
this.i18n.resolveLanguage(setting.value as string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default values before creating a `Setting` document.
|
||||
*
|
||||
* If the setting is part of the `nlp_settings` group, it sets specific values
|
||||
* for `languages` and `default_lang` labels, using configuration values from the
|
||||
* chatbot settings.
|
||||
*
|
||||
* @param setting The `Setting` document to be created.
|
||||
*/
|
||||
async preCreate(
|
||||
setting: Document<unknown, unknown, Setting> &
|
||||
Setting & { _id: Types.ObjectId },
|
||||
) {
|
||||
if (setting.group === 'nlp_settings') {
|
||||
if (setting.label === 'languages') {
|
||||
setting.value = config.chatbot.lang.available;
|
||||
} else if (setting.label === 'default_lang') {
|
||||
setting.value = config.chatbot.lang.default;
|
||||
setting.options = config.chatbot.lang.available;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
api/src/setting/schemas/setting.schema.ts
Normal file
61
api/src/setting/schemas/setting.schema.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
|
||||
import { IsArray, IsIn } from 'class-validator';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
|
||||
|
||||
import { SettingType } from './types';
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class Setting extends BaseSchema {
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
group: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
label: string;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
@IsIn(Object.values(SettingType))
|
||||
type: SettingType;
|
||||
|
||||
@Prop({ type: JSON })
|
||||
value: any;
|
||||
|
||||
@IsArray()
|
||||
@Prop({ type: JSON })
|
||||
options?: string[];
|
||||
|
||||
@Prop({ type: JSON, default: {} })
|
||||
config?: Record<string, any>;
|
||||
|
||||
@Prop({
|
||||
type: Number,
|
||||
default: 0,
|
||||
})
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
export const SettingModel: ModelDefinition = LifecycleHookManager.attach({
|
||||
name: Setting.name,
|
||||
schema: SchemaFactory.createForClass(Setting),
|
||||
});
|
||||
|
||||
export default SettingModel.schema;
|
||||
120
api/src/setting/schemas/types.ts
Normal file
120
api/src/setting/schemas/types.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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 { Setting } from './setting.schema';
|
||||
|
||||
export enum SettingType {
|
||||
text = 'text',
|
||||
textarea = 'textarea',
|
||||
secret = 'secret',
|
||||
multiple_text = 'multiple_text',
|
||||
checkbox = 'checkbox',
|
||||
select = 'select',
|
||||
number = 'number',
|
||||
attachment = 'attachment',
|
||||
multiple_attachment = 'multiple_attachment',
|
||||
}
|
||||
|
||||
/**
|
||||
* The following interfaces are declared, and currently not used
|
||||
* TextSetting
|
||||
* MultiTextSetting
|
||||
* CheckboxSetting
|
||||
* SelectSetting
|
||||
* NumberSetting
|
||||
* AttachmentSetting
|
||||
* MultipleAttachmentSetting
|
||||
* AnySetting
|
||||
**/
|
||||
export interface TextSetting extends Setting {
|
||||
type: SettingType.text;
|
||||
value: string;
|
||||
options: never;
|
||||
config: never;
|
||||
}
|
||||
|
||||
export interface MultiTextSetting extends Setting {
|
||||
type: SettingType.multiple_text;
|
||||
value: string[];
|
||||
options: never;
|
||||
config: never;
|
||||
}
|
||||
|
||||
export interface CheckboxSetting extends Setting {
|
||||
type: SettingType.checkbox;
|
||||
value: boolean;
|
||||
options: never;
|
||||
config: never;
|
||||
}
|
||||
|
||||
export interface SelectSetting extends Setting {
|
||||
type: SettingType.select;
|
||||
value: string;
|
||||
options: string[];
|
||||
config: never;
|
||||
}
|
||||
|
||||
export interface NumberSetting extends Setting {
|
||||
type: SettingType.number;
|
||||
value: number;
|
||||
options: never;
|
||||
config?: {
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AttachmentSetting extends Setting {
|
||||
type: SettingType.attachment;
|
||||
value: string | null; // attachment id
|
||||
options: never;
|
||||
config: never;
|
||||
}
|
||||
|
||||
export interface MultipleAttachmentSetting extends Setting {
|
||||
type: SettingType.multiple_attachment;
|
||||
value: string[]; // attachment ids
|
||||
options: never;
|
||||
config: never;
|
||||
}
|
||||
|
||||
export type AnySetting =
|
||||
| TextSetting
|
||||
| MultiTextSetting
|
||||
| CheckboxSetting
|
||||
| SelectSetting
|
||||
| NumberSetting
|
||||
| AttachmentSetting
|
||||
| MultipleAttachmentSetting;
|
||||
|
||||
export type SettingDict = { [group: string]: Setting[] };
|
||||
|
||||
export type Settings = {
|
||||
nlp_settings: {
|
||||
default_lang: string;
|
||||
languages: string[];
|
||||
threshold: string;
|
||||
provider: string;
|
||||
endpoint: string;
|
||||
token: string;
|
||||
};
|
||||
contact: { [key: string]: string };
|
||||
chatbot_settings: {
|
||||
global_fallback: boolean;
|
||||
fallback_message: string[];
|
||||
fallback_block: string;
|
||||
};
|
||||
email_settings: {
|
||||
mailer: string;
|
||||
auth_user: string;
|
||||
auth_pass: string;
|
||||
from: string;
|
||||
};
|
||||
} & Record<string, any>;
|
||||
220
api/src/setting/seeds/setting.seed-model.ts
Normal file
220
api/src/setting/seeds/setting.seed-model.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* 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 { SettingCreateDto } from '../dto/setting.dto';
|
||||
import { SettingType } from '../schemas/types';
|
||||
|
||||
export const settingModels: SettingCreateDto[] = [
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'global_fallback',
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'fallback_block',
|
||||
value: '',
|
||||
options: [],
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: false,
|
||||
allowCreate: false,
|
||||
source: '/Block/',
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'chatbot_settings',
|
||||
label: 'fallback_message',
|
||||
value: [
|
||||
"Sorry but i didn't understand your request. Maybe you can check the menu",
|
||||
"I'm really sorry but i don't quite understand what you are saying :(",
|
||||
],
|
||||
type: SettingType.multiple_text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'provider',
|
||||
value: 'default',
|
||||
options: ['default'],
|
||||
type: SettingType.select,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'endpoint',
|
||||
value: 'http://nlu-api:5000/',
|
||||
type: SettingType.text,
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'token',
|
||||
value: 'token123',
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'languages',
|
||||
value: [],
|
||||
options: [],
|
||||
type: SettingType.select,
|
||||
config: {
|
||||
multiple: true,
|
||||
allowCreate: true,
|
||||
},
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'default_lang',
|
||||
value: '',
|
||||
options: [], // NOTE : will be set onBeforeCreate from config
|
||||
type: SettingType.select,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'nlp_settings',
|
||||
label: 'threshold',
|
||||
value: 0.9,
|
||||
type: SettingType.number,
|
||||
config: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'from',
|
||||
value: 'no-reply@domain.com',
|
||||
type: SettingType.text,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'mailer',
|
||||
value: 'sendmail',
|
||||
options: ['sendmail', 'smtp'],
|
||||
type: SettingType.select,
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'host',
|
||||
value: 'localhost',
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'port',
|
||||
value: '25',
|
||||
type: SettingType.text,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'secure',
|
||||
value: true,
|
||||
type: SettingType.checkbox,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'auth_user',
|
||||
value: '',
|
||||
type: SettingType.text,
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: 'email_settings',
|
||||
label: 'auth_pass',
|
||||
value: '',
|
||||
type: SettingType.text,
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'contact_email_recipient',
|
||||
value: 'admin@example.com',
|
||||
type: SettingType.text,
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_name',
|
||||
value: 'Your company name',
|
||||
type: SettingType.text,
|
||||
weight: 2,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_phone',
|
||||
value: '(+999) 9999 9999 999',
|
||||
type: SettingType.text,
|
||||
weight: 3,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_email',
|
||||
value: 'contact[at]mycompany.com',
|
||||
type: SettingType.text,
|
||||
weight: 4,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_address1',
|
||||
value: '71 Pilgrim Avenue',
|
||||
type: SettingType.text,
|
||||
weight: 5,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_address2',
|
||||
value: '',
|
||||
type: SettingType.text,
|
||||
weight: 6,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_city',
|
||||
value: 'Chevy Chase',
|
||||
type: SettingType.text,
|
||||
weight: 7,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_zipcode',
|
||||
value: '85705',
|
||||
type: SettingType.text,
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_state',
|
||||
value: 'Orlando',
|
||||
type: SettingType.text,
|
||||
weight: 9,
|
||||
},
|
||||
{
|
||||
group: 'contact',
|
||||
label: 'company_country',
|
||||
value: 'US',
|
||||
type: SettingType.text,
|
||||
weight: 10,
|
||||
},
|
||||
];
|
||||
41
api/src/setting/seeds/setting.seed.ts
Normal file
41
api/src/setting/seeds/setting.seed.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { Injectable } from '@nestjs/common';
|
||||
|
||||
import { BaseSchema } from '@/utils/generics/base-schema';
|
||||
import { BaseSeeder } from '@/utils/generics/base-seeder';
|
||||
|
||||
import { SettingRepository } from '../repositories/setting.repository';
|
||||
import { Setting } from '../schemas/setting.schema';
|
||||
|
||||
@Injectable()
|
||||
export class SettingSeeder extends BaseSeeder<Setting> {
|
||||
constructor(private readonly settingRepository: SettingRepository) {
|
||||
super(settingRepository);
|
||||
}
|
||||
|
||||
async seed(models: Omit<Setting, keyof BaseSchema>[]): Promise<boolean> {
|
||||
const grouppedModels = models.reduce(
|
||||
(acc, model) => {
|
||||
if (!acc[model.group]) acc[model.group] = [model];
|
||||
else acc[model.group].push(model);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Omit<Setting, keyof BaseSchema>[]>,
|
||||
);
|
||||
|
||||
Object.entries(grouppedModels).forEach(async ([group, models]) => {
|
||||
if ((await this.repository.count({ group })) === 0)
|
||||
await this.repository.createMany(models);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
32
api/src/setting/setting.module.ts
Normal file
32
api/src/setting/setting.module.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 { Global, Module } from '@nestjs/common';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { SettingController } from './controllers/setting.controller';
|
||||
import { SettingRepository } from './repositories/setting.repository';
|
||||
import { SettingModel } from './schemas/setting.schema';
|
||||
import { SettingSeeder } from './seeds/setting.seed';
|
||||
import { SettingService } from './services/setting.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([SettingModel]),
|
||||
PassportModule.register({
|
||||
session: true,
|
||||
}),
|
||||
],
|
||||
providers: [SettingRepository, SettingSeeder, SettingService],
|
||||
controllers: [SettingController],
|
||||
exports: [SettingService],
|
||||
})
|
||||
export class SettingModule {}
|
||||
Reference in New Issue
Block a user