some progress

This commit is contained in:
Evert Prants 2022-12-06 17:47:52 +02:00
parent c4d2cba4cb
commit b8ff56847a
16 changed files with 312 additions and 55 deletions

View File

@ -3,7 +3,7 @@ services:
mongodb:
image: mongo:6
container_name: mongo
restart: unless-stopped
restart: 'no'
ports:
- 27017:27017
environment:

View File

@ -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;
},
);

20
src/guards/auth.guard.ts Normal file
View File

@ -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<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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],

View File

@ -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<IndustryChangeApplicationDocument>,
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);
}
}

View File

@ -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(

View File

@ -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();

View File

@ -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<Resident> {
return this.residentModel.findOne({ sub });
}
}

17
src/utility/equate.ts Normal file
View File

@ -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<T, K>(
object1: T,
object2: K,
keys: string[],
): string[] {
return keys.reduce<string[]>((list, current) => {
if (object1[current] === object2[current]) return [...list, current];
return list;
}, []);
}

3
src/utility/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { default as take } from './take';
export { default as equate } from './equate';
export { default as takeMongoObject } from './take-mongo-object';

View File

@ -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<T>(mongoObj: T): T {
const dirty = (mongoObj as Record<string, unknown>)._doc as Record<
string,
unknown
>;
dirty.id = dirty._id.toString();
delete dirty._id;
delete dirty.__v;
return dirty as T;
}

9
src/utility/take.ts Normal file
View File

@ -0,0 +1,9 @@
export default function take<T>(object: T, keys: string[]): Partial<T> {
if (!object) return null;
return Object.keys(object).reduce<Partial<T>>((obj, field) => {
if (keys.includes(field)) {
obj[field] = object[field];
}
return obj;
}, {});
}

View File