mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
refactor(api): content logic
This commit is contained in:
parent
7b5846d721
commit
7c74348001
@ -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).
|
* 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 {
|
import { forwardRef, Module } from '@nestjs/common';
|
||||||
forwardRef,
|
|
||||||
MiddlewareConsumer,
|
|
||||||
Module,
|
|
||||||
NestModule,
|
|
||||||
RequestMethod,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
|
||||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||||
@ -21,7 +15,6 @@ import { ChatModule } from '@/chat/chat.module';
|
|||||||
import { ContentTypeController } from './controllers/content-type.controller';
|
import { ContentTypeController } from './controllers/content-type.controller';
|
||||||
import { ContentController } from './controllers/content.controller';
|
import { ContentController } from './controllers/content.controller';
|
||||||
import { MenuController } from './controllers/menu.controller';
|
import { MenuController } from './controllers/menu.controller';
|
||||||
import { ContentMiddleWare } from './middlewares/content.middleware';
|
|
||||||
import { ContentTypeRepository } from './repositories/content-type.repository';
|
import { ContentTypeRepository } from './repositories/content-type.repository';
|
||||||
import { ContentRepository } from './repositories/content.repository';
|
import { ContentRepository } from './repositories/content.repository';
|
||||||
import { MenuRepository } from './repositories/menu.repository';
|
import { MenuRepository } from './repositories/menu.repository';
|
||||||
@ -55,13 +48,4 @@ import { AttachmentModel } from '../attachment/schemas/attachment.schema';
|
|||||||
],
|
],
|
||||||
exports: [MenuService, ContentService, ContentTypeService],
|
exports: [MenuService, ContentService, ContentTypeService],
|
||||||
})
|
})
|
||||||
export class CmsModule implements NestModule {
|
export class CmsModule {}
|
||||||
configure(consumer: MiddlewareConsumer) {
|
|
||||||
consumer
|
|
||||||
.apply(ContentMiddleWare)
|
|
||||||
.forRoutes(
|
|
||||||
{ path: 'content', method: RequestMethod.POST },
|
|
||||||
{ path: 'content/:id', method: RequestMethod.PATCH },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -35,7 +35,6 @@ import {
|
|||||||
|
|
||||||
import { ContentController } from './content.controller';
|
import { ContentController } from './content.controller';
|
||||||
import { ContentCreateDto } from '../dto/content.dto';
|
import { ContentCreateDto } from '../dto/content.dto';
|
||||||
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
|
|
||||||
import { ContentTypeRepository } from '../repositories/content-type.repository';
|
import { ContentTypeRepository } from '../repositories/content-type.repository';
|
||||||
import { ContentRepository } from '../repositories/content.repository';
|
import { ContentRepository } from '../repositories/content.repository';
|
||||||
import { ContentType, ContentTypeModel } from '../schemas/content-type.schema';
|
import { ContentType, ContentTypeModel } from '../schemas/content-type.schema';
|
||||||
@ -48,7 +47,6 @@ describe('ContentController', () => {
|
|||||||
let contentService: ContentService;
|
let contentService: ContentService;
|
||||||
let contentTypeService: ContentTypeService;
|
let contentTypeService: ContentTypeService;
|
||||||
let attachmentService: AttachmentService;
|
let attachmentService: AttachmentService;
|
||||||
let transformInterceptor: ContentTransformInterceptor;
|
|
||||||
let contentType: ContentType;
|
let contentType: ContentType;
|
||||||
let content: Content;
|
let content: Content;
|
||||||
let attachment: Attachment;
|
let attachment: Attachment;
|
||||||
@ -74,7 +72,6 @@ describe('ContentController', () => {
|
|||||||
AttachmentService,
|
AttachmentService,
|
||||||
ContentTypeRepository,
|
ContentTypeRepository,
|
||||||
AttachmentRepository,
|
AttachmentRepository,
|
||||||
ContentTransformInterceptor,
|
|
||||||
EventEmitter2,
|
EventEmitter2,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
@ -82,9 +79,6 @@ describe('ContentController', () => {
|
|||||||
contentService = module.get<ContentService>(ContentService);
|
contentService = module.get<ContentService>(ContentService);
|
||||||
attachmentService = module.get<AttachmentService>(AttachmentService);
|
attachmentService = module.get<AttachmentService>(AttachmentService);
|
||||||
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
|
contentTypeService = module.get<ContentTypeService>(ContentTypeService);
|
||||||
transformInterceptor = module.get<ContentTransformInterceptor>(
|
|
||||||
ContentTransformInterceptor,
|
|
||||||
);
|
|
||||||
contentType = await contentTypeService.findOne({ name: 'Product' });
|
contentType = await contentTypeService.findOne({ name: 'Product' });
|
||||||
content = await contentService.findOne({
|
content = await contentService.findOne({
|
||||||
title: 'Jean',
|
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', () => {
|
describe('count', () => {
|
||||||
it('should return the number of contents', async () => {
|
it('should return the number of contents', async () => {
|
||||||
jest.spyOn(contentService, 'count');
|
jest.spyOn(contentService, 'count');
|
||||||
|
@ -40,7 +40,6 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
|||||||
import { ContentTypeService } from './../services/content-type.service';
|
import { ContentTypeService } from './../services/content-type.service';
|
||||||
import { ContentService } from './../services/content.service';
|
import { ContentService } from './../services/content.service';
|
||||||
import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
|
import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
|
||||||
import { ContentTransformInterceptor } from '../interceptors/content.interceptor';
|
|
||||||
import { ContentType } from '../schemas/content-type.schema';
|
import { ContentType } from '../schemas/content-type.schema';
|
||||||
import {
|
import {
|
||||||
Content,
|
Content,
|
||||||
@ -48,9 +47,8 @@ import {
|
|||||||
ContentPopulate,
|
ContentPopulate,
|
||||||
ContentStub,
|
ContentStub,
|
||||||
} from '../schemas/content.schema';
|
} from '../schemas/content.schema';
|
||||||
import { preprocessDynamicFields } from '../utilities';
|
|
||||||
|
|
||||||
@UseInterceptors(ContentTransformInterceptor, CsrfInterceptor)
|
@UseInterceptors(CsrfInterceptor)
|
||||||
@Controller('content')
|
@Controller('content')
|
||||||
export class ContentController extends BaseController<
|
export class ContentController extends BaseController<
|
||||||
Content,
|
Content,
|
||||||
@ -116,8 +114,7 @@ export class ContentController extends BaseController<
|
|||||||
entity: contentType?.id,
|
entity: contentType?.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const newContent = this.filterDynamicFields(contentDto, contentType);
|
return await this.contentService.create(contentDto);
|
||||||
return await this.contentService.create(newContent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,12 +183,22 @@ export class ContentController extends BaseController<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentsDto = result.data.map((content) => {
|
const contentsDto = result.data.reduce(
|
||||||
content.entity = targetContentType;
|
(acc, { title, status, ...rest }) => [
|
||||||
const dto = preprocessDynamicFields(content);
|
...acc,
|
||||||
// Match headers against entity fields
|
{
|
||||||
return this.filterDynamicFields(dto, contentType);
|
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
|
// Create content
|
||||||
return await this.contentService.createMany(contentsDto);
|
return await this.contentService.createMany(contentsDto);
|
||||||
|
@ -43,4 +43,8 @@ export class ContentUpdateDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
status?: boolean;
|
status?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Content dynamic fields', type: Object })
|
||||||
|
@IsOptional()
|
||||||
|
dynamicFields?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user