refactor(api): content logic

This commit is contained in:
yassinedorbozgithub 2024-10-03 16:34:01 +01:00
parent 7b5846d721
commit 7c74348001
7 changed files with 24 additions and 155 deletions

View File

@ -6,13 +6,7 @@
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import {
forwardRef,
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AttachmentModule } from '@/attachment/attachment.module';
@ -21,7 +15,6 @@ import { ChatModule } from '@/chat/chat.module';
import { ContentTypeController } from './controllers/content-type.controller';
import { ContentController } from './controllers/content.controller';
import { MenuController } from './controllers/menu.controller';
import { ContentMiddleWare } from './middlewares/content.middleware';
import { ContentTypeRepository } from './repositories/content-type.repository';
import { ContentRepository } from './repositories/content.repository';
import { MenuRepository } from './repositories/menu.repository';
@ -55,13 +48,4 @@ import { AttachmentModel } from '../attachment/schemas/attachment.schema';
],
exports: [MenuService, ContentService, ContentTypeService],
})
export class CmsModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(ContentMiddleWare)
.forRoutes(
{ path: 'content', method: RequestMethod.POST },
{ path: 'content/:id', method: RequestMethod.PATCH },
);
}
}
export class CmsModule {}

View File

@ -35,7 +35,6 @@ import {
import { ContentController } from './content.controller';
import { ContentCreateDto } from '../dto/content.dto';
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
import { ContentTypeRepository } from '../repositories/content-type.repository';
import { ContentRepository } from '../repositories/content.repository';
import { ContentType, ContentTypeModel } from '../schemas/content-type.schema';
@ -48,7 +47,6 @@ describe('ContentController', () => {
let contentService: ContentService;
let contentTypeService: ContentTypeService;
let attachmentService: AttachmentService;
let transformInterceptor: ContentTransformInterceptor;
let contentType: ContentType;
let content: Content;
let attachment: Attachment;
@ -74,7 +72,6 @@ describe('ContentController', () => {
AttachmentService,
ContentTypeRepository,
AttachmentRepository,
ContentTransformInterceptor,
EventEmitter2,
],
}).compile();
@ -82,9 +79,6 @@ describe('ContentController', () => {
contentService = module.get<ContentService>(ContentService);
attachmentService = module.get<AttachmentService>(AttachmentService);
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
transformInterceptor = module.get<ContentTransformInterceptor>(
ContentTransformInterceptor,
);
contentType = await contentTypeService.findOne({ name: 'Product' });
content = await contentService.findOne({
title: 'Jean',
@ -237,28 +231,6 @@ describe('ContentController', () => {
});
});
describe('filterDynamicFields', () => {
it('should flatten dynamic fields', () => {
const result = transformInterceptor.transformDynamicFields(
contentFixtures[0],
);
expect(result).toEqualPayload(
{
title: 'Jean',
status: true,
subtitle: 'Jean Droit Taille Normale',
image: {
payload: {
url: 'https://images-na.ssl-images-amazon.com/images/I/31DY09uzLDL._SX38_SY50_CR,0,0,38,50_.jpg',
},
},
},
['entity', 'rag', ...IGNORED_TEST_FIELDS],
);
});
});
describe('count', () => {
it('should return the number of contents', async () => {
jest.spyOn(contentService, 'count');

View File

@ -40,7 +40,6 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { ContentTypeService } from './../services/content-type.service';
import { ContentService } from './../services/content.service';
import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
import { ContentType } from '../schemas/content-type.schema';
import {
Content,
@ -48,9 +47,8 @@ import {
ContentPopulate,
ContentStub,
} from '../schemas/content.schema';
import { preprocessDynamicFields } from '../utilities';
@UseInterceptors(ContentTransformInterceptor, CsrfInterceptor)
@UseInterceptors(CsrfInterceptor)
@Controller('content')
export class ContentController extends BaseController<
Content,
@ -116,8 +114,7 @@ export class ContentController extends BaseController<
entity: contentType?.id,
},
});
const newContent = this.filterDynamicFields(contentDto, contentType);
return await this.contentService.create(newContent);
return await this.contentService.create(contentDto);
}
/**
@ -186,12 +183,22 @@ export class ContentController extends BaseController<
});
}
const contentsDto = result.data.map((content) => {
content.entity = targetContentType;
const dto = preprocessDynamicFields(content);
// Match headers against entity fields
return this.filterDynamicFields(dto, contentType);
});
const contentsDto = result.data.reduce(
(acc, { title, status, ...rest }) => [
...acc,
{
title,
status,
entity: targetContentType,
dynamicFields: Object.keys(rest)
.filter((key) =>
contentType.fields.map((field) => field.name).includes(key),
)
.reduce((filtered, key) => ({ ...filtered, [key]: rest[key] }), {}),
},
],
[],
);
// Create content
return await this.contentService.createMany(contentsDto);

View File

@ -43,4 +43,8 @@ export class ContentUpdateDto {
@IsBoolean()
@IsOptional()
status?: boolean;
@ApiPropertyOptional({ description: 'Content dynamic fields', type: Object })
@IsOptional()
dynamicFields?: Record<string, any>;
}

View File

@ -1,54 +0,0 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { Content } from '../schemas/content.schema';
@Injectable()
export class ContentTransformInterceptor
implements NestInterceptor<Content, Content>
{
/*
-This interceptor is designed to provide a flattened representation of the 'dynamicFields'.
-The incoming data contains a 'dynamicField' object, and the interceptor is expanding it,
extracting its content as separate entries.
-After the expansion, the 'dynamicFields' property is removed.
-The interceptor will be applied on each endpoint of this controller.
*/
transformDynamicFields(data) {
if (data.dynamicFields) {
Object.keys(data.dynamicFields).forEach((key) => {
data[key] = data.dynamicFields[key];
});
delete data.dynamicFields;
}
return data;
}
intercept(context: ExecutionContext, next: CallHandler): Observable<Content> {
return next.handle().pipe(
map((data) => {
// If the data is not an array, the 'transformDynamicFields' method is applied once
if (!Array.isArray(data)) {
return this.transformDynamicFields(data);
}
return data.map((content) => {
return this.transformDynamicFields(content);
});
}),
);
}
}

View File

@ -1,20 +0,0 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
import { preprocessDynamicFields } from '../utilities';
@Injectable()
export class ContentMiddleWare implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
req.body = preprocessDynamicFields(req.body);
next();
}
}

View File

@ -1,24 +0,0 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import { ContentCreateDto } from '../dto/content.dto';
export const preprocessDynamicFields = (
content: Record<string, string | boolean | number>,
) => {
const { _csrf, title, status, entity, ...dynamicFields } = content;
const processed: ContentCreateDto & { _csrf?: string } = {
_csrf: _csrf?.toString(),
entity: entity?.toString(),
status: !!status,
title: title?.toString(),
dynamicFields,
};
return processed;
};