fix(api): resolve file conflicts

This commit is contained in:
yassinedorbozgithub 2025-06-18 17:19:31 +01:00
commit 6438ccda3c
19 changed files with 607 additions and 468 deletions

View File

@ -24,14 +24,11 @@ export class CategoryRepository extends BaseRepository<
never,
CategoryDto
> {
private readonly blockService: BlockService;
constructor(
@InjectModel(Category.name) readonly model: Model<Category>,
@Optional() blockService?: BlockService,
@Optional() private readonly blockService?: BlockService,
) {
super(model, Category);
this.blockService = blockService!;
}
/**
@ -42,7 +39,7 @@ export class CategoryRepository extends BaseRepository<
* @param criteria - The filter criteria for finding blocks to delete.
*/
async preDelete(
query: Query<
_query: Query<
DeleteResult,
Document<Category, any, any>,
unknown,
@ -51,23 +48,18 @@ export class CategoryRepository extends BaseRepository<
>,
criteria: TFilterQuery<Category>,
) {
criteria = query.getQuery();
const ids = Array.isArray(criteria._id?.$in)
? criteria._id.$in
: Array.isArray(criteria._id)
? criteria._id
: [criteria._id];
for (const id of ids) {
const associatedBlocks = await this.blockService.findOne({
category: id,
if (criteria._id) {
const block = await this.blockService?.findOneAndPopulate({
category: criteria._id,
});
if (associatedBlocks) {
const category = await this.findOne({ _id: id });
if (block) {
throw new ForbiddenException(
`Category ${category?.label || id} has blocks associated with it`,
`Category ${block.category?.label} has at least one associated block`,
);
}
} else {
throw new Error('Attempted to delete category using unknown criteria');
}
}
}

View File

@ -8,16 +8,14 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query } from 'mongoose';
import { Model } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { TFilterQuery } from '@/utils/types/filter.types';
import { BaseRepository } from '@/utils/generics/base-repository';
import { LabelDto } from '../dto/label.dto';
import {
Label,
LABEL_POPULATE,
LabelDocument,
LabelFull,
LabelPopulate,
} from '../schemas/label.schema';
@ -32,59 +30,4 @@ export class LabelRepository extends BaseRepository<
constructor(@InjectModel(Label.name) readonly model: Model<Label>) {
super(model, Label, LABEL_POPULATE, LabelFull);
}
/**
* After creating a `Label`, this method emits an event and updates the `label_id` field.
*
* @param created - The created label document instance.
*
* @returns A promise that resolves when the update operation is complete.
*/
async postCreate(created: LabelDocument): Promise<void> {
this.eventEmitter.emit(
'hook:label:create',
created,
async (result: Record<string, any>) => {
await this.model.updateOne(
{ _id: created._id },
{
$set: {
label_id: {
...(created.label_id || {}),
...result,
},
},
},
);
},
);
}
/**
* Before deleting a label, this method fetches the label(s) based on the given criteria and emits a delete event.
*
* @param query - The Mongoose query object used for deletion.
* @param criteria - The filter criteria for finding the labels to be deleted.
*
* @returns {Promise<void>} A promise that resolves once the event is emitted.
*/
async preDelete(
_query: Query<
DeleteResult,
Document<Label, any, any>,
unknown,
Label,
'deleteOne' | 'deleteMany'
>,
_criteria: TFilterQuery<Label>,
): Promise<void> {
const ids = Array.isArray(_criteria._id?.$in)
? _criteria._id.$in
: Array.isArray(_criteria._id)
? _criteria._id
: [_criteria._id];
const labels = await this.find({ _id: { $in: ids } });
this.eventEmitter.emit('hook:label:delete', labels);
}
}

View File

@ -22,6 +22,7 @@ import { SettingService } from '@/setting/services/setting.service';
import { FALLBACK_DEFAULT_NLU_PENALTY_FACTOR } from '@/utils/constants/nlp';
import { BaseService } from '@/utils/generics/base-service';
import { getRandomElement } from '@/utils/helpers/safeRandom';
import { TFilterQuery } from '@/utils/types/filter.types';
import { getDefaultFallbackOptions } from '../constants/block';
import { BlockDto } from '../dto/block.dto';
@ -790,28 +791,31 @@ export class BlockService extends BaseService<
/**
* Updates the `trigger_labels` and `assign_labels` fields of a block when a label is deleted.
*
*
* This method removes the deleted label from the `trigger_labels` and `assign_labels` fields of all blocks that have the label.
*
* @param label The label that is being deleted.
* @param _query - The Mongoose query object used for deletion.
* @param criteria - The filter criteria for finding the labels to be deleted.
*/
@OnEvent('hook:label:delete')
async handleLabelDelete(labels: Label[]) {
const blocks = await this.find({
$or: [
{ trigger_labels: { $in: labels.map((l) => l.id) } },
{ assign_labels: { $in: labels.map((l) => l.id) } },
],
});
for (const block of blocks) {
const trigger_labels = block.trigger_labels.filter(
(labelId) => !labels.find((l) => l.id === labelId),
@OnEvent('hook:label:preDelete')
async handleLabelPreDelete(
_query: unknown,
criteria: TFilterQuery<Label>,
): Promise<void> {
if (criteria._id) {
await this.getRepository().model.updateMany(
{
$or: [
{ trigger_labels: criteria._id },
{ assign_labels: criteria._id },
],
},
{
$pull: {
trigger_labels: criteria._id,
assign_labels: criteria._id,
},
},
);
const assign_labels = block.assign_labels.filter(
(labelId) => !labels.find((l) => l.id === labelId),
);
await this.updateOne(block.id, { trigger_labels, assign_labels });
} else {
throw new Error('Attempted to delete label using unknown criteria');
}
}
}

View File

@ -86,7 +86,7 @@ describe('BotService', () => {
]);
});
afterEach(jest.clearAllMocks);
afterEach(jest.resetAllMocks);
afterAll(closeInMongodConnection);
describe('startConversation', () => {
afterAll(() => {

View File

@ -11,6 +11,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { BotStatsType } from '@/analytics/schemas/bot-stats.schema';
import EventWrapper from '@/channel/lib/EventWrapper';
import { HelperService } from '@/helper/helper.service';
import { FlowEscape, HelperType } from '@/helper/types';
import { LoggerService } from '@/logger/logger.service';
import { SettingService } from '@/setting/services/setting.service';
@ -24,7 +26,7 @@ import {
OutgoingMessageFormat,
StdOutgoingMessageEnvelope,
} from '../schemas/types/message';
import { BlockOptions, FallbackOptions } from '../schemas/types/options';
import { FallbackOptions } from '../schemas/types/options';
import { BlockService } from './block.service';
import { ConversationService } from './conversation.service';
@ -39,6 +41,7 @@ export class BotService {
private readonly conversationService: ConversationService,
private readonly subscriberService: SubscriberService,
private readonly settingService: SettingService,
private readonly helperService: HelperService,
) {}
/**
@ -244,10 +247,12 @@ export class BotService {
/**
* Handles advancing the conversation to the specified *next* block.
*
* 1. Updates popular blocks stats.
* 2. Persists the updated conversation context.
* 3. Triggers the next block.
* 4. Ends the conversation if an unrecoverable error occurs.
* @param convo - The current conversation object containing context and state.
* @param next - The next block to proceed to in the conversation flow.
* @param event - The incoming event that triggered the conversation flow.
* @param fallback - Boolean indicating if this is a fallback response in case no appropriate reply was found.
*
* @returns A promise that resolves to a boolean indicating whether the next block was successfully triggered.
*/
async proceedToNextBlock(
convo: ConversationFull,
@ -258,10 +263,7 @@ export class BotService {
// Increment stats about popular blocks
this.eventEmitter.emit('hook:stats:entry', BotStatsType.popular, next.name);
this.logger.debug(
'Proceeding to next block ',
next.id,
' for conversation ',
convo.id,
`Proceeding to next block ${next.id} for conversation ${convo.id}`,
);
try {
@ -348,45 +350,16 @@ export class BotService {
) {
try {
let fallback = false;
const currentBlock = convo.current;
const fallbackOptions: BlockOptions['fallback'] = convo.current?.options
?.fallback
? convo.current.options.fallback
: {
active: false,
max_attempts: 0,
message: [],
};
// We will avoid having multiple matches when we are not at the start of a conversation
// and only if local fallback is enabled
const canHaveMultipleMatches = !fallbackOptions.active;
// Find the next block that matches
const nextBlocks = await this.blockService.findAndPopulate({
_id: { $in: convo.next.map(({ id }) => id) },
});
const matchedBlock = await this.blockService.match(
nextBlocks,
event,
canHaveMultipleMatches,
);
// If there is no match in next block then loopback (current fallback)
// This applies only to text messages + there's a max attempt to be specified
this.logger.debug('Handling ongoing conversation message ...', convo.id);
const matchedBlock = await this.findNextMatchingBlock(convo, event);
let fallbackBlock: BlockFull | undefined = undefined;
if (!matchedBlock && this.shouldAttemptLocalFallback(convo, event)) {
// Trigger block fallback
// NOTE : current is not populated, this may cause some anomaly
fallbackBlock = {
...currentBlock,
nextBlocks: convo.next,
// If there's labels, they should be already have been assigned
assign_labels: [],
trigger_labels: [],
attachedBlock: null,
category: null,
previousBlocks: [],
};
fallback = true;
const fallbackResult = await this.handleFlowEscapeFallback(
convo,
event,
);
fallbackBlock = fallbackResult.nextBlock;
fallback = fallbackResult.fallback;
}
const next = matchedBlock || fallbackBlock;
@ -410,6 +383,91 @@ export class BotService {
}
}
/**
* Handles the flow escape fallback logic for a conversation.
*
* This method adjudicates the flow escape event and helps determine the next block to execute based on the helper's response.
* It can coerce the event to a specific next block, create a new context, or reprompt the user with a fallback message.
* If the helper cannot handle the flow escape, it returns a fallback block with the current conversation's state.
*
* @param convo - The current conversation object.
* @param event - The incoming event that triggered the fallback.
*
* @returns An object containing the next block to execute (if any) and a flag indicating if a fallback should occur.
*/
async handleFlowEscapeFallback(
convo: ConversationFull,
event: EventWrapper<any, any>,
): Promise<{ nextBlock?: BlockFull; fallback: boolean }> {
const currentBlock = convo.current;
const fallbackOptions: FallbackOptions =
this.blockService.getFallbackOptions(currentBlock);
const fallbackBlock: BlockFull = {
...currentBlock,
nextBlocks: convo.next,
assign_labels: [],
trigger_labels: [],
attachedBlock: null,
category: null,
previousBlocks: [],
};
try {
const helper = await this.helperService.getDefaultHelper(
HelperType.FLOW_ESCAPE,
);
if (!helper.canHandleFlowEscape(currentBlock)) {
return { nextBlock: fallbackBlock, fallback: true };
}
// Adjudicate the flow escape event
this.logger.debug(
`Adjudicating flow escape for block '${currentBlock.id}' in conversation '${convo.id}'.`,
);
const result = await helper.adjudicate(event, currentBlock);
switch (result.action) {
case FlowEscape.Action.COERCE: {
// Coerce the option to the next block
this.logger.debug(`Coercing option to the next block ...`, convo.id);
const proxiedEvent = new Proxy(event, {
get(target, prop, receiver) {
if (prop === 'getText') {
return () => result.coercedOption + '';
}
return Reflect.get(target, prop, receiver);
},
});
const matchedBlock = await this.findNextMatchingBlock(
convo,
proxiedEvent,
);
return { nextBlock: matchedBlock, fallback: false };
}
case FlowEscape.Action.NEW_CTX:
return { nextBlock: undefined, fallback: false };
case FlowEscape.Action.REPROMPT:
default:
if (result.repromptMessage) {
fallbackBlock.options.fallback = {
...fallbackOptions,
message: [result.repromptMessage],
};
}
return { nextBlock: fallbackBlock, fallback: true };
}
} catch (err) {
this.logger.warn(
'Unable to handle flow escape, using default local fallback ...',
err,
);
return { nextBlock: fallbackBlock, fallback: true };
}
}
/**
* Determines if the incoming message belongs to an active conversation and processes it accordingly.
* If an active conversation is found, the message is handled as part of that conversation.

View File

@ -24,6 +24,7 @@ import {
} from '@/attachment/types';
import { config } from '@/config';
import { BaseService } from '@/utils/generics/base-service';
import { TFilterQuery } from '@/utils/types/filter.types';
import {
SocketGet,
SocketPost,
@ -260,22 +261,23 @@ export class SubscriberService extends BaseService<
}
/**
* Updates the `labels` field of a subscriber when a label is deleted.
* Before deleting a `Label`, this method updates the `labels` field of a subscriber.
*
* This method removes the deleted label from the `labels` field of all subscribers that have the label.
*
* @param label The label that is being deleted.
* @param _query - The Mongoose query object used for deletion.
* @param criteria - The filter criteria for finding the labels to be deleted.
*/
@OnEvent('hook:label:delete')
async handleLabelDelete(labels: Label[]) {
const subscribers = await this.find({
labels: { $in: labels.map((l) => l.id) },
});
for (const subscriber of subscribers) {
const updatedLabels = subscriber.labels.filter(
(label) => !labels.find((l) => l.id === label),
@OnEvent('hook:label:preDelete')
async handleLabelDelete(
_query: unknown,
criteria: TFilterQuery<Label>,
): Promise<void> {
if (criteria._id) {
await this.getRepository().model.updateMany(
{ labels: criteria._id },
{ $pull: { labels: criteria._id } },
);
await this.updateOne(subscriber.id, { labels: updatedLabels });
} else {
throw new Error('Attempted to delete label using unknown criteria');
}
}
}

View File

@ -52,14 +52,15 @@ export class ContentTypeRepository extends BaseRepository<
>,
criteria: TFilterQuery<ContentType>,
) {
const entityId: string = criteria._id as string;
const associatedBlocks = await this.blockService?.findOne({
'options.content.entity': entityId,
});
if (associatedBlocks) {
throw new ForbiddenException(`Content type have blocks associated to it`);
}
if (criteria._id) {
const associatedBlock = await this.blockService?.findOne({
'options.content.entity': criteria._id,
});
if (associatedBlock) {
throw new ForbiddenException(
'Content type have blocks associated to it',
);
}
await this.contentModel.deleteMany({ entity: criteria._id });
} else {
throw new Error(

View File

@ -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.
@ -10,6 +10,7 @@ import { HttpModule } from '@nestjs/axios';
import { Global, Module } from '@nestjs/common';
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
import { CmsModule } from '@/cms/cms.module';
import { NlpModule } from '@/nlp/nlp.module';
import { HelperController } from './helper.controller';
@ -25,7 +26,7 @@ import { HelperService } from './helper.service';
'dist/.hexabot/custom/extensions/helpers/**/*.helper.js',
)
@Module({
imports: [HttpModule, NlpModule],
imports: [HttpModule, NlpModule, CmsModule],
controllers: [HelperController],
providers: [HelperService],
exports: [HelperService],

View File

@ -36,7 +36,7 @@ export default abstract class BaseFlowEscapeHelper<
* @param _blockMessage - The block message to check.
* @returns - Whether the helper can handle the flow escape for the given block message.
*/
abstract canHandleFlowEscape<T extends BlockStub>(_blockMessage: T): boolean;
abstract canHandleFlowEscape<T extends BlockStub>(block: T): boolean;
/**
* Adjudicates the flow escape event.
@ -46,7 +46,7 @@ export default abstract class BaseFlowEscapeHelper<
* @returns - A promise that resolves to a FlowEscape.AdjudicationResult.
*/
abstract adjudicate<T extends BlockStub>(
_event: EventWrapper<any, any>,
_block: T,
event: EventWrapper<any, any>,
block: T,
): Promise<FlowEscape.AdjudicationResult>;
}

View File

@ -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 LlmNluHelper from '@/extensions/helpers/llm-nlu/index.helper';
import { HelperService } from '@/helper/helper.service';
import { SettingService } from '@/setting/services/setting.service';
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
import { installNlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
import { getPageQuery } from '@/utils/test/pagination';
@ -16,6 +20,8 @@ import {
import { buildTestingMocks } from '@/utils/test/utils';
import { NlpEntity } from '../schemas/nlp-entity.schema';
import { NlpEntityService } from '../services/nlp-entity.service';
import { NlpService } from '../services/nlp.service';
import { NlpEntityRepository } from './nlp-entity.repository';
import { NlpValueRepository } from './nlp-value.repository';
@ -24,20 +30,42 @@ describe('NlpEntityRepository', () => {
let nlpEntityRepository: NlpEntityRepository;
let nlpValueRepository: NlpValueRepository;
let firstNameNlpEntity: NlpEntity | null;
let nlpService: NlpService;
let llmNluHelper: LlmNluHelper;
let nlpEntityService: NlpEntityService;
beforeAll(async () => {
const { getMocks } = await buildTestingMocks({
const { getMocks, module } = await buildTestingMocks({
autoInjectFrom: ['providers'],
imports: [rootMongooseTestModule(installNlpValueFixtures)],
providers: [NlpEntityRepository],
providers: [
NlpService,
LlmNluHelper,
{
provide: SettingService,
useValue: {
getSettings: jest.fn(() => ({
chatbot_settings: {
default_nlu_helper: 'llm-nlu-helper',
},
})),
},
},
],
});
[nlpEntityRepository, nlpValueRepository] = await getMocks([
NlpEntityRepository,
NlpValueRepository,
]);
[nlpEntityRepository, nlpValueRepository, nlpService, nlpEntityService] =
await getMocks([
NlpEntityRepository,
NlpValueRepository,
NlpService,
NlpEntityService,
]);
firstNameNlpEntity = await nlpEntityRepository.findOne({
name: 'firstname',
});
llmNluHelper = module.get(LlmNluHelper);
module.get(HelperService).register(llmNluHelper);
});
afterAll(closeInMongodConnection);
@ -46,6 +74,12 @@ describe('NlpEntityRepository', () => {
describe('The deleteCascadeOne function', () => {
it('should delete a nlp entity', async () => {
nlpValueRepository.eventEmitter.once(
'hook:nlpEntity:preDelete',
async (...[query, criteria]) => {
await nlpService.handleEntityDelete(query, criteria);
},
);
const intentNlpEntity = await nlpEntityRepository.findOne({
name: 'intent',
});
@ -97,4 +131,83 @@ describe('NlpEntityRepository', () => {
]);
});
});
describe('postCreate', () => {
it('should create and attach a foreign_id to the newly created nlp entity', async () => {
nlpEntityRepository.eventEmitter.once(
'hook:nlpEntity:postCreate',
async (...[created]) => {
const helperSpy = jest.spyOn(llmNluHelper, 'addEntity');
jest.spyOn(nlpEntityService, 'updateOne');
await nlpService.handleEntityPostCreate(created);
expect(helperSpy).toHaveBeenCalledWith(created);
expect(nlpEntityService.updateOne).toHaveBeenCalledWith(
{
_id: created._id,
},
{ foreign_id: await helperSpy.mock.results[0].value },
);
},
);
const result = await nlpEntityRepository.create({
name: 'test1',
});
const intentNlpEntity = await nlpEntityRepository.findOne(result.id);
expect(intentNlpEntity?.foreign_id).toBeDefined();
expect(intentNlpEntity).toEqualPayload(result, [
...IGNORED_TEST_FIELDS,
'foreign_id',
]);
});
it('should not create and attach a foreign_id to the newly created nlp entity with builtin set to true', async () => {
nlpEntityRepository.eventEmitter.once(
'hook:nlpEntity:postCreate',
async (...[created]) => {
await nlpService.handleEntityPostCreate(created);
},
);
const result = await nlpEntityRepository.create({
name: 'test2',
builtin: true,
});
const nlpEntity = await nlpEntityRepository.findOne(result.id);
expect(nlpEntity?.foreign_id).toBeUndefined();
expect(nlpEntity).toEqualPayload(result);
});
});
describe('postUpdate', () => {
it('should update an NlpEntity and trigger a postUpdate event', async () => {
jest.spyOn(nlpService, 'handleEntityPostUpdate');
jest.spyOn(llmNluHelper, 'updateEntity');
nlpEntityRepository.eventEmitter.once(
'hook:nlpEntity:postUpdate',
async (...[query, updated]) => {
await nlpService.handleEntityPostUpdate(query, updated);
expect(llmNluHelper.updateEntity).toHaveBeenCalledWith(updated);
},
);
const updatedNlpEntity = await nlpEntityRepository.updateOne(
{
name: 'test2',
},
{ value: 'test3' },
);
expect(nlpService.handleEntityPostUpdate).toHaveBeenCalledTimes(1);
expect(llmNluHelper.updateEntity).toHaveBeenCalledTimes(1);
const result = await nlpEntityRepository.findOne(updatedNlpEntity.id);
expect(result).toEqualPayload(updatedNlpEntity);
});
});
});

View File

@ -8,23 +8,18 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query } from 'mongoose';
import { Model } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { TFilterQuery } from '@/utils/types/filter.types';
import { BaseRepository } from '@/utils/generics/base-repository';
import { NlpEntityDto } from '../dto/nlp-entity.dto';
import {
NLP_ENTITY_POPULATE,
NlpEntity,
NlpEntityDocument,
NlpEntityFull,
NlpEntityPopulate,
} from '../schemas/nlp-entity.schema';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
import { NlpValueRepository } from './nlp-value.repository';
@Injectable()
export class NlpEntityRepository extends BaseRepository<
NlpEntity,
@ -32,85 +27,7 @@ export class NlpEntityRepository extends BaseRepository<
NlpEntityFull,
NlpEntityDto
> {
constructor(
@InjectModel(NlpEntity.name) readonly model: Model<NlpEntity>,
private readonly nlpValueRepository: NlpValueRepository,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
) {
constructor(@InjectModel(NlpEntity.name) readonly model: Model<NlpEntity>) {
super(model, NlpEntity, NLP_ENTITY_POPULATE, NlpEntityFull);
}
/**
* Post-create hook that triggers after an NLP entity is created.
* Emits an event to notify other parts of the system about the creation.
* Bypasses built-in entities.
*
* @param created - The newly created NLP entity document.
*/
async postCreate(_created: NlpEntityDocument): Promise<void> {
if (!_created.builtin) {
// Bypass builtin entities (probably fixtures)
this.eventEmitter.emit('hook:nlpEntity:create', _created);
}
}
/**
* Post-update hook that triggers after an NLP entity is updated.
* Emits an event to notify other parts of the system about the update.
* Bypasses built-in entities.
*
* @param query - The query used to find and update the entity.
* @param updated - The updated NLP entity document.
*/
async postUpdate(
_query: Query<
Document<NlpEntity, any, any>,
Document<NlpEntity, any, any>,
unknown,
NlpEntity,
'findOneAndUpdate'
>,
updated: NlpEntity,
): Promise<void> {
if (!updated?.builtin) {
// Bypass builtin entities (probably fixtures)
this.eventEmitter.emit('hook:nlpEntity:update', updated);
}
}
/**
* Pre-delete hook that triggers before an NLP entity is deleted.
* Deletes related NLP values and sample entities before the entity deletion.
* Emits an event to notify other parts of the system about the deletion.
* Bypasses built-in entities.
*
* @param query The query used to delete the entity.
* @param criteria The filter criteria used to find the entity for deletion.
*/
async preDelete(
_query: Query<
DeleteResult,
Document<NlpEntity, any, any>,
unknown,
NlpEntity,
'deleteOne' | 'deleteMany'
>,
criteria: TFilterQuery<NlpEntity>,
): Promise<void> {
if (criteria._id) {
await this.nlpValueRepository.deleteMany({ entity: criteria._id });
await this.nlpSampleEntityRepository.deleteMany({ entity: criteria._id });
const entities = await this.find(
typeof criteria === 'string' ? { _id: criteria } : criteria,
);
entities
.filter((e) => !e.builtin)
.map((e) => {
this.eventEmitter.emit('hook:nlpEntity:delete', e);
});
} else {
throw new Error('Attempted to delete NLP entity using unknown criteria');
}
}
}

View File

@ -42,7 +42,7 @@ describe('NlpSampleEntityRepository', () => {
beforeAll(async () => {
const { getMocks } = await buildTestingMocks({
models: ['NlpSampleModel'],
models: ['NlpSampleModel', 'NlpValueModel'],
autoInjectFrom: ['providers'],
imports: [rootMongooseTestModule(installNlpSampleEntityFixtures)],
providers: [

View File

@ -42,7 +42,12 @@ describe('NlpSampleRepository', () => {
const { getMocks } = await buildTestingMocks({
autoInjectFrom: ['providers'],
imports: [rootMongooseTestModule(installNlpSampleEntityFixtures)],
providers: [NlpSampleRepository, NlpValueRepository, LanguageRepository],
providers: [
NlpSampleRepository,
NlpValueRepository,
LanguageRepository,
NlpSampleEntityRepository,
],
});
[
nlpSampleRepository,

View File

@ -24,6 +24,7 @@ import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { TFilterQuery } from '@/utils/types/filter.types';
import { TNlpSampleDto } from '../dto/nlp-sample.dto';
import { NlpSampleEntity } from '../schemas/nlp-sample-entity.schema';
import {
NLP_SAMPLE_POPULATE,
NlpSample,
@ -33,8 +34,6 @@ import {
} from '../schemas/nlp-sample.schema';
import { NlpValue } from '../schemas/nlp-value.schema';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
@Injectable()
export class NlpSampleRepository extends BaseRepository<
NlpSample,
@ -44,7 +43,8 @@ export class NlpSampleRepository extends BaseRepository<
> {
constructor(
@InjectModel(NlpSample.name) readonly model: Model<NlpSample>,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
@InjectModel(NlpSampleEntity.name)
private readonly nlpSampleEntityModel: Model<NlpSampleEntity>,
) {
super(model, NlpSample, NLP_SAMPLE_POPULATE, NlpSampleFull);
}
@ -310,7 +310,7 @@ export class NlpSampleRepository extends BaseRepository<
criteria: TFilterQuery<NlpSample>,
) {
if (criteria._id) {
await this.nlpSampleEntityRepository.deleteMany({
await this.nlpSampleEntityModel.deleteMany({
sample: criteria._id,
});
} else {

View File

@ -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 LlmNluHelper from '@/extensions/helpers/llm-nlu/index.helper';
import { HelperService } from '@/helper/helper.service';
import { SettingService } from '@/setting/services/setting.service';
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
import { nlpEntityFixtures } from '@/utils/test/fixtures/nlpentity';
import { installNlpSampleEntityFixtures } from '@/utils/test/fixtures/nlpsampleentity';
import { nlpValueFixtures } from '@/utils/test/fixtures/nlpvalue';
@ -18,7 +22,10 @@ import { TFixtures } from '@/utils/test/types';
import { buildTestingMocks } from '@/utils/test/utils';
import { NlpValue, NlpValueFull } from '../schemas/nlp-value.schema';
import { NlpValueService } from '../services/nlp-value.service';
import { NlpService } from '../services/nlp.service';
import { NlpEntityRepository } from './nlp-entity.repository';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
import { NlpValueRepository } from './nlp-value.repository';
@ -26,20 +33,47 @@ describe('NlpValueRepository', () => {
let nlpValueRepository: NlpValueRepository;
let nlpSampleEntityRepository: NlpSampleEntityRepository;
let nlpValues: NlpValue[];
let nlpService: NlpService;
let nlpEntityRepository: NlpEntityRepository;
let llmNluHelper: LlmNluHelper;
let nlpValueService: NlpValueService;
beforeAll(async () => {
const { getMocks } = await buildTestingMocks({
models: ['NlpEntityModel'],
const { getMocks, module } = await buildTestingMocks({
autoInjectFrom: ['providers'],
imports: [rootMongooseTestModule(installNlpSampleEntityFixtures)],
providers: [NlpValueRepository],
providers: [
NlpService,
LlmNluHelper,
{
provide: SettingService,
useValue: {
getSettings: jest.fn(() => ({
chatbot_settings: {
default_nlu_helper: 'llm-nlu-helper',
},
})),
},
},
],
});
[nlpValueRepository, nlpSampleEntityRepository] = await getMocks([
[
nlpValueRepository,
nlpSampleEntityRepository,
nlpService,
nlpEntityRepository,
nlpValueService,
] = await getMocks([
NlpValueRepository,
NlpSampleEntityRepository,
NlpService,
NlpEntityRepository,
NlpValueService,
]);
nlpValues = await nlpValueRepository.findAll();
llmNluHelper = module.get(LlmNluHelper);
module.get(HelperService).register(llmNluHelper);
});
afterAll(closeInMongodConnection);
@ -94,7 +128,14 @@ describe('NlpValueRepository', () => {
describe('The deleteCascadeOne function', () => {
it('should delete a nlp Value', async () => {
nlpValueRepository.eventEmitter.once(
'hook:nlpValue:preDelete',
async (...[query, criteria]) => {
await nlpService.handleValueDelete(query, criteria);
},
);
const result = await nlpValueRepository.deleteOne(nlpValues[1].id);
expect(result.deletedCount).toEqual(1);
const sampleEntities = await nlpSampleEntityRepository.find({
value: nlpValues[1].id,
@ -102,4 +143,93 @@ describe('NlpValueRepository', () => {
expect(sampleEntities.length).toEqual(0);
});
});
describe('postCreate', () => {
it('should create and attach a foreign_id to the newly created nlp value', async () => {
nlpValueRepository.eventEmitter.once(
'hook:nlpValue:postCreate',
async (...[created]) => {
const helperSpy = jest.spyOn(llmNluHelper, 'addValue');
jest.spyOn(nlpValueService, 'updateOne');
await nlpService.handleValuePostCreate(created);
expect(helperSpy).toHaveBeenCalledWith(created);
expect(nlpValueService.updateOne).toHaveBeenCalledWith(
{
_id: created._id,
},
{ foreign_id: await helperSpy.mock.results[0].value },
);
},
);
const createdNlpEntity = await nlpEntityRepository.create({
name: 'test1',
});
const result = await nlpValueRepository.create({
entity: createdNlpEntity.id,
value: 'test',
});
const intentNlpEntity = await nlpValueRepository.findOne(result.id);
expect(intentNlpEntity?.foreign_id).toBeDefined();
expect(intentNlpEntity).toEqualPayload(result, [
...IGNORED_TEST_FIELDS,
'foreign_id',
]);
});
it('should not create and attach a foreign_id to the newly created nlp value with builtin set to true', async () => {
nlpValueRepository.eventEmitter.once(
'hook:nlpValue:postCreate',
async (...[created]) => {
await nlpService.handleValuePostCreate(created);
},
);
const createdNlpEntity = await nlpEntityRepository.create({
name: 'nlpEntityTest2',
});
const result = await nlpValueRepository.create({
entity: createdNlpEntity.id,
value: 'nlpValueTest2',
builtin: true,
});
const nlpValue = await nlpValueRepository.findOne(result.id);
expect(nlpValue?.foreign_id).toBeUndefined();
expect(nlpValue).toEqualPayload(result);
});
});
describe('postUpdate', () => {
it('should update an NlpValue and trigger a postUpdate event', async () => {
jest.spyOn(nlpService, 'handleValuePostUpdate');
jest.spyOn(llmNluHelper, 'updateValue');
nlpValueRepository.eventEmitter.once(
'hook:nlpValue:postUpdate',
async (...[query, updated]) => {
await nlpService.handleValuePostUpdate(query, updated);
expect(llmNluHelper.updateValue).toHaveBeenCalledWith(updated);
},
);
const updatedNlpValue = await nlpValueRepository.updateOne(
{
value: 'test',
},
{ value: 'test2' },
);
expect(nlpService.handleValuePostUpdate).toHaveBeenCalledTimes(1);
expect(llmNluHelper.updateValue).toHaveBeenCalledTimes(1);
const result = await nlpValueRepository.findOne(updatedNlpValue.id);
expect(result).toEqualPayload(updatedNlpValue);
});
});
});

View File

@ -9,16 +9,9 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { plainToInstance } from 'class-transformer';
import {
Document,
Model,
PipelineStage,
Query,
SortOrder,
Types,
} from 'mongoose';
import { Model, PipelineStage, SortOrder, Types } from 'mongoose';
import { BaseRepository, DeleteResult } from '@/utils/generics/base-repository';
import { BaseRepository } from '@/utils/generics/base-repository';
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
import { TFilterQuery } from '@/utils/types/filter.types';
import { Format } from '@/utils/types/format.types';
@ -27,7 +20,6 @@ import { NlpValueDto } from '../dto/nlp-value.dto';
import {
NLP_VALUE_POPULATE,
NlpValue,
NlpValueDocument,
NlpValueFull,
NlpValueFullWithCount,
NlpValuePopulate,
@ -35,8 +27,6 @@ import {
TNlpValueCount,
} from '../schemas/nlp-value.schema';
import { NlpSampleEntityRepository } from './nlp-sample-entity.repository';
@Injectable()
export class NlpValueRepository extends BaseRepository<
NlpValue,
@ -44,82 +34,10 @@ export class NlpValueRepository extends BaseRepository<
NlpValueFull,
NlpValueDto
> {
constructor(
@InjectModel(NlpValue.name) readonly model: Model<NlpValue>,
private readonly nlpSampleEntityRepository: NlpSampleEntityRepository,
) {
constructor(@InjectModel(NlpValue.name) readonly model: Model<NlpValue>) {
super(model, NlpValue, NLP_VALUE_POPULATE, NlpValueFull);
}
/**
* Emits an event after a new NLP value is created, bypassing built-in values.
*
* @param created - The newly created NLP value document.
*/
async postCreate(created: NlpValueDocument): Promise<void> {
if (!created.builtin) {
// Bypass builtin entities (probably fixtures)
this.eventEmitter.emit('hook:nlpValue:create', created);
}
}
/**
* Emits an event after an NLP value is updated, bypassing built-in values.
*
* @param query - The query that was used to update the NLP value.
* @param updated - The updated NLP value document.
*/
async postUpdate(
_query: Query<
Document<NlpValue, any, any>,
Document<NlpValue, any, any>,
unknown,
NlpValue,
'findOneAndUpdate'
>,
updated: NlpValue,
): Promise<void> {
if (!updated?.builtin) {
// Bypass builtin entities (probably fixtures)
this.eventEmitter.emit('hook:nlpValue:update', updated);
}
}
/**
* Handles deletion of NLP values and associated entities. If the criteria includes an ID,
* emits an event for each deleted entity.
*
* @param _query - The query used to delete the NLP value(s).
* @param criteria - The filter criteria used to identify the NLP value(s) to delete.
*/
async preDelete(
_query: Query<
DeleteResult,
Document<NlpValue, any, any>,
unknown,
NlpValue,
'deleteOne' | 'deleteMany'
>,
criteria: TFilterQuery<NlpValue>,
): Promise<void> {
if (criteria._id) {
await this.nlpSampleEntityRepository.deleteMany({ value: criteria._id });
const entities = await this.find(
typeof criteria === 'string' ? { _id: criteria } : criteria,
);
entities
.filter((e) => !e.builtin)
.map((e) => {
this.eventEmitter.emit('hook:nlpValue:delete', e);
});
} else if (criteria.entity) {
// Do nothing : cascading deletes coming from Nlp Sample Entity
} else {
throw new Error('Attempted to delete a NLP value using unknown criteria');
}
}
private getSortDirection(sortOrder: SortOrder) {
return typeof sortOrder === 'number'
? sortOrder

View File

@ -8,15 +8,18 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Document, Query } from 'mongoose';
import { HelperService } from '@/helper/helper.service';
import { HelperType, NLU } from '@/helper/types';
import { LoggerService } from '@/logger/logger.service';
import { TFilterQuery } from '@/utils/types/filter.types';
import { NlpEntity, NlpEntityDocument } from '../schemas/nlp-entity.schema';
import { NlpValue, NlpValueDocument } from '../schemas/nlp-value.schema';
import { NlpEntityService } from './nlp-entity.service';
import { NlpSampleEntityService } from './nlp-sample-entity.service';
import { NlpSampleService } from './nlp-sample.service';
import { NlpValueService } from './nlp-value.service';
@ -28,6 +31,7 @@ export class NlpService {
protected readonly nlpEntityService: NlpEntityService,
protected readonly nlpValueService: NlpValueService,
protected readonly helperService: HelperService,
protected readonly nlpSampleEntityService: NlpSampleEntityService,
) {}
/**
@ -66,21 +70,25 @@ export class NlpService {
*
* @param entity - The NLP entity to be created.
*/
@OnEvent('hook:nlpEntity:create')
async handleEntityCreate(entity: NlpEntityDocument) {
// Synchonize new entity with NLP
try {
const helper = await this.helperService.getDefaultHelper(HelperType.NLU);
const foreignId = await helper.addEntity(entity);
this.logger.debug('New entity successfully synced!', foreignId);
await this.nlpEntityService.updateOne(
{ _id: entity._id },
{
foreign_id: foreignId,
},
);
} catch (err) {
this.logger.error('Unable to sync a new entity', err);
@OnEvent('hook:nlpEntity:postCreate')
async handleEntityPostCreate(created: NlpEntityDocument) {
if (!created.builtin) {
// Synchonize new entity with NLP
try {
const helper = await this.helperService.getDefaultHelper(
HelperType.NLU,
);
const foreignId = await helper.addEntity(created);
this.logger.debug('New entity successfully synced!', foreignId);
await this.nlpEntityService.updateOne(
{ _id: created._id },
{
foreign_id: foreignId,
},
);
} catch (err) {
this.logger.error('Unable to sync a new entity', err);
}
}
}
@ -89,37 +97,68 @@ export class NlpService {
*
* @param entity - The NLP entity to be updated.
*/
@OnEvent('hook:nlpEntity:update')
async handleEntityUpdate(entity: NlpEntity) {
// Synchonize new entity with NLP provider
try {
const helper = await this.helperService.getDefaultNluHelper();
await helper.updateEntity(entity);
this.logger.debug('Updated entity successfully synced!', entity);
} catch (err) {
this.logger.error('Unable to sync updated entity', err);
@OnEvent('hook:nlpEntity:postUpdate')
async handleEntityPostUpdate(
_query: Query<
Document<NlpEntity>,
Document<NlpEntity>,
unknown,
NlpEntity,
'findOneAndUpdate'
>,
updated: NlpEntity,
) {
if (!updated?.builtin) {
// Synchonize new entity with NLP provider
try {
const helper = await this.helperService.getDefaultHelper(
HelperType.NLU,
);
await helper.updateEntity(updated);
this.logger.debug('Updated entity successfully synced!', updated);
} catch (err) {
this.logger.error('Unable to sync updated entity', err);
}
}
}
/**
* Handles the event triggered when an NLP entity is deleted. Synchronizes the deletion with the external NLP provider.
* Before deleting a `nlpEntity`, this method deletes the related `nlpValue` and `nlpSampleEntity`. Synchronizes the deletion with the external NLP provider
*
* @param entity - The NLP entity to be deleted.
* @param _query - The Mongoose query object used for deletion.
* @param criteria - The filter criteria for finding the nlpEntities to be deleted.
*/
@OnEvent('hook:nlpEntity:delete')
async handleEntityDelete(entity: NlpEntity) {
// Synchonize new entity with NLP provider
try {
if (entity.foreign_id) {
const helper = await this.helperService.getDefaultNluHelper();
await helper.deleteEntity(entity.foreign_id);
this.logger.debug('Deleted entity successfully synced!', entity);
} else {
this.logger.error(`Entity ${entity} is missing foreign_id`);
throw new NotFoundException(`Entity ${entity} is missing foreign_id`);
@OnEvent('hook:nlpEntity:preDelete')
async handleEntityDelete(
_query: unknown,
criteria: TFilterQuery<NlpEntity>,
): Promise<void> {
if (criteria._id) {
await this.nlpValueService.deleteMany({ entity: criteria._id });
await this.nlpSampleEntityService.deleteMany({ entity: criteria._id });
const entities = await this.nlpEntityService.find({
...(typeof criteria === 'string' ? { _id: criteria } : criteria),
builtin: false,
});
const helper = await this.helperService.getDefaultHelper(HelperType.NLU);
for (const entity of entities) {
// Synchonize new entity with NLP provider
try {
if (entity.foreign_id) {
await helper.deleteEntity(entity.foreign_id);
this.logger.debug('Deleted entity successfully synced!', entity);
} else {
this.logger.error(`Entity ${entity} is missing foreign_id`);
throw new NotFoundException(
`Entity ${entity} is missing foreign_id`,
);
}
} catch (err) {
this.logger.error('Unable to sync deleted entity', err);
}
}
} catch (err) {
this.logger.error('Unable to sync deleted entity', err);
}
}
@ -128,21 +167,25 @@ export class NlpService {
*
* @param value - The NLP value to be created.
*/
@OnEvent('hook:nlpValue:create')
async handleValueCreate(value: NlpValueDocument) {
// Synchonize new value with NLP provider
try {
const helper = await this.helperService.getDefaultNluHelper();
const foreignId = await helper.addValue(value);
this.logger.debug('New value successfully synced!', foreignId);
await this.nlpValueService.updateOne(
{ _id: value._id },
{
foreign_id: foreignId,
},
);
} catch (err) {
this.logger.error('Unable to sync a new value', err);
@OnEvent('hook:nlpValue:postCreate')
async handleValuePostCreate(created: NlpValueDocument) {
if (!created.builtin) {
// Synchonize new value with NLP provider
try {
const helper = await this.helperService.getDefaultHelper(
HelperType.NLU,
);
const foreignId = await helper.addValue(created);
this.logger.debug('New value successfully synced!', foreignId);
await this.nlpValueService.updateOne(
{ _id: created._id },
{
foreign_id: foreignId,
},
);
} catch (err) {
this.logger.error('Unable to sync a new value', err);
}
}
}
@ -151,37 +194,71 @@ export class NlpService {
*
* @param value - The NLP value to be updated.
*/
@OnEvent('hook:nlpValue:update')
async handleValueUpdate(value: NlpValue) {
// Synchonize new value with NLP provider
try {
const helper = await this.helperService.getDefaultNluHelper();
await helper.updateValue(value);
this.logger.debug('Updated value successfully synced!', value);
} catch (err) {
this.logger.error('Unable to sync updated value', err);
@OnEvent('hook:nlpValue:postUpdate')
async handleValuePostUpdate(
_query: Query<
Document<NlpValue, any, any>,
Document<NlpValue, any, any>,
unknown,
NlpValue,
'findOneAndUpdate'
>,
updated: NlpValue,
) {
if (!updated?.builtin) {
// Synchonize new value with NLP provider
try {
const helper = await this.helperService.getDefaultHelper(
HelperType.NLU,
);
await helper.updateValue(updated);
this.logger.debug('Updated value successfully synced!', updated);
} catch (err) {
this.logger.error('Unable to sync updated value', err);
}
}
}
/**
* Handles the event triggered when an NLP value is deleted. Synchronizes the deletion with the external NLP provider.
* Before deleting a `nlpValue`, this method deletes the related `nlpSampleEntity`. Synchronizes the deletion with the external NLP provider
*
* @param value - The NLP value to be deleted.
* @param _query - The Mongoose query object used for deletion.
* @param criteria - The filter criteria for finding the nlpValues to be deleted.
*/
@OnEvent('hook:nlpValue:delete')
async handleValueDelete(value: NlpValue) {
// Synchonize new value with NLP provider
try {
const helper = await this.helperService.getDefaultNluHelper();
const populatedValue = await this.nlpValueService.findOneAndPopulate(
value.id,
);
if (populatedValue) {
await helper.deleteValue(populatedValue);
this.logger.debug('Deleted value successfully synced!', value);
@OnEvent('hook:nlpValue:preDelete')
async handleValueDelete(
_query: unknown,
criteria: TFilterQuery<NlpValue>,
): Promise<void> {
if (criteria._id) {
await this.nlpSampleEntityService.deleteMany({
value: criteria._id,
});
const values = await this.nlpValueService.find({
...(typeof criteria === 'string' ? { _id: criteria } : criteria),
builtin: false,
});
const helper = await this.helperService.getDefaultHelper(HelperType.NLU);
for (const value of values) {
// Synchonize new value with NLP provider
try {
const populatedValue = await this.nlpValueService.findOneAndPopulate(
value.id,
);
if (populatedValue) {
await helper.deleteValue(populatedValue);
this.logger.debug('Deleted value successfully synced!', value);
}
} catch (err) {
this.logger.error('Unable to sync deleted value', err);
}
}
} catch (err) {
this.logger.error('Unable to sync deleted value', err);
} else if (criteria.entity) {
// Do nothing : cascading deletes coming from Nlp Sample Entity
} else {
throw new Error('Attempted to delete a NLP value using unknown criteria');
}
}
}

View File

@ -81,7 +81,7 @@ const position = {
y: 0,
};
export const baseBlockInstance = {
export const baseBlockInstance: Partial<BlockFull> = {
trigger_labels: [labelMock],
assign_labels: [labelMock],
options: blockOptions,
@ -90,7 +90,7 @@ export const baseBlockInstance = {
position,
builtin: true,
attachedBlock: null,
category: undefined,
category: null,
previousBlocks: [],
trigger_channels: [],
nextBlocks: [],

View File

@ -19,7 +19,7 @@ import type { Block, BlockFull } from '@/chat/schemas/block.schema';
import { type Category } from '@/chat/schemas/category.schema';
import { type ContextVar } from '@/chat/schemas/context-var.schema';
import { type Conversation } from '@/chat/schemas/conversation.schema';
import type { Label, LabelDocument } from '@/chat/schemas/label.schema';
import type { Label } from '@/chat/schemas/label.schema';
import { type Message } from '@/chat/schemas/message.schema';
import { type Subscriber } from '@/chat/schemas/subscriber.schema';
import { type ContentType } from '@/cms/schemas/content-type.schema';
@ -27,16 +27,10 @@ import { type Content } from '@/cms/schemas/content.schema';
import { type Menu } from '@/cms/schemas/menu.schema';
import { type Language } from '@/i18n/schemas/language.schema';
import { type Translation } from '@/i18n/schemas/translation.schema';
import type {
NlpEntity,
NlpEntityDocument,
} from '@/nlp/schemas/nlp-entity.schema';
import type { NlpEntity } from '@/nlp/schemas/nlp-entity.schema';
import { type NlpSampleEntity } from '@/nlp/schemas/nlp-sample-entity.schema';
import { type NlpSample } from '@/nlp/schemas/nlp-sample.schema';
import type {
NlpValue,
NlpValueDocument,
} from '@/nlp/schemas/nlp-value.schema';
import type { NlpValue } from '@/nlp/schemas/nlp-value.schema';
import { type Setting } from '@/setting/schemas/setting.schema';
import { type Invitation } from '@/user/schemas/invitation.schema';
import { type Model } from '@/user/schemas/model.schema';
@ -130,35 +124,18 @@ declare module '@nestjs/event-emitter' {
category: TDefinition<Category>;
contextVar: TDefinition<ContextVar>;
conversation: TDefinition<Conversation, { end: unknown; close: unknown }>;
label: TDefinition<
Label,
{ create: LabelDocument; delete: Label | Label[] }
>;
label: TDefinition<Label>;
message: TDefinition<Message>;
subscriber: TDefinition<Subscriber, { assign: SubscriberUpdateDto }>;
contentType: TDefinition<ContentType>;
content: TDefinition<Content>;
menu: TDefinition<Menu>;
language: TDefinition<Language, { delete: Language | Language[] }>;
language: TDefinition<Language>;
translation: TDefinition<Translation>;
nlpEntity: TDefinition<
NlpEntity,
{
create: NlpEntityDocument;
update: NlpEntity;
delete: NlpEntity | NlpEntity[];
}
>;
nlpEntity: TDefinition<NlpEntity>;
nlpSampleEntity: TDefinition<NlpSampleEntity>;
nlpSample: TDefinition<NlpSample>;
nlpValue: TDefinition<
NlpValue,
{
create: NlpValueDocument;
update: NlpValue;
delete: NlpValue | NlpValue[];
}
>;
nlpValue: TDefinition<NlpValue>;
setting: TDefinition<Setting>;
invitation: TDefinition<Invitation>;
model: TDefinition<Model>;
@ -212,7 +189,8 @@ declare module '@nestjs/event-emitter' {
type TPostUpdateValidate<T> = FilterQuery<T>;
type TPostUpdate<T> = THydratedDocument<T>;
// TODO this type will be optimized soon in a separated PR
type TPostUpdate<T> = T & any;
type TPostDelete = DeleteResult;