From b8ff56847a142bf6cf12e76cab32848c0ef36874 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Tue, 6 Dec 2022 17:47:52 +0200 Subject: [PATCH] some progress --- docker-compose.yml | 2 +- src/decorators/token.decorator.ts | 8 + src/guards/auth.guard.ts | 20 +++ .../dtos/list-query.dto.ts | 14 ++ ...egister-industry-change-application.dto.ts | 57 +++---- .../industry-change-application.controller.ts | 57 +++++-- .../industry-change-application.module.ts | 2 + .../industry-change-application.service.ts | 143 +++++++++++++++++- .../IndustryChangeApplication.schema.ts | 11 +- src/main.ts | 2 + src/resident/resident.service.ts | 5 +- src/utility/equate.ts | 17 +++ src/utility/index.ts | 3 + src/utility/take-mongo-object.ts | 17 +++ src/utility/take.ts | 9 ++ src/validators/registration.validator.ts | 0 16 files changed, 312 insertions(+), 55 deletions(-) create mode 100644 src/decorators/token.decorator.ts create mode 100644 src/guards/auth.guard.ts create mode 100644 src/industry-change-application/dtos/list-query.dto.ts create mode 100644 src/utility/equate.ts create mode 100644 src/utility/index.ts create mode 100644 src/utility/take-mongo-object.ts create mode 100644 src/utility/take.ts create mode 100644 src/validators/registration.validator.ts diff --git a/docker-compose.yml b/docker-compose.yml index 6dba8e9..d4ad5d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: mongodb: image: mongo:6 container_name: mongo - restart: unless-stopped + restart: 'no' ports: - 27017:27017 environment: diff --git a/src/decorators/token.decorator.ts b/src/decorators/token.decorator.ts new file mode 100644 index 0000000..9a808e7 --- /dev/null +++ b/src/decorators/token.decorator.ts @@ -0,0 +1,8 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export const Token = createParamDecorator( + (data: unknown, ctx: ExecutionContext) => { + const response = ctx.switchToHttp().getResponse(); + return response.locals.token as string; + }, +); diff --git a/src/guards/auth.guard.ts b/src/guards/auth.guard.ts new file mode 100644 index 0000000..e1210f3 --- /dev/null +++ b/src/guards/auth.guard.ts @@ -0,0 +1,20 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Observable } from 'rxjs'; +import type { Response, Request } from 'express'; + +@Injectable() +export class AuthGuard implements CanActivate { + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const authHeader = request.header('Authorization'); + if (!authHeader) return true; // false; + const [, token] = authHeader.split(' '); + if (!token) return true; // false + // Validate `token` JWT here + response.locals.token = token; + return true; + } +} diff --git a/src/industry-change-application/dtos/list-query.dto.ts b/src/industry-change-application/dtos/list-query.dto.ts new file mode 100644 index 0000000..a08e7b9 --- /dev/null +++ b/src/industry-change-application/dtos/list-query.dto.ts @@ -0,0 +1,14 @@ +import { Transform } from 'class-transformer'; +import { IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { ApplicationStatus } from 'src/enums/status.enum'; + +export class ListQueryDto { + @IsOptional() + @Transform(({ value }) => value.split(',')) + @IsEnum(ApplicationStatus, { each: true }) + readonly statuses?: ApplicationStatus[]; + + @IsNotEmpty() + @IsString() + readonly residentSub: string; +} diff --git a/src/industry-change-application/dtos/register-industry-change-application.dto.ts b/src/industry-change-application/dtos/register-industry-change-application.dto.ts index 1c8f12f..75eabe6 100644 --- a/src/industry-change-application/dtos/register-industry-change-application.dto.ts +++ b/src/industry-change-application/dtos/register-industry-change-application.dto.ts @@ -1,43 +1,30 @@ -import { Exclude, Expose } from 'class-transformer'; +import { + IsBoolean, + IsEnum, + IsOptional, + IsString, + ValidateIf, +} from 'class-validator'; import { Industry } from 'src/enums/industry.enum'; import { RegulatoryElection } from 'src/enums/regulatory-election.enums'; -import { ApplicationStatus, ObjectStatus } from 'src/enums/status.enum'; -import { Resident } from 'src/resident/schemas/Resident.schema'; -export class ApplicationInformationDto { - @Exclude() - _id: string; +export class RegisterIndustryChangeApplicationDto { + @IsString() + residentSub: string; - willWorkInPhysicalJuristiction: string; + @IsBoolean() + willWorkInPhysicalJurisdiction: boolean; + + @IsEnum(Industry) + @ValidateIf((o) => o.willWorkInPhysicalJurisdiction === true) industry: Industry; + + @IsEnum(RegulatoryElection) + @ValidateIf((o) => o.willWorkInPhysicalJurisdiction === true) regulatoryElection: RegulatoryElection; + + @IsOptional() + @IsString() + @ValidateIf((o) => o.willWorkInPhysicalJurisdiction === true) regulatoryElectionSub: string; } - -export class DecisionDto { - @Exclude() - _id: string; - - decidedAt: Date; - rejctionReason: string; -} - -export class GetRegisterIndustryChangeApplicationDto { - @Expose({ name: 'id' }) - _id: string; - - @Exclude() - resident: Resident; - - @Expose({ name: 'residentSub' }) - getResidentSub() { - return this.resident.sub; - } - - current: ApplicationInformationDto; - requested: ApplicationInformationDto; - status: ApplicationStatus; - submittedAt: Date; - decision: DecisionDto; - objectStatus: ObjectStatus; -} diff --git a/src/industry-change-application/industry-change-application.controller.ts b/src/industry-change-application/industry-change-application.controller.ts index 927ec3d..fc27a51 100644 --- a/src/industry-change-application/industry-change-application.controller.ts +++ b/src/industry-change-application/industry-change-application.controller.ts @@ -1,24 +1,61 @@ -import { Controller, Get } from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; -import { GetRegisterIndustryChangeApplicationDto } from './dtos/register-industry-change-application.dto'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Query, + UseGuards, + ValidationPipe, +} from '@nestjs/common'; +import { Token } from 'src/decorators/token.decorator'; +import { AuthGuard } from 'src/guards/auth.guard'; +import { ListQueryDto } from './dtos/list-query.dto'; +import { RegisterIndustryChangeApplicationDto } from './dtos/register-industry-change-application.dto'; import { IndustryChangeApplicationService } from './industry-change-application.service'; @Controller({ path: '/resident-register/industry-change-applications', }) +@UseGuards(AuthGuard) export class IndustryChangeApplicationController { constructor( private readonly applicationsSerivce: IndustryChangeApplicationService, ) {} @Get() - async getList() { - const allApplications = await this.applicationsSerivce.getAll(); - const transformed = plainToInstance( - GetRegisterIndustryChangeApplicationDto, - allApplications, - { excludeExtraneousValues: true }, + async getList( + @Query(new ValidationPipe({ transform: true })) queryOpts: ListQueryDto, + ) { + const allApplications = await this.applicationsSerivce.getAll(queryOpts); + return this.applicationsSerivce.makeReadable(allApplications); + } + + @Get(':id') + async getSingle(@Param('id') id: string) { + const findInstance = await this.applicationsSerivce.getById(id); + if (!findInstance) return null; + return this.applicationsSerivce.makeReadable(findInstance); + } + + @Delete(':id') + async delete(@Param('id') id: string, @Token() authToken: string) { + return this.applicationsSerivce.makeReadable( + await this.applicationsSerivce.markDeleted(id, authToken), ); - return transformed; + } + + @Post() + async create( + @Body() application: RegisterIndustryChangeApplicationDto, + @Token() authToken: string, + ) { + const newApplication = await this.applicationsSerivce.create( + application, + authToken, + ); + + return this.applicationsSerivce.makeReadable(newApplication); } } diff --git a/src/industry-change-application/industry-change-application.module.ts b/src/industry-change-application/industry-change-application.module.ts index f5d9393..52d3241 100644 --- a/src/industry-change-application/industry-change-application.module.ts +++ b/src/industry-change-application/industry-change-application.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { ResidentModule } from 'src/resident/resident.module'; import { IndustryChangeApplicationController } from './industry-change-application.controller'; import { IndustryChangeApplicationService } from './industry-change-application.service'; import { Decision, DecisionSchema } from './schemas/Decision.schema'; @@ -28,6 +29,7 @@ import { schema: IndustryChangeApplicationSchema, }, ]), + ResidentModule, ], controllers: [IndustryChangeApplicationController], providers: [IndustryChangeApplicationService], diff --git a/src/industry-change-application/industry-change-application.service.ts b/src/industry-change-application/industry-change-application.service.ts index 55ba377..15c8a5b 100644 --- a/src/industry-change-application/industry-change-application.service.ts +++ b/src/industry-change-application/industry-change-application.service.ts @@ -1,19 +1,156 @@ -import { Injectable } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; +import { + ApplicationStatus, + ObjectStatus, + ResidentStatus, +} from 'src/enums/status.enum'; +import { TypeOfRegistration } from 'src/enums/type-of-registration.enums'; +import { ResidentService } from 'src/resident/resident.service'; +import { takeMongoObject, equate, take } from 'src/utility'; +import { ListQueryDto } from './dtos/list-query.dto'; +import { RegisterIndustryChangeApplicationDto } from './dtos/register-industry-change-application.dto'; import { IndustryChangeApplication, IndustryChangeApplicationDocument, } from './schemas/IndustryChangeApplication.schema'; +const requestedFields = [ + 'industry', + 'willWorkInPhysicalJurisdiction', + 'regulatoryElection', + 'regulatoryElectionSub', +]; + +const fieldsToExpose = [ + 'id', + 'residentSub', + 'current', + 'requested', + 'status', + 'submittedAt', + 'decision', + 'objectStatus', +]; + @Injectable() export class IndustryChangeApplicationService { constructor( @InjectModel(IndustryChangeApplication.name) private applicationModel: Model, + private resident: ResidentService, ) {} - async getAll() { - return this.applicationModel.find(); + async getAll(options: ListQueryDto) { + const getResident = await this.resident.getResidentBySub( + options.residentSub, + ); + + if (!getResident) { + throw new NotFoundException('Resident not found'); + } + + return this.applicationModel.find({ + residentSub: options.residentSub, + status: { + $in: options.statuses || Object.values(ApplicationStatus), + }, + }); + } + + async getById(id: string) { + return this.applicationModel.findById(id); + } + + async create(data: RegisterIndustryChangeApplicationDto, token: string) { + // This might be possible to turn into a custom validator for class-validator + if ( + data.willWorkInPhysicalJurisdiction === false && + (!!data.industry || + !!data.regulatoryElection || + !!data.regulatoryElectionSub) + ) { + throw new BadRequestException( + 'industry, regulatoryElection and regulatoryElectionSub are not allowed when willWorkInPhysicalJurisdiction is false', + ); + } + + const getResident = await this.resident.getResidentBySub(data.residentSub); + if (!getResident) { + throw new BadRequestException('Resident not found'); + } + + if (getResident.status !== ResidentStatus.ACTIVE) { + throw new BadRequestException('Resident must be active!'); + } + + if ( + ![TypeOfRegistration.E_RESIDENCY, TypeOfRegistration.RESIDENCY].includes( + getResident.typeOfRegistration, + ) + ) { + throw new BadRequestException( + 'Resident must be either an E-resident or a resident', + ); + } + + if ( + equate(data, getResident, requestedFields).length === + requestedFields.length + ) { + throw new BadRequestException('Cannot request what is already the case.'); + } + + const status = + data.willWorkInPhysicalJurisdiction === true + ? ApplicationStatus.IN_REVIEW + : ApplicationStatus.APPROVED; + + const newApplication = new this.applicationModel({ + residentSub: getResident.sub, + current: take(takeMongoObject(getResident), requestedFields), + requested: take(data, requestedFields), + status, + submittedAt: new Date(), + createdBy: token ?? 'no token provided for testing', + }); + + return newApplication.save(); + } + + async markDeleted(id: string, token: string) { + const findApplication = await this.applicationModel.findById(id); + + if (!findApplication) { + throw new NotFoundException('Application was not found'); + } + + if (findApplication.status !== ApplicationStatus.IN_REVIEW) { + throw new BadRequestException( + 'Only applications which are currently in review can be deleted.', + ); + } + + if (findApplication.objectStatus === ObjectStatus.DELETED) { + throw new BadRequestException('The object has already been deleted.'); + } + + findApplication.objectStatus = ObjectStatus.DELETED; + findApplication.updatedBy = token || 'no token provided for testing'; + return findApplication.save(); + } + + // I wrote this because I could not for the life of me get class-transformer to + // play along with mongo documents. I do not have experience with either, so this + // was a last ditch effort. + makeReadable(input: IndustryChangeApplication | IndustryChangeApplication[]) { + return Array.isArray(input) + ? input.map((object) => take(takeMongoObject(object), fieldsToExpose)) + : take(takeMongoObject(input), fieldsToExpose); } } diff --git a/src/industry-change-application/schemas/IndustryChangeApplication.schema.ts b/src/industry-change-application/schemas/IndustryChangeApplication.schema.ts index 1d15f20..0de6301 100644 --- a/src/industry-change-application/schemas/IndustryChangeApplication.schema.ts +++ b/src/industry-change-application/schemas/IndustryChangeApplication.schema.ts @@ -1,6 +1,5 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import mongoose, { HydratedDocument } from 'mongoose'; -import { Resident } from 'src/resident/schemas/Resident.schema'; +import { HydratedDocument } from 'mongoose'; import { Decision } from './Decision.schema'; import { ApplicationStatus, ObjectStatus } from 'src/enums/status.enum'; import { ICAInformation } from './ICAInformation.schema'; @@ -14,10 +13,8 @@ export type IndustryChangeApplicationDocument = export class IndustryChangeApplication { @Prop({ required: true, - ref: 'Resident', - type: mongoose.Schema.Types.ObjectId, }) - resident: Resident; + residentSub: string; @Prop({ required: true, @@ -58,6 +55,10 @@ export class IndustryChangeApplication { required: true, }) objectStatus: ObjectStatus; + + createdAt: Date; + + updatedAt: Date; } export const IndustryChangeApplicationSchema = SchemaFactory.createForClass( diff --git a/src/main.ts b/src/main.ts index 13cad38..5a5fd57 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,10 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap(); diff --git a/src/resident/resident.service.ts b/src/resident/resident.service.ts index c227012..4e46677 100644 --- a/src/resident/resident.service.ts +++ b/src/resident/resident.service.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Resident, ResidentDocument } from './schemas/Resident.schema'; -import { ResidentAddress } from './schemas/ResidentAddress.schema'; @Injectable() export class ResidentService { @@ -14,4 +13,8 @@ export class ResidentService { const createdResident = new this.residentModel(resident); return createdResident.save(); } + + public async getResidentBySub(sub: string): Promise { + return this.residentModel.findOne({ sub }); + } } diff --git a/src/utility/equate.ts b/src/utility/equate.ts new file mode 100644 index 0000000..8438e08 --- /dev/null +++ b/src/utility/equate.ts @@ -0,0 +1,17 @@ +/** + * Equate keys of two objects + * @param object1 First object + * @param object2 Second object + * @param keys Keys to equate + * @returns Keys which are equal in both objects + */ +export default function equate( + object1: T, + object2: K, + keys: string[], +): string[] { + return keys.reduce((list, current) => { + if (object1[current] === object2[current]) return [...list, current]; + return list; + }, []); +} diff --git a/src/utility/index.ts b/src/utility/index.ts new file mode 100644 index 0000000..8c94618 --- /dev/null +++ b/src/utility/index.ts @@ -0,0 +1,3 @@ +export { default as take } from './take'; +export { default as equate } from './equate'; +export { default as takeMongoObject } from './take-mongo-object'; diff --git a/src/utility/take-mongo-object.ts b/src/utility/take-mongo-object.ts new file mode 100644 index 0000000..463e62c --- /dev/null +++ b/src/utility/take-mongo-object.ts @@ -0,0 +1,17 @@ +/** + * Take the plain javascript object from a mongo document. + * I wrote this because I could not for the life of me get class-transformer to + * play along with mongo documents. + * @param mongoObj Mongo database response + * @returns Plain javascript object + */ +export default function takeMongoObject(mongoObj: T): T { + const dirty = (mongoObj as Record)._doc as Record< + string, + unknown + >; + dirty.id = dirty._id.toString(); + delete dirty._id; + delete dirty.__v; + return dirty as T; +} diff --git a/src/utility/take.ts b/src/utility/take.ts new file mode 100644 index 0000000..b7144dd --- /dev/null +++ b/src/utility/take.ts @@ -0,0 +1,9 @@ +export default function take(object: T, keys: string[]): Partial { + if (!object) return null; + return Object.keys(object).reduce>((obj, field) => { + if (keys.includes(field)) { + obj[field] = object[field]; + } + return obj; + }, {}); +} diff --git a/src/validators/registration.validator.ts b/src/validators/registration.validator.ts new file mode 100644 index 0000000..e69de29