diff --git a/api/src/attachment/services/attachment.service.ts b/api/src/attachment/services/attachment.service.ts index 62305537..1757919b 100644 --- a/api/src/attachment/services/attachment.service.ts +++ b/api/src/attachment/services/attachment.service.ts @@ -76,11 +76,13 @@ export class AttachmentService extends BaseService { * @param foreign_id The unique identifier of the user, used to locate the profile picture. * @returns A `StreamableFile` containing the user's profile picture. */ - async downloadProfilePic(foreign_id: string): Promise { + async downloadProfilePic( + foreign_id: string, + ): Promise { if (this.getStoragePlugin()) { try { const pict = foreign_id + '.jpeg'; - const picture = await this.getStoragePlugin().downloadProfilePic(pict); + const picture = await this.getStoragePlugin()?.downloadProfilePic(pict); return picture; } catch (err) { this.logger.error('Error downloading profile picture', err); @@ -111,16 +113,16 @@ export class AttachmentService extends BaseService { buffer: Buffer.isBuffer(data) ? data : await data.buffer(), } as Express.Multer.File; try { - await this.getStoragePlugin().uploadAvatar(picture); + await this.getStoragePlugin()?.uploadAvatar(picture); this.logger.log( `Profile picture uploaded successfully to ${ - this.getStoragePlugin().name + this.getStoragePlugin()?.name }`, ); } catch (err) { this.logger.error( `Error while uploading profile picture to ${ - this.getStoragePlugin().name + this.getStoragePlugin()?.name }`, err, ); @@ -165,9 +167,11 @@ export class AttachmentService extends BaseService { if (this.getStoragePlugin()) { for (const file of files?.file) { - const dto = await this.getStoragePlugin().upload(file); - const uploadedFile = await this.create(dto); - uploadedFiles.push(uploadedFile); + const dto = await this.getStoragePlugin()?.upload(file); + if (dto) { + const uploadedFile = await this.create(dto); + uploadedFiles.push(uploadedFile); + } } } else { if (Array.isArray(files?.file)) { @@ -197,10 +201,10 @@ export class AttachmentService extends BaseService { async store( file: Buffer | Readable | Express.Multer.File, metadata: AttachmentMetadataDto, - ): Promise { + ): Promise { if (this.getStoragePlugin()) { - const storedDto = await this.getStoragePlugin().store(file, metadata); - return await this.create(storedDto); + const storedDto = await this.getStoragePlugin()?.store?.(file, metadata); + return storedDto ? await this.create(storedDto) : undefined; } else { const dirPath = path.join(config.parameters.uploadDir); const uniqueFilename = generateUniqueFilename(metadata.name); @@ -246,7 +250,7 @@ export class AttachmentService extends BaseService { */ async download(attachment: Attachment) { if (this.getStoragePlugin()) { - return await this.getStoragePlugin().download(attachment); + return await this.getStoragePlugin()?.download(attachment); } else { if (!fileExists(attachment.location)) { throw new NotFoundException('No file was found'); @@ -275,9 +279,9 @@ export class AttachmentService extends BaseService { * @param attachment - The attachment to download. * @returns A promise that resolves to a Buffer representing the downloaded attachment. */ - async readAsBuffer(attachment: Attachment): Promise { + async readAsBuffer(attachment: Attachment): Promise { if (this.getStoragePlugin()) { - return await this.getStoragePlugin().readAsBuffer(attachment); + return await this.getStoragePlugin()?.readAsBuffer?.(attachment); } else { if (!fileExists(attachment.location)) { throw new NotFoundException('No file was found'); diff --git a/api/src/chat/repositories/block.repository.ts b/api/src/chat/repositories/block.repository.ts index 11987943..4e8f02c9 100644 --- a/api/src/chat/repositories/block.repository.ts +++ b/api/src/chat/repositories/block.repository.ts @@ -56,7 +56,7 @@ export class BlockRepository extends BaseRepository< block.message.attachment.payload && 'url' in block.message.attachment.payload ) { - this.logger.error( + this.logger?.error( 'NOTE: `url` payload has been deprecated in favor of `attachment_id`', block.name, ); @@ -97,7 +97,7 @@ export class BlockRepository extends BaseRepository< const update: BlockUpdateDto = updates?.['$set']; if (update?.category) { - const movedBlock: Block = await this.findOne(criteria); + const movedBlock = await this.findOne(criteria); if (!movedBlock) { return; @@ -178,14 +178,14 @@ export class BlockRepository extends BaseRepository< ids: string[], ): Promise { for (const id of ids) { - const oldState: Block = await this.findOne(id); - if (oldState.category !== category) { - const updatedNextBlocks = oldState.nextBlocks.filter((nextBlock) => + const oldState = await this.findOne(id); + if (oldState?.category !== category) { + const updatedNextBlocks = oldState?.nextBlocks?.filter((nextBlock) => ids.includes(nextBlock), ); - const updatedAttachedBlock = ids.includes(oldState.attachedBlock || '') - ? oldState.attachedBlock + const updatedAttachedBlock = ids.includes(oldState?.attachedBlock || '') + ? oldState?.attachedBlock : null; await this.updateOne(id, { @@ -209,15 +209,15 @@ export class BlockRepository extends BaseRepository< ids: string[], ): Promise { for (const block of otherBlocks) { - if (ids.includes(block.attachedBlock)) { + if (ids.includes(block.attachedBlock || '')) { await this.updateOne(block.id, { attachedBlock: null }); } - const nextBlocks = block.nextBlocks.filter( + const nextBlocks = block.nextBlocks?.filter( (nextBlock) => !ids.includes(nextBlock), ); - if (nextBlocks.length > 0) { + if (nextBlocks?.length) { await this.updateOne(block.id, { nextBlocks }); } } diff --git a/api/src/chat/services/block.service.ts b/api/src/chat/services/block.service.ts index e3dd3eaa..0b2713cf 100644 --- a/api/src/chat/services/block.service.ts +++ b/api/src/chat/services/block.service.ts @@ -161,7 +161,7 @@ export class BlockService extends BaseService { payload: string | Payload, block: BlockFull | Block, ): PayloadPattern | undefined { - const payloadPatterns = block.patterns.filter( + const payloadPatterns = block.patterns?.filter( (p) => typeof p === 'object' && 'label' in p, ) as PayloadPattern[]; @@ -190,57 +190,56 @@ export class BlockService extends BaseService { block: Block | BlockFull, ): (RegExpMatchArray | string)[] | false { // Filter text patterns & Instanciate Regex patterns - const patterns: (string | RegExp | Pattern)[] = block.patterns.map( - (pattern) => { - if ( - typeof pattern === 'string' && - pattern.endsWith('/') && - pattern.startsWith('/') - ) { - return new RegExp(pattern.slice(1, -1), 'i'); - } - return pattern; - }, - ); - - // Return first match - for (let i = 0; i < patterns.length; i++) { - const pattern = patterns[i]; - if (pattern instanceof RegExp) { - if (pattern.test(text)) { - const matches = text.match(pattern); - if (matches) { - if (matches.length >= 2) { - // Remove global match if needed - matches.shift(); - } - return matches; - } - } - continue; - } else if ( - typeof pattern === 'object' && - 'label' in pattern && - text.trim().toLowerCase() === pattern.label.toLowerCase() - ) { - // Payload (quick reply) - return [text]; - } else if ( + const patterns: undefined | Pattern[] = block.patterns?.map((pattern) => { + if ( typeof pattern === 'string' && - text.trim().toLowerCase() === pattern.toLowerCase() + pattern.endsWith('/') && + pattern.startsWith('/') ) { - // Equals - return [text]; + return new RegExp(pattern.slice(1, -1), 'i'); + } + return pattern; + }); + + if (patterns?.length) + // Return first match + for (let i = 0; i < patterns.length; i++) { + const pattern = patterns[i]; + if (pattern instanceof RegExp) { + if (pattern.test(text)) { + const matches = text.match(pattern); + if (matches) { + if (matches.length >= 2) { + // Remove global match if needed + matches.shift(); + } + return matches; + } + } + continue; + } else if ( + typeof pattern === 'object' && + 'label' in pattern && + text.trim().toLowerCase() === pattern.label.toLowerCase() + ) { + // Payload (quick reply) + return [text]; + } else if ( + typeof pattern === 'string' && + text.trim().toLowerCase() === pattern.toLowerCase() + ) { + // Equals + return [text]; + } + // @deprecated + // else if ( + // typeof pattern === 'string' && + // Soundex(text) === Soundex(pattern) + // ) { + // // Sound like + // return [text]; + // } } - // @deprecated - // else if ( - // typeof pattern === 'string' && - // Soundex(text) === Soundex(pattern) - // ) { - // // Sound like - // return [text]; - // } - } // No match return false; } @@ -262,7 +261,7 @@ export class BlockService extends BaseService { return undefined; } - const nlpPatterns = block.patterns.filter((p) => { + const nlpPatterns = block.patterns?.filter((p) => { return Array.isArray(p); }) as NlpPattern[][]; @@ -437,10 +436,10 @@ export class BlockService extends BaseService { subscriberContext: SubscriberContext, fallback = false, conversationId?: string, - ): Promise { + ) { const settings = await this.settingService.getSettings(); const blockMessage: BlockMessage = - fallback && block.options.fallback + fallback && block.options?.fallback ? [...block.options.fallback.message] : Array.isArray(block.message) ? [...block.message] @@ -557,7 +556,7 @@ export class BlockService extends BaseService { } else if ( blockMessage && 'elements' in blockMessage && - block.options.content + block.options?.content ) { const contentBlockOptions = block.options.content; // Hadnle pagination for list/carousel @@ -599,7 +598,7 @@ export class BlockService extends BaseService { ); // Process custom plugin block try { - return await plugin.process(block, context, conversationId); + return await plugin?.process(block, context, conversationId); } catch (e) { this.logger.error('Plugin was unable to load/process ', e); throw new Error(`Unknown plugin - ${JSON.stringify(blockMessage)}`); diff --git a/api/src/chat/services/bot.service.ts b/api/src/chat/services/bot.service.ts index 94e2e31e..e10ec508 100644 --- a/api/src/chat/services/bot.service.ts +++ b/api/src/chat/services/bot.service.ts @@ -74,7 +74,7 @@ export class BotService { await this.blockService.processMessage( block, context, - recipient.context, + recipient?.context, fallback, conservationId, ); @@ -112,7 +112,7 @@ export class BotService { // Apply updates : Assign block labels to user const blockLabels = (block.assign_labels || []).map(({ id }) => id); - const assignTo = block.options.assignTo || null; + const assignTo = block.options?.assignTo || null; await this.subscriberService.applyUpdates( event.getSender(), blockLabels, @@ -223,13 +223,12 @@ export class BotService { _id: { $in: nextIds }, }); let fallback = false; - const fallbackOptions = - convo.current && convo.current.options.fallback - ? convo.current.options.fallback - : { - active: false, - max_attempts: 0, - }; + const fallbackOptions = convo.current?.options?.fallback + ? convo.current.options.fallback + : { + active: false, + max_attempts: 0, + }; // Find the next block that matches const matchedBlock = await this.blockService.match(nextBlocks, event); @@ -240,7 +239,8 @@ export class BotService { !matchedBlock && event.getMessageType() === IncomingMessageType.message && fallbackOptions.active && - convo.context.attempt < fallbackOptions.max_attempts + convo.context?.attempt && + convo.context?.attempt < fallbackOptions.max_attempts ) { // Trigger block fallback // NOTE : current is not populated, this may cause some anomaly diff --git a/api/src/cms/controllers/content-type.controller.spec.ts b/api/src/cms/controllers/content-type.controller.spec.ts index aef9d0cf..29d7646c 100644 --- a/api/src/cms/controllers/content-type.controller.spec.ts +++ b/api/src/cms/controllers/content-type.controller.spec.ts @@ -39,7 +39,7 @@ describe('ContentTypeController', () => { let contentTypeController: ContentTypeController; let contentTypeService: ContentTypeService; let contentService: ContentService; - let contentType: ContentType; + let contentType: ContentType | null; let blockService: BlockService; beforeAll(async () => { @@ -76,12 +76,10 @@ describe('ContentTypeController', () => { ); contentTypeService = module.get(ContentTypeService); contentService = module.get(ContentService); - contentType = await contentTypeService.findOne({ name: 'Product' }); + contentType = await contentTypeService.findOne({ name: 'Product' })!; }); - afterAll(async () => { - await closeInMongodConnection(); - }); + afterAll(closeInMongodConnection); afterEach(jest.clearAllMocks); @@ -138,10 +136,10 @@ describe('ContentTypeController', () => { describe('findOne', () => { it('should find a content type by id', async () => { jest.spyOn(contentTypeService, 'findOne'); - const result = await contentTypeController.findOne(contentType.id); - expect(contentTypeService.findOne).toHaveBeenCalledWith(contentType.id); + const result = await contentTypeController.findOne(contentType!.id); + expect(contentTypeService.findOne).toHaveBeenCalledWith(contentType!.id); expect(result).toEqualPayload( - contentTypeFixtures.find(({ name }) => name === 'Product'), + contentTypeFixtures.find(({ name }) => name === 'Product')!, ); }); @@ -160,10 +158,10 @@ describe('ContentTypeController', () => { jest.spyOn(contentTypeService, 'updateOne'); const result = await contentTypeController.updateOne( updatedContent, - contentType.id, + contentType!.id, ); expect(contentTypeService.updateOne).toHaveBeenCalledWith( - contentType.id, + contentType!.id, updatedContent, ); expect(result).toEqualPayload({ @@ -190,17 +188,19 @@ describe('ContentTypeController', () => { const contentType = await contentTypeService.findOne({ name: 'Restaurant', }); - const result = await contentTypeController.deleteOne(contentType.id); + const result = await contentTypeController.deleteOne(contentType!.id); expect(contentTypeService.deleteCascadeOne).toHaveBeenCalledWith( - contentType.id, + contentType!.id, ); expect(result).toEqual({ acknowledged: true, deletedCount: 1 }); await expect( - contentTypeController.findOne(contentType.id), + contentTypeController.findOne(contentType!.id), ).rejects.toThrow(NotFoundException); - expect(await contentService.find({ entity: contentType.id })).toEqual([]); + expect(await contentService.find({ entity: contentType!.id })).toEqual( + [], + ); }); it('should throw NotFoundException if the content type is not found', async () => { diff --git a/api/src/cms/controllers/content.controller.spec.ts b/api/src/cms/controllers/content.controller.spec.ts index 53bb39cd..c6958345 100644 --- a/api/src/cms/controllers/content.controller.spec.ts +++ b/api/src/cms/controllers/content.controller.spec.ts @@ -15,8 +15,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AttachmentRepository } from '@/attachment/repositories/attachment.repository'; import { - AttachmentModel, Attachment, + AttachmentModel, } from '@/attachment/schemas/attachment.schema'; import { AttachmentService } from '@/attachment/services/attachment.service'; import { LoggerService } from '@/logger/logger.service'; @@ -48,9 +48,9 @@ describe('ContentController', () => { let contentService: ContentService; let contentTypeService: ContentTypeService; let attachmentService: AttachmentService; - let contentType: ContentType; - let content: Content; - let attachment: Attachment; + let contentType: ContentType | null; + let content: Content | null; + let attachment: Attachment | null; let updatedContent; let pageQuery: PageQueryDto; @@ -94,21 +94,19 @@ describe('ContentController', () => { }); }); - afterAll(async () => { - await closeInMongodConnection(); - }); + afterAll(closeInMongodConnection); afterEach(jest.clearAllMocks); describe('findOne', () => { it('should find content by ID', async () => { - const contentType = await contentTypeService.findOne(content.entity); + const contentType = await contentTypeService.findOne(content!.entity); jest.spyOn(contentService, 'findOne'); - const result = await contentController.findOne(content.id, []); - expect(contentService.findOne).toHaveBeenCalledWith(content.id); + const result = await contentController.findOne(content!.id, []); + expect(contentService.findOne).toHaveBeenCalledWith(content!.id); expect(result).toEqualPayload({ ...contentFixtures.find(({ title }) => title === 'Jean'), - entity: contentType.id, + entity: contentType!.id, }); }); @@ -121,8 +119,8 @@ describe('ContentController', () => { }); it('should find content by ID and populate its corresponding content type', async () => { - const result = await contentController.findOne(content.id, ['entity']); - const contentType = await contentTypeService.findOne(content.entity); + const result = await contentController.findOne(content!.id, ['entity']); + const contentType = await contentTypeService.findOne(content!.entity); expect(result).toEqualPayload({ ...contentFixtures.find(({ title }) => title === 'Jean'), @@ -137,7 +135,7 @@ describe('ContentController', () => { expect(result).toEqualPayload([ { ...contentFixtures.find(({ title }) => title === 'Jean'), - entity: contentType.id, + entity: contentType!.id, }, ]); }); @@ -160,12 +158,14 @@ describe('ContentController', () => { describe('findByType', () => { it('should find contents by content type', async () => { const result = await contentController.findByType( - contentType.id, + contentType!.id, pageQuery, ); const contents = contentFixtures.filter(({ entity }) => entity === '0'); contents.reduce((acc, curr) => { - curr['entity'] = contentType.id; + if (contentType?.id) { + curr['entity'] = contentType.id; + } return acc; }, []); expect(result).toEqualPayload([contents[0]]); @@ -174,15 +174,15 @@ describe('ContentController', () => { describe('update', () => { it('should update and return the updated content', async () => { - const contentType = await contentTypeService.findOne(content.entity); + const contentType = await contentTypeService.findOne(content!.entity); updatedContent = { ...contentFixtures.find(({ title }) => title === 'Jean'), - entity: contentType.id, + entity: contentType!.id, title: 'modified Jean', }; const result = await contentController.updateOne( updatedContent, - content.id, + content!.id, ); expect(result).toEqualPayload(updatedContent, [ @@ -201,7 +201,7 @@ describe('ContentController', () => { describe('deleteOne', () => { it('should delete an existing Content', async () => { const content = await contentService.findOne({ title: 'Adaptateur' }); - const result = await contentService.deleteOne(content.id); + const result = await contentService.deleteOne(content!.id); expect(result).toEqual({ acknowledged: true, deletedCount: 1 }); }); }); @@ -219,7 +219,7 @@ describe('ContentController', () => { }, }, }, - entity: contentType.id, + entity: contentType!.id, status: true, }; jest.spyOn(contentService, 'create'); @@ -263,18 +263,18 @@ should not appear,store 3,true,image.jpg`; }); const result = await contentController.import({ - idFileToImport: attachment.id, - idTargetContentType: contentType.id, + idFileToImport: attachment!.id, + idTargetContentType: contentType!.id, }); expect(contentService.createMany).toHaveBeenCalledWith([ - { ...mockCsvContentDto, entity: contentType.id }, + { ...mockCsvContentDto, entity: contentType!.id }, ]); expect(result).toEqualPayload( [ { ...mockCsvContentDto, - entity: contentType.id, + entity: contentType!.id, }, ], [...IGNORED_TEST_FIELDS, 'rag'], @@ -284,7 +284,7 @@ should not appear,store 3,true,image.jpg`; it('should throw NotFoundException if content type is not found', async () => { await expect( contentController.import({ - idFileToImport: attachment.id, + idFileToImport: attachment!.id, idTargetContentType: NOT_FOUND_ID, }), ).rejects.toThrow(new NotFoundException('Content type is not found')); @@ -298,7 +298,7 @@ should not appear,store 3,true,image.jpg`; await expect( contentController.import({ idFileToImport: NOT_FOUND_ID, - idTargetContentType: contentType.id.toString(), + idTargetContentType: contentType!.id.toString(), }), ).rejects.toThrow(new NotFoundException('File does not exist')); }); @@ -307,8 +307,8 @@ should not appear,store 3,true,image.jpg`; jest.spyOn(fs, 'existsSync').mockReturnValue(false); await expect( contentController.import({ - idFileToImport: attachment.id, - idTargetContentType: contentType.id, + idFileToImport: attachment!.id, + idTargetContentType: contentType!.id, }), ).rejects.toThrow(new NotFoundException('File does not exist')); }); diff --git a/api/src/cms/controllers/content.controller.ts b/api/src/cms/controllers/content.controller.ts index 5f4bf0c5..a4903bec 100644 --- a/api/src/cms/controllers/content.controller.ts +++ b/api/src/cms/controllers/content.controller.ts @@ -131,7 +131,7 @@ export class ContentController extends BaseController< ? path.join(config.parameters.uploadDir, file.location) : undefined; - if (!file || !fs.existsSync(filePath)) { + if (!file || !filePath || !fs.existsSync(filePath)) { this.logger.warn(`Failed to find file type with id ${fileToImport}.`); throw new NotFoundException(`File does not exist`); } @@ -159,12 +159,12 @@ export class ContentController extends BaseController< (acc, { title, status, ...rest }) => [ ...acc, { - title, - status, + title: String(title), + status: Boolean(status), entity: targetContentType, dynamicFields: Object.keys(rest) .filter((key) => - contentType.fields.map((field) => field.name).includes(key), + contentType.fields?.map((field) => field.name).includes(key), ) .reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}), }, diff --git a/api/src/cms/controllers/menu.controller.spec.ts b/api/src/cms/controllers/menu.controller.spec.ts index 5c2cf0a3..37464af3 100644 --- a/api/src/cms/controllers/menu.controller.spec.ts +++ b/api/src/cms/controllers/menu.controller.spec.ts @@ -60,9 +60,7 @@ describe('MenuController', () => { menuService = module.get(MenuService); }); - afterAll(async () => { - await closeInMongodConnection(); - }); + afterAll(closeInMongodConnection); afterEach(jest.clearAllMocks); describe('create', () => { @@ -81,7 +79,7 @@ describe('MenuController', () => { const websiteMenu = await menuService.findOne({ title: websiteMenuFixture.title, }); - const search = await menuController.findOne(websiteMenu.id); + const search = await menuController.findOne(websiteMenu!.id); expect(search).toEqualPayload(websiteMenuFixture); }); }); @@ -98,12 +96,12 @@ describe('MenuController', () => { const offersEntry = await menuService.findOne({ title: offerMenuFixture.title, }); - await menuController.delete(offersEntry.id); + await menuController.delete(offersEntry!.id); const offersChildren = await menuService.find({ title: { $in: [ - offersEntry.title, + offersEntry!.title, ...offersMenuFixtures.map((menu) => menu.title), ], }, diff --git a/api/src/cms/controllers/menu.controller.ts b/api/src/cms/controllers/menu.controller.ts index f2343d1d..cf9cc164 100644 --- a/api/src/cms/controllers/menu.controller.ts +++ b/api/src/cms/controllers/menu.controller.ts @@ -94,7 +94,9 @@ export class MenuController extends BaseController< this.validate({ dto: body, allowedIds: { - parent: (await this.menuService.findOne(body.parent))?.id, + parent: body?.parent + ? (await this.menuService.findOne(body.parent))?.id + : undefined, }, }); return await this.menuService.create(body); diff --git a/api/src/cms/dto/content.dto.ts b/api/src/cms/dto/content.dto.ts index be795f12..ffa8dca9 100644 --- a/api/src/cms/dto/content.dto.ts +++ b/api/src/cms/dto/content.dto.ts @@ -7,7 +7,7 @@ */ import { ApiProperty, ApiPropertyOptional, PartialType } from '@nestjs/swagger'; -import { IsString, IsBoolean, IsNotEmpty, IsOptional } from 'class-validator'; +import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; import { IsObjectId } from '@/utils/validation-rules/is-object-id'; @@ -30,7 +30,7 @@ export class ContentCreateDto { @ApiPropertyOptional({ description: 'Content dynamic fields', type: Object }) @IsOptional() - dynamicFields?: Record; + dynamicFields: Record; } export class ContentUpdateDto extends PartialType(ContentCreateDto) {} diff --git a/api/src/cms/repositories/content-type.repository.spec.ts b/api/src/cms/repositories/content-type.repository.spec.ts index 95b7ab8d..cb8f380a 100644 --- a/api/src/cms/repositories/content-type.repository.spec.ts +++ b/api/src/cms/repositories/content-type.repository.spec.ts @@ -68,9 +68,7 @@ describe('ContentTypeRepository', () => { contentModel = module.get>(getModelToken('Content')); }); - afterAll(async () => { - await closeInMongodConnection(); - }); + afterAll(closeInMongodConnection); afterEach(jest.clearAllMocks); @@ -78,10 +76,10 @@ describe('ContentTypeRepository', () => { it('should delete a contentType by id if no associated block was found', async () => { jest.spyOn(blockService, 'findOne').mockResolvedValueOnce(null); const contentType = await contentTypeModel.findOne({ name: 'Store' }); - const result = await contentTypeRepository.deleteOne(contentType.id); + const result = await contentTypeRepository.deleteOne(contentType!.id); expect(result).toEqual({ acknowledged: true, deletedCount: 1 }); const contents = await contentModel.find({ - entity: contentType.id, + entity: contentType!.id, }); expect(contents).toEqual([]); }); diff --git a/api/src/cms/repositories/content-type.repository.ts b/api/src/cms/repositories/content-type.repository.ts index a3b465c6..9548e925 100644 --- a/api/src/cms/repositories/content-type.repository.ts +++ b/api/src/cms/repositories/content-type.repository.ts @@ -49,7 +49,7 @@ export class ContentTypeRepository extends BaseRepository { criteria: TFilterQuery, ) { const entityId: string = criteria._id as string; - const associatedBlocks = await this.blockService.findOne({ + const associatedBlocks = await this.blockService?.findOne({ 'options.content.entity': entityId, }); if (associatedBlocks) { diff --git a/api/src/cms/repositories/content.repository.spec.ts b/api/src/cms/repositories/content.repository.spec.ts index 2c80c2e0..9c26495f 100644 --- a/api/src/cms/repositories/content.repository.spec.ts +++ b/api/src/cms/repositories/content.repository.spec.ts @@ -51,23 +51,23 @@ describe('ContentRepository', () => { ); }); - afterAll(async () => { - await closeInMongodConnection(); - }); + afterAll(closeInMongodConnection); afterEach(jest.clearAllMocks); describe('findOneAndPopulate', () => { it('should find a content and populate its content type', async () => { const findSpy = jest.spyOn(contentModel, 'findById'); - const content = await contentModel.findOne({ title: 'Jean' }); - const contentType = await contentTypeModel.findById(content.entity); - const result = await contentRepository.findOneAndPopulate(content.id); - expect(findSpy).toHaveBeenCalledWith(content.id, undefined); + const content = await contentModel.findOne({ + title: 'Jean', + }); + const contentType = await contentTypeModel.findById(content!.entity); + const result = await contentRepository.findOneAndPopulate(content!.id); + expect(findSpy).toHaveBeenCalledWith(content!.id, undefined); expect(result).toEqualPayload({ ...contentFixtures.find(({ title }) => title === 'Jean'), entity: contentTypeFixtures.find( - ({ name }) => name === contentType.name, + ({ name }) => name === contentType?.name, ), }); }); diff --git a/api/src/cms/schemas/content.schema.ts b/api/src/cms/schemas/content.schema.ts index 16a2cd06..97088bc8 100644 --- a/api/src/cms/schemas/content.schema.ts +++ b/api/src/cms/schemas/content.schema.ts @@ -44,7 +44,7 @@ export class ContentStub extends BaseSchema { status?: boolean; @Prop({ type: mongoose.Schema.Types.Mixed }) - dynamicFields?: Record; + dynamicFields: Record; @Prop({ type: String }) rag?: string; diff --git a/api/src/cms/schemas/menu.schema.ts b/api/src/cms/schemas/menu.schema.ts index 125533e7..18bed2b6 100644 --- a/api/src/cms/schemas/menu.schema.ts +++ b/api/src/cms/schemas/menu.schema.ts @@ -59,7 +59,7 @@ export class MenuStub extends BaseSchema { @Schema({ timestamps: true }) export class Menu extends MenuStub { @Transform(({ obj }) => obj.parent?.toString()) - parent?: string; + parent?: string | undefined; } @Schema({ timestamps: true }) diff --git a/api/src/cms/services/content-type.service.spec.ts b/api/src/cms/services/content-type.service.spec.ts index a2f157bf..b8c529d7 100644 --- a/api/src/cms/services/content-type.service.spec.ts +++ b/api/src/cms/services/content-type.service.spec.ts @@ -84,9 +84,12 @@ describe('ContentTypeService', () => { ); jest.spyOn(blockService, 'findOne').mockResolvedValueOnce(null); const contentType = await contentTypeService.findOne({ name: 'Product' }); - const result = await contentTypeService.deleteCascadeOne(contentType.id); - expect(deleteContentTypeSpy).toHaveBeenCalledWith(contentType.id); - expect(await contentService.find({ entity: contentType.id })).toEqual([]); + + const result = await contentTypeService.deleteCascadeOne(contentType!.id); + expect(deleteContentTypeSpy).toHaveBeenCalledWith(contentType!.id); + expect(await contentService.find({ entity: contentType!.id })).toEqual( + [], + ); expect(result).toEqual({ acknowledged: true, deletedCount: 1 }); }); }); diff --git a/api/src/cms/services/content.service.spec.ts b/api/src/cms/services/content.service.spec.ts index 9003276d..696732cd 100644 --- a/api/src/cms/services/content.service.spec.ts +++ b/api/src/cms/services/content.service.spec.ts @@ -82,9 +82,10 @@ describe('ContentService', () => { it('should return a content and populate its corresponding content type', async () => { const findSpy = jest.spyOn(contentRepository, 'findOneAndPopulate'); const content = await contentService.findOne({ title: 'Jean' }); - const contentType = await contentTypeService.findOne(content.entity); - const result = await contentService.findOneAndPopulate(content.id); - expect(findSpy).toHaveBeenCalledWith(content.id, undefined); + + const contentType = await contentTypeService.findOne(content!.entity); + const result = await contentService.findOneAndPopulate(content!.id); + expect(findSpy).toHaveBeenCalledWith(content!.id, undefined); expect(result).toEqualPayload({ ...contentFixtures.find(({ title }) => title === 'Jean'), entity: contentType, @@ -235,14 +236,14 @@ describe('ContentService', () => { it('should get content for a specific entity', async () => { const contentType = await contentTypeService.findOne({ name: 'Product' }); const actualData = await contentService.findPage( - { status: true, entity: contentType.id }, + { status: true, entity: contentType!.id }, { skip: 0, limit: 10, sort: ['createdAt', 'desc'] }, ); const flattenedElements = actualData.map(Content.toElement); const content = await contentService.getContent( { ...contentOptions, - entity: contentType.id, + entity: contentType!.id, }, 0, ); @@ -253,7 +254,7 @@ describe('ContentService', () => { contentOptions.entity = 1; const contentType = await contentTypeService.findOne({ name: 'Product' }); const actualData = await contentService.findPage( - { status: true, entity: contentType.id, title: /^Jean/ }, + { status: true, entity: contentType!.id, title: /^Jean/ }, { skip: 0, limit: 10, sort: ['createdAt', 'desc'] }, ); const flattenedElements = actualData.map(Content.toElement); diff --git a/api/src/cms/services/content.service.ts b/api/src/cms/services/content.service.ts index 0ce0eace..f17ecfcb 100644 --- a/api/src/cms/services/content.service.ts +++ b/api/src/cms/services/content.service.ts @@ -150,7 +150,7 @@ export class ContentService extends BaseService< async getContent( options: ContentOptions, skip: number, - ): Promise | undefined> { + ): Promise> { let query: TFilterQuery = { status: true }; const limit = options.limit; diff --git a/api/src/cms/services/menu.service.ts b/api/src/cms/services/menu.service.ts index 2da4e090..c55cd099 100644 --- a/api/src/cms/services/menu.service.ts +++ b/api/src/cms/services/menu.service.ts @@ -94,17 +94,20 @@ export class MenuService extends BaseService { const parents: Map = new Map(); parents.set(this.RootSymbol, []); - menuItems.forEach((m) => { - const menuParent = m.parent?.toString(); - if (!m.parent) { - parents.get(this.RootSymbol).push(m); + menuItems.forEach((menuItem) => { + const menuParent = menuItem.parent?.toString(); + if (!menuItem.parent) { + parents.get(this.RootSymbol)!.push(menuItem); return; } - if (parents.has(menuParent)) { - parents.get(menuParent).push(m); - return; + if (menuParent) { + if (parents.has(menuParent)) { + parents.get(menuParent)!.push(menuItem); + return; + } + + parents.set(menuParent, [menuItem]); } - parents.set(menuParent, [m]); }); return parents; @@ -122,8 +125,9 @@ export class MenuService extends BaseService { parents: Map, parent: string | symbol = this.RootSymbol, ): MenuTree { - if (!parents.has(parent)) return undefined; - const children: MenuTree = parents.get(parent).map((menu) => { + const item = parents.get(parent); + if (!item) return []; + const children: MenuTree = item.map((menu) => { return { ...menu, call_to_actions: diff --git a/api/src/cms/utilities/verifyTree.ts b/api/src/cms/utilities/verifyTree.ts index c05be335..88520b57 100644 --- a/api/src/cms/utilities/verifyTree.ts +++ b/api/src/cms/utilities/verifyTree.ts @@ -33,7 +33,7 @@ const verifyMenu = ( return true; }; -export const verifyTree = (menuTree: MenuTree) => { +export const verifyTree = (menuTree?: MenuTree) => { if (!Array.isArray(menuTree)) return true; return !menuTree.some((v) => { const valid = verifyMenu(v); diff --git a/api/src/utils/generics/base-seeder.ts b/api/src/utils/generics/base-seeder.ts index 9dc561d3..139d1d2c 100644 --- a/api/src/utils/generics/base-seeder.ts +++ b/api/src/utils/generics/base-seeder.ts @@ -6,11 +6,13 @@ * 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 { FlattenMaps } from 'mongoose'; + import { BaseRepository } from './base-repository'; import { BaseSchema } from './base-schema'; export abstract class BaseSeeder< - T, + T extends FlattenMaps, P extends string = never, TFull extends Omit = never, > { diff --git a/api/src/utils/pagination/pagination-query.pipe.ts b/api/src/utils/pagination/pagination-query.pipe.ts index c69076a1..82716f8c 100644 --- a/api/src/utils/pagination/pagination-query.pipe.ts +++ b/api/src/utils/pagination/pagination-query.pipe.ts @@ -23,9 +23,10 @@ export class PageQueryPipe let skip: number | undefined = undefined; let limit: number | undefined = undefined; if ('limit' in value) { - skip = parseInt(value.skip) > -1 ? parseInt(value.skip) : 0; + skip = + value?.skip && parseInt(value.skip) > -1 ? parseInt(value.skip) : 0; limit = - parseInt(value.limit) > 0 + value?.limit && parseInt(value.limit) > 0 ? parseInt(value.limit) : config.pagination.limit; } diff --git a/api/src/utils/pipes/search-filter.pipe.ts b/api/src/utils/pipes/search-filter.pipe.ts index 15d266f7..64221585 100644 --- a/api/src/utils/pipes/search-filter.pipe.ts +++ b/api/src/utils/pipes/search-filter.pipe.ts @@ -29,7 +29,10 @@ export class SearchFilterPipe { constructor( private readonly props: { - allowedFields: TFilterNestedKeysOfType[]; + allowedFields: TFilterNestedKeysOfType< + T, + undefined | string | string[] + >[]; }, ) {} @@ -45,7 +48,7 @@ export class SearchFilterPipe private isAllowedField(field: string) { if ( this.props.allowedFields.includes( - field as TFilterNestedKeysOfType, + field as TFilterNestedKeysOfType, ) ) return true; @@ -58,29 +61,39 @@ export class SearchFilterPipe if (Types.ObjectId.isValid(String(val))) { return { _operator: 'eq', - [field === 'id' ? '_id' : field]: this.getNullableValue(String(val)), + data: { + [field === 'id' ? '_id' : field]: this.getNullableValue( + String(val), + ), + }, }; } return {}; - } else if (val['contains'] || val[field]?.['contains']) { + } else if (val?.['contains'] || val?.[field]?.['contains']) { return { _operator: 'iLike', - [field]: this.getRegexValue( - String(val['contains'] || val[field]['contains']), - ), + data: { + [field]: this.getRegexValue( + String(val['contains'] || val[field]['contains']), + ), + }, }; - } else if (val['!=']) { + } else if (val?.['!=']) { return { _operator: 'neq', - [field]: this.getNullableValue(val['!=']), + data: { + [field]: this.getNullableValue(val['!=']), + }, }; } return { _operator: 'eq', - [field]: Array.isArray(val) - ? val.map((v) => this.getNullableValue(v)).filter((v) => v) - : this.getNullableValue(String(val)), + data: { + [field]: Array.isArray(val) + ? val.map((v) => this.getNullableValue(v)).filter((v) => v) + : this.getNullableValue(String(val)), + }, }; } @@ -90,10 +103,11 @@ export class SearchFilterPipe if (whereParams?.['or']) { Object.values(whereParams['or']) - .filter((val) => this.isAllowedField(Object.keys(val)[0])) + .filter((val) => val && this.isAllowedField(Object.keys(val)[0])) .map((val) => { + if (!val) return false; const [field] = Object.keys(val); - const filter = this.transformField(field, val[field]); + const filter = this.transformField(field, val?.[field]); if (filter._operator) filters.push({ ...filter, @@ -119,24 +133,24 @@ export class SearchFilterPipe }); } - return filters.reduce((acc, { _context, _operator, ...filter }) => { + return filters.reduce((acc, { _context, _operator, data, ...filter }) => { switch (_operator) { case 'neq': return { ...acc, - $nor: [...(acc?.$nor || []), filter], + $nor: [...(acc?.$nor || []), { ...filter, ...data }], }; default: switch (_context) { case 'or': return { ...acc, - $or: [...(acc?.$or || []), filter], + $or: [...(acc?.$or || []), { ...filter, ...data }], }; case 'and': return { ...acc, - $and: [...(acc?.$and || []), filter], + $and: [...(acc?.$and || []), { ...filter, ...data }], }; default: return acc; // Handle any other cases if necessary diff --git a/api/src/utils/test/fixtures/menu.ts b/api/src/utils/test/fixtures/menu.ts index 119222c1..04af7b6d 100644 --- a/api/src/utils/test/fixtures/menu.ts +++ b/api/src/utils/test/fixtures/menu.ts @@ -108,7 +108,7 @@ export const installMenuFixtures = async () => { const offerDocs = await Menu.insertMany( offersMenuFixtures.map((m) => ({ ...m, - parent: docs[parseInt(m.parent)].id, + parent: m.parent && docs[parseInt(m.parent)].id, })), ); @@ -117,7 +117,7 @@ export const installMenuFixtures = async () => { await Menu.insertMany( devicesMenuFixtures.map((m) => ({ ...m, - parent: allDocs[parseInt(m.parent)].id, + parent: m.parent && allDocs[parseInt(m.parent)].id, })), ); @@ -125,7 +125,7 @@ export const installMenuFixtures = async () => { accountMenuFixtures.map((m) => { return { ...m, - parent: docs[parseInt(m.parent)].id, + parent: m.parent && docs[parseInt(m.parent)].id, }; }), ); diff --git a/api/src/utils/types/filter.types.ts b/api/src/utils/types/filter.types.ts index ebeff1ff..fd14b748 100644 --- a/api/src/utils/types/filter.types.ts +++ b/api/src/utils/types/filter.types.ts @@ -69,7 +69,7 @@ export type TValidateProps = { dto: | Partial> | Partial>; - allowedIds: TAllowedKeys & TAllowedKeys; + allowedIds: Partial & TAllowedKeys>; }; //populate types @@ -105,10 +105,12 @@ type TOperator = 'eq' | 'iLike' | 'neq'; type TContext = 'and' | 'or'; export type TTransformFieldProps = { - [x: string]: string | RegExp | string[]; _id?: string; _context?: TContext; _operator?: TOperator; + data?: { + [x: string]: undefined | string | RegExp | (string | undefined)[]; + }; }; /* mongoose */