diff --git a/.eslintrc.js b/.eslintrc.js index 361b9cd5d..041ccaaaa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,8 +33,8 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', 'react/jsx-closing-bracket-location': 'off', '@typescript-eslint/no-var-requires': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['off'], + 'no-unused-vars': 'error', + '@typescript-eslint/no-unused-vars': ['error'], 'mocha/no-mocha-arrows': 'off', '@typescript-eslint/default-param-last': 'off', 'no-return-await': 'off', diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index dd9b1ec62..de481564a 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -3,8 +3,13 @@ import { Type } from '@nestjs/common/interfaces/type.interface'; import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface'; import { SharedModule } from './app/shared/shared.module'; import { ProjectModule } from './app/project/project.module'; +import { TemplateModule } from './app/template/template.module'; -const modules: Array | ForwardReference> = [ProjectModule, SharedModule]; +const modules: Array | ForwardReference> = [ + ProjectModule, + SharedModule, + TemplateModule, +]; const providers = []; diff --git a/apps/api/src/app/project/dtos/create-project-request.dto.ts b/apps/api/src/app/project/dtos/create-project-request.dto.ts new file mode 100644 index 000000000..1e63b5798 --- /dev/null +++ b/apps/api/src/app/project/dtos/create-project-request.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDefined, IsString, Validate } from 'class-validator'; +import { UniqueValidator } from '../../shared/framework/IsUniqueValidator'; + +export class CreateProjectRequestDto { + @ApiProperty({ + description: 'Name of the project', + }) + @IsString() + @IsDefined() + name: string; + + @ApiProperty({ + description: 'Code of the project', + }) + @IsString() + @IsDefined() + @Validate(UniqueValidator, ['Project', 'code'], { + message: 'Code is already taken', + }) + code: string; +} diff --git a/apps/api/src/app/project/dtos/project-response.dto.ts b/apps/api/src/app/project/dtos/project-response.dto.ts new file mode 100644 index 000000000..3293ea503 --- /dev/null +++ b/apps/api/src/app/project/dtos/project-response.dto.ts @@ -0,0 +1,25 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDefined, IsString } from 'class-validator'; + +export class ProjectResponseDto { + @ApiPropertyOptional({ + description: 'Id of the project', + }) + @IsString() + @IsDefined() + _id?: string; + + @ApiProperty({ + description: 'Name of the project', + }) + @IsString() + @IsDefined() + name: string; + + @ApiProperty({ + description: 'Code of the project', + }) + @IsString() + @IsDefined() + code: string; +} diff --git a/apps/api/src/app/project/dtos/projects-response.dto.ts b/apps/api/src/app/project/dtos/projects-response.dto.ts deleted file mode 100644 index 0cfe9632f..000000000 --- a/apps/api/src/app/project/dtos/projects-response.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class ProjectResponseDto { - @ApiProperty() - name: string; - @ApiProperty() - code: string; -} diff --git a/apps/api/src/app/project/dtos/update-project-request.dto.ts b/apps/api/src/app/project/dtos/update-project-request.dto.ts new file mode 100644 index 000000000..7ce5f1056 --- /dev/null +++ b/apps/api/src/app/project/dtos/update-project-request.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDefined, IsString } from 'class-validator'; + +export class UpdateProjectRequestDto { + @ApiProperty({ + description: 'Name of the project', + }) + @IsString() + @IsDefined() + name: string; +} diff --git a/apps/api/src/app/project/project.controller.ts b/apps/api/src/app/project/project.controller.ts index 44867e87f..cbcdc9f9a 100644 --- a/apps/api/src/app/project/project.controller.ts +++ b/apps/api/src/app/project/project.controller.ts @@ -1,18 +1,89 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { ProjectResponseDto } from './dtos/projects-response.dto'; +import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { ApiOperation, ApiTags, ApiOkResponse } from '@nestjs/swagger'; +import { CreateProjectRequestDto } from './dtos/create-project-request.dto'; +import { ProjectResponseDto } from './dtos/project-response.dto'; import { GetProjects } from './usecases/get-projects/get-projects.usecase'; +import { CreateProject } from './usecases/create-project/create-project.usecase'; +import { CreateProjectCommand } from './usecases/create-project/create-project.command'; +import { UpdateProjectRequestDto } from './dtos/update-project-request.dto'; +import { UpdateProject } from './usecases/update-project/update-project.usecase'; +import { UpdateProjectCommand } from './usecases/update-project/update-project.command'; +import { DeleteProject } from './usecases/delete-project/delete-project.usecase'; +import { DocumentNotFoundException } from '../shared/exceptions/document-not-found.exception'; +import { ValidateMongoId } from '../shared/validations/valid-mongo-id.validation'; @Controller('/project') @ApiTags('Project') export class ProjectController { - constructor(private getProjectsUsecase: GetProjects) {} + constructor( + private getProjectsUsecase: GetProjects, + private createProjectUsecase: CreateProject, + private updateProjectUsecase: UpdateProject, + private deleteProjectUsecase: DeleteProject + ) {} @Get('') @ApiOperation({ summary: 'Get projects', }) + @ApiOkResponse({ + type: [ProjectResponseDto], + }) getProjects(): Promise { return this.getProjectsUsecase.execute(); } + + @Post('') + @ApiOperation({ + summary: 'Create project', + }) + @ApiOkResponse({ + type: ProjectResponseDto, + }) + createProject(@Body() body: CreateProjectRequestDto): Promise { + return this.createProjectUsecase.execute( + CreateProjectCommand.create({ + code: body.code, + name: body.name, + }) + ); + } + + @Put(':projectId') + @ApiOperation({ + summary: 'Update project', + }) + @ApiOkResponse({ + type: ProjectResponseDto, + }) + async updateProject( + @Body() body: UpdateProjectRequestDto, + @Param('projectId', ValidateMongoId) projectId: string + ): Promise { + const document = await this.updateProjectUsecase.execute( + UpdateProjectCommand.create({ name: body.name }), + projectId + ); + if (!document) { + throw new DocumentNotFoundException('Project', projectId); + } + + return document; + } + + @Delete(':projectId') + @ApiOperation({ + summary: 'Delete project', + }) + @ApiOkResponse({ + type: ProjectResponseDto, + }) + async deleteProject(@Param('projectId', ValidateMongoId) projectId: string): Promise { + const document = await this.deleteProjectUsecase.execute(projectId); + if (!document) { + throw new DocumentNotFoundException('Project', projectId); + } + + return document; + } } diff --git a/apps/api/src/app/project/project.module.ts b/apps/api/src/app/project/project.module.ts index 68afc5017..53402a1e2 100644 --- a/apps/api/src/app/project/project.module.ts +++ b/apps/api/src/app/project/project.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { USE_CASES } from './usecases'; -import { ProjectController } from './project.controller'; import { SharedModule } from '../shared/shared.module'; +import { ProjectController } from './project.controller'; +import { UniqueValidator } from '../shared/framework/IsUniqueValidator'; @Module({ - imports: [SharedModule], + imports: [SharedModule, UniqueValidator], providers: [...USE_CASES], controllers: [ProjectController], }) diff --git a/apps/api/src/app/project/usecases/create-project/create-project.command.ts b/apps/api/src/app/project/usecases/create-project/create-project.command.ts new file mode 100644 index 000000000..4c5fd7c5a --- /dev/null +++ b/apps/api/src/app/project/usecases/create-project/create-project.command.ts @@ -0,0 +1,12 @@ +import { IsDefined, IsString } from 'class-validator'; +import { BaseCommand } from '../../../shared/commands/base.command'; + +export class CreateProjectCommand extends BaseCommand { + @IsDefined() + @IsString() + name: string; + + @IsDefined() + @IsString() + code: string; +} diff --git a/apps/api/src/app/project/usecases/create-project/create-project.usecase.ts b/apps/api/src/app/project/usecases/create-project/create-project.usecase.ts new file mode 100644 index 000000000..61ed4030a --- /dev/null +++ b/apps/api/src/app/project/usecases/create-project/create-project.usecase.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { ProjectRepository } from '@impler/dal'; +import { CreateProjectCommand } from './create-project.command'; + +@Injectable() +export class CreateProject { + constructor(private projectRepository: ProjectRepository) {} + + async execute(command: CreateProjectCommand) { + return this.projectRepository.create(command); + } +} diff --git a/apps/api/src/app/project/usecases/delete-project/delete-project.usecase.ts b/apps/api/src/app/project/usecases/delete-project/delete-project.usecase.ts new file mode 100644 index 000000000..9b43729f9 --- /dev/null +++ b/apps/api/src/app/project/usecases/delete-project/delete-project.usecase.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { ProjectRepository } from '@impler/dal'; + +@Injectable() +export class DeleteProject { + constructor(private projectRepository: ProjectRepository) {} + + async execute(id: string) { + return this.projectRepository.delete({ _id: id }); + } +} diff --git a/apps/api/src/app/project/usecases/get-projects/get-projects.usecase.ts b/apps/api/src/app/project/usecases/get-projects/get-projects.usecase.ts index 1bdc7d1a0..3d8123fd5 100644 --- a/apps/api/src/app/project/usecases/get-projects/get-projects.usecase.ts +++ b/apps/api/src/app/project/usecases/get-projects/get-projects.usecase.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ProjectRepository } from '@impler/dal'; -import { ProjectResponseDto } from '../../dtos/projects-response.dto'; +import { ProjectResponseDto } from '../../dtos/project-response.dto'; @Injectable() export class GetProjects { @@ -11,7 +11,7 @@ export class GetProjects { return projects.map((project) => { return { - id: project._id, + _id: project._id, name: project.name, code: project.code, }; diff --git a/apps/api/src/app/project/usecases/index.ts b/apps/api/src/app/project/usecases/index.ts index 6cdb5770a..6a77192e0 100644 --- a/apps/api/src/app/project/usecases/index.ts +++ b/apps/api/src/app/project/usecases/index.ts @@ -1,6 +1,12 @@ +import { CreateProject } from './create-project/create-project.usecase'; import { GetProjects } from './get-projects/get-projects.usecase'; +import { UpdateProject } from './update-project/update-project.usecase'; +import { DeleteProject } from './delete-project/delete-project.usecase'; export const USE_CASES = [ GetProjects, + CreateProject, + UpdateProject, + DeleteProject, // ]; diff --git a/apps/api/src/app/project/usecases/update-project/update-project.command.ts b/apps/api/src/app/project/usecases/update-project/update-project.command.ts new file mode 100644 index 000000000..1ec6db4a3 --- /dev/null +++ b/apps/api/src/app/project/usecases/update-project/update-project.command.ts @@ -0,0 +1,8 @@ +import { IsDefined, IsString } from 'class-validator'; +import { BaseCommand } from '../../../shared/commands/base.command'; + +export class UpdateProjectCommand extends BaseCommand { + @IsDefined() + @IsString() + name: string; +} diff --git a/apps/api/src/app/project/usecases/update-project/update-project.usecase.ts b/apps/api/src/app/project/usecases/update-project/update-project.usecase.ts new file mode 100644 index 000000000..01eba6063 --- /dev/null +++ b/apps/api/src/app/project/usecases/update-project/update-project.usecase.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { ProjectRepository } from '@impler/dal'; +import { UpdateProjectCommand } from './update-project.command'; + +@Injectable() +export class UpdateProject { + constructor(private projectRepository: ProjectRepository) {} + + async execute(command: UpdateProjectCommand, id: string) { + return this.projectRepository.findOneAndUpdate({ _id: id }, command); + } +} diff --git a/apps/api/src/app/shared/commands/base.command.ts b/apps/api/src/app/shared/commands/base.command.ts new file mode 100644 index 000000000..0044a4b75 --- /dev/null +++ b/apps/api/src/app/shared/commands/base.command.ts @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { plainToClass } from 'class-transformer'; +import { validateSync } from 'class-validator'; +import { BadRequestException, flatten } from '@nestjs/common'; + +export abstract class BaseCommand { + static create(this: new (...args: any[]) => T, data: T): T { + const convertedObject = plainToClass(this, { + ...data, + }); + + const errors = validateSync(convertedObject as unknown as object); + if (errors?.length) { + const mappedErrors = flatten(errors.map((item) => Object.values(item.constraints))); + + throw new BadRequestException(mappedErrors); + } + + return convertedObject; + } +} diff --git a/apps/api/src/app/shared/exceptions/document-not-found.exception.ts b/apps/api/src/app/shared/exceptions/document-not-found.exception.ts new file mode 100644 index 000000000..a8832950c --- /dev/null +++ b/apps/api/src/app/shared/exceptions/document-not-found.exception.ts @@ -0,0 +1,7 @@ +import { NotFoundException } from '@nestjs/common'; + +export class DocumentNotFoundException extends NotFoundException { + constructor(name: string, id: string) { + super(`${name} with id ${id} does not exist`); + } +} diff --git a/apps/api/src/app/shared/framework/IsUniqueValidator.ts b/apps/api/src/app/shared/framework/IsUniqueValidator.ts new file mode 100644 index 000000000..e8d645f94 --- /dev/null +++ b/apps/api/src/app/shared/framework/IsUniqueValidator.ts @@ -0,0 +1,21 @@ +import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; +import { CommonRepository, ProjectEntity } from '@impler/dal'; + +@ValidatorConstraint({ name: 'IsUniqueUser', async: true }) +export class UniqueValidator implements ValidatorConstraintInterface { + private commonRepository: CommonRepository; + constructor() { + this.commonRepository = new CommonRepository(); + } + + async validate(value: any, args: ValidationArguments) { + const [modelName, field] = args.constraints; + const count = await this.commonRepository.count(modelName, { [field]: value }); + + return !count; + } + + defaultMessage(args: ValidationArguments) { + return `${args.value} is already taken`; + } +} diff --git a/apps/api/src/app/shared/shared.module.ts b/apps/api/src/app/shared/shared.module.ts index e45b7e63f..89e4a66a8 100644 --- a/apps/api/src/app/shared/shared.module.ts +++ b/apps/api/src/app/shared/shared.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { DalService, ProjectRepository } from '@impler/dal'; +import { DalService, ProjectRepository, TemplateRepository } from '@impler/dal'; -const DAL_MODELS = [ProjectRepository]; +const DAL_MODELS = [ProjectRepository, TemplateRepository]; const dalService = new DalService(); diff --git a/apps/api/src/app/shared/validations/valid-mongo-id.validation.ts b/apps/api/src/app/shared/validations/valid-mongo-id.validation.ts new file mode 100644 index 000000000..5b1339ad8 --- /dev/null +++ b/apps/api/src/app/shared/validations/valid-mongo-id.validation.ts @@ -0,0 +1,17 @@ +import { CommonRepository } from '@impler/dal'; +import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; + +@Injectable() +export class ValidateMongoId implements PipeTransform { + private commonRepository: CommonRepository; + constructor() { + this.commonRepository = new CommonRepository(); + } + + transform(value: string): string { + if (value && this.commonRepository.validMongoId(value)) { + return value; + } + throw new BadRequestException(); + } +} diff --git a/apps/api/src/app/template/dtos/create-template-request.dto.ts b/apps/api/src/app/template/dtos/create-template-request.dto.ts new file mode 100644 index 000000000..9805915ce --- /dev/null +++ b/apps/api/src/app/template/dtos/create-template-request.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDefined, IsString, Validate, IsNumber } from 'class-validator'; +import { UniqueValidator } from '../../shared/framework/IsUniqueValidator'; + +export class CreateTemplateRequestDto { + @ApiProperty({ + description: 'Name of the template', + }) + @IsString() + @IsDefined() + name: string; + + @ApiProperty({ + description: 'Code of the template', + }) + @IsString() + @IsDefined() + @Validate(UniqueValidator, ['Template', 'code'], { + message: 'Code is already taken', + }) + code: string; + + @ApiProperty({ + description: 'Callback URL of the template, gets called when sending data to the application', + }) + @IsString() + @IsDefined() + callbackUrl: string; + + @ApiProperty({ + description: 'Size of data in rows that gets sent to the application', + }) + @IsNumber() + @IsDefined() + chunkSize: number; + + @ApiProperty({ + description: 'Id of project related to the template', + }) + @IsString() + @IsDefined() + _projectId: string; +} diff --git a/apps/api/src/app/template/dtos/template-response.dto.ts b/apps/api/src/app/template/dtos/template-response.dto.ts new file mode 100644 index 000000000..5468c6c36 --- /dev/null +++ b/apps/api/src/app/template/dtos/template-response.dto.ts @@ -0,0 +1,46 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDefined, IsNumber, IsString } from 'class-validator'; + +export class TemplateResponseDto { + @ApiPropertyOptional({ + description: 'Id of the template', + }) + @IsString() + @IsDefined() + _id?: string; + + @ApiProperty({ + description: 'Name of the template', + }) + @IsString() + @IsDefined() + name: string; + + @ApiProperty({ + description: 'Code of the template', + }) + @IsString() + @IsDefined() + code: string; + + @ApiProperty({ + description: 'Callback URL of the template, gets called when sending data to the application', + }) + @IsString() + @IsDefined() + callbackUrl: string; + + @ApiProperty({ + description: 'Size of data in rows that gets sent to the application', + }) + @IsNumber() + @IsDefined() + chunkSize: number; + + @ApiProperty({ + description: 'Id of project related to the template', + }) + @IsString() + @IsDefined() + _projectId: string; +} diff --git a/apps/api/src/app/template/dtos/update-template-request.dto.ts b/apps/api/src/app/template/dtos/update-template-request.dto.ts new file mode 100644 index 000000000..c2071d22c --- /dev/null +++ b/apps/api/src/app/template/dtos/update-template-request.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsOptional, IsNumber, IsNotEmpty } from 'class-validator'; + +export class UpdateTemplateRequestDto { + @ApiProperty({ + description: 'Name of the template', + nullable: false, + }) + @IsOptional() + name?: string; + + @ApiProperty({ + description: 'Callback URL of the template, gets called when sending data to the application', + nullable: false, + }) + @IsString() + @IsOptional() + callbackUrl?: string; + + @ApiProperty({ + description: 'Size of data in rows that gets sent to the application', + format: 'number', + nullable: false, + }) + @IsNumber({ + allowNaN: false, + }) + @IsOptional() + @IsNotEmpty() + chunkSize?: number; + + @ApiProperty({ + description: 'Id of project related to the template', + nullable: false, + }) + @IsString() + @IsOptional() + _projectId?: string; +} diff --git a/apps/api/src/app/template/template.controller.ts b/apps/api/src/app/template/template.controller.ts new file mode 100644 index 000000000..658f05244 --- /dev/null +++ b/apps/api/src/app/template/template.controller.ts @@ -0,0 +1,97 @@ +import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { ApiOperation, ApiTags, ApiOkResponse } from '@nestjs/swagger'; +import { DocumentNotFoundException } from '../shared/exceptions/document-not-found.exception'; +import { ValidateMongoId } from '../shared/validations/valid-mongo-id.validation'; +import { CreateTemplateRequestDto } from './dtos/create-template-request.dto'; +import { TemplateResponseDto } from './dtos/template-response.dto'; +import { UpdateTemplateRequestDto } from './dtos/update-template-request.dto'; +import { CreateTemplateCommand } from './usecases/create-template/create-template.command'; +import { CreateTemplate } from './usecases/create-template/create-template.usecase'; +import { DeleteTemplate } from './usecases/delete-template/delete-template.usecase'; +import { GetTemplates } from './usecases/get-templates/get-templates.usecase'; +import { UpdateTemplateCommand } from './usecases/update-template/update-template.command'; +import { UpdateTemplate } from './usecases/update-template/update-template.usecase'; + +@Controller('/template') +@ApiTags('Template') +export class TemplateController { + constructor( + private getTemplatesUsecase: GetTemplates, + private createTemplateUsecase: CreateTemplate, + private updateTemplateUsecase: UpdateTemplate, + private deleteTemplateUsecase: DeleteTemplate + ) {} + + @Get(':projectId') + @ApiOperation({ + summary: 'Get project templates', + }) + @ApiOkResponse({ + type: [TemplateResponseDto], + }) + getTemplates(@Param('projectId', ValidateMongoId) projectId: string): Promise { + return this.getTemplatesUsecase.execute(projectId); + } + + @Post('') + @ApiOperation({ + summary: 'Create template', + }) + @ApiOkResponse({ + type: TemplateResponseDto, + }) + createTemplate(@Body() body: CreateTemplateRequestDto): Promise { + return this.createTemplateUsecase.execute( + CreateTemplateCommand.create({ + _projectId: body._projectId, + callbackUrl: body.callbackUrl, + chunkSize: body.chunkSize, + code: body.code, + name: body.name, + }) + ); + } + + @Put(':templateId') + @ApiOperation({ + summary: 'Update template', + }) + @ApiOkResponse({ + type: TemplateResponseDto, + }) + async updateTemplate( + @Param('templateId', ValidateMongoId) templateId: string, + @Body() body: UpdateTemplateRequestDto + ): Promise { + const document = await this.updateTemplateUsecase.execute( + UpdateTemplateCommand.create({ + _projectId: body._projectId, + callbackUrl: body.callbackUrl, + chunkSize: body.chunkSize, + name: body.name, + }), + templateId + ); + if (!document) { + throw new DocumentNotFoundException('Template', templateId); + } + + return document; + } + + @Delete(':templateId') + @ApiOperation({ + summary: 'Delete template', + }) + @ApiOkResponse({ + type: TemplateResponseDto, + }) + async deleteTemplate(@Param('templateId', ValidateMongoId) templateId: string): Promise { + const document = await this.deleteTemplateUsecase.execute(templateId); + if (!document) { + throw new DocumentNotFoundException('Template', templateId); + } + + return document; + } +} diff --git a/apps/api/src/app/template/template.module.ts b/apps/api/src/app/template/template.module.ts new file mode 100644 index 000000000..acd0f4b64 --- /dev/null +++ b/apps/api/src/app/template/template.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { USE_CASES } from './usecases'; +import { TemplateController } from './template.controller'; +import { SharedModule } from '../shared/shared.module'; + +@Module({ + imports: [SharedModule], + providers: [...USE_CASES], + controllers: [TemplateController], +}) +export class TemplateModule {} diff --git a/apps/api/src/app/template/usecases/create-template/create-template.command.ts b/apps/api/src/app/template/usecases/create-template/create-template.command.ts new file mode 100644 index 000000000..694e4e8e6 --- /dev/null +++ b/apps/api/src/app/template/usecases/create-template/create-template.command.ts @@ -0,0 +1,24 @@ +import { IsDefined, IsString, IsNumber } from 'class-validator'; +import { BaseCommand } from '../../../shared/commands/base.command'; + +export class CreateTemplateCommand extends BaseCommand { + @IsDefined() + @IsString() + name: string; + + @IsDefined() + @IsString() + code: string; + + @IsString() + @IsDefined() + callbackUrl: string; + + @IsNumber() + @IsDefined() + chunkSize: number; + + @IsString() + @IsDefined() + _projectId: string; +} diff --git a/apps/api/src/app/template/usecases/create-template/create-template.usecase.ts b/apps/api/src/app/template/usecases/create-template/create-template.usecase.ts new file mode 100644 index 000000000..5f8769046 --- /dev/null +++ b/apps/api/src/app/template/usecases/create-template/create-template.usecase.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { TemplateRepository } from '@impler/dal'; +import { CreateTemplateCommand } from './create-template.command'; + +@Injectable() +export class CreateTemplate { + constructor(private templateRepository: TemplateRepository) {} + + async execute(command: CreateTemplateCommand) { + return this.templateRepository.create(command); + } +} diff --git a/apps/api/src/app/template/usecases/delete-template/delete-template.usecase.ts b/apps/api/src/app/template/usecases/delete-template/delete-template.usecase.ts new file mode 100644 index 000000000..d33666314 --- /dev/null +++ b/apps/api/src/app/template/usecases/delete-template/delete-template.usecase.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { TemplateRepository } from '@impler/dal'; + +@Injectable() +export class DeleteTemplate { + constructor(private templateRepository: TemplateRepository) {} + + async execute(id: string) { + return this.templateRepository.delete({ _id: id }); + } +} diff --git a/apps/api/src/app/template/usecases/get-templates/get-templates.usecase.ts b/apps/api/src/app/template/usecases/get-templates/get-templates.usecase.ts new file mode 100644 index 000000000..d34053c48 --- /dev/null +++ b/apps/api/src/app/template/usecases/get-templates/get-templates.usecase.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { TemplateRepository } from '@impler/dal'; +import { TemplateResponseDto } from '../../dtos/template-response.dto'; + +@Injectable() +export class GetTemplates { + constructor(private templateRepository: TemplateRepository) {} + + async execute(projectId: string): Promise { + const templates = await this.templateRepository.find({ projectId }); + + return templates.map((template) => ({ + _projectId: template._projectId, + callbackUrl: template.callbackUrl, + chunkSize: template.chunkSize, + code: template.code, + name: template.name, + _id: template._id, + })); + } +} diff --git a/apps/api/src/app/template/usecases/index.ts b/apps/api/src/app/template/usecases/index.ts new file mode 100644 index 000000000..c98a08823 --- /dev/null +++ b/apps/api/src/app/template/usecases/index.ts @@ -0,0 +1,12 @@ +import { GetTemplates } from './get-templates/get-templates.usecase'; +import { CreateTemplate } from './create-template/create-template.usecase'; +import { UpdateTemplate } from './update-template/update-template.usecase'; +import { DeleteTemplate } from './delete-template/delete-template.usecase'; + +export const USE_CASES = [ + GetTemplates, + CreateTemplate, + UpdateTemplate, + DeleteTemplate, + // +]; diff --git a/apps/api/src/app/template/usecases/update-template/update-template.command.ts b/apps/api/src/app/template/usecases/update-template/update-template.command.ts new file mode 100644 index 000000000..0474e19b9 --- /dev/null +++ b/apps/api/src/app/template/usecases/update-template/update-template.command.ts @@ -0,0 +1,26 @@ +import { IsString, IsNumber, IsOptional, IsNotEmpty, IsMongoId, Min } from 'class-validator'; +import { BaseCommand } from '../../../shared/commands/base.command'; + +export class UpdateTemplateCommand extends BaseCommand { + @IsString() + @IsOptional() + name?: string; + + @IsString() + @IsOptional() + callbackUrl?: string; + + @IsNumber({ + allowNaN: false, + }) + @IsOptional() + @IsNotEmpty() + @Min(1) + chunkSize?: number; + + @IsMongoId({ + message: '_projectId is not valid', + }) + @IsOptional() + _projectId?: string; +} diff --git a/apps/api/src/app/template/usecases/update-template/update-template.usecase.ts b/apps/api/src/app/template/usecases/update-template/update-template.usecase.ts new file mode 100644 index 000000000..b96f0589d --- /dev/null +++ b/apps/api/src/app/template/usecases/update-template/update-template.usecase.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; +import { TemplateRepository } from '@impler/dal'; +import { UpdateTemplateCommand } from './update-template.command'; + +@Injectable() +export class UpdateTemplate { + constructor(private templateRepository: TemplateRepository) {} + + async execute(command: UpdateTemplateCommand, id: string) { + return this.templateRepository.findOneAndUpdate({ _id: id }, command); + } +} diff --git a/libs/dal/src/index.ts b/libs/dal/src/index.ts index eb53cc71f..3aa9bf4a5 100644 --- a/libs/dal/src/index.ts +++ b/libs/dal/src/index.ts @@ -1,2 +1,4 @@ export * from './dal.service'; export * from './repositories/project'; +export * from './repositories/common'; +export * from './repositories/template'; diff --git a/libs/dal/src/repositories/base-repository.ts b/libs/dal/src/repositories/base-repository.ts index d20623947..01fe4d8ff 100644 --- a/libs/dal/src/repositories/base-repository.ts +++ b/libs/dal/src/repositories/base-repository.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ClassConstructor, plainToClass } from 'class-transformer'; -import { Document, FilterQuery, Model, Types } from 'mongoose'; +import { Document, FilterQuery, Model, QueryOptions, Types, UpdateQuery } from 'mongoose'; export class BaseRepository { public _model: Model; @@ -36,7 +36,7 @@ export class BaseRepository { } async delete(query: FilterQuery) { - const data = await this.MongooseModel.remove(query); + const data = await this.MongooseModel.findOneAndDelete(query); return data; } @@ -105,6 +105,14 @@ export class BaseRepository { }; } + async findOneAndUpdate( + query: FilterQuery, + updateBody: UpdateQuery, + options: QueryOptions = { new: true } // By default return updated document + ): Promise { + return this.MongooseModel.findOneAndUpdate(query, updateBody, options); + } + protected mapEntity(data: any): T { return plainToClass(this.entity, JSON.parse(JSON.stringify(data))) as any; } diff --git a/libs/dal/src/repositories/common/common.repository.ts b/libs/dal/src/repositories/common/common.repository.ts new file mode 100644 index 000000000..2acbf46e0 --- /dev/null +++ b/libs/dal/src/repositories/common/common.repository.ts @@ -0,0 +1,18 @@ +import { FilterQuery, models, Types } from 'mongoose'; + +export class CommonRepository { + async count(name: string, query: FilterQuery): Promise { + const model = models[name]; + if (model) { + const count = await model.count(query); + + return count; + } + + throw new Error(`Model ${name} does not exists`); + } + + validMongoId(id: string): boolean { + return Types.ObjectId.isValid(id); + } +} diff --git a/libs/dal/src/repositories/common/index.ts b/libs/dal/src/repositories/common/index.ts new file mode 100644 index 000000000..8c96f8234 --- /dev/null +++ b/libs/dal/src/repositories/common/index.ts @@ -0,0 +1 @@ +export * from './common.repository'; diff --git a/libs/dal/src/repositories/template/index.ts b/libs/dal/src/repositories/template/index.ts new file mode 100644 index 000000000..a5693e67a --- /dev/null +++ b/libs/dal/src/repositories/template/index.ts @@ -0,0 +1,3 @@ +export * from './template.entity'; +export * from './template.repository'; +export * from './template.schema'; diff --git a/libs/dal/src/repositories/template/template.entity.ts b/libs/dal/src/repositories/template/template.entity.ts new file mode 100644 index 000000000..7495a708e --- /dev/null +++ b/libs/dal/src/repositories/template/template.entity.ts @@ -0,0 +1,13 @@ +export class TemplateEntity { + _id?: string; + + name: string; + + code: string; + + callbackUrl: string; + + chunkSize: number; + + _projectId: string; +} diff --git a/libs/dal/src/repositories/template/template.repository.ts b/libs/dal/src/repositories/template/template.repository.ts new file mode 100644 index 000000000..696fdf71c --- /dev/null +++ b/libs/dal/src/repositories/template/template.repository.ts @@ -0,0 +1,9 @@ +import { BaseRepository } from '../base-repository'; +import { TemplateEntity } from './template.entity'; +import { Template } from './template.schema'; + +export class TemplateRepository extends BaseRepository { + constructor() { + super(Template, TemplateEntity); + } +} diff --git a/libs/dal/src/repositories/template/template.schema.ts b/libs/dal/src/repositories/template/template.schema.ts new file mode 100644 index 000000000..2cdb8906e --- /dev/null +++ b/libs/dal/src/repositories/template/template.schema.ts @@ -0,0 +1,32 @@ +import { Schema, Document, model, models } from 'mongoose'; +import { schemaOptions } from '../schema-default.options'; +import { TemplateEntity } from './template.entity'; + +const templateSchema = new Schema( + { + name: { + type: Schema.Types.String, + }, + code: { + type: Schema.Types.String, + }, + callbackUrl: { + type: Schema.Types.String, + }, + chunkSize: { + type: Schema.Types.Number, + }, + _projectId: { + type: Schema.Types.ObjectId, + ref: 'Project', + index: true, + }, + }, + { ...schemaOptions } +); + +interface ITemplateDocument extends TemplateEntity, Document { + _id: never; +} + +export const Template = models.Template || model('Template', templateSchema); diff --git a/package.json b/package.json index 3f5ac6169..3b41cc765 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "eslint-plugin-promise": "^6.0.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-react-hooks": "^4.6.0", + "fs-extra": "^10.1.0", "husky": "^8.0.1", "lint-staged": "^13.0.3", "nx": "^14.7.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d19eaabe..0ea816961 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,7 @@ importers: eslint-plugin-promise: ^6.0.1 eslint-plugin-react: ^7.31.8 eslint-plugin-react-hooks: ^4.6.0 + fs-extra: ^10.1.0 husky: ^8.0.1 lint-staged: ^13.0.3 nx: ^14.7.11 @@ -45,6 +46,7 @@ importers: eslint-plugin-promise: 6.0.1_eslint@8.23.1 eslint-plugin-react: 7.31.8_eslint@8.23.1 eslint-plugin-react-hooks: 4.6.0_eslint@8.23.1 + fs-extra: 10.1.0 husky: 8.0.1 lint-staged: 13.0.3 nx: 14.7.11