fix: CMS module issues

This commit is contained in:
yassinedorbozgithub 2025-01-07 11:37:03 +01:00
parent 2910de0058
commit 0101655e33
25 changed files with 235 additions and 207 deletions

View File

@ -76,11 +76,13 @@ export class AttachmentService extends BaseService<Attachment> {
* @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<StreamableFile> {
async downloadProfilePic(
foreign_id: string,
): Promise<StreamableFile | undefined> {
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<Attachment> {
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<Attachment> {
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<Attachment> {
async store(
file: Buffer | Readable | Express.Multer.File,
metadata: AttachmentMetadataDto,
): Promise<Attachment> {
): Promise<Attachment | undefined> {
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<Attachment> {
*/
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<Attachment> {
* @param attachment - The attachment to download.
* @returns A promise that resolves to a Buffer representing the downloaded attachment.
*/
async readAsBuffer(attachment: Attachment): Promise<Buffer> {
async readAsBuffer(attachment: Attachment): Promise<Buffer | undefined> {
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');

View File

@ -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<void> {
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<void> {
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 });
}
}

View File

@ -161,7 +161,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
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, BlockPopulate, BlockFull> {
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<Block, BlockPopulate, BlockFull> {
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<Block, BlockPopulate, BlockFull> {
subscriberContext: SubscriberContext,
fallback = false,
conversationId?: string,
): Promise<StdOutgoingEnvelope> {
) {
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<Block, BlockPopulate, BlockFull> {
} 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<Block, BlockPopulate, BlockFull> {
);
// 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)}`);

View File

@ -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

View File

@ -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>(ContentTypeService);
contentService = module.get<ContentService>(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 () => {

View File

@ -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<Content>;
@ -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'));
});

View File

@ -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] }), {}),
},

View File

@ -60,9 +60,7 @@ describe('MenuController', () => {
menuService = module.get<MenuService>(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),
],
},

View File

@ -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);

View File

@ -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<string, any>;
dynamicFields: Record<string, any>;
}
export class ContentUpdateDto extends PartialType(ContentCreateDto) {}

View File

@ -68,9 +68,7 @@ describe('ContentTypeRepository', () => {
contentModel = module.get<Model<Content>>(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([]);
});

View File

@ -49,7 +49,7 @@ export class ContentTypeRepository extends BaseRepository<ContentType> {
criteria: TFilterQuery<ContentType>,
) {
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) {

View File

@ -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,
),
});
});

View File

@ -44,7 +44,7 @@ export class ContentStub extends BaseSchema {
status?: boolean;
@Prop({ type: mongoose.Schema.Types.Mixed })
dynamicFields?: Record<string, any>;
dynamicFields: Record<string, any>;
@Prop({ type: String })
rag?: string;

View File

@ -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 })

View File

@ -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 });
});
});

View File

@ -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);

View File

@ -150,7 +150,7 @@ export class ContentService extends BaseService<
async getContent(
options: ContentOptions,
skip: number,
): Promise<Omit<StdOutgoingListMessage, 'options'> | undefined> {
): Promise<Omit<StdOutgoingListMessage, 'options'>> {
let query: TFilterQuery<Content> = { status: true };
const limit = options.limit;

View File

@ -94,17 +94,20 @@ export class MenuService extends BaseService<Menu, MenuPopulate, MenuFull> {
const parents: Map<string | symbol, AnyMenu[]> = 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<Menu, MenuPopulate, MenuFull> {
parents: Map<string | symbol, AnyMenu[]>,
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:

View File

@ -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);

View File

@ -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<unknown>,
P extends string = never,
TFull extends Omit<T, P> = never,
> {

View File

@ -23,9 +23,10 @@ export class PageQueryPipe<T>
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;
}

View File

@ -29,7 +29,10 @@ export class SearchFilterPipe<T>
{
constructor(
private readonly props: {
allowedFields: TFilterNestedKeysOfType<T, string | string[]>[];
allowedFields: TFilterNestedKeysOfType<
T,
undefined | string | string[]
>[];
},
) {}
@ -45,7 +48,7 @@ export class SearchFilterPipe<T>
private isAllowedField(field: string) {
if (
this.props.allowedFields.includes(
field as TFilterNestedKeysOfType<T, string | string[]>,
field as TFilterNestedKeysOfType<T, undefined | string | string[]>,
)
)
return true;
@ -58,29 +61,39 @@ export class SearchFilterPipe<T>
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<T>
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<T>
});
}
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

View File

@ -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,
};
}),
);

View File

@ -69,7 +69,7 @@ export type TValidateProps<T, TStub> = {
dto:
| Partial<TAllowedKeys<T, TStub>>
| Partial<TAllowedKeys<T, TStub, string>>;
allowedIds: TAllowedKeys<T, TStub> & TAllowedKeys<T, TStub, string>;
allowedIds: Partial<TAllowedKeys<T, TStub> & TAllowedKeys<T, TStub, string>>;
};
//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 */