Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Project & Template APIs #3

Merged
merged 12 commits into from
Oct 3, 2022
4 changes: 2 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
7 changes: 6 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [ProjectModule, SharedModule];
const modules: Array<Type | DynamicModule | Promise<DynamicModule> | ForwardReference> = [
ProjectModule,
SharedModule,
TemplateModule,
];

const providers = [];

Expand Down
22 changes: 22 additions & 0 deletions apps/api/src/app/project/dtos/create-project-request.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
25 changes: 25 additions & 0 deletions apps/api/src/app/project/dtos/project-response.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 0 additions & 8 deletions apps/api/src/app/project/dtos/projects-response.dto.ts

This file was deleted.

11 changes: 11 additions & 0 deletions apps/api/src/app/project/dtos/update-project-request.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
79 changes: 75 additions & 4 deletions apps/api/src/app/project/project.controller.ts
Original file line number Diff line number Diff line change
@@ -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<ProjectResponseDto[]> {
return this.getProjectsUsecase.execute();
}

@Post('')
@ApiOperation({
summary: 'Create project',
})
@ApiOkResponse({
type: ProjectResponseDto,
})
createProject(@Body() body: CreateProjectRequestDto): Promise<ProjectResponseDto> {
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<ProjectResponseDto> {
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<ProjectResponseDto> {
const document = await this.deleteProjectUsecase.execute(projectId);
if (!document) {
throw new DocumentNotFoundException('Project', projectId);
}

return document;
}
}
5 changes: 3 additions & 2 deletions apps/api/src/app/project/project.module.ts
Original file line number Diff line number Diff line change
@@ -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],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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 });
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -11,7 +11,7 @@ export class GetProjects {

return projects.map((project) => {
return {
id: project._id,
_id: project._id,
name: project.name,
code: project.code,
};
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/app/project/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -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,
//
];
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
21 changes: 21 additions & 0 deletions apps/api/src/app/shared/commands/base.command.ts
Original file line number Diff line number Diff line change
@@ -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<T extends BaseCommand>(this: new (...args: any[]) => T, data: T): T {
const convertedObject = plainToClass<T, any>(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;
}
}
Original file line number Diff line number Diff line change
@@ -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`);
}
}
21 changes: 21 additions & 0 deletions apps/api/src/app/shared/framework/IsUniqueValidator.ts
Original file line number Diff line number Diff line change
@@ -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<ProjectEntity>(modelName, { [field]: value });

return !count;
}

defaultMessage(args: ValidationArguments) {
return `${args.value} is already taken`;
}
}
4 changes: 2 additions & 2 deletions apps/api/src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand Down
17 changes: 17 additions & 0 deletions apps/api/src/app/shared/validations/valid-mongo-id.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CommonRepository } from '@impler/dal';
import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ValidateMongoId implements PipeTransform<string> {
private commonRepository: CommonRepository;
constructor() {
this.commonRepository = new CommonRepository();
}

transform(value: string): string {
if (value && this.commonRepository.validMongoId(value)) {
return value;
}
throw new BadRequestException();
}
}
Loading