mirror of
https://github.com/hexastack/hexabot
synced 2025-04-29 02:33:00 +00:00
fix: CMS module issues
This commit is contained in:
parent
2910de0058
commit
0101655e33
@ -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');
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
@ -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)}`);
|
||||
|
@ -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
|
||||
|
@ -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 () => {
|
||||
|
@ -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'));
|
||||
});
|
||||
|
@ -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] }), {}),
|
||||
},
|
||||
|
@ -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),
|
||||
],
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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) {}
|
||||
|
@ -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([]);
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
),
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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 })
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
> {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
6
api/src/utils/test/fixtures/menu.ts
vendored
6
api/src/utils/test/fixtures/menu.ts
vendored
@ -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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
@ -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 */
|
||||
|
Loading…
Reference in New Issue
Block a user