mirror of
https://github.com/hexastack/hexabot
synced 2025-03-12 15:12:08 +00:00
313 lines
9.5 KiB
TypeScript
313 lines
9.5 KiB
TypeScript
/*
|
|
* 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 fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
NotFoundException,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
UseInterceptors,
|
|
} from '@nestjs/common';
|
|
import { BadRequestException } from '@nestjs/common/exceptions';
|
|
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
|
|
import Papa from 'papaparse';
|
|
|
|
import { AttachmentService } from '@/attachment/services/attachment.service';
|
|
import { config } from '@/config';
|
|
import { CsrfInterceptor } from '@/interceptors/csrf.interceptor';
|
|
import { LoggerService } from '@/logger/logger.service';
|
|
import { BaseController } from '@/utils/generics/base-controller';
|
|
import { PageQueryDto } from '@/utils/pagination/pagination-query.dto';
|
|
import { PageQueryPipe } from '@/utils/pagination/pagination-query.pipe';
|
|
import { PopulatePipe } from '@/utils/pipes/populate.pipe';
|
|
import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
|
|
import { TFilterQuery } from '@/utils/types/filter.types';
|
|
|
|
import { ContentCreateDto, ContentUpdateDto } from '../dto/content.dto';
|
|
import {
|
|
Content,
|
|
ContentFull,
|
|
ContentPopulate,
|
|
ContentStub,
|
|
} from '../schemas/content.schema';
|
|
|
|
import { ContentTypeService } from './../services/content-type.service';
|
|
import { ContentService } from './../services/content.service';
|
|
|
|
@UseInterceptors(CsrfInterceptor)
|
|
@Controller('content')
|
|
export class ContentController extends BaseController<
|
|
Content,
|
|
ContentStub,
|
|
ContentPopulate,
|
|
ContentFull
|
|
> {
|
|
constructor(
|
|
private readonly contentService: ContentService,
|
|
private readonly contentTypeService: ContentTypeService,
|
|
private readonly attachmentService: AttachmentService,
|
|
private readonly logger: LoggerService,
|
|
) {
|
|
super(contentService);
|
|
}
|
|
|
|
/**
|
|
* Creates new content based on the provided DTO, filtering dynamic fields to match
|
|
* the associated content type before persisting it.
|
|
*
|
|
* @param contentDto - The DTO containing the content data to be created.
|
|
*
|
|
* @returns The created content document.
|
|
*/
|
|
@CsrfCheck(true)
|
|
@Post()
|
|
async create(@Body() contentDto: ContentCreateDto): Promise<Content> {
|
|
// Find the content type that corresponds to the given content
|
|
const contentType = await this.contentTypeService.findOne(
|
|
contentDto.entity,
|
|
);
|
|
this.validate({
|
|
dto: contentDto,
|
|
allowedIds: {
|
|
entity: contentType?.id,
|
|
},
|
|
});
|
|
return await this.contentService.create(contentDto);
|
|
}
|
|
|
|
/**
|
|
* Imports content from a CSV file based on the provided content type and file ID.
|
|
*
|
|
* @param idTargetContentType - The content type to match the CSV data against.
|
|
* @param idFileToImport - The ID of the file to be imported.
|
|
*
|
|
* @returns A promise that resolves to the newly created content documents.
|
|
*/
|
|
@Get('import/:idTargetContentType/:idFileToImport')
|
|
async import(
|
|
@Param()
|
|
{
|
|
idTargetContentType: targetContentType,
|
|
idFileToImport: fileToImport,
|
|
}: {
|
|
idTargetContentType: string;
|
|
idFileToImport: string;
|
|
},
|
|
) {
|
|
// Check params
|
|
if (!fileToImport || !targetContentType) {
|
|
this.logger.warn(`Parameters are missing`);
|
|
throw new NotFoundException(`Missing params`);
|
|
}
|
|
|
|
// Find the content type that corresponds to the given content
|
|
const contentType =
|
|
await this.contentTypeService.findOne(targetContentType);
|
|
if (!contentType) {
|
|
this.logger.warn(
|
|
`Failed to fetch content type with id ${targetContentType}. Content type not found.`,
|
|
);
|
|
throw new NotFoundException(`Content type is not found`);
|
|
}
|
|
|
|
// Get file location
|
|
const file = await this.attachmentService.findOne(fileToImport);
|
|
// Check if file is present
|
|
const filePath = file
|
|
? path.join(config.parameters.uploadDir, file.location)
|
|
: undefined;
|
|
|
|
if (!file || !fs.existsSync(filePath)) {
|
|
this.logger.warn(`Failed to find file type with id ${fileToImport}.`);
|
|
throw new NotFoundException(`File does not exist`);
|
|
}
|
|
//read file sync
|
|
const data = fs.readFileSync(filePath, 'utf8');
|
|
|
|
const result = Papa.parse<Record<string, string | boolean | number>>(data, {
|
|
header: true,
|
|
skipEmptyLines: true,
|
|
dynamicTyping: true,
|
|
});
|
|
|
|
if (result.errors.length > 0) {
|
|
this.logger.warn(
|
|
`Errors parsing the file: ${JSON.stringify(result.errors)}`,
|
|
);
|
|
|
|
throw new BadRequestException(result.errors, {
|
|
cause: result.errors,
|
|
description: 'Error while parsing CSV',
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Retrieves paginated content based on filters and optional population of related entities.
|
|
*
|
|
* @param pageQuery - Pagination parameters.
|
|
* @param populate - Fields to populate in the query.
|
|
* @param filters - Filters for content retrieval.
|
|
*
|
|
* @returns Paginated content list.
|
|
*/
|
|
@Get()
|
|
async findPage(
|
|
@Query(PageQueryPipe) pageQuery: PageQueryDto<Content>,
|
|
@Query(PopulatePipe) populate: string[],
|
|
@Query(
|
|
new SearchFilterPipe<Content>({ allowedFields: ['entity', 'title'] }),
|
|
)
|
|
filters: TFilterQuery<Content>,
|
|
) {
|
|
return this.canPopulate(populate)
|
|
? await this.contentService.findAndPopulate(filters, pageQuery)
|
|
: await this.contentService.find(filters, pageQuery);
|
|
}
|
|
|
|
/**
|
|
* Counts the filtered number of contents based on the provided filters.
|
|
*
|
|
* @param filters - Optional filters for counting content.
|
|
*
|
|
* @returns The count of content matching the filters.
|
|
*/
|
|
@Get('count')
|
|
async filterCount(
|
|
@Query(
|
|
new SearchFilterPipe<Content>({ allowedFields: ['entity', 'title'] }),
|
|
)
|
|
filters?: TFilterQuery<Content>,
|
|
) {
|
|
return await this.count(filters);
|
|
}
|
|
|
|
/**
|
|
* Retrieves a single content by ID, with optional population of related entities.
|
|
*
|
|
* @param id - The ID of the content to retrieve.
|
|
* @param populate - Fields to populate in the query.
|
|
*
|
|
* @returns The requested content document.
|
|
*/
|
|
@Get(':id')
|
|
async findOne(
|
|
@Param('id') id: string,
|
|
@Query(PopulatePipe) populate: string[],
|
|
) {
|
|
const doc = this.canPopulate(populate)
|
|
? await this.contentService.findOneAndPopulate(id)
|
|
: await this.contentService.findOne(id);
|
|
|
|
if (!doc) {
|
|
this.logger.warn(
|
|
`Failed to fetch content with id ${id}. Content not found.`,
|
|
);
|
|
throw new NotFoundException(`Content of id ${id} not found`);
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
/**
|
|
* Deletes a content document by ID.
|
|
*
|
|
* @param id - The ID of the content to delete.
|
|
*
|
|
* @returns The result of the delete operation.
|
|
*/
|
|
@CsrfCheck(true)
|
|
@Delete(':id')
|
|
@HttpCode(204)
|
|
async deleteOne(@Param('id') id: string) {
|
|
const removedContent = await this.contentService.deleteOne(id);
|
|
if (removedContent.deletedCount === 0) {
|
|
this.logger.warn(
|
|
`Failed to delete content with id ${id}. Content not found.`,
|
|
);
|
|
throw new NotFoundException(`Content of id ${id} not found`);
|
|
}
|
|
return removedContent;
|
|
}
|
|
|
|
/**
|
|
* Retrieves content based on content type ID with optional pagination.
|
|
*
|
|
* @param contentType - The content type ID to filter by.
|
|
* @param pageQuery - Pagination parameters.
|
|
*
|
|
* @returns List of content documents matching the content type.
|
|
*/
|
|
@Get('/type/:id')
|
|
async findByType(
|
|
@Param('id') contentType: string,
|
|
@Query(PageQueryPipe) pageQuery: PageQueryDto<Content>,
|
|
): Promise<Content[]> {
|
|
const type = await this.contentTypeService.findOne(contentType);
|
|
if (!type) {
|
|
this.logger.warn(
|
|
`Failed to find content with contentType ${contentType}. ContentType not found.`,
|
|
);
|
|
throw new NotFoundException(`ContentType of id ${contentType} not found`);
|
|
}
|
|
return await this.contentService.find({ entity: contentType }, pageQuery);
|
|
}
|
|
|
|
/**
|
|
* Updates a content document by ID, after filtering dynamic fields to match the associated content type.
|
|
*
|
|
* @param contentDto - The DTO containing the updated content data.
|
|
* @param id - The ID of the content to update.
|
|
*
|
|
* @returns The updated content document.
|
|
*/
|
|
@CsrfCheck(true)
|
|
@Patch(':id')
|
|
async updateOne(
|
|
@Body() contentDto: ContentUpdateDto,
|
|
@Param('id') id: string,
|
|
): Promise<Content> {
|
|
const updatedContent = await this.contentService.updateOne(id, contentDto);
|
|
if (!updatedContent) {
|
|
this.logger.warn(
|
|
`Failed to update content with id ${id}. Content not found.`,
|
|
);
|
|
throw new NotFoundException(`Content of id ${id} not found`);
|
|
}
|
|
return updatedContent;
|
|
}
|
|
}
|