diff --git a/api/src/setting/repositories/setting.repository.ts b/api/src/setting/repositories/setting.repository.ts index 67ee0cde..9c3f5e49 100644 --- a/api/src/setting/repositories/setting.repository.ts +++ b/api/src/setting/repositories/setting.repository.ts @@ -12,10 +12,19 @@ import { IHookSettingsGroupLabelOperationMap, } from '@nestjs/event-emitter'; import { InjectModel } from '@nestjs/mongoose'; -import { Document, Model, Query, Types } from 'mongoose'; +import { + Document, + FilterQuery, + Model, + Query, + Types, + UpdateQuery, + UpdateWithAggregationPipeline, +} from 'mongoose'; import { I18nService } from '@/i18n/services/i18n.service'; -import { BaseRepository } from '@/utils/generics/base-repository'; +import { BaseRepository, EHook } from '@/utils/generics/base-repository'; +import { TFilterQuery } from '@/utils/types/filter.types'; import { Setting } from '../schemas/setting.schema'; import { SettingType } from '../schemas/types'; @@ -30,48 +39,38 @@ export class SettingRepository extends BaseRepository { super(eventEmitter, 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 preCreateValidate( - setting: Document & + doc: Document & Setting & { _id: Types.ObjectId }, + filterCriteria: FilterQuery, + updates: UpdateWithAggregationPipeline | UpdateQuery, ) { - if ( - (setting.type === SettingType.text || - setting.type === SettingType.textarea) && - typeof setting.value !== 'string' && - setting.value !== null - ) { - throw new Error('Setting Model : Value must be a string!'); - } else if (setting.type === SettingType.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 === SettingType.checkbox && - typeof setting.value !== 'boolean' && - setting.value !== null - ) { - throw new Error('Setting Model : Value must be a boolean!'); - } else if ( - setting.type === SettingType.number && - typeof setting.value !== 'number' && - setting.value !== null - ) { - throw new Error('Setting Model : Value must be a number!'); + this.validateSettingValue(doc.type, doc.value); + if (filterCriteria && updates) { + this.eventEmitter.emit( + `hook:setting:${EHook.preUpdateValidate}`, + filterCriteria, + updates, + ); + } + } + + async preUpdateValidate( + criteria: string | TFilterQuery, + dto: UpdateQuery, + filterCriteria: FilterQuery, + updates: UpdateWithAggregationPipeline | UpdateQuery, + ): Promise { + const payload = dto.$set ? dto.$set : dto; + if (typeof payload.value !== 'undefined') { + const { type } = + 'type' in payload ? payload : await this.findOne(criteria); + this.validateSettingValue(type, payload.value); + this.eventEmitter.emit( + `hook:setting:${EHook.preUpdateValidate}`, + filterCriteria, + updates, + ); } } @@ -100,4 +99,45 @@ export class SettingRepository extends BaseRepository { // Sync global settings var this.eventEmitter.emit(`hook:${group}:${label}`, 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. + */ + private validateSettingValue(type: SettingType, value: any) { + if ( + (type === SettingType.text || type === SettingType.textarea) && + typeof value !== 'string' && + value !== null + ) { + throw new Error('Setting Model : Value must be a string!'); + } else if (type === SettingType.multiple_text) { + const isStringArray = + Array.isArray(value) && + value.every((v) => { + return typeof v === 'string'; + }); + if (!isStringArray) { + throw new Error('Setting Model : Value must be a string array!'); + } + } else if ( + type === SettingType.checkbox && + typeof value !== 'boolean' && + value !== null + ) { + throw new Error('Setting Model : Value must be a boolean!'); + } else if ( + type === SettingType.number && + typeof value !== 'number' && + value !== null + ) { + throw new Error('Setting Model : Value must be a number!'); + } + } } diff --git a/api/src/utils/generics/base-repository.ts b/api/src/utils/generics/base-repository.ts index 5657af4a..c5bd631a 100644 --- a/api/src/utils/generics/base-repository.ts +++ b/api/src/utils/generics/base-repository.ts @@ -14,6 +14,7 @@ import { import { ClassTransformOptions, plainToClass } from 'class-transformer'; import { Document, + FilterQuery, FlattenMaps, HydratedDocument, Model, @@ -48,6 +49,8 @@ export enum EHook { postUpdateMany = 'postUpdateMany', postDelete = 'postDelete', postCreateValidate = 'postCreateValidate', + preUpdateValidate = 'preUpdateValidate', + postUpdateValidate = 'postUpdateValidate', } export abstract class BaseRepository< @@ -460,6 +463,10 @@ export abstract class BaseRepository< new: true, }, ); + const filterCriteria = query.getFilter(); + const queryUpdates = query.getUpdate(); + await this.preUpdateValidate(criteria, dto, filterCriteria, queryUpdates); + await this.postUpdateValidate(criteria, dto, filterCriteria, queryUpdates); return await this.executeOne(query, this.cls); } @@ -482,7 +489,11 @@ export abstract class BaseRepository< return await this.model.deleteMany(criteria); } - async preCreateValidate(_doc: HydratedDocument): Promise { + async preCreateValidate( + _doc: HydratedDocument, + _filterCriteria?: FilterQuery, + _updates?: UpdateWithAggregationPipeline | UpdateQuery, + ): Promise { // Nothing ... } @@ -490,6 +501,24 @@ export abstract class BaseRepository< // Nothing ... } + async preUpdateValidate>( + _criteria: string | TFilterQuery, + _dto: UpdateQuery, + _filterCriteria: FilterQuery, + _updates: UpdateWithAggregationPipeline | UpdateQuery, + ): Promise { + // Nothing ... + } + + async postUpdateValidate>( + _criteria: string | TFilterQuery, + _dto: UpdateQuery, + _filterCriteria: FilterQuery, + _updates: UpdateWithAggregationPipeline | UpdateQuery, + ): Promise { + // Nothing ... + } + async preCreate(_doc: HydratedDocument): Promise { // Nothing ... } diff --git a/api/types/event-emitter.d.ts b/api/types/event-emitter.d.ts index 92bd9037..a0245507 100644 --- a/api/types/event-emitter.d.ts +++ b/api/types/event-emitter.d.ts @@ -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 type { Document, Query } from 'mongoose'; +import type { Document, FilterQuery, Query } from 'mongoose'; import { type Socket } from 'socket.io'; import { type BotStats } from '@/analytics/schemas/bot-stats.schema'; @@ -207,6 +207,10 @@ declare module '@nestjs/event-emitter' { type TPostUpdate = THydratedDocument; + type TPreUpdateValidate = FilterQuery; + + type TPostUpdateValidate = THydratedDocument; + type TPostDelete = DeleteResult; type TPostUnion = @@ -269,6 +273,12 @@ declare module '@nestjs/event-emitter' { } | { [EHook.postDelete]: TPostDelete; + } + | { + [EHook.preUpdateValidate]: TPreUpdateValidate; + } + | { + [EHook.postUpdateValidate]: TPostUpdateValidate; }; type TNormalizedHook = Extract<