Skip to content

Commit 82f46ad

Browse files
committed
fix: enhacement of content import
1 parent 06c999b commit 82f46ad

File tree

9 files changed

+204
-332
lines changed

9 files changed

+204
-332
lines changed

api/src/cms/controllers/content.controller.spec.ts

Lines changed: 60 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
/*
2-
* Copyright © 2024 Hexastack. All rights reserved.
2+
* Copyright © 2025 Hexastack. All rights reserved.
33
*
44
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
55
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
66
* 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).
77
*/
88

9-
import fs from 'fs';
10-
119
import { NotFoundException } from '@nestjs/common/exceptions';
1210
import { EventEmitter2 } from '@nestjs/event-emitter';
1311
import { MongooseModule } from '@nestjs/mongoose';
1412
import { Test, TestingModule } from '@nestjs/testing';
1513

16-
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
17-
import {
18-
Attachment,
19-
AttachmentModel,
20-
} from '@/attachment/schemas/attachment.schema';
21-
import { AttachmentService } from '@/attachment/services/attachment.service';
2214
import { LoggerService } from '@/logger/logger.service';
2315
import { NOT_FOUND_ID } from '@/utils/constants/mock';
2416
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
@@ -48,10 +40,8 @@ describe('ContentController', () => {
4840
let contentController: ContentController;
4941
let contentService: ContentService;
5042
let contentTypeService: ContentTypeService;
51-
let attachmentService: AttachmentService;
5243
let contentType: ContentType | null;
5344
let content: Content | null;
54-
let attachment: Attachment | null;
5545
let updatedContent;
5646
let pageQuery: PageQueryDto<Content>;
5747

@@ -60,34 +50,24 @@ describe('ContentController', () => {
6050
controllers: [ContentController],
6151
imports: [
6252
rootMongooseTestModule(installContentFixtures),
63-
MongooseModule.forFeature([
64-
ContentTypeModel,
65-
ContentModel,
66-
AttachmentModel,
67-
]),
53+
MongooseModule.forFeature([ContentTypeModel, ContentModel]),
6854
],
6955
providers: [
7056
LoggerService,
7157
ContentTypeService,
7258
ContentService,
7359
ContentRepository,
74-
AttachmentService,
7560
ContentTypeRepository,
76-
AttachmentRepository,
7761
EventEmitter2,
7862
],
7963
}).compile();
8064
contentController = module.get<ContentController>(ContentController);
8165
contentService = module.get<ContentService>(ContentService);
82-
attachmentService = module.get<AttachmentService>(AttachmentService);
8366
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
8467
contentType = await contentTypeService.findOne({ name: 'Product' });
8568
content = await contentService.findOne({
8669
title: 'Jean',
8770
});
88-
attachment = await attachmentService.findOne({
89-
name: 'store1.jpg',
90-
});
9171

9272
pageQuery = getPageQuery<Content>({
9373
limit: 1,
@@ -243,91 +223,76 @@ describe('ContentController', () => {
243223
});
244224

245225
describe('import', () => {
246-
it('should import content from a CSV file', async () => {
247-
const mockCsvData: string = `other,title,status,image
248-
should not appear,store 3,true,image.jpg`;
249-
250-
const mockCsvContentDto: ContentCreateDto = {
251-
entity: '0',
252-
title: 'store 3',
253-
status: true,
254-
dynamicFields: {
255-
image: 'image.jpg',
256-
},
257-
};
258-
jest.spyOn(contentService, 'createMany');
259-
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(true);
260-
jest.spyOn(fs, 'readFileSync').mockReturnValueOnce(mockCsvData);
226+
const mockCsvData: string = `other,title,status,image
227+
should not appear,store 3,true,image.jpg`;
228+
229+
const file: Express.Multer.File = {
230+
buffer: Buffer.from(mockCsvData, 'utf-8'),
231+
originalname: 'test.csv',
232+
mimetype: 'text/csv',
233+
size: mockCsvData.length,
234+
fieldname: 'file',
235+
encoding: '7bit',
236+
stream: null,
237+
destination: '',
238+
filename: '',
239+
path: '',
240+
} as unknown as Express.Multer.File;
261241

262-
const contentType = await contentTypeService.findOne({
242+
it('should import content from a CSV file', async () => {
243+
const mockContentType = {
244+
id: '0',
263245
name: 'Store',
264-
});
265-
266-
const result = await contentController.import({
267-
idFileToImport: attachment!.id,
268-
idTargetContentType: contentType!.id,
269-
});
270-
expect(contentService.createMany).toHaveBeenCalledWith([
271-
{ ...mockCsvContentDto, entity: contentType!.id },
246+
} as unknown as ContentType;
247+
jest
248+
.spyOn(contentTypeService, 'findOne')
249+
.mockResolvedValueOnce(mockContentType);
250+
jest.spyOn(contentService, 'parseAndSaveDataset').mockResolvedValueOnce([
251+
{
252+
entity: mockContentType.id,
253+
title: 'store 3',
254+
status: true,
255+
dynamicFields: {
256+
image: 'image.jpg',
257+
},
258+
id: '',
259+
createdAt: null as unknown as Date,
260+
updatedAt: null as unknown as Date,
261+
},
272262
]);
273263

274-
expect(result).toEqualPayload(
275-
[
276-
{
277-
...mockCsvContentDto,
278-
entity: contentType!.id,
279-
},
280-
],
281-
[...IGNORED_TEST_FIELDS, 'rag'],
264+
const result = await contentController.import(file, mockContentType.id);
265+
expect(contentService.parseAndSaveDataset).toHaveBeenCalledWith(
266+
mockCsvData,
267+
mockContentType.id,
268+
mockContentType,
282269
);
270+
expect(result).toEqual([
271+
{
272+
entity: mockContentType.id,
273+
title: 'store 3',
274+
status: true,
275+
dynamicFields: {
276+
image: 'image.jpg',
277+
},
278+
id: '',
279+
createdAt: null as unknown as Date,
280+
updatedAt: null as unknown as Date,
281+
},
282+
]);
283283
});
284284

285285
it('should throw NotFoundException if content type is not found', async () => {
286+
jest.spyOn(contentTypeService, 'findOne').mockResolvedValueOnce(null);
286287
await expect(
287-
contentController.import({
288-
idFileToImport: attachment!.id,
289-
idTargetContentType: NOT_FOUND_ID,
290-
}),
288+
contentController.import(file, 'INVALID_ID'),
291289
).rejects.toThrow(new NotFoundException('Content type is not found'));
292290
});
293291

294-
it('should throw NotFoundException if file is not found in attachment database', async () => {
295-
const contentType = await contentTypeService.findOne({
296-
name: 'Product',
297-
});
298-
jest.spyOn(contentTypeService, 'findOne');
299-
await expect(
300-
contentController.import({
301-
idFileToImport: NOT_FOUND_ID,
302-
idTargetContentType: contentType!.id.toString(),
303-
}),
304-
).rejects.toThrow(new NotFoundException('File does not exist'));
305-
});
306-
307-
it('should throw NotFoundException if file does not exist in the given path ', async () => {
308-
jest.spyOn(fs, 'existsSync').mockReturnValue(false);
309-
await expect(
310-
contentController.import({
311-
idFileToImport: attachment!.id,
312-
idTargetContentType: contentType!.id,
313-
}),
314-
).rejects.toThrow(new NotFoundException('File does not exist'));
292+
it('should throw NotFoundException if idTargetContentType is missing', async () => {
293+
await expect(contentController.import(file, '')).rejects.toThrow(
294+
new NotFoundException('Missing parameter'),
295+
);
315296
});
316-
317-
it.each([
318-
['file param and content type params are missing', '', ''],
319-
['content type param is missing', '', NOT_FOUND_ID],
320-
['file param is missing', NOT_FOUND_ID, ''],
321-
])(
322-
'should throw NotFoundException if %s',
323-
async (_message, fileToImport, targetContentType) => {
324-
await expect(
325-
contentController.import({
326-
idFileToImport: fileToImport,
327-
idTargetContentType: targetContentType,
328-
}),
329-
).rejects.toThrow(new NotFoundException('Missing params'));
330-
},
331-
);
332297
});
333298
});

api/src/cms/controllers/content.controller.ts

Lines changed: 18 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
/*
2-
* Copyright © 2024 Hexastack. All rights reserved.
2+
* Copyright © 2025 Hexastack. All rights reserved.
33
*
44
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
55
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
66
* 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).
77
*/
88

9-
import fs from 'fs';
10-
import path from 'path';
11-
129
import {
1310
Body,
1411
Controller,
@@ -20,14 +17,12 @@ import {
2017
Patch,
2118
Post,
2219
Query,
20+
UploadedFile,
2321
UseInterceptors,
2422
} from '@nestjs/common';
25-
import { BadRequestException } from '@nestjs/common/exceptions';
23+
import { FileInterceptor } from '@nestjs/platform-express';
2624
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
27-
import Papa from 'papaparse';
2825

29-
import { AttachmentService } from '@/attachment/services/attachment.service';
30-
import { config } from '@/config';
3126
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
3227
import { LoggerService } from '@/logger/logger.service';
3328
import { BaseController } from '@/utils/generics/base-controller';
@@ -59,7 +54,6 @@ export class ContentController extends BaseController<
5954
constructor(
6055
private readonly contentService: ContentService,
6156
private readonly contentTypeService: ContentTypeService,
62-
private readonly attachmentService: AttachmentService,
6357
private readonly logger: LoggerService,
6458
) {
6559
super(contentService);
@@ -92,29 +86,22 @@ export class ContentController extends BaseController<
9286
/**
9387
* Imports content from a CSV file based on the provided content type and file ID.
9488
*
95-
* @param idTargetContentType - The content type to match the CSV data against.
96-
* @param idFileToImport - The ID of the file to be imported.
97-
*
89+
* @param idTargetContentType - The content type to match the CSV data against. *
9890
* @returns A promise that resolves to the newly created content documents.
9991
*/
100-
@Get('import/:idTargetContentType/:idFileToImport')
92+
@CsrfCheck(true)
93+
@Post('import')
94+
@UseInterceptors(FileInterceptor('file'))
10195
async import(
102-
@Param()
103-
{
104-
idTargetContentType: targetContentType,
105-
idFileToImport: fileToImport,
106-
}: {
107-
idTargetContentType: string;
108-
idFileToImport: string;
109-
},
96+
@UploadedFile() file: Express.Multer.File,
97+
@Query('idTargetContentType')
98+
targetContentType: string,
11099
) {
111-
// Check params
112-
if (!fileToImport || !targetContentType) {
113-
this.logger.warn(`Parameters are missing`);
114-
throw new NotFoundException(`Missing params`);
100+
const datasetContent = file.buffer.toString('utf-8');
101+
if (!targetContentType) {
102+
this.logger.warn(`Parameter is missing`);
103+
throw new NotFoundException(`Missing parameter`);
115104
}
116-
117-
// Find the content type that corresponds to the given content
118105
const contentType =
119106
await this.contentTypeService.findOne(targetContentType);
120107
if (!contentType) {
@@ -124,56 +111,11 @@ export class ContentController extends BaseController<
124111
throw new NotFoundException(`Content type is not found`);
125112
}
126113

127-
// Get file location
128-
const file = await this.attachmentService.findOne(fileToImport);
129-
// Check if file is present
130-
const filePath = file
131-
? path.join(config.parameters.uploadDir, file.location)
132-
: undefined;
133-
134-
if (!file || !filePath || !fs.existsSync(filePath)) {
135-
this.logger.warn(`Failed to find file type with id ${fileToImport}.`);
136-
throw new NotFoundException(`File does not exist`);
137-
}
138-
//read file sync
139-
const data = fs.readFileSync(filePath, 'utf8');
140-
141-
const result = Papa.parse<Record<string, string | boolean | number>>(data, {
142-
header: true,
143-
skipEmptyLines: true,
144-
dynamicTyping: true,
145-
});
146-
147-
if (result.errors.length > 0) {
148-
this.logger.warn(
149-
`Errors parsing the file: ${JSON.stringify(result.errors)}`,
150-
);
151-
152-
throw new BadRequestException(result.errors, {
153-
cause: result.errors,
154-
description: 'Error while parsing CSV',
155-
});
156-
}
157-
158-
const contentsDto = result.data.reduce(
159-
(acc, { title, status, ...rest }) => [
160-
...acc,
161-
{
162-
title: String(title),
163-
status: Boolean(status),
164-
entity: targetContentType,
165-
dynamicFields: Object.keys(rest)
166-
.filter((key) =>
167-
contentType.fields?.map((field) => field.name).includes(key),
168-
)
169-
.reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}),
170-
},
171-
],
172-
[],
114+
return await this.contentService.parseAndSaveDataset(
115+
datasetContent,
116+
targetContentType,
117+
contentType,
173118
);
174-
175-
// Create content
176-
return await this.contentService.createMany(contentsDto);
177119
}
178120

179121
/**

api/src/cms/services/content.service.spec.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
1010
import { MongooseModule } from '@nestjs/mongoose';
1111
import { Test, TestingModule } from '@nestjs/testing';
1212

13-
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
14-
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
15-
import { AttachmentService } from '@/attachment/services/attachment.service';
1613
import { OutgoingMessageFormat } from '@/chat/schemas/types/message';
1714
import { ContentOptions } from '@/chat/schemas/types/options';
1815
import { LoggerService } from '@/logger/logger.service';
@@ -44,19 +41,13 @@ describe('ContentService', () => {
4441
const module: TestingModule = await Test.createTestingModule({
4542
imports: [
4643
rootMongooseTestModule(installContentFixtures),
47-
MongooseModule.forFeature([
48-
ContentTypeModel,
49-
ContentModel,
50-
AttachmentModel,
51-
]),
44+
MongooseModule.forFeature([ContentTypeModel, ContentModel]),
5245
],
5346
providers: [
5447
ContentTypeRepository,
5548
ContentRepository,
56-
AttachmentRepository,
5749
ContentTypeService,
5850
ContentService,
59-
AttachmentService,
6051
LoggerService,
6152
EventEmitter2,
6253
],

0 commit comments

Comments
 (0)