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,
foreign_id: req.session.passport.user.id,
first_name: req.session.passport.user.first_name,
last_name: req.session.passport.user.last_name,
first_name: req.session.passport.user.first_name || 'Anonymous',
last_name: req.session.passport.user.last_name || 'Anonymous',
locale: '',
language: '',
gender: '',

View File

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

View File

@ -36,8 +36,8 @@ import { ContextVarController } from './context-var.controller';
describe('ContextVarController', () => {
let contextVarController: ContextVarController;
let contextVarService: ContextVarService;
let contextVar: ContextVar;
let contextVarToDelete: ContextVar;
let contextVar: ContextVar | null;
let contextVarToDelete: ContextVar | null;
beforeAll(async () => {
const module = await Test.createTestingModule({
@ -91,11 +91,11 @@ describe('ContextVarController', () => {
describe('findOne', () => {
it('should return the existing contextVar', async () => {
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(
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 () => {
jest.spyOn(contextVarService, 'deleteOne');
const result = await contextVarController.deleteOne(
contextVarToDelete.id,
contextVarToDelete!.id,
);
expect(contextVarService.deleteOne).toHaveBeenCalledWith(
contextVarToDelete.id,
contextVarToDelete!.id,
);
expect(result).toEqual({
acknowledged: true,
@ -135,10 +135,10 @@ describe('ContextVarController', () => {
it('should throw a NotFoundException when attempting to delete a contextVar by id', async () => {
await expect(
contextVarController.deleteOne(contextVarToDelete.id),
contextVarController.deleteOne(contextVarToDelete!.id),
).rejects.toThrow(
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')
.mockResolvedValue(deleteResult);
const result = await contextVarController.deleteMany([
contextVarToDelete.id,
contextVar.id,
contextVarToDelete!.id,
contextVar!.id,
]);
expect(contextVarService.deleteMany).toHaveBeenCalledWith({
_id: { $in: [contextVarToDelete.id, contextVar.id] },
_id: { $in: [contextVarToDelete!.id, contextVar!.id] },
});
expect(result).toEqual(deleteResult);
});
@ -175,7 +175,10 @@ describe('ContextVarController', () => {
});
await expect(
contextVarController.deleteMany([contextVarToDelete.id, contextVar.id]),
contextVarController.deleteMany([
contextVarToDelete!.id,
contextVar!.id,
]),
).rejects.toThrow(
new NotFoundException('Context vars with provided IDs not found'),
);
@ -189,16 +192,16 @@ describe('ContextVarController', () => {
it('should return updated contextVar', async () => {
jest.spyOn(contextVarService, 'updateOne');
const result = await contextVarController.updateOne(
contextVar.id,
contextVar!.id,
contextVarUpdatedDto,
);
expect(contextVarService.updateOne).toHaveBeenCalledWith(
contextVar.id,
contextVar!.id,
contextVarUpdatedDto,
);
expect(result).toEqualPayload({
...contextVarFixtures.find(({ label }) => label === contextVar.label),
...contextVarFixtures.find(({ label }) => label === contextVar!.label),
...contextVarUpdatedDto,
});
});
@ -206,12 +209,12 @@ describe('ContextVarController', () => {
it('should throw a NotFoundException when attempting to update an non existing contextVar by id', async () => {
await expect(
contextVarController.updateOne(
contextVarToDelete.id,
contextVarToDelete!.id,
contextVarUpdatedDto,
),
).rejects.toThrow(
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 subscriberService: SubscriberService;
let userService: UserService;
let sender: Subscriber;
let recipient: Subscriber;
let user: User;
let message: Message;
let sender: Subscriber | null;
let recipient: Subscriber | null;
let user: User | null;
let message: Message | null;
let allMessages: Message[];
let allUsers: User[];
let allSubscribers: Subscriber[];
@ -129,9 +129,9 @@ describe('MessageController', () => {
subscriberService = module.get<SubscriberService>(SubscriberService);
messageController = module.get<MessageController>(MessageController);
message = await messageService.findOne({ mid: 'mid-1' });
sender = await subscriberService.findOne(message.sender);
recipient = await subscriberService.findOne(message.recipient);
user = await userService.findOne(message.sentBy);
sender = await subscriberService.findOne(message!.sender!);
recipient = await subscriberService.findOne(message!.recipient!);
user = await userService.findOne(message!.sentBy!);
allSubscribers = await subscriberService.findAll();
allUsers = await userService.findAll();
allMessages = await messageService.findAll();
@ -153,31 +153,31 @@ describe('MessageController', () => {
describe('findOne', () => {
it('should find message by id, and populate its corresponding sender and recipient', async () => {
jest.spyOn(messageService, 'findOneAndPopulate');
const result = await messageController.findOne(message.id, [
const result = await messageController.findOne(message!.id, [
'sender',
'recipient',
]);
expect(messageService.findOneAndPopulate).toHaveBeenCalledWith(
message.id,
message!.id,
);
expect(result).toEqualPayload({
...messageFixtures.find(({ mid }) => mid === message.mid),
...messageFixtures.find(({ mid }) => mid === message!.mid),
sender,
recipient,
sentBy: user.id,
sentBy: user!.id,
});
});
it('should find message by id', async () => {
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({
...messageFixtures.find(({ mid }) => mid === message.mid),
sender: sender.id,
recipient: recipient.id,
sentBy: user.id,
...messageFixtures.find(({ mid }) => mid === message!.mid),
sender: sender!.id,
recipient: recipient!.id,
sentBy: user!.id,
});
});
});
@ -189,10 +189,10 @@ describe('MessageController', () => {
const result = await messageController.findPage(pageQuery, [], {});
const messagesWithSenderAndRecipient = allMessages.map((message) => ({
...message,
sender: allSubscribers.find(({ id }) => id === message['sender']).id,
recipient: allSubscribers.find(({ id }) => id === message['recipient'])
.id,
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id,
sender: allSubscribers.find(({ id }) => id === message.sender)?.id,
recipient: allSubscribers.find(({ id }) => id === message.recipient)
?.id,
sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
}));
expect(messageService.find).toHaveBeenCalledWith({}, pageQuery);
@ -208,9 +208,9 @@ describe('MessageController', () => {
);
const messages = allMessages.map((message) => ({
...message,
sender: allSubscribers.find(({ id }) => id === message['sender']),
recipient: allSubscribers.find(({ id }) => id === message['recipient']),
sentBy: allUsers.find(({ id }) => id === message['sentBy']).id,
sender: allSubscribers.find(({ id }) => id === message.sender),
recipient: allSubscribers.find(({ id }) => id === message.recipient),
sentBy: allUsers.find(({ id }) => id === message.sentBy)?.id,
}));
expect(messageService.findAndPopulate).toHaveBeenCalledWith(

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ export class ContextVarRepository extends BaseRepository<ContextVar> {
@Optional() blockService?: BlockService,
) {
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 () => {
jest.spyOn(messageModel, 'findById');
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(
message['recipient'],
message!['recipient'],
);
const user = await userRepository.findOne(message['sentBy']);
const result = await messageRepository.findOneAndPopulate(message.id);
const user = await userRepository.findOne(message!['sentBy']);
const result = await messageRepository.findOneAndPopulate(message!.id);
expect(messageModel.findById).toHaveBeenCalledWith(message.id, undefined);
expect(messageModel.findById).toHaveBeenCalledWith(
message!.id,
undefined,
);
expect(result).toEqualPayload({
...messageFixtures.find(({ mid }) => mid === message.mid),
...messageFixtures.find(({ mid }) => mid === message!.mid),
sender,
recipient,
sentBy: user.id,
sentBy: user!.id,
});
});
});
@ -92,7 +95,7 @@ describe('MessageRepository', () => {
...message,
sender: allSubscribers.find(({ id }) => id === message['sender']),
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);

View File

@ -107,20 +107,20 @@ describe('SubscriberRepository', () => {
});
const allLabels = await labelRepository.findAll();
const result = await subscriberRepository.findOneAndPopulate(
subscriber.id,
subscriber!.id,
);
const subscriberWithLabels = {
...subscriberFixtures.find(
({ first_name }) => first_name === subscriber.first_name,
({ first_name }) => first_name === subscriber!.first_name,
),
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(
subscriber.id,
subscriber!.id,
undefined,
);
expect(result).toEqualPayload(subscriberWithLabels);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import {
IncomingMessageType,
StdOutgoingEnvelope,
} from '../schemas/types/message';
import { SubscriberContext } from '../schemas/types/subscriberContext';
import { BlockService } from './block.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
const recipient = event.getSender();
const envelope: StdOutgoingEnvelope =
await this.blockService.processMessage(
block,
context,
recipient?.context,
fallback,
conservationId,
);
const envelope = (await this.blockService.processMessage(
block,
context,
recipient?.context as SubscriberContext,
fallback,
conservationId,
)) as StdOutgoingEnvelope;
// Send message through the right channel
const response = await event
@ -252,13 +252,13 @@ export class BotService {
assign_labels: [],
trigger_labels: [],
attachedBlock: undefined,
category: undefined,
category: undefined as any,
previousBlocks: [],
};
convo.context.attempt++;
fallback = true;
} else {
convo.context.attempt = 0;
if (convo.context) convo.context.attempt = 0;
fallbackBlock = undefined;
}

View File

@ -30,7 +30,7 @@ export class ContextVarService extends BaseService<ContextVar> {
block: Block | BlockFull,
): Promise<Record<string, ContextVar>> {
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) => {
acc[cv.name] = cv;

View File

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

View File

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

View File

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

View File

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

View File

@ -51,7 +51,7 @@ export class SubscriberService extends BaseService<
@Optional() gateway?: WebsocketGateway,
) {
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:
* 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).
*/
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { Request, Response } from 'express';
import multer, { diskStorage, memoryStorage } from 'multer';
@ -231,7 +231,7 @@ export default abstract class BaseWebChannelHandler<
...message,
author: 'chatbot',
read: true, // Temporary fix as read is false in the bd
mid: anyMessage.mid,
mid: anyMessage.mid || 'DEFAULT_MID',
handover: !!anyMessage.handover,
createdAt: anyMessage.createdAt,
});
@ -519,6 +519,9 @@ export default abstract class BaseWebChannelHandler<
const fetchMessages = async (req: Request, res: Response, retrials = 1) => {
try {
if (!req.query.since)
throw new BadRequestException(`QueryParam 'since' is missing`);
const since = new Date(req.query.since.toString());
const messages = await this.pollMessages(req, since);
if (messages.length === 0 && retrials <= 5) {
@ -630,10 +633,12 @@ export default abstract class BaseWebChannelHandler<
size: Buffer.byteLength(data.file),
type: data.type,
});
next(null, {
type: Attachment.getTypeByMime(attachment.type),
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
});
if (attachment)
next(null, {
type: Attachment.getTypeByMime(attachment.type),
url: Attachment.getAttachmentUrl(attachment.id, attachment?.name),
});
} catch (err) {
this.logger.error(
'Web Channel Handler : Unable to write uploaded file',
@ -677,6 +682,12 @@ export default abstract class BaseWebChannelHandler<
size: file.size,
type: file.mimetype,
});
if (!attachment) {
this.logger.debug(
'Web Channel Handler : failed to store attachment',
);
return next(null);
}
next(null, {
type: Attachment.getTypeByMime(attachment.type),
url: Attachment.getAttachmentUrl(attachment.id, attachment.name),
@ -721,7 +732,7 @@ export default abstract class BaseWebChannelHandler<
return {
isSocket: this.isSocketRequest(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,
data: {
type: message.attachment.type,
url: message.attachment.payload.url,
url: message.attachment.payload.url!,
},
};
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;
}

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

View File

@ -9,7 +9,7 @@
import mongoose from 'mongoose';
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 { FileType } from '@/chat/schemas/types/attachment';
import { ButtonType } from '@/chat/schemas/types/button';
@ -171,7 +171,6 @@ export const blockDefaultValues: TFixturesDefaultValues<Block> = {
assign_labels: [],
trigger_labels: [],
starts_conversation: false,
attachedToBlock: null,
};
export const blockFixtures = getFixturesWithDefaultValues<Block>({

View File

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

View File

@ -13,14 +13,16 @@ type TSortProps<T> = {
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,
row2,
field = 'createdAt',
order = 'desc',
}: 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,
row2: T,
field?: keyof T,

View File

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

View File

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