fix: apply strict null checks updates to the Chat Module

This commit is contained in:
yassinedorbozgithub 2025-01-09 17:58:26 +01:00
parent 4f88370c69
commit 90a7c2f5c2
31 changed files with 302 additions and 241 deletions

View File

@ -163,8 +163,8 @@ export class ChannelService {
{ {
id: req.session.passport.user.id, id: req.session.passport.user.id,
foreign_id: req.session.passport.user.id, foreign_id: req.session.passport.user.id,
first_name: req.session.passport.user.first_name, first_name: req.session.passport.user.first_name || 'Anonymous',
last_name: req.session.passport.user.last_name, last_name: req.session.passport.user.last_name || 'Anonymous',
locale: '', locale: '',
language: '', language: '',
gender: '', gender: '',

View File

@ -61,11 +61,11 @@ describe('BlockController', () => {
let blockController: BlockController; let blockController: BlockController;
let blockService: BlockService; let blockService: BlockService;
let categoryService: CategoryService; let categoryService: CategoryService;
let category: Category; let category: Category | null;
let block: Block; let block: Block | null;
let blockToDelete: Block; let blockToDelete: Block | null;
let hasNextBlocks: Block; let hasNextBlocks: Block | null;
let hasPreviousBlocks: Block; let hasPreviousBlocks: Block | null;
const FIELDS_TO_POPULATE = [ const FIELDS_TO_POPULATE = [
'trigger_labels', 'trigger_labels',
'assign_labels', 'assign_labels',
@ -162,9 +162,9 @@ describe('BlockController', () => {
const result = await blockController.find([], {}); const result = await blockController.find([], {});
const blocksWithCategory = blockFixtures.map((blockFixture) => ({ const blocksWithCategory = blockFixtures.map((blockFixture) => ({
...blockFixture, ...blockFixture,
category: category.id, category: category!.id,
nextBlocks: nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks.id] : [], blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks!.id] : [],
})); }));
expect(blockService.find).toHaveBeenCalledWith({}, undefined); expect(blockService.find).toHaveBeenCalledWith({}, undefined);
@ -185,6 +185,7 @@ describe('BlockController', () => {
blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [], blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [],
nextBlocks: nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [], blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
attachedToBlock: null,
})); }));
expect(blockService.findAndPopulate).toHaveBeenCalledWith({}, undefined); expect(blockService.findAndPopulate).toHaveBeenCalledWith({}, undefined);
@ -195,13 +196,13 @@ describe('BlockController', () => {
describe('findOne', () => { describe('findOne', () => {
it('should find one block by id', async () => { it('should find one block by id', async () => {
jest.spyOn(blockService, 'findOne'); jest.spyOn(blockService, 'findOne');
const result = await blockController.findOne(hasNextBlocks.id, []); const result = await blockController.findOne(hasNextBlocks!.id, []);
expect(blockService.findOne).toHaveBeenCalledWith(hasNextBlocks.id); expect(blockService.findOne).toHaveBeenCalledWith(hasNextBlocks!.id);
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...blockFixtures.find(({ name }) => name === hasNextBlocks.name), ...blockFixtures.find(({ name }) => name === hasNextBlocks!.name),
category: category.id, category: category!.id,
nextBlocks: [hasPreviousBlocks.id], nextBlocks: [hasPreviousBlocks!.id],
}, },
[...IGNORED_TEST_FIELDS, 'attachedToBlock'], [...IGNORED_TEST_FIELDS, 'attachedToBlock'],
); );
@ -210,16 +211,17 @@ describe('BlockController', () => {
it('should find one block by id, and populate its category and previousBlocks', async () => { it('should find one block by id, and populate its category and previousBlocks', async () => {
jest.spyOn(blockService, 'findOneAndPopulate'); jest.spyOn(blockService, 'findOneAndPopulate');
const result = await blockController.findOne( const result = await blockController.findOne(
hasPreviousBlocks.id, hasPreviousBlocks!.id,
FIELDS_TO_POPULATE, FIELDS_TO_POPULATE,
); );
expect(blockService.findOneAndPopulate).toHaveBeenCalledWith( expect(blockService.findOneAndPopulate).toHaveBeenCalledWith(
hasPreviousBlocks.id, hasPreviousBlocks!.id,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...blockFixtures.find(({ name }) => name === 'hasPreviousBlocks'), ...blockFixtures.find(({ name }) => name === 'hasPreviousBlocks'),
category, category,
previousBlocks: [hasNextBlocks], previousBlocks: [hasNextBlocks],
attachedToBlock: null,
}); });
}); });
@ -227,14 +229,15 @@ describe('BlockController', () => {
jest.spyOn(blockService, 'findOneAndPopulate'); jest.spyOn(blockService, 'findOneAndPopulate');
block = await blockService.findOne({ name: 'attachment' }); block = await blockService.findOne({ name: 'attachment' });
const result = await blockController.findOne( const result = await blockController.findOne(
block.id, block!.id,
FIELDS_TO_POPULATE, FIELDS_TO_POPULATE,
); );
expect(blockService.findOneAndPopulate).toHaveBeenCalledWith(block.id); expect(blockService.findOneAndPopulate).toHaveBeenCalledWith(block!.id);
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...blockFixtures.find(({ name }) => name === 'attachment'), ...blockFixtures.find(({ name }) => name === 'attachment'),
category, category,
previousBlocks: [], previousBlocks: [],
attachedToBlock: null,
}); });
}); });
}); });
@ -244,12 +247,12 @@ describe('BlockController', () => {
jest.spyOn(blockService, 'create'); jest.spyOn(blockService, 'create');
const mockedBlockCreateDto: BlockCreateDto = { const mockedBlockCreateDto: BlockCreateDto = {
name: 'block with nextBlocks', name: 'block with nextBlocks',
nextBlocks: [hasNextBlocks.id], nextBlocks: [hasNextBlocks!.id],
patterns: ['Hi'], patterns: ['Hi'],
trigger_labels: [], trigger_labels: [],
assign_labels: [], assign_labels: [],
trigger_channels: [], trigger_channels: [],
category: category.id, category: category!.id,
options: { options: {
typing: 0, typing: 0,
fallback: { fallback: {
@ -281,15 +284,17 @@ describe('BlockController', () => {
describe('deleteOne', () => { describe('deleteOne', () => {
it('should delete block', async () => { it('should delete block', async () => {
jest.spyOn(blockService, 'deleteOne'); jest.spyOn(blockService, 'deleteOne');
const result = await blockController.deleteOne(blockToDelete.id); const result = await blockController.deleteOne(blockToDelete!.id);
expect(blockService.deleteOne).toHaveBeenCalledWith(blockToDelete.id); expect(blockService.deleteOne).toHaveBeenCalledWith(blockToDelete!.id);
expect(result).toEqual({ acknowledged: true, deletedCount: 1 }); expect(result).toEqual({ acknowledged: true, deletedCount: 1 });
}); });
it('should throw NotFoundException when attempting to delete a block by id', async () => { it('should throw NotFoundException when attempting to delete a block by id', async () => {
await expect(blockController.deleteOne(blockToDelete.id)).rejects.toThrow( await expect(
new NotFoundException(`Block with ID ${blockToDelete.id} not found`), blockController.deleteOne(blockToDelete!.id),
).rejects.toThrow(
new NotFoundException(`Block with ID ${blockToDelete!.id} not found`),
); );
}); });
}); });
@ -300,16 +305,16 @@ describe('BlockController', () => {
const updateBlock: BlockUpdateDto = { const updateBlock: BlockUpdateDto = {
name: 'modified block name', name: 'modified block name',
}; };
const result = await blockController.updateOne(block.id, updateBlock); const result = await blockController.updateOne(block!.id, updateBlock);
expect(blockService.updateOne).toHaveBeenCalledWith( expect(blockService.updateOne).toHaveBeenCalledWith(
block.id, block!.id,
updateBlock, updateBlock,
); );
expect(result).toEqualPayload( expect(result).toEqualPayload(
{ {
...blockFixtures.find(({ name }) => name === block.name), ...blockFixtures.find(({ name }) => name === block!.name),
category: category.id, category: category!.id,
...updateBlock, ...updateBlock,
}, },
[...IGNORED_TEST_FIELDS, 'attachedToBlock'], [...IGNORED_TEST_FIELDS, 'attachedToBlock'],
@ -322,9 +327,9 @@ describe('BlockController', () => {
}; };
await expect( await expect(
blockController.updateOne(blockToDelete.id, updateBlock), blockController.updateOne(blockToDelete!.id, updateBlock),
).rejects.toThrow( ).rejects.toThrow(
new NotFoundException(`Block with ID ${blockToDelete.id} not found`), new NotFoundException(`Block with ID ${blockToDelete!.id} not found`),
); );
}); });
}); });

View File

@ -219,9 +219,12 @@ export class BlockController extends BaseController<
this.validate({ this.validate({
dto: block, dto: block,
allowedIds: { allowedIds: {
category: (await this.categoryService.findOne(block.category))?.id, category: block.category
attachedBlock: (await this.blockService.findOne(block.attachedBlock)) ? (await this.categoryService.findOne(block.category))?.id
?.id, : null,
attachedBlock: block.attachedBlock
? (await this.blockService.findOne(block.attachedBlock))?.id
: null,
nextBlocks: ( nextBlocks: (
await this.blockService.find({ await this.blockService.find({
_id: { _id: {

View File

@ -36,8 +36,8 @@ import { ContextVarController } from './context-var.controller';
describe('ContextVarController', () => { describe('ContextVarController', () => {
let contextVarController: ContextVarController; let contextVarController: ContextVarController;
let contextVarService: ContextVarService; let contextVarService: ContextVarService;
let contextVar: ContextVar; let contextVar: ContextVar | null;
let contextVarToDelete: ContextVar; let contextVarToDelete: ContextVar | null;
beforeAll(async () => { beforeAll(async () => {
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
@ -91,11 +91,11 @@ describe('ContextVarController', () => {
describe('findOne', () => { describe('findOne', () => {
it('should return the existing contextVar', async () => { it('should return the existing contextVar', async () => {
jest.spyOn(contextVarService, 'findOne'); jest.spyOn(contextVarService, 'findOne');
const result = await contextVarController.findOne(contextVar.id); const result = await contextVarController.findOne(contextVar!.id);
expect(contextVarService.findOne).toHaveBeenCalledWith(contextVar.id); expect(contextVarService.findOne).toHaveBeenCalledWith(contextVar!.id);
expect(result).toEqualPayload( expect(result).toEqualPayload(
contextVarFixtures.find(({ label }) => label === contextVar.label), contextVarFixtures.find(({ label }) => label === contextVar!.label)!,
); );
}); });
}); });
@ -121,11 +121,11 @@ describe('ContextVarController', () => {
it('should delete a contextVar by id', async () => { it('should delete a contextVar by id', async () => {
jest.spyOn(contextVarService, 'deleteOne'); jest.spyOn(contextVarService, 'deleteOne');
const result = await contextVarController.deleteOne( const result = await contextVarController.deleteOne(
contextVarToDelete.id, contextVarToDelete!.id,
); );
expect(contextVarService.deleteOne).toHaveBeenCalledWith( expect(contextVarService.deleteOne).toHaveBeenCalledWith(
contextVarToDelete.id, contextVarToDelete!.id,
); );
expect(result).toEqual({ expect(result).toEqual({
acknowledged: true, acknowledged: true,
@ -135,10 +135,10 @@ describe('ContextVarController', () => {
it('should throw a NotFoundException when attempting to delete a contextVar by id', async () => { it('should throw a NotFoundException when attempting to delete a contextVar by id', async () => {
await expect( await expect(
contextVarController.deleteOne(contextVarToDelete.id), contextVarController.deleteOne(contextVarToDelete!.id),
).rejects.toThrow( ).rejects.toThrow(
new NotFoundException( new NotFoundException(
`Context var with ID ${contextVarToDelete.id} not found.`, `Context var with ID ${contextVarToDelete!.id} not found.`,
), ),
); );
}); });
@ -152,12 +152,12 @@ describe('ContextVarController', () => {
.spyOn(contextVarService, 'deleteMany') .spyOn(contextVarService, 'deleteMany')
.mockResolvedValue(deleteResult); .mockResolvedValue(deleteResult);
const result = await contextVarController.deleteMany([ const result = await contextVarController.deleteMany([
contextVarToDelete.id, contextVarToDelete!.id,
contextVar.id, contextVar!.id,
]); ]);
expect(contextVarService.deleteMany).toHaveBeenCalledWith({ expect(contextVarService.deleteMany).toHaveBeenCalledWith({
_id: { $in: [contextVarToDelete.id, contextVar.id] }, _id: { $in: [contextVarToDelete!.id, contextVar!.id] },
}); });
expect(result).toEqual(deleteResult); expect(result).toEqual(deleteResult);
}); });
@ -175,7 +175,10 @@ describe('ContextVarController', () => {
}); });
await expect( await expect(
contextVarController.deleteMany([contextVarToDelete.id, contextVar.id]), contextVarController.deleteMany([
contextVarToDelete!.id,
contextVar!.id,
]),
).rejects.toThrow( ).rejects.toThrow(
new NotFoundException('Context vars with provided IDs not found'), new NotFoundException('Context vars with provided IDs not found'),
); );
@ -189,16 +192,16 @@ describe('ContextVarController', () => {
it('should return updated contextVar', async () => { it('should return updated contextVar', async () => {
jest.spyOn(contextVarService, 'updateOne'); jest.spyOn(contextVarService, 'updateOne');
const result = await contextVarController.updateOne( const result = await contextVarController.updateOne(
contextVar.id, contextVar!.id,
contextVarUpdatedDto, contextVarUpdatedDto,
); );
expect(contextVarService.updateOne).toHaveBeenCalledWith( expect(contextVarService.updateOne).toHaveBeenCalledWith(
contextVar.id, contextVar!.id,
contextVarUpdatedDto, contextVarUpdatedDto,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...contextVarFixtures.find(({ label }) => label === contextVar.label), ...contextVarFixtures.find(({ label }) => label === contextVar!.label),
...contextVarUpdatedDto, ...contextVarUpdatedDto,
}); });
}); });
@ -206,12 +209,12 @@ describe('ContextVarController', () => {
it('should throw a NotFoundException when attempting to update an non existing contextVar by id', async () => { it('should throw a NotFoundException when attempting to update an non existing contextVar by id', async () => {
await expect( await expect(
contextVarController.updateOne( contextVarController.updateOne(
contextVarToDelete.id, contextVarToDelete!.id,
contextVarUpdatedDto, contextVarUpdatedDto,
), ),
).rejects.toThrow( ).rejects.toThrow(
new NotFoundException( new NotFoundException(
`ContextVar with ID ${contextVarToDelete.id} not found`, `ContextVar with ID ${contextVarToDelete!.id} not found`,
), ),
); );
}); });

View File

@ -53,10 +53,10 @@ describe('MessageController', () => {
let messageService: MessageService; let messageService: MessageService;
let subscriberService: SubscriberService; let subscriberService: SubscriberService;
let userService: UserService; let userService: UserService;
let sender: Subscriber; let sender: Subscriber | null;
let recipient: Subscriber; let recipient: Subscriber | null;
let user: User; let user: User | null;
let message: Message; let message: Message | null;
let allMessages: Message[]; let allMessages: Message[];
let allUsers: User[]; let allUsers: User[];
let allSubscribers: Subscriber[]; let allSubscribers: Subscriber[];
@ -129,9 +129,9 @@ describe('MessageController', () => {
subscriberService = module.get<SubscriberService>(SubscriberService); subscriberService = module.get<SubscriberService>(SubscriberService);
messageController = module.get<MessageController>(MessageController); messageController = module.get<MessageController>(MessageController);
message = await messageService.findOne({ mid: 'mid-1' }); message = await messageService.findOne({ mid: 'mid-1' });
sender = await subscriberService.findOne(message.sender); sender = await subscriberService.findOne(message!.sender!);
recipient = await subscriberService.findOne(message.recipient); recipient = await subscriberService.findOne(message!.recipient!);
user = await userService.findOne(message.sentBy); user = await userService.findOne(message!.sentBy!);
allSubscribers = await subscriberService.findAll(); allSubscribers = await subscriberService.findAll();
allUsers = await userService.findAll(); allUsers = await userService.findAll();
allMessages = await messageService.findAll(); allMessages = await messageService.findAll();
@ -153,31 +153,31 @@ describe('MessageController', () => {
describe('findOne', () => { describe('findOne', () => {
it('should find message by id, and populate its corresponding sender and recipient', async () => { it('should find message by id, and populate its corresponding sender and recipient', async () => {
jest.spyOn(messageService, 'findOneAndPopulate'); jest.spyOn(messageService, 'findOneAndPopulate');
const result = await messageController.findOne(message.id, [ const result = await messageController.findOne(message!.id, [
'sender', 'sender',
'recipient', 'recipient',
]); ]);
expect(messageService.findOneAndPopulate).toHaveBeenCalledWith( expect(messageService.findOneAndPopulate).toHaveBeenCalledWith(
message.id, message!.id,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...messageFixtures.find(({ mid }) => mid === message.mid), ...messageFixtures.find(({ mid }) => mid === message!.mid),
sender, sender,
recipient, recipient,
sentBy: user.id, sentBy: user!.id,
}); });
}); });
it('should find message by id', async () => { it('should find message by id', async () => {
jest.spyOn(messageService, 'findOne'); jest.spyOn(messageService, 'findOne');
const result = await messageController.findOne(message.id, []); const result = await messageController.findOne(message!.id, []);
expect(messageService.findOne).toHaveBeenCalledWith(message.id); expect(messageService.findOne).toHaveBeenCalledWith(message!.id);
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...messageFixtures.find(({ mid }) => mid === message.mid), ...messageFixtures.find(({ mid }) => mid === message!.mid),
sender: sender.id, sender: sender!.id,
recipient: recipient.id, recipient: recipient!.id,
sentBy: user.id, sentBy: user!.id,
}); });
}); });
}); });
@ -189,10 +189,10 @@ describe('MessageController', () => {
const result = await messageController.findPage(pageQuery, [], {}); const result = await messageController.findPage(pageQuery, [], {});
const messagesWithSenderAndRecipient = allMessages.map((message) => ({ const messagesWithSenderAndRecipient = allMessages.map((message) => ({
...message, ...message,
sender: allSubscribers.find(({ id }) => id === message['sender']).id, sender: allSubscribers.find(({ id }) => id === message.sender)?.id,
recipient: allSubscribers.find(({ id }) => id === message['recipient']) recipient: allSubscribers.find(({ id }) => id === message.recipient)
.id, ?.id,
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id, sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
})); }));
expect(messageService.find).toHaveBeenCalledWith({}, pageQuery); expect(messageService.find).toHaveBeenCalledWith({}, pageQuery);
@ -208,9 +208,9 @@ describe('MessageController', () => {
); );
const messages = allMessages.map((message) => ({ const messages = allMessages.map((message) => ({
...message, ...message,
sender: allSubscribers.find(({ id }) => id === message['sender']), sender: allSubscribers.find(({ id }) => id === message.sender),
recipient: allSubscribers.find(({ id }) => id === message['recipient']), recipient: allSubscribers.find(({ id }) => id === message.recipient),
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id, sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
})); }));
expect(messageService.findAndPopulate).toHaveBeenCalledWith( expect(messageService.findAndPopulate).toHaveBeenCalledWith(

View File

@ -48,7 +48,7 @@ describe('SubscriberController', () => {
let subscriberService: SubscriberService; let subscriberService: SubscriberService;
let labelService: LabelService; let labelService: LabelService;
let userService: UserService; let userService: UserService;
let subscriber: Subscriber; let subscriber: Subscriber | null;
let allLabels: Label[]; let allLabels: Label[];
let allSubscribers: Subscriber[]; let allSubscribers: Subscriber[];
let allUsers: User[]; let allUsers: User[];
@ -114,38 +114,39 @@ describe('SubscriberController', () => {
describe('findOne', () => { describe('findOne', () => {
it('should find one subscriber by id', async () => { it('should find one subscriber by id', async () => {
jest.spyOn(subscriberService, 'findOne'); jest.spyOn(subscriberService, 'findOne');
const result = await subscriberService.findOne(subscriber.id); const result = await subscriberService.findOne(subscriber!.id);
const labelIDs = allLabels const labelIDs = allLabels
.filter((label) => subscriber.labels.includes(label.id)) .filter((label) => subscriber!.labels.includes(label.id))
.map(({ id }) => id); .map(({ id }) => id);
expect(subscriberService.findOne).toHaveBeenCalledWith(subscriber.id); expect(subscriberService.findOne).toHaveBeenCalledWith(subscriber!.id);
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...subscriberFixtures.find( ...subscriberFixtures.find(
({ first_name }) => first_name === subscriber.first_name, ({ first_name }) => first_name === subscriber!.first_name,
), ),
labels: labelIDs, labels: labelIDs,
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id).id, assignedTo: allUsers.find(({ id }) => subscriber!.assignedTo === id)
?.id,
}); });
}); });
it('should find one subscriber by id, and populate its corresponding labels', async () => { it('should find one subscriber by id, and populate its corresponding labels', async () => {
jest.spyOn(subscriberService, 'findOneAndPopulate'); jest.spyOn(subscriberService, 'findOneAndPopulate');
const result = await subscriberController.findOne(subscriber.id, [ const result = await subscriberController.findOne(subscriber!.id, [
'labels', 'labels',
]); ]);
expect(subscriberService.findOneAndPopulate).toHaveBeenCalledWith( expect(subscriberService.findOneAndPopulate).toHaveBeenCalledWith(
subscriber.id, subscriber!.id,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...subscriberFixtures.find( ...subscriberFixtures.find(
({ first_name }) => first_name === subscriber.first_name, ({ first_name }) => first_name === subscriber!.first_name,
), ),
labels: allLabels.filter((label) => labels: allLabels.filter((label) =>
subscriber.labels.includes(label.id), subscriber!.labels.includes(label.id),
), ),
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id), assignedTo: allUsers.find(({ id }) => subscriber!.assignedTo === id),
}); });
}); });
}); });
@ -177,7 +178,7 @@ describe('SubscriberController', () => {
({ labels, ...rest }) => ({ ({ labels, ...rest }) => ({
...rest, ...rest,
labels: allLabels.filter((label) => labels.includes(label.id)), labels: allLabels.filter((label) => labels.includes(label.id)),
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id), assignedTo: allUsers.find(({ id }) => subscriber!.assignedTo === id),
}), }),
); );

View File

@ -145,7 +145,9 @@ export class SubscriberController extends BaseController<
* @returns A streamable file containing the avatar image. * @returns A streamable file containing the avatar image.
*/ */
@Get(':id/profile_pic') @Get(':id/profile_pic')
async getAvatar(@Param('id') id: string): Promise<StreamableFile> { async getAvatar(
@Param('id') id: string,
): Promise<StreamableFile | undefined> {
const subscriber = await this.subscriberService.findOneAndPopulate(id); const subscriber = await this.subscriberService.findOneAndPopulate(id);
if (!subscriber) { if (!subscriber) {

View File

@ -89,7 +89,7 @@ export class BlockCreateDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
@IsObjectId({ message: 'Category must be a valid objectId' }) @IsObjectId({ message: 'Category must be a valid objectId' })
category: string; category: string | null;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Block has started conversation', description: 'Block has started conversation',

View File

@ -31,9 +31,9 @@ describe('BlockRepository', () => {
let blockRepository: BlockRepository; let blockRepository: BlockRepository;
let categoryRepository: CategoryRepository; let categoryRepository: CategoryRepository;
let blockModel: Model<Block>; let blockModel: Model<Block>;
let category: Category; let category: Category | null;
let hasPreviousBlocks: Block; let hasPreviousBlocks: Block | null;
let hasNextBlocks: Block; let hasNextBlocks: Block | null;
let validIds: string[]; let validIds: string[];
let validCategory: string; let validCategory: string;
@ -67,16 +67,19 @@ describe('BlockRepository', () => {
it('should find one block by id, and populate its trigger_labels, assign_labels, nextBlocks, attachedBlock, category,previousBlocks', async () => { it('should find one block by id, and populate its trigger_labels, assign_labels, nextBlocks, attachedBlock, category,previousBlocks', async () => {
jest.spyOn(blockModel, 'findById'); jest.spyOn(blockModel, 'findById');
const result = await blockRepository.findOneAndPopulate(hasNextBlocks.id); const result = await blockRepository.findOneAndPopulate(
hasNextBlocks!.id,
);
expect(blockModel.findById).toHaveBeenCalledWith( expect(blockModel.findById).toHaveBeenCalledWith(
hasNextBlocks.id, hasNextBlocks!.id,
undefined, undefined,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...blockFixtures.find(({ name }) => name === hasNextBlocks.name), ...blockFixtures.find(({ name }) => name === hasNextBlocks!.name),
category, category,
nextBlocks: [hasPreviousBlocks], nextBlocks: [hasPreviousBlocks],
previousBlocks: [], previousBlocks: [],
attachedToBlock: null,
}); });
}); });
}); });
@ -93,6 +96,7 @@ describe('BlockRepository', () => {
blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [], blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [],
nextBlocks: nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [], blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
attachedToBlock: null,
})); }));
expect(blockModel.find).toHaveBeenCalledWith({}, undefined); expect(blockModel.find).toHaveBeenCalledWith({}, undefined);
@ -110,6 +114,7 @@ describe('BlockRepository', () => {
blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [], blockFixture.name === 'hasPreviousBlocks' ? [hasNextBlocks] : [],
nextBlocks: nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [], blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
attachedToBlock: null,
})); }));
expect(blockModel.find).toHaveBeenCalledWith({}, undefined); expect(blockModel.find).toHaveBeenCalledWith({}, undefined);
@ -191,7 +196,7 @@ describe('BlockRepository', () => {
category: validCategory, category: validCategory,
nextBlocks: [], nextBlocks: [],
attachedBlock: null, attachedBlock: null,
} as Block); } as unknown as Block);
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne'); const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');
@ -233,7 +238,7 @@ describe('BlockRepository', () => {
attachedBlock: null, attachedBlock: null,
nextBlocks: [validIds[0], validIds[1]], nextBlocks: [validIds[0], validIds[1]],
}, },
] as Block[]; ] as unknown as Block[];
const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne'); const mockUpdateOne = jest.spyOn(blockRepository, 'updateOne');

View File

@ -32,7 +32,7 @@ export class ContextVarRepository extends BaseRepository<ContextVar> {
@Optional() blockService?: BlockService, @Optional() blockService?: BlockService,
) { ) {
super(eventEmitter, model, ContextVar); super(eventEmitter, model, ContextVar);
this.blockService = blockService; if (blockService) this.blockService = blockService;
} }
/** /**

View File

@ -63,19 +63,22 @@ describe('MessageRepository', () => {
it('should find one message by id, and populate its sender and recipient', async () => { it('should find one message by id, and populate its sender and recipient', async () => {
jest.spyOn(messageModel, 'findById'); jest.spyOn(messageModel, 'findById');
const message = await messageRepository.findOne({ mid: 'mid-1' }); const message = await messageRepository.findOne({ mid: 'mid-1' });
const sender = await subscriberRepository.findOne(message['sender']); const sender = await subscriberRepository.findOne(message!['sender']);
const recipient = await subscriberRepository.findOne( const recipient = await subscriberRepository.findOne(
message['recipient'], message!['recipient'],
); );
const user = await userRepository.findOne(message['sentBy']); const user = await userRepository.findOne(message!['sentBy']);
const result = await messageRepository.findOneAndPopulate(message.id); const result = await messageRepository.findOneAndPopulate(message!.id);
expect(messageModel.findById).toHaveBeenCalledWith(message.id, undefined); expect(messageModel.findById).toHaveBeenCalledWith(
message!.id,
undefined,
);
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...messageFixtures.find(({ mid }) => mid === message.mid), ...messageFixtures.find(({ mid }) => mid === message!.mid),
sender, sender,
recipient, recipient,
sentBy: user.id, sentBy: user!.id,
}); });
}); });
}); });
@ -92,7 +95,7 @@ describe('MessageRepository', () => {
...message, ...message,
sender: allSubscribers.find(({ id }) => id === message['sender']), sender: allSubscribers.find(({ id }) => id === message['sender']),
recipient: allSubscribers.find(({ id }) => id === message['recipient']), recipient: allSubscribers.find(({ id }) => id === message['recipient']),
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id, sentBy: allUsers.find(({ id }) => id === message['sentBy'])?.id,
})); }));
expect(messageModel.find).toHaveBeenCalledWith({}, undefined); expect(messageModel.find).toHaveBeenCalledWith({}, undefined);

View File

@ -107,20 +107,20 @@ describe('SubscriberRepository', () => {
}); });
const allLabels = await labelRepository.findAll(); const allLabels = await labelRepository.findAll();
const result = await subscriberRepository.findOneAndPopulate( const result = await subscriberRepository.findOneAndPopulate(
subscriber.id, subscriber!.id,
); );
const subscriberWithLabels = { const subscriberWithLabels = {
...subscriberFixtures.find( ...subscriberFixtures.find(
({ first_name }) => first_name === subscriber.first_name, ({ first_name }) => first_name === subscriber!.first_name,
), ),
labels: allLabels.filter((label) => labels: allLabels.filter((label) =>
subscriber.labels.includes(label.id), subscriber!.labels.includes(label.id),
), ),
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id), assignedTo: allUsers.find(({ id }) => subscriber!.assignedTo === id),
}; };
expect(subscriberModel.findById).toHaveBeenCalledWith( expect(subscriberModel.findById).toHaveBeenCalledWith(
subscriber.id, subscriber!.id,
undefined, undefined,
); );
expect(result).toEqualPayload(subscriberWithLabels); expect(result).toEqualPayload(subscriberWithLabels);

View File

@ -134,7 +134,7 @@ export class SubscriberRepository extends BaseRepository<
*/ */
async findOneByForeignIdAndPopulate(id: string): Promise<SubscriberFull> { async findOneByForeignIdAndPopulate(id: string): Promise<SubscriberFull> {
const query = this.findByForeignIdQuery(id).populate(this.populate); const query = this.findByForeignIdQuery(id).populate(this.populate);
const [result] = await this.execute(query, this.clsPopulate); const [result] = await this.execute(query, SubscriberFull);
return result; return result;
} }
@ -149,7 +149,7 @@ export class SubscriberRepository extends BaseRepository<
async updateOneByForeignIdQuery( async updateOneByForeignIdQuery(
id: string, id: string,
updates: SubscriberUpdateDto, updates: SubscriberUpdateDto,
): Promise<Subscriber> { ): Promise<Subscriber | null> {
return await this.updateOne({ foreign_id: id }, updates); return await this.updateOne({ foreign_id: id }, updates);
} }
@ -160,7 +160,9 @@ export class SubscriberRepository extends BaseRepository<
* *
* @returns The updated subscriber entity. * @returns The updated subscriber entity.
*/ */
async handBackByForeignIdQuery(foreignId: string): Promise<Subscriber> { async handBackByForeignIdQuery(
foreignId: string,
): Promise<Subscriber | null> {
return await this.updateOne( return await this.updateOne(
{ {
foreign_id: foreignId, foreign_id: foreignId,
@ -183,7 +185,7 @@ export class SubscriberRepository extends BaseRepository<
async handOverByForeignIdQuery( async handOverByForeignIdQuery(
foreignId: string, foreignId: string,
userId: string, userId: string,
): Promise<Subscriber> { ): Promise<Subscriber | null> {
return await this.updateOne( return await this.updateOne(
{ {
foreign_id: foreignId, foreign_id: foreignId,

View File

@ -143,13 +143,13 @@ export class Block extends BlockStub {
attachedBlock?: string; attachedBlock?: string;
@Transform(({ obj }) => obj.category.toString()) @Transform(({ obj }) => obj.category.toString())
category: string; category: string | null;
@Exclude() @Exclude()
previousBlocks?: never; previousBlocks?: never;
@Exclude() @Exclude()
attachedToBlock?: never | null; attachedToBlock?: never;
} }
@Schema({ timestamps: true }) @Schema({ timestamps: true })
@ -161,7 +161,7 @@ export class BlockFull extends BlockStub {
assign_labels: Label[]; assign_labels: Label[];
@Type(() => Block) @Type(() => Block)
nextBlocks?: Block[]; nextBlocks: Block[];
@Type(() => Block) @Type(() => Block)
attachedBlock?: Block; attachedBlock?: Block;

View File

@ -86,7 +86,7 @@ export class SubscriberStub extends BaseSchema {
type: Date, type: Date,
default: null, default: null,
}) })
assignedAt?: Date; assignedAt?: Date | null;
@Prop({ @Prop({
type: Date, type: Date,
@ -132,10 +132,10 @@ export class Subscriber extends SubscriberStub {
labels: string[]; labels: string[];
@Transform(({ obj }) => (obj.assignedTo ? obj.assignedTo.toString() : null)) @Transform(({ obj }) => (obj.assignedTo ? obj.assignedTo.toString() : null))
assignedTo?: string; assignedTo?: string | null;
@Transform(({ obj }) => obj.avatar?.toString() || null) @Transform(({ obj }) => obj.avatar?.toString() || null)
avatar?: string; avatar?: string | null;
} }
@Schema({ timestamps: true }) @Schema({ timestamps: true })

View File

@ -74,10 +74,10 @@ import { CategoryService } from './category.service';
describe('BlockService', () => { describe('BlockService', () => {
let blockRepository: BlockRepository; let blockRepository: BlockRepository;
let categoryRepository: CategoryRepository; let categoryRepository: CategoryRepository;
let category: Category; let category: Category | null;
let block: Block; let block: Block | null;
let blockService: BlockService; let blockService: BlockService;
let hasPreviousBlocks: Block; let hasPreviousBlocks: Block | null;
let contentService: ContentService; let contentService: ContentService;
let contentTypeService: ContentTypeService; let contentTypeService: ContentTypeService;
let settingService: SettingService; let settingService: SettingService;
@ -168,10 +168,10 @@ describe('BlockService', () => {
describe('findOneAndPopulate', () => { describe('findOneAndPopulate', () => {
it('should find one block by id, and populate its trigger_labels, assign_labels,attachedBlock,category,nextBlocks', async () => { it('should find one block by id, and populate its trigger_labels, assign_labels,attachedBlock,category,nextBlocks', async () => {
jest.spyOn(blockRepository, 'findOneAndPopulate'); jest.spyOn(blockRepository, 'findOneAndPopulate');
const result = await blockService.findOneAndPopulate(block.id); const result = await blockService.findOneAndPopulate(block!.id);
expect(blockRepository.findOneAndPopulate).toHaveBeenCalledWith( expect(blockRepository.findOneAndPopulate).toHaveBeenCalledWith(
block.id, block!.id,
undefined, undefined,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
@ -179,6 +179,7 @@ describe('BlockService', () => {
category, category,
nextBlocks: [hasPreviousBlocks], nextBlocks: [hasPreviousBlocks],
previousBlocks: [], previousBlocks: [],
attachedToBlock: null,
}); });
}); });
}); });
@ -194,6 +195,7 @@ describe('BlockService', () => {
blockFixture.name === 'hasPreviousBlocks' ? [block] : [], blockFixture.name === 'hasPreviousBlocks' ? [block] : [],
nextBlocks: nextBlocks:
blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [], blockFixture.name === 'hasNextBlocks' ? [hasPreviousBlocks] : [],
attachedToBlock: null,
})); }));
expect(blockRepository.findAndPopulate).toHaveBeenCalledWith( expect(blockRepository.findAndPopulate).toHaveBeenCalledWith(
@ -380,7 +382,7 @@ describe('BlockService', () => {
}, },
blockGetStarted, blockGetStarted,
); );
expect(result).toEqual(blockGetStarted.patterns[3]); expect(result).toEqual(blockGetStarted.patterns?.[3]);
}); });
it("should match payload when it's an attachment file", () => { it("should match payload when it's an attachment file", () => {
@ -396,7 +398,7 @@ describe('BlockService', () => {
}, },
blockGetStarted, blockGetStarted,
); );
expect(result).toEqual(blockGetStarted.patterns[4]); expect(result).toEqual(blockGetStarted.patterns?.[4]);
}); });
}); });
@ -439,7 +441,7 @@ describe('BlockService', () => {
describe('processMessage', () => { describe('processMessage', () => {
it('should process list message (with limit = 2 and skip = 0)', async () => { it('should process list message (with limit = 2 and skip = 0)', async () => {
const contentType = await contentTypeService.findOne({ name: 'Product' }); const contentType = await contentTypeService.findOne({ name: 'Product' });
blockProductListMock.options.content.entity = contentType.id; blockProductListMock.options!.content!.entity = contentType!.id;
const result = await blockService.processMessage( const result = await blockService.processMessage(
blockProductListMock, blockProductListMock,
{ {
@ -451,27 +453,27 @@ describe('BlockService', () => {
'conv_id', 'conv_id',
); );
const elements = await contentService.findPage( const elements = await contentService.findPage(
{ status: true, entity: contentType.id }, { status: true, entity: contentType!.id },
{ skip: 0, limit: 2, sort: ['createdAt', 'desc'] }, { skip: 0, limit: 2, sort: ['createdAt', 'desc'] },
); );
const flattenedElements = elements.map(Content.toElement); const flattenedElements = elements.map(Content.toElement);
expect(result.format).toEqualPayload( expect(result!.format).toEqualPayload(
blockProductListMock.options.content?.display, blockProductListMock.options!.content!.display,
); );
expect( expect(
(result.message as StdOutgoingListMessage).elements, (result!.message as StdOutgoingListMessage).elements,
).toEqualPayload(flattenedElements); ).toEqualPayload(flattenedElements);
expect((result.message as StdOutgoingListMessage).options).toEqualPayload(
blockProductListMock.options.content,
);
expect( expect(
(result.message as StdOutgoingListMessage).pagination, (result!.message as StdOutgoingListMessage).options,
).toEqualPayload(blockProductListMock.options!.content!);
expect(
(result!.message as StdOutgoingListMessage).pagination,
).toEqualPayload({ total: 4, skip: 0, limit: 2 }); ).toEqualPayload({ total: 4, skip: 0, limit: 2 });
}); });
it('should process list message (with limit = 2 and skip = 2)', async () => { it('should process list message (with limit = 2 and skip = 2)', async () => {
const contentType = await contentTypeService.findOne({ name: 'Product' }); const contentType = await contentTypeService.findOne({ name: 'Product' });
blockProductListMock.options.content.entity = contentType.id; blockProductListMock.options!.content!.entity = contentType!.id;
const result = await blockService.processMessage( const result = await blockService.processMessage(
blockProductListMock, blockProductListMock,
{ {
@ -483,20 +485,20 @@ describe('BlockService', () => {
'conv_id', 'conv_id',
); );
const elements = await contentService.findPage( const elements = await contentService.findPage(
{ status: true, entity: contentType.id }, { status: true, entity: contentType!.id },
{ skip: 2, limit: 2, sort: ['createdAt', 'desc'] }, { skip: 2, limit: 2, sort: ['createdAt', 'desc'] },
); );
const flattenedElements = elements.map(Content.toElement); const flattenedElements = elements.map(Content.toElement);
expect(result.format).toEqual( expect(result!.format).toEqual(
blockProductListMock.options.content?.display, blockProductListMock.options!.content?.display,
); );
expect((result.message as StdOutgoingListMessage).elements).toEqual( expect((result!.message as StdOutgoingListMessage).elements).toEqual(
flattenedElements, flattenedElements,
); );
expect((result.message as StdOutgoingListMessage).options).toEqual( expect((result!.message as StdOutgoingListMessage).options).toEqual(
blockProductListMock.options.content, blockProductListMock.options!.content,
); );
expect((result.message as StdOutgoingListMessage).pagination).toEqual({ expect((result!.message as StdOutgoingListMessage).pagination).toEqual({
total: 4, total: 4,
skip: 2, skip: 2,
limit: 2, limit: 2,

View File

@ -197,7 +197,7 @@ describe('BlockService', () => {
foreign_id: 'foreign-id-web-1', foreign_id: 'foreign-id-web-1',
}); });
event.setSender(webSubscriber); event.setSender(webSubscriber!);
let hasBotSpoken = false; let hasBotSpoken = false;
const clearMock = jest const clearMock = jest
@ -210,15 +210,15 @@ describe('BlockService', () => {
isFallback: boolean, isFallback: boolean,
) => { ) => {
expect(actualConversation).toEqualPayload({ expect(actualConversation).toEqualPayload({
sender: webSubscriber.id, sender: webSubscriber!.id,
active: true, active: true,
next: [], next: [],
context: { context: {
user: { user: {
first_name: webSubscriber.first_name, first_name: webSubscriber!.first_name,
last_name: webSubscriber.last_name, last_name: webSubscriber!.last_name,
language: 'en', language: 'en',
id: webSubscriber.id, id: webSubscriber!.id,
}, },
user_location: { user_location: {
lat: 0, lat: 0,
@ -263,7 +263,7 @@ describe('BlockService', () => {
const webSubscriber = await subscriberService.findOne({ const webSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-web-1', foreign_id: 'foreign-id-web-1',
}); });
event.setSender(webSubscriber); event.setSender(webSubscriber!);
const clearMock = jest const clearMock = jest
.spyOn(botService, 'handleIncomingMessage') .spyOn(botService, 'handleIncomingMessage')
@ -278,10 +278,10 @@ describe('BlockService', () => {
active: true, active: true,
context: { context: {
user: { user: {
first_name: webSubscriber.first_name, first_name: webSubscriber!.first_name,
last_name: webSubscriber.last_name, last_name: webSubscriber!.last_name,
language: 'en', language: 'en',
id: webSubscriber.id, id: webSubscriber!.id,
}, },
user_location: { lat: 0, lon: 0 }, user_location: { lat: 0, lon: 0 },
vars: {}, vars: {},
@ -317,7 +317,7 @@ describe('BlockService', () => {
const webSubscriber = await subscriberService.findOne({ const webSubscriber = await subscriberService.findOne({
foreign_id: 'foreign-id-web-2', foreign_id: 'foreign-id-web-2',
}); });
event.setSender(webSubscriber); event.setSender(webSubscriber!);
const captured = await botService.processConversationMessage(event); const captured = await botService.processConversationMessage(event);
expect(captured).toBe(false); expect(captured).toBe(false);

View File

@ -25,6 +25,7 @@ import {
IncomingMessageType, IncomingMessageType,
StdOutgoingEnvelope, StdOutgoingEnvelope,
} from '../schemas/types/message'; } from '../schemas/types/message';
import { SubscriberContext } from '../schemas/types/subscriberContext';
import { BlockService } from './block.service'; import { BlockService } from './block.service';
import { ConversationService } from './conversation.service'; import { ConversationService } from './conversation.service';
@ -70,14 +71,13 @@ export class BotService {
); );
// Process message : Replace tokens with context data and then send the message // Process message : Replace tokens with context data and then send the message
const recipient = event.getSender(); const recipient = event.getSender();
const envelope: StdOutgoingEnvelope = const envelope = (await this.blockService.processMessage(
await this.blockService.processMessage( block,
block, context,
context, recipient?.context as SubscriberContext,
recipient?.context, fallback,
fallback, conservationId,
conservationId, )) as StdOutgoingEnvelope;
);
// Send message through the right channel // Send message through the right channel
const response = await event const response = await event
@ -252,13 +252,13 @@ export class BotService {
assign_labels: [], assign_labels: [],
trigger_labels: [], trigger_labels: [],
attachedBlock: undefined, attachedBlock: undefined,
category: undefined, category: undefined as any,
previousBlocks: [], previousBlocks: [],
}; };
convo.context.attempt++; convo.context.attempt++;
fallback = true; fallback = true;
} else { } else {
convo.context.attempt = 0; if (convo.context) convo.context.attempt = 0;
fallbackBlock = undefined; fallbackBlock = undefined;
} }

View File

@ -30,7 +30,7 @@ export class ContextVarService extends BaseService<ContextVar> {
block: Block | BlockFull, block: Block | BlockFull,
): Promise<Record<string, ContextVar>> { ): Promise<Record<string, ContextVar>> {
const vars = await this.find({ const vars = await this.find({
name: { $in: block.capture_vars.map(({ context_var }) => context_var) }, name: { $in: block.capture_vars?.map(({ context_var }) => context_var) },
}); });
return vars.reduce((acc, cv) => { return vars.reduce((acc, cv) => {
acc[cv.name] = cv; acc[cv.name] = cv;

View File

@ -68,6 +68,9 @@ export class ConversationService extends BaseService<
) { ) {
const msgType = event.getMessageType(); const msgType = event.getMessageType();
const profile = event.getSender(); const profile = event.getSender();
if (!convo.context) throw new Error('Missing conversation context');
// Capture channel specific context data // Capture channel specific context data
convo.context.channel = event.getHandler().getName(); convo.context.channel = event.getHandler().getName();
convo.context.text = event.getText(); convo.context.text = event.getText();
@ -81,7 +84,7 @@ export class ConversationService extends BaseService<
// Capture user entry in context vars // Capture user entry in context vars
if (captureVars && next.capture_vars && next.capture_vars.length > 0) { if (captureVars && next.capture_vars && next.capture_vars.length > 0) {
next.capture_vars.forEach((capture) => { next.capture_vars.forEach((capture) => {
let contextValue: string | Payload; let contextValue: string | Payload | undefined;
const nlp = event.getNLP(); const nlp = event.getNLP();
@ -103,7 +106,7 @@ export class ConversationService extends BaseService<
if (capture.entity === -1) { if (capture.entity === -1) {
// Capture the whole message // Capture the whole message
contextValue = contextValue =
['message', 'quick_reply'].indexOf(msgType) !== -1 msgType && ['message', 'quick_reply'].indexOf(msgType) !== -1
? event.getText() ? event.getText()
: event.getPayload(); : event.getPayload();
} else if (capture.entity === -2) { } else if (capture.entity === -2) {
@ -113,13 +116,16 @@ export class ConversationService extends BaseService<
contextValue = contextValue =
typeof contextValue === 'string' ? contextValue.trim() : contextValue; typeof contextValue === 'string' ? contextValue.trim() : contextValue;
if (contextVars[capture.context_var]?.permanent) { if (
profile.context?.vars &&
contextVars[capture.context_var]?.permanent
) {
Logger.debug( Logger.debug(
`Adding context var to subscriber: ${capture.context_var} = ${contextValue}`, `Adding context var to subscriber: ${capture.context_var} = ${contextValue}`,
); );
profile.context.vars[capture.context_var] = contextValue; profile.context.vars[capture.context_var] = contextValue;
} else { } else {
convo.context.vars[capture.context_var] = contextValue; convo.context!.vars[capture.context_var] = contextValue;
} }
}); });
} }
@ -158,6 +164,7 @@ export class ConversationService extends BaseService<
// Deal with load more in the case of a list display // Deal with load more in the case of a list display
if ( if (
next.options &&
next.options.content && next.options.content &&
(next.options.content.display === OutgoingMessageFormat.list || (next.options.content.display === OutgoingMessageFormat.list ||
next.options.content.display === OutgoingMessageFormat.carousel) next.options.content.display === OutgoingMessageFormat.carousel)

View File

@ -48,11 +48,11 @@ describe('MessageService', () => {
let allMessages: Message[]; let allMessages: Message[];
let allSubscribers: Subscriber[]; let allSubscribers: Subscriber[];
let allUsers: User[]; let allUsers: User[];
let message: Message; let message: Message | null;
let sender: Subscriber; let sender: Subscriber | null;
let recipient: Subscriber; let recipient: Subscriber | null;
let messagesWithSenderAndRecipient: Message[]; let messagesWithSenderAndRecipient: Message[];
let user: User; let user: User | null;
beforeAll(async () => { beforeAll(async () => {
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
@ -91,15 +91,14 @@ describe('MessageService', () => {
allUsers = await userRepository.findAll(); allUsers = await userRepository.findAll();
allMessages = await messageRepository.findAll(); allMessages = await messageRepository.findAll();
message = await messageRepository.findOne({ mid: 'mid-1' }); message = await messageRepository.findOne({ mid: 'mid-1' });
sender = await subscriberRepository.findOne(message['sender']); sender = await subscriberRepository.findOne(message!.sender!);
recipient = await subscriberRepository.findOne(message['recipient']); recipient = await subscriberRepository.findOne(message!.recipient!);
user = await userRepository.findOne(message['sentBy']); user = await userRepository.findOne(message!.sentBy!);
messagesWithSenderAndRecipient = allMessages.map((message) => ({ messagesWithSenderAndRecipient = allMessages.map((message) => ({
...message, ...message,
sender: allSubscribers.find(({ id }) => id === message['sender']).id, sender: allSubscribers.find(({ id }) => id === message.sender)?.id,
recipient: allSubscribers.find(({ id }) => id === message['recipient']) recipient: allSubscribers.find(({ id }) => id === message.recipient)?.id,
.id, sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id,
})); }));
}); });
@ -109,17 +108,17 @@ describe('MessageService', () => {
describe('findOneAndPopulate', () => { describe('findOneAndPopulate', () => {
it('should find message by id, and populate its corresponding sender and recipient', async () => { it('should find message by id, and populate its corresponding sender and recipient', async () => {
jest.spyOn(messageRepository, 'findOneAndPopulate'); jest.spyOn(messageRepository, 'findOneAndPopulate');
const result = await messageService.findOneAndPopulate(message.id); const result = await messageService.findOneAndPopulate(message!.id);
expect(messageRepository.findOneAndPopulate).toHaveBeenCalledWith( expect(messageRepository.findOneAndPopulate).toHaveBeenCalledWith(
message.id, message!.id,
undefined, undefined,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...messageFixtures.find(({ mid }) => mid === message.mid), ...messageFixtures.find(({ mid }) => mid === message!.mid),
sender, sender,
recipient, recipient,
sentBy: user.id, sentBy: user!.id,
}); });
}); });
}); });
@ -131,9 +130,9 @@ describe('MessageService', () => {
const result = await messageService.findPageAndPopulate({}, pageQuery); const result = await messageService.findPageAndPopulate({}, pageQuery);
const messagesWithSenderAndRecipient = allMessages.map((message) => ({ const messagesWithSenderAndRecipient = allMessages.map((message) => ({
...message, ...message,
sender: allSubscribers.find(({ id }) => id === message['sender']), sender: allSubscribers.find(({ id }) => id === message.sender),
recipient: allSubscribers.find(({ id }) => id === message['recipient']), recipient: allSubscribers.find(({ id }) => id === message.recipient),
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id, sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
})); }));
expect(messageRepository.findPageAndPopulate).toHaveBeenCalledWith( expect(messageRepository.findPageAndPopulate).toHaveBeenCalledWith(
@ -150,7 +149,7 @@ describe('MessageService', () => {
new Date().setMonth(new Date().getMonth() + 1), new Date().setMonth(new Date().getMonth() + 1),
); );
const result = await messageService.findHistoryUntilDate( const result = await messageService.findHistoryUntilDate(
sender, sender!,
until, until,
30, 30,
); );
@ -166,16 +165,16 @@ describe('MessageService', () => {
it('should return history since given date', async () => { it('should return history since given date', async () => {
const since: Date = new Date(); const since: Date = new Date();
const result = await messageService.findHistorySinceDate( const result = await messageService.findHistorySinceDate(
sender, sender!,
since, since,
30, 30,
); );
const messagesWithSenderAndRecipient = allMessages.map((message) => ({ const messagesWithSenderAndRecipient = allMessages.map((message) => ({
...message, ...message,
sender: allSubscribers.find(({ id }) => id === message['sender']).id, sender: allSubscribers.find(({ id }) => id === message.sender)?.id,
recipient: allSubscribers.find(({ id }) => id === message['recipient']) recipient: allSubscribers.find(({ id }) => id === message.recipient)
.id, ?.id,
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id, sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
})); }));
const historyMessages = messagesWithSenderAndRecipient.filter( const historyMessages = messagesWithSenderAndRecipient.filter(
(message) => message.createdAt > since, (message) => message.createdAt > since,

View File

@ -46,8 +46,8 @@ export class MessageService extends BaseService<
@Optional() gateway?: WebsocketGateway, @Optional() gateway?: WebsocketGateway,
) { ) {
super(messageRepository); super(messageRepository);
this.logger = logger; if (logger) this.logger = logger;
this.gateway = gateway; if (gateway) this.gateway = gateway;
} }
/** /**

View File

@ -93,17 +93,17 @@ describe('SubscriberService', () => {
const subscriber = await subscriberRepository.findOne({ const subscriber = await subscriberRepository.findOne({
first_name: 'Jhon', first_name: 'Jhon',
}); });
const result = await subscriberService.findOneAndPopulate(subscriber.id); const result = await subscriberService.findOneAndPopulate(subscriber!.id);
expect(subscriberService.findOneAndPopulate).toHaveBeenCalledWith( expect(subscriberService.findOneAndPopulate).toHaveBeenCalledWith(
subscriber.id, subscriber!.id,
); );
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...subscriber, ...subscriber,
labels: allLabels.filter((label) => labels: allLabels.filter((label) =>
subscriber.labels.includes(label.id), subscriber!.labels.includes(label.id),
), ),
assignedTo: allUsers.find(({ id }) => subscriber.assignedTo === id), assignedTo: allUsers.find(({ id }) => subscriber!.assignedTo === id),
}); });
}); });
}); });
@ -139,7 +139,7 @@ describe('SubscriberService', () => {
expect(result).toEqualPayload({ expect(result).toEqualPayload({
...subscriber, ...subscriber,
labels: allLabels labels: allLabels
.filter((label) => subscriber.labels.includes(label.id)) .filter((label) => subscriber!.labels.includes(label.id))
.map((label) => label.id), .map((label) => label.id),
}); });
}); });

View File

@ -51,7 +51,7 @@ export class SubscriberService extends BaseService<
@Optional() gateway?: WebsocketGateway, @Optional() gateway?: WebsocketGateway,
) { ) {
super(repository); super(repository);
this.gateway = gateway; if (gateway) this.gateway = gateway;
} }
/** /**

View File

@ -1,12 +1,12 @@
/* /*
* Copyright © 2025 Hexastack. All rights reserved. * Copyright © 2024 Hexastack. All rights reserved.
* *
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 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. * 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). * 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 { Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import multer, { diskStorage, memoryStorage } from 'multer'; import multer, { diskStorage, memoryStorage } from 'multer';
@ -231,7 +231,7 @@ export default abstract class BaseWebChannelHandler<
...message, ...message,
author: 'chatbot', author: 'chatbot',
read: true, // Temporary fix as read is false in the bd read: true, // Temporary fix as read is false in the bd
mid: anyMessage.mid, mid: anyMessage.mid || 'DEFAULT_MID',
handover: !!anyMessage.handover, handover: !!anyMessage.handover,
createdAt: anyMessage.createdAt, createdAt: anyMessage.createdAt,
}); });
@ -519,6 +519,9 @@ export default abstract class BaseWebChannelHandler<
const fetchMessages = async (req: Request, res: Response, retrials = 1) => { const fetchMessages = async (req: Request, res: Response, retrials = 1) => {
try { try {
if (!req.query.since)
throw new BadRequestException(`QueryParam 'since' is missing`);
const since = new Date(req.query.since.toString()); const since = new Date(req.query.since.toString());
const messages = await this.pollMessages(req, since); const messages = await this.pollMessages(req, since);
if (messages.length === 0 && retrials <= 5) { if (messages.length === 0 && retrials <= 5) {
@ -630,10 +633,12 @@ export default abstract class BaseWebChannelHandler<
size: Buffer.byteLength(data.file), size: Buffer.byteLength(data.file),
type: data.type, type: data.type,
}); });
next(null, {
type: Attachment.getTypeByMime(attachment.type), if (attachment)
url: Attachment.getAttachmentUrl(attachment.id, attachment.name), next(null, {
}); type: Attachment.getTypeByMime(attachment.type),
url: Attachment.getAttachmentUrl(attachment.id, attachment?.name),
});
} catch (err) { } catch (err) {
this.logger.error( this.logger.error(
'Web Channel Handler : Unable to write uploaded file', 'Web Channel Handler : Unable to write uploaded file',
@ -677,6 +682,12 @@ export default abstract class BaseWebChannelHandler<
size: file.size, size: file.size,
type: file.mimetype, type: file.mimetype,
}); });
if (!attachment) {
this.logger.debug(
'Web Channel Handler : failed to store attachment',
);
return next(null);
}
next(null, { next(null, {
type: Attachment.getTypeByMime(attachment.type), type: Attachment.getTypeByMime(attachment.type),
url: Attachment.getAttachmentUrl(attachment.id, attachment.name), url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
@ -721,7 +732,7 @@ export default abstract class BaseWebChannelHandler<
return { return {
isSocket: this.isSocketRequest(req), isSocket: this.isSocketRequest(req),
ipAddress: this.getIpAddress(req), ipAddress: this.getIpAddress(req),
agent: req.headers['user-agent'], agent: req.headers['user-agent'] || 'browser',
}; };
} }
@ -965,11 +976,17 @@ export default abstract class BaseWebChannelHandler<
type: Web.OutgoingMessageType.file, type: Web.OutgoingMessageType.file,
data: { data: {
type: message.attachment.type, type: message.attachment.type,
url: message.attachment.payload.url, url: message.attachment.payload.url!,
}, },
}; };
if (message.quickReplies && message.quickReplies.length > 0) { if (message.quickReplies && message.quickReplies.length > 0) {
payload.data.quick_replies = message.quickReplies; return {
...payload,
data: {
...payload.data,
quick_replies: message.quickReplies,
} as Web.OutgoingFileMessageData,
};
} }
return payload; return payload;
} }

View File

@ -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). * 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 { Injectable } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter'; import { OnEvent } from '@nestjs/event-emitter';
import { HelperService } from '@/helper/helper.service'; import { HelperService } from '@/helper/helper.service';
@ -77,9 +77,14 @@ export class NlpService {
async handleEntityDelete(entity: NlpEntity) { async handleEntityDelete(entity: NlpEntity) {
// Synchonize new entity with NLP provider // Synchonize new entity with NLP provider
try { try {
const helper = await this.helperService.getDefaultNluHelper(); if (entity.foreign_id) {
await helper.deleteEntity(entity.foreign_id); const helper = await this.helperService.getDefaultNluHelper();
this.logger.debug('Deleted entity successfully synced!', entity); 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) { } catch (err) {
this.logger.error('Unable to sync deleted entity', err); this.logger.error('Unable to sync deleted entity', err);
} }
@ -138,8 +143,10 @@ export class NlpService {
const populatedValue = await this.nlpValueService.findOneAndPopulate( const populatedValue = await this.nlpValueService.findOneAndPopulate(
value.id, value.id,
); );
await helper.deleteValue(populatedValue); if (populatedValue) {
this.logger.debug('Deleted value successfully synced!', value); await helper.deleteValue(populatedValue);
this.logger.debug('Deleted value successfully synced!', value);
}
} catch (err) { } catch (err) {
this.logger.error('Unable to sync deleted value', err); this.logger.error('Unable to sync deleted value', err);
} }

View File

@ -9,7 +9,7 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { BlockCreateDto } from '@/chat/dto/block.dto'; import { BlockCreateDto } from '@/chat/dto/block.dto';
import { BlockModel, Block } from '@/chat/schemas/block.schema'; import { Block, BlockModel } from '@/chat/schemas/block.schema';
import { CategoryModel } from '@/chat/schemas/category.schema'; import { CategoryModel } from '@/chat/schemas/category.schema';
import { FileType } from '@/chat/schemas/types/attachment'; import { FileType } from '@/chat/schemas/types/attachment';
import { ButtonType } from '@/chat/schemas/types/button'; import { ButtonType } from '@/chat/schemas/types/button';
@ -171,7 +171,6 @@ export const blockDefaultValues: TFixturesDefaultValues<Block> = {
assign_labels: [], assign_labels: [],
trigger_labels: [], trigger_labels: [],
starts_conversation: false, starts_conversation: false,
attachedToBlock: null,
}; };
export const blockFixtures = getFixturesWithDefaultValues<Block>({ export const blockFixtures = getFixturesWithDefaultValues<Block>({

View File

@ -98,22 +98,22 @@ export const baseBlockInstance = {
...modelInstance, ...modelInstance,
}; };
export const blockEmpty: BlockFull = { export const blockEmpty = {
...baseBlockInstance, ...baseBlockInstance,
name: 'Empty', name: 'Empty',
patterns: [], patterns: [],
message: [''], message: [''],
}; } as unknown as BlockFull;
// Translation Data // Translation Data
export const textResult = ['Hi back !']; export const textResult = ['Hi back !'];
export const textBlock: BlockFull = { export const textBlock = {
name: 'message', name: 'message',
patterns: ['Hi'], patterns: ['Hi'],
message: textResult, message: textResult,
...baseBlockInstance, ...baseBlockInstance,
}; } as unknown as BlockFull;
export const quickRepliesResult = [ export const quickRepliesResult = [
"What's your favorite color?", "What's your favorite color?",
@ -122,7 +122,7 @@ export const quickRepliesResult = [
'Red', 'Red',
]; ];
export const quickRepliesBlock: BlockFull = { export const quickRepliesBlock = {
name: 'message', name: 'message',
patterns: ['colors'], patterns: ['colors'],
message: { message: {
@ -146,7 +146,7 @@ export const quickRepliesBlock: BlockFull = {
], ],
}, },
...baseBlockInstance, ...baseBlockInstance,
}; } as unknown as BlockFull;
export const buttonsResult = [ export const buttonsResult = [
'What would you like to know about us?', 'What would you like to know about us?',
@ -155,7 +155,7 @@ export const buttonsResult = [
'Approach', 'Approach',
]; ];
export const buttonsBlock: BlockFull = { export const buttonsBlock = {
name: 'message', name: 'message',
patterns: ['about'], patterns: ['about'],
message: { message: {
@ -179,9 +179,9 @@ export const buttonsBlock: BlockFull = {
], ],
}, },
...baseBlockInstance, ...baseBlockInstance,
}; } as unknown as BlockFull;
export const attachmentBlock: BlockFull = { export const attachmentBlock = {
name: 'message', name: 'message',
patterns: ['image'], patterns: ['image'],
message: { message: {
@ -195,7 +195,7 @@ export const attachmentBlock: BlockFull = {
quickReplies: [], quickReplies: [],
}, },
...baseBlockInstance, ...baseBlockInstance,
}; } as unknown as BlockFull;
export const allBlocksStringsResult = [ export const allBlocksStringsResult = [
'Hi back !', 'Hi back !',
@ -214,7 +214,7 @@ export const allBlocksStringsResult = [
///////// /////////
export const blockGetStarted: BlockFull = { export const blockGetStarted = {
...baseBlockInstance, ...baseBlockInstance,
name: 'Get Started', name: 'Get Started',
patterns: [ patterns: [
@ -245,7 +245,7 @@ export const blockGetStarted: BlockFull = {
], ],
trigger_labels: customerLabelsMock, trigger_labels: customerLabelsMock,
message: ['Welcome! How are you ? '], message: ['Welcome! How are you ? '],
}; } as unknown as BlockFull;
const patternsProduct: Pattern[] = [ const patternsProduct: Pattern[] = [
'produit', 'produit',
@ -262,7 +262,7 @@ const patternsProduct: Pattern[] = [
], ],
]; ];
export const blockProductListMock: BlockFull = { export const blockProductListMock = {
...baseBlockInstance, ...baseBlockInstance,
name: 'test_list', name: 'test_list',
patterns: patternsProduct, patterns: patternsProduct,
@ -278,11 +278,11 @@ export const blockProductListMock: BlockFull = {
limit: 0, limit: 0,
}, },
}, },
}; } as unknown as BlockFull;
export const blockCarouselMock: BlockFull = { export const blockCarouselMock = {
...blockProductListMock, ...blockProductListMock,
options: blockCarouselOptions, options: blockCarouselOptions,
}; } as unknown as BlockFull;
export const blocks: BlockFull[] = [blockGetStarted, blockEmpty]; export const blocks: BlockFull[] = [blockGetStarted, blockEmpty];

View File

@ -13,14 +13,16 @@ type TSortProps<T> = {
order?: 'desc' | 'asc'; order?: 'desc' | 'asc';
}; };
const sort = <R, S, T extends { createdAt?: string } = R & S>({ type TCreateAt = { createdAt?: string | Date };
const sort = <R extends TCreateAt, S, T extends TCreateAt = R & S>({
row1, row1,
row2, row2,
field = 'createdAt', field = 'createdAt',
order = 'desc', order = 'desc',
}: TSortProps<T>) => (order === 'asc' && row1[field] > row2[field] ? 1 : -1); }: TSortProps<T>) => (order === 'asc' && row1[field] > row2[field] ? 1 : -1);
export const sortRowsBy = <R, S, T = R & S>( export const sortRowsBy = <R extends TCreateAt, S, T extends TCreateAt = R & S>(
row1: T, row1: T,
row2: T, row2: T,
field?: keyof T, field?: keyof T,

View File

@ -65,12 +65,16 @@ type TAllowedKeys<T, TStub, TValue = (string | null | undefined)[]> = {
>]: TValue; >]: TValue;
}; };
type TVirtualFields<T> = Pick<T, TFilterKeysOfType<T, undefined>>;
export type TValidateProps<T, TStub> = { export type TValidateProps<T, TStub> = {
dto: dto:
| Partial<TAllowedKeys<T, TStub>> | Partial<TAllowedKeys<T, TStub>>
| Partial<TAllowedKeys<T, TStub, string>>; | Partial<TAllowedKeys<T, TStub, string>>;
allowedIds: TAllowedKeys<T, TStub> & allowedIds: Omit<
TAllowedKeys<T, TStub, string | null | undefined>; TAllowedKeys<T, TStub, null | undefined | string | string[]>,
keyof TVirtualFields<T>
>;
}; };
//populate types //populate types

View File

@ -93,7 +93,7 @@ declare module '@nestjs/event-emitter' {
object, object,
{ {
block: BlockFull; block: BlockFull;
passation: Subscriber; passation: Subscriber | null;
'fallback-local': BlockFull; 'fallback-local': BlockFull;
'fallback-global': EventWrapper<any, any>; 'fallback-global': EventWrapper<any, any>;
intervention: Subscriber; intervention: Subscriber;