Skip to content

Commit

Permalink
Merge pull request #403 from hpi-dhc/issue/258-validate-dtos
Browse files Browse the repository at this point in the history
Validate DTOs
  • Loading branch information
Benjamin-Frost authored Jul 31, 2022
2 parents 6d66a38 + 357ac90 commit 6b6eeab
Show file tree
Hide file tree
Showing 16 changed files with 112 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .markdownlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ default: true
# Allow inline html by disabling rule MD033
MD033: false

# Allow bare urls
MD034: false

# Allow other elements but h1 in first line
MD041: false

Expand Down
7 changes: 7 additions & 0 deletions annotation-server/src/common/dtos/instantiable.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class InstantiableDto<T> {
constructor(dto: T) {
Object.entries(dto).forEach(([key, value]) => {
this[key] = value;
});
}
}
32 changes: 29 additions & 3 deletions annotation-server/src/common/dtos/patch-body.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,30 @@
import { BaseEntity } from '../entities/base.entity';
import { ParseArrayOptions, ParseArrayPipe } from '@nestjs/common';
import {
IntersectionType,
OmitType,
PartialType,
PickType,
} from '@nestjs/mapped-types';

export type PatchBodyDto<T extends BaseEntity> = (Pick<T, 'id'> &
Partial<Omit<T, 'id'>>)[];
import { Guideline } from '../../guidelines/entities/guideline.entity';
import { Medication } from '../../medications/medication.entity';

export class PatchMedicationDto extends IntersectionType(
PickType(Medication, ['id'] as const),
PartialType(Medication),
) {}

export class PatchGuidelineDto extends IntersectionType(
PickType(Guideline, ['id'] as const),
OmitType(PartialType(Guideline), ['id'] as const),
) {}

export function getPatchArrayPipe(
items: ParseArrayOptions['items'],
): ParseArrayPipe {
return new ParseArrayPipe({
items,
whitelist: true,
forbidNonWhitelisted: true,
});
}
2 changes: 2 additions & 0 deletions annotation-server/src/common/entities/base.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IsNumber } from 'class-validator';
import { PrimaryGeneratedColumn } from 'typeorm';

export abstract class BaseEntity {
@PrimaryGeneratedColumn()
@IsNumber()
id: number;
}
4 changes: 3 additions & 1 deletion annotation-server/src/guidelines/dtos/cpic-guideline.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IsNotEmpty, IsNumber, IsString, IsUrl } from 'class-validator';

export class CpicGuidelineDto {
import { InstantiableDto } from '../../common/dtos/instantiable.dto';

export class CpicGuidelineDto extends InstantiableDto<CpicGuidelineDto> {
@IsNumber()
@IsNotEmpty()
id: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IsNotEmpty, IsNumber, IsObject, IsString } from 'class-validator';

export class CpicRecommendationDto {
import { InstantiableDto } from '../../common/dtos/instantiable.dto';

export class CpicRecommendationDto extends InstantiableDto<CpicRecommendationDto> {
@IsString()
@IsNotEmpty()
drugid: string;
Expand Down
4 changes: 4 additions & 0 deletions annotation-server/src/guidelines/entities/guideline.entity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IsEnum, IsString } from 'class-validator';
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';

import { BaseEntity } from '../../common/entities/base.entity';
Expand All @@ -15,16 +16,19 @@ export enum WarningLevel {
@Entity()
export class Guideline extends BaseEntity {
@Column({ nullable: true })
@IsString()
implication: string;

@Column({ nullable: true })
@IsString()
recommendation: string;

@Column({
type: 'enum',
enum: WarningLevel,
nullable: true,
})
@IsEnum(WarningLevel)
warningLevel: WarningLevel;

@ManyToOne(() => Medication, (medication) => medication.guidelines, {
Expand Down
10 changes: 7 additions & 3 deletions annotation-server/src/guidelines/guidelines.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';

import { ApiBodyPatch } from '../common/api/bodies';
import { ApiParamGetById } from '../common/api/params';
import { PatchBodyDto } from '../common/dtos/patch-body.dto';
import {
getPatchArrayPipe,
PatchGuidelineDto,
} from '../common/dtos/patch-body.dto';
import {
ApiFindGuidelineErrorsQueries,
FindGuidelineErrorQueryDto,
Expand Down Expand Up @@ -53,9 +56,10 @@ export class GuidelinesController {
@ApiBodyPatch('guideline')
@Patch()
async patchGuidelines(
@Body() patch: PatchBodyDto<Guideline>,
@Body(getPatchArrayPipe(PatchGuidelineDto))
patches: PatchGuidelineDto[],
): Promise<void> {
return this.guidelinesService.patch(patch);
return this.guidelinesService.patch(patches);
}

@ApiOperation({ summary: 'Fetch all guidelines' })
Expand Down
25 changes: 18 additions & 7 deletions annotation-server/src/guidelines/guidelines.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { HttpService } from '@nestjs/axios';
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { validateOrReject } from 'class-validator';
import { lastValueFrom } from 'rxjs';
import { FindOptionsOrder, FindOptionsOrderValue, Repository } from 'typeorm';

import { PatchBodyDto } from '../common/dtos/patch-body.dto';
import { PatchGuidelineDto } from '../common/dtos/patch-body.dto';
import { fetchSpreadsheetCells } from '../common/utils/google-sheets';
import { FetchTarget } from '../fetch-dates/fetch-date.entity';
import { FetchDatesService } from '../fetch-dates/fetch-dates.service';
Expand Down Expand Up @@ -167,7 +168,13 @@ export class GuidelinesService {
);
const recommendationDtos: CpicRecommendationDto[] = (
await lastValueFrom(response)
).data;
).data.map(
(response: CpicRecommendationDto) =>
new CpicRecommendationDto(response),
);
await Promise.all(
recommendationDtos.map((dto) => validateOrReject(dto)),
);

const guidelinesByMeds: Map<string, Guideline[]> = new Map();
const guidelineErrors: Set<GuidelineError> = new Set();
Expand Down Expand Up @@ -234,7 +241,9 @@ export class GuidelinesService {
);
const guidelineDtos: CpicGuidelineDto[] = (
await lastValueFrom(response)
).data;
).data.map((data: CpicGuidelineDto) => new CpicGuidelineDto(data));
await Promise.all(guidelineDtos.map((dto) => validateOrReject(dto)));

const guidelineDtoById: Map<number, CpicGuidelineDto> = new Map();
guidelineDtos.forEach((guidelineDto) =>
guidelineDtoById.set(guidelineDto.id, guidelineDto),
Expand Down Expand Up @@ -405,11 +414,13 @@ export class GuidelinesService {
return guidelinesForPhenotype;
}

async patch(patch: PatchBodyDto<Guideline>): Promise<void> {
async patch(patches: PatchGuidelineDto[]): Promise<void> {
await Promise.all(
patch.map(({ id, ...update }) =>
this.guidelinesRepository.update(id, update),
),
patches
.filter((patch) => Object.keys(patch).length > 1)
.map(({ id, ...update }) =>
this.guidelinesRepository.update(id, update),
),
);
}

Expand Down
2 changes: 2 additions & 0 deletions annotation-server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
Expand All @@ -11,6 +12,7 @@ async function bootstrap() {
const configService = app.get(ConfigService);

app.setGlobalPrefix('/api/v1');
app.useGlobalPipes(new ValidationPipe());
app.useGlobalInterceptors(new TypeormErrorInterceptor());

const config = new DocumentBuilder()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { applyDecorators } from '@nestjs/common';
import { ApiQuery } from '@nestjs/swagger';
import { IsBooleanString, IsIn } from 'class-validator';
import { IsBooleanString, IsIn, IsOptional } from 'class-validator';

import {
ApiQueryLimit,
Expand All @@ -16,13 +16,16 @@ export class FindMedicationQueryDto extends SearchableFindQueryDto {
sortby: string;

@IsBooleanString()
withGuidelines: string;
@IsOptional()
withGuidelines?: string;

@IsBooleanString()
getGuidelines: string;
@IsOptional()
getGuidelines?: string;

@IsBooleanString()
onlyIds: string;
@IsOptional()
onlyIds?: string;
}

export function ApiFindMedicationsQueries(): MethodDecorator {
Expand Down
3 changes: 3 additions & 0 deletions annotation-server/src/medications/medication.entity.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IsString } from 'class-validator';
import { Entity, Column, OneToMany, ViewEntity, ViewColumn } from 'typeorm';

import { BaseEntity } from '../common/entities/base.entity';
Expand All @@ -22,9 +23,11 @@ export class Medication extends BaseEntity {
synonyms: string[];

@Column({ nullable: true })
@IsString()
drugclass: string;

@Column({ nullable: true })
@IsString()
indication: string;

@OneToMany(() => Guideline, (guideline) => guideline.medication)
Expand Down
10 changes: 7 additions & 3 deletions annotation-server/src/medications/medications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';

import { ApiBodyPatch } from '../common/api/bodies';
import { ApiParamGetById } from '../common/api/params';
import { PatchBodyDto } from '../common/dtos/patch-body.dto';
import {
getPatchArrayPipe,
PatchMedicationDto,
} from '../common/dtos/patch-body.dto';
import {
ApiFindMedicationsQueries,
ApiFindMedicationQueries,
Expand Down Expand Up @@ -35,9 +38,10 @@ export class MedicationsController {
@ApiBodyPatch('medication')
@Patch()
async patchMedications(
@Body() patch: PatchBodyDto<Medication>,
@Body(getPatchArrayPipe(PatchMedicationDto))
patches: PatchMedicationDto[],
): Promise<void> {
return this.medicationsService.patch(patch);
return this.medicationsService.patch(patches);
}

@ApiOperation({ summary: 'Fetch all medications' })
Expand Down
12 changes: 7 additions & 5 deletions annotation-server/src/medications/medications.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import * as JSONStream from 'JSONStream';
import { FindOneOptions, Repository } from 'typeorm';

import { PatchBodyDto } from '../common/dtos/patch-body.dto';
import { PatchMedicationDto } from '../common/dtos/patch-body.dto';
import { fetchSpreadsheetCells } from '../common/utils/google-sheets';
import { FetchTarget } from '../fetch-dates/fetch-date.entity';
import { FetchDatesService } from '../fetch-dates/fetch-dates.service';
Expand Down Expand Up @@ -166,11 +166,13 @@ export class MedicationsService {
return (await this.medicationRepository.count()) > 0;
}

async patch(patch: PatchBodyDto<Medication>): Promise<void> {
async patch(patches: PatchMedicationDto[]): Promise<void> {
await Promise.all(
patch.map(({ id, ...update }) =>
this.medicationRepository.update(id, update),
),
patches
.filter((patch) => Object.keys(patch).length > 1)
.map(({ id, ...update }) =>
this.medicationRepository.update(id, update),
),
);
}

Expand Down
8 changes: 5 additions & 3 deletions annotation-server/src/phenotypes/dtos/diplotype.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IsNotEmpty, IsString } from 'class-validator';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';

export class DiplotypeDto {
import { InstantiableDto } from '../../common/dtos/instantiable.dto';

export class DiplotypeDto extends InstantiableDto<DiplotypeDto> {
@IsString()
@IsNotEmpty()
genesymbol: string;
Expand All @@ -10,6 +12,6 @@ export class DiplotypeDto {
generesult: string;

@IsString()
@IsNotEmpty()
@IsOptional()
consultationtext: string;
}
7 changes: 5 additions & 2 deletions annotation-server/src/phenotypes/phenotypes.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { validateOrReject } from 'class-validator';
import { lastValueFrom } from 'rxjs';
import { FindOneOptions, Repository } from 'typeorm';

Expand Down Expand Up @@ -40,8 +41,10 @@ export class PhenotypesService {
},
);

const diplotypeDtos: DiplotypeDto[] = (await lastValueFrom(response))
.data;
const diplotypeDtos: DiplotypeDto[] = (
await lastValueFrom(response)
).data.map((data: DiplotypeDto) => new DiplotypeDto(data));
await Promise.all(diplotypeDtos.map((dto) => validateOrReject(dto)));

this.savePhenotypes(diplotypeDtos);

Expand Down

0 comments on commit 6b6eeab

Please sign in to comment.