From 0e62bd8357d40847e68e51429ca70c92b205d8a9 Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Tue, 3 Sep 2024 18:45:26 -0700 Subject: [PATCH 1/8] Add fsp-tracker module and service code. --- api/src/app/app.module.ts | 12 ++-- .../app/modules/external/external.module.ts | 13 +++++ .../fsp-tracker/fsp-tracker.controller.ts | 27 +++++++++ .../external/fsp-tracker/fsp-tracker.dto.ts | 17 ++++++ .../fsp-tracker/fsp-tracker.module.ts | 18 ++++++ .../fsp-tracker/fsp-tracker.service.ts | 57 +++++++++++++++++++ 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 api/src/app/modules/external/external.module.ts create mode 100644 api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts create mode 100644 api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts create mode 100644 api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts create mode 100644 api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts diff --git a/api/src/app/app.module.ts b/api/src/app/app.module.ts index b03a287f..9d772a5e 100644 --- a/api/src/app/app.module.ts +++ b/api/src/app/app.module.ts @@ -1,23 +1,24 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { ScheduleModule } from '@nestjs/schedule'; +import { TypeOrmModule } from '@nestjs/typeorm'; // Core Modules import { AttachmentModule } from './modules/attachment/attachment.module'; -import { InteractionModule } from './modules/interaction/interaction.module'; import { DistrictModule } from './modules/district/district.module'; import { ForestClientModule } from './modules/forest-client/forest-client.module'; -import { ProjectModule } from './modules/project/project.module'; +import { InteractionModule } from './modules/interaction/interaction.module'; import { ProjectAuthModule } from './modules/project/project-auth.module'; +import { ProjectModule } from './modules/project/project.module'; import { PublicCommentModule } from './modules/public-comment/public-comment.module'; -import { SubmissionModule } from './modules/submission/submission.module'; import { SpatialFeatureModule } from './modules/spatial-feature/spatial-feature.module'; +import { SubmissionModule } from './modules/submission/submission.module'; // Other Modules +import { ExternalModule } from '@src/app/modules/external/external.module'; import { LoggerModule } from 'nestjs-pino'; +import { SecurityModule } from '../core/security/security.module'; import { AppConfigModule } from './modules/app-config/app-config.module'; import { AppConfigService } from './modules/app-config/app-config.provider'; -import { SecurityModule } from '../core/security/security.module' function getLogLevel():string { return process.env.LOG_LEVEL || 'info'; @@ -66,6 +67,7 @@ function getLogLevel():string { PublicCommentModule, SubmissionModule, SpatialFeatureModule, + ExternalModule ], }) diff --git a/api/src/app/modules/external/external.module.ts b/api/src/app/modules/external/external.module.ts new file mode 100644 index 00000000..b64295b2 --- /dev/null +++ b/api/src/app/modules/external/external.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { FspTrackerExternalModule } from '@src/app/modules/external/fsp-tracker/fsp-tracker.module'; + +/** + * "External" module provides external facing APIs for other system to + * interface with FOM. + */ +@Module({ + imports: [ + FspTrackerExternalModule + ] +}) +export class ExternalModule {} diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts b/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts new file mode 100644 index 00000000..147ee8fe --- /dev/null +++ b/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts @@ -0,0 +1,27 @@ +import { ProjectFindCriteria } from '@api-modules/project/project.service'; +import { Controller, Get, HttpStatus, ParseIntPipe, Query } from '@nestjs/common'; +import { ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { FomFspTrackerResponse } from '@src/app/modules/external/fsp-tracker/fsp-tracker.dto'; +import { FspTrackerService } from '@src/app/modules/external/fsp-tracker/fsp-tracker.service'; +import { PinoLogger } from 'nestjs-pino'; + +@ApiTags("external") +@Controller("external") +export class FspTrackerController { + constructor( + private readonly service: FspTrackerService, + private readonly _logger: PinoLogger) { + } + + @Get("fsp-tracker") + @ApiQuery({ name: 'fspId', required: true}) + @ApiResponse({ status: HttpStatus.OK, type: [FomFspTrackerResponse] }) + async fspTracker( + @Query('fspId') fspId: string + ): Promise { + const findCriteria: ProjectFindCriteria = new ProjectFindCriteria(); + findCriteria.fspId = await new ParseIntPipe().transform(fspId, null); + return this.service.find(findCriteria); + } + +} diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts b/api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts new file mode 100644 index 00000000..6e4f966a --- /dev/null +++ b/api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { ForestClientResponse } from "@src/app/modules/forest-client/forest-client.dto"; + +export class FomFspTrackerResponse { + + @ApiProperty() + fomId: number; + + @ApiProperty() + name: string; + + @ApiProperty() + fspId: number; + + @ApiPropertyOptional() + forestClient: ForestClientResponse; + } diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts b/api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts new file mode 100644 index 00000000..6e5b9f9e --- /dev/null +++ b/api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FspTrackerController } from '@src/app/modules/external/fsp-tracker/fsp-tracker.controller'; +import { FspTrackerService } from '@src/app/modules/external/fsp-tracker/fsp-tracker.service'; +import { ForestClientModule } from '@src/app/modules/forest-client/forest-client.module'; +import { Project } from '@src/app/modules/project/project.entity'; +import { ProjectModule } from '@src/app/modules/project/project.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Project]), + ProjectModule, + ForestClientModule + ], + controllers: [FspTrackerController], + providers: [FspTrackerService] +}) +export class FspTrackerExternalModule {} diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts b/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts new file mode 100644 index 00000000..8e9cf3aa --- /dev/null +++ b/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts @@ -0,0 +1,57 @@ +import { FomFspTrackerResponse } from "@api-modules/external/fsp-tracker/fsp-tracker.dto"; +import { Project } from "@api-modules/project/project.entity"; +import { DataService } from "@core"; +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { ForestClientService } from "@src/app/modules/forest-client/forest-client.service"; +import { ProjectFindCriteria } from "@src/app/modules/project/project.service"; +import { User } from "@utility/security/user"; +import { PinoLogger } from "nestjs-pino"; +import { Repository } from "typeorm"; + +@Injectable() +export class FspTrackerService extends DataService, FomFspTrackerResponse> { + + constructor( + @InjectRepository(Project) + repository: Repository, + private forestClientService: ForestClientService, + logger: PinoLogger + ) { + super(repository, new Project(), logger); + } + + async isViewAuthorized(_entity: Project, _user?: User): Promise { + return true; + } + + async find(findCriteria: ProjectFindCriteria):Promise { + this.logger.debug('Find criteria: %o', findCriteria); + + const query = this.repository.createQueryBuilder("p") + .leftJoinAndSelect("p.forestClient", "forestClient") + .addOrderBy('p.project_id', 'DESC'); + findCriteria.applyFindCriteria(query); + query.limit(2500); // Can't use take(). Limit # of results to avoid system strain. + + const queryResults:Project[] = await query.getMany(); + if (queryResults && queryResults.length > 0) { + this.logger.debug(`${queryResults.length} project(s) found.`); + return queryResults.map(project => this.convertEntity(project)); + } + this.logger.debug('No result found.'); + return []; + } + + convertEntity(entity: Project): FomFspTrackerResponse { + const response = new FomFspTrackerResponse(); + response.fomId = entity.id + response.name = entity.name + response.fspId = entity.fspId; + if (entity.forestClient != null) { + response.forestClient = this.forestClientService.convertEntity(entity.forestClient); + } + return response; + } + +} \ No newline at end of file From 3717bc9750812b47652553887d5261ec88429a46 Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Tue, 3 Sep 2024 21:33:59 -0700 Subject: [PATCH 2/8] Remove unnecessary code. --- .../app/modules/external/fsp-tracker/fsp-tracker.service.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts b/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts index 8e9cf3aa..946c987c 100644 --- a/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts +++ b/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts @@ -5,7 +5,6 @@ import { Injectable } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { ForestClientService } from "@src/app/modules/forest-client/forest-client.service"; import { ProjectFindCriteria } from "@src/app/modules/project/project.service"; -import { User } from "@utility/security/user"; import { PinoLogger } from "nestjs-pino"; import { Repository } from "typeorm"; @@ -21,10 +20,6 @@ export class FspTrackerService extends DataService, super(repository, new Project(), logger); } - async isViewAuthorized(_entity: Project, _user?: User): Promise { - return true; - } - async find(findCriteria: ProjectFindCriteria):Promise { this.logger.debug('Find criteria: %o', findCriteria); From e6240b9dae58670e2b2b299809a282b5ccc9f83d Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Tue, 3 Sep 2024 22:24:19 -0700 Subject: [PATCH 3/8] Use PositiveIntPipe --- .../fsp-tracker/fsp-tracker.controller.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts b/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts index 147ee8fe..cf6265dd 100644 --- a/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts +++ b/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts @@ -1,10 +1,26 @@ import { ProjectFindCriteria } from '@api-modules/project/project.service'; -import { Controller, Get, HttpStatus, ParseIntPipe, Query } from '@nestjs/common'; +import { ArgumentMetadata, BadRequestException, Controller, Get, HttpStatus, Injectable, PipeTransform, Query } from '@nestjs/common'; import { ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { FomFspTrackerResponse } from '@src/app/modules/external/fsp-tracker/fsp-tracker.dto'; import { FspTrackerService } from '@src/app/modules/external/fsp-tracker/fsp-tracker.service'; import { PinoLogger } from 'nestjs-pino'; +// Custom pipe transformer for controller parameter conversion. +@Injectable() +export class PositiveIntPipe implements PipeTransform { + transform(value: any, metadata: ArgumentMetadata) { + + if(/^\d+$/.test(value)) { + const intValue = parseInt(value); + if ( intValue > 0) { + return value; + } + } + + throw new BadRequestException('Value must be positive integer.'); + } +} + @ApiTags("external") @Controller("external") export class FspTrackerController { @@ -17,11 +33,10 @@ export class FspTrackerController { @ApiQuery({ name: 'fspId', required: true}) @ApiResponse({ status: HttpStatus.OK, type: [FomFspTrackerResponse] }) async fspTracker( - @Query('fspId') fspId: string + @Query('fspId', PositiveIntPipe) fspId: number ): Promise { const findCriteria: ProjectFindCriteria = new ProjectFindCriteria(); - findCriteria.fspId = await new ParseIntPipe().transform(fspId, null); + findCriteria.fspId = fspId return this.service.find(findCriteria); } - -} +} \ No newline at end of file From 5cb29d3b68a480ca8e99f8ea4d74c34c6c27dce7 Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Wed, 4 Sep 2024 15:09:05 -0700 Subject: [PATCH 4/8] Minor rename folders/files. --- api/src/app/modules/external/external.module.ts | 4 ++-- .../projects-by-fsp.controller.ts} | 16 ++++++++-------- .../projects-by-fsp.dto.ts} | 2 +- .../projects-by-fsp.module.ts} | 10 +++++----- .../projects-by-fsp.service.ts} | 10 +++++----- 5 files changed, 21 insertions(+), 21 deletions(-) rename api/src/app/modules/external/{fsp-tracker/fsp-tracker.controller.ts => projects-by-fsp/projects-by-fsp.controller.ts} (71%) rename api/src/app/modules/external/{fsp-tracker/fsp-tracker.dto.ts => projects-by-fsp/projects-by-fsp.dto.ts} (90%) rename api/src/app/modules/external/{fsp-tracker/fsp-tracker.module.ts => projects-by-fsp/projects-by-fsp.module.ts} (56%) rename api/src/app/modules/external/{fsp-tracker/fsp-tracker.service.ts => projects-by-fsp/projects-by-fsp.service.ts} (80%) diff --git a/api/src/app/modules/external/external.module.ts b/api/src/app/modules/external/external.module.ts index b64295b2..ebc2ac5f 100644 --- a/api/src/app/modules/external/external.module.ts +++ b/api/src/app/modules/external/external.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { FspTrackerExternalModule } from '@src/app/modules/external/fsp-tracker/fsp-tracker.module'; +import { ProjectsByFspExternalModule } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.module'; /** * "External" module provides external facing APIs for other system to @@ -7,7 +7,7 @@ import { FspTrackerExternalModule } from '@src/app/modules/external/fsp-tracker/ */ @Module({ imports: [ - FspTrackerExternalModule + ProjectsByFspExternalModule ] }) export class ExternalModule {} diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts similarity index 71% rename from api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts rename to api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts index cf6265dd..544a6fb3 100644 --- a/api/src/app/modules/external/fsp-tracker/fsp-tracker.controller.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts @@ -1,8 +1,8 @@ import { ProjectFindCriteria } from '@api-modules/project/project.service'; import { ArgumentMetadata, BadRequestException, Controller, Get, HttpStatus, Injectable, PipeTransform, Query } from '@nestjs/common'; import { ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { FomFspTrackerResponse } from '@src/app/modules/external/fsp-tracker/fsp-tracker.dto'; -import { FspTrackerService } from '@src/app/modules/external/fsp-tracker/fsp-tracker.service'; +import { FrojectByFspResponse as ProjectsByFspResponse } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.dto'; +import { ProjectsByFspService } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.service'; import { PinoLogger } from 'nestjs-pino'; // Custom pipe transformer for controller parameter conversion. @@ -23,18 +23,18 @@ export class PositiveIntPipe implements PipeTransform { @ApiTags("external") @Controller("external") -export class FspTrackerController { +export class ProjectsByFspController { constructor( - private readonly service: FspTrackerService, + private readonly service: ProjectsByFspService, private readonly _logger: PinoLogger) { } - @Get("fsp-tracker") + @Get("fom-by-fsp") @ApiQuery({ name: 'fspId', required: true}) - @ApiResponse({ status: HttpStatus.OK, type: [FomFspTrackerResponse] }) - async fspTracker( + @ApiResponse({ status: HttpStatus.OK, type: [ProjectsByFspResponse] }) + async find( @Query('fspId', PositiveIntPipe) fspId: number - ): Promise { + ): Promise { const findCriteria: ProjectFindCriteria = new ProjectFindCriteria(); findCriteria.fspId = fspId return this.service.find(findCriteria); diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts similarity index 90% rename from api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts rename to api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts index 6e4f966a..2835ad0a 100644 --- a/api/src/app/modules/external/fsp-tracker/fsp-tracker.dto.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ForestClientResponse } from "@src/app/modules/forest-client/forest-client.dto"; -export class FomFspTrackerResponse { +export class FrojectByFspResponse { @ApiProperty() fomId: number; diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.module.ts similarity index 56% rename from api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts rename to api/src/app/modules/external/projects-by-fsp/projects-by-fsp.module.ts index 6e5b9f9e..c1fee1c1 100644 --- a/api/src/app/modules/external/fsp-tracker/fsp-tracker.module.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { FspTrackerController } from '@src/app/modules/external/fsp-tracker/fsp-tracker.controller'; -import { FspTrackerService } from '@src/app/modules/external/fsp-tracker/fsp-tracker.service'; +import { ProjectsByFspController } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.controller'; +import { ProjectsByFspService } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.service'; import { ForestClientModule } from '@src/app/modules/forest-client/forest-client.module'; import { Project } from '@src/app/modules/project/project.entity'; import { ProjectModule } from '@src/app/modules/project/project.module'; @@ -12,7 +12,7 @@ import { ProjectModule } from '@src/app/modules/project/project.module'; ProjectModule, ForestClientModule ], - controllers: [FspTrackerController], - providers: [FspTrackerService] + controllers: [ProjectsByFspController], + providers: [ProjectsByFspService] }) -export class FspTrackerExternalModule {} +export class ProjectsByFspExternalModule {} diff --git a/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts similarity index 80% rename from api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts rename to api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts index 946c987c..c270921a 100644 --- a/api/src/app/modules/external/fsp-tracker/fsp-tracker.service.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts @@ -1,4 +1,4 @@ -import { FomFspTrackerResponse } from "@api-modules/external/fsp-tracker/fsp-tracker.dto"; +import { FrojectByFspResponse } from "@api-modules/external/projects-by-fsp/projects-by-fsp.dto"; import { Project } from "@api-modules/project/project.entity"; import { DataService } from "@core"; import { Injectable } from "@nestjs/common"; @@ -9,7 +9,7 @@ import { PinoLogger } from "nestjs-pino"; import { Repository } from "typeorm"; @Injectable() -export class FspTrackerService extends DataService, FomFspTrackerResponse> { +export class ProjectsByFspService extends DataService, FrojectByFspResponse> { constructor( @InjectRepository(Project) @@ -20,7 +20,7 @@ export class FspTrackerService extends DataService, super(repository, new Project(), logger); } - async find(findCriteria: ProjectFindCriteria):Promise { + async find(findCriteria: ProjectFindCriteria):Promise { this.logger.debug('Find criteria: %o', findCriteria); const query = this.repository.createQueryBuilder("p") @@ -38,8 +38,8 @@ export class FspTrackerService extends DataService, return []; } - convertEntity(entity: Project): FomFspTrackerResponse { - const response = new FomFspTrackerResponse(); + convertEntity(entity: Project): FrojectByFspResponse { + const response = new FrojectByFspResponse(); response.fomId = entity.id response.name = entity.name response.fspId = entity.fspId; From 35606af1c607862d183e31c593ff499920eca418 Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Wed, 4 Sep 2024 15:20:36 -0700 Subject: [PATCH 5/8] Minor method argument adjustment. --- .../projects-by-fsp/projects-by-fsp.controller.ts | 7 ++----- .../external/projects-by-fsp/projects-by-fsp.service.ts | 8 +++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts index 544a6fb3..8314763d 100644 --- a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts @@ -1,4 +1,3 @@ -import { ProjectFindCriteria } from '@api-modules/project/project.service'; import { ArgumentMetadata, BadRequestException, Controller, Get, HttpStatus, Injectable, PipeTransform, Query } from '@nestjs/common'; import { ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { FrojectByFspResponse as ProjectsByFspResponse } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.dto'; @@ -32,11 +31,9 @@ export class ProjectsByFspController { @Get("fom-by-fsp") @ApiQuery({ name: 'fspId', required: true}) @ApiResponse({ status: HttpStatus.OK, type: [ProjectsByFspResponse] }) - async find( + async findByFsp( @Query('fspId', PositiveIntPipe) fspId: number ): Promise { - const findCriteria: ProjectFindCriteria = new ProjectFindCriteria(); - findCriteria.fspId = fspId - return this.service.find(findCriteria); + return this.service.findByFspId(fspId); } } \ No newline at end of file diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts index c270921a..57405903 100644 --- a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts @@ -7,6 +7,7 @@ import { ForestClientService } from "@src/app/modules/forest-client/forest-clien import { ProjectFindCriteria } from "@src/app/modules/project/project.service"; import { PinoLogger } from "nestjs-pino"; import { Repository } from "typeorm"; +import _ = require('lodash'); @Injectable() export class ProjectsByFspService extends DataService, FrojectByFspResponse> { @@ -20,7 +21,12 @@ export class ProjectsByFspService extends DataService { + async findByFspId(fspId: number):Promise { + if (_.isNil(fspId)) { + return [] + } + const findCriteria: ProjectFindCriteria = new ProjectFindCriteria(); + findCriteria.fspId = fspId; this.logger.debug('Find criteria: %o', findCriteria); const query = this.repository.createQueryBuilder("p") From 282400d623b46ed597ef8c4f8ed80cee05821da7 Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Wed, 4 Sep 2024 22:22:11 -0700 Subject: [PATCH 6/8] Refactoring. --- .../projects-by-fsp/projects-by-fsp.controller.ts | 2 +- .../external/projects-by-fsp/projects-by-fsp.dto.ts | 2 +- .../projects-by-fsp/projects-by-fsp.service.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts index 8314763d..ba4726dc 100644 --- a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.controller.ts @@ -1,6 +1,6 @@ import { ArgumentMetadata, BadRequestException, Controller, Get, HttpStatus, Injectable, PipeTransform, Query } from '@nestjs/common'; import { ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { FrojectByFspResponse as ProjectsByFspResponse } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.dto'; +import { ProjectByFspResponse as ProjectsByFspResponse } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.dto'; import { ProjectsByFspService } from '@src/app/modules/external/projects-by-fsp/projects-by-fsp.service'; import { PinoLogger } from 'nestjs-pino'; diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts index 2835ad0a..cd3074fb 100644 --- a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ForestClientResponse } from "@src/app/modules/forest-client/forest-client.dto"; -export class FrojectByFspResponse { +export class ProjectByFspResponse { @ApiProperty() fomId: number; diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts index 57405903..6447c6e7 100644 --- a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.ts @@ -1,4 +1,4 @@ -import { FrojectByFspResponse } from "@api-modules/external/projects-by-fsp/projects-by-fsp.dto"; +import { ProjectByFspResponse } from "@api-modules/external/projects-by-fsp/projects-by-fsp.dto"; import { Project } from "@api-modules/project/project.entity"; import { DataService } from "@core"; import { Injectable } from "@nestjs/common"; @@ -10,7 +10,7 @@ import { Repository } from "typeorm"; import _ = require('lodash'); @Injectable() -export class ProjectsByFspService extends DataService, FrojectByFspResponse> { +export class ProjectsByFspService extends DataService, ProjectByFspResponse> { constructor( @InjectRepository(Project) @@ -21,7 +21,7 @@ export class ProjectsByFspService extends DataService { + async findByFspId(fspId: number):Promise { if (_.isNil(fspId)) { return [] } @@ -44,8 +44,8 @@ export class ProjectsByFspService extends DataService Date: Wed, 4 Sep 2024 22:36:45 -0700 Subject: [PATCH 7/8] Add tests --- .../projects-by-fsp.service.spec.ts | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts new file mode 100644 index 00000000..8e999d7a --- /dev/null +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts @@ -0,0 +1,135 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { getRepositoryToken } from "@nestjs/typeorm"; +import { ProjectByFspResponse } from "@src/app/modules/external/projects-by-fsp/projects-by-fsp.dto"; +import { ProjectsByFspService } from "@src/app/modules/external/projects-by-fsp/projects-by-fsp.service"; +import { ForestClientService } from "@src/app/modules/forest-client/forest-client.service"; +import { ProjectResponse } from "@src/app/modules/project/project.dto"; +import { Project } from "@src/app/modules/project/project.entity"; +import { PinoLogger } from "nestjs-pino"; +import { DataSource, Repository } from "typeorm"; + +describe('ProjectsByFspService', () => { + let service: ProjectsByFspService; + let forestClientService: ForestClientService; + let repository: Repository; + let createQueryBuilderMock: any; + + // service and dependencies setup. + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + providers: provideDependencyMock() + }).compile(); + + forestClientService = moduleRef.get(ForestClientService); + service = moduleRef.get(ProjectsByFspService); + repository = moduleRef.get(getRepositoryToken(Project)); + + createQueryBuilderMock = { + leftJoinAndSelect: () => createQueryBuilderMock, + where: () => createQueryBuilderMock, + andWhere: () => createQueryBuilderMock, + orWhere: () => createQueryBuilderMock, + addOrderBy: () => createQueryBuilderMock, + limit: () => createQueryBuilderMock, + getMany: () => [], // default empty + }; + + }); + + // This is prerequisite to make sure services/repository are setup for testing. + it('Services/repository for testing should be defined', () => { + expect(service).toBeDefined(); + expect(forestClientService).toBeDefined(); + expect(repository).toBeDefined(); + }); + + describe('findByFspId', () => { + it('returns empty array when no fspId param', async () => { + expect(await service.findByFspId(null)).toEqual([]); + }); + + it('returns empty array when query builder result is empty.', async () => { + // use default createQueryBuilderMock + const createQueryBuilderSpy = jest.spyOn(repository, 'createQueryBuilder').mockImplementation(() => createQueryBuilderMock); + const fspIdWithNoFom = 999; + expect(await service.findByFspId(fspIdWithNoFom)).toEqual([]); + expect(createQueryBuilderSpy).toHaveBeenCalled(); + }); + + it('returns correct result when query builder finds records.', async () => { + const foundProjects = getSimpleProjectResponseData(); + createQueryBuilderMock.getMany = jest.fn().mockReturnValue(foundProjects); + const createQueryBuilderSpy = jest.spyOn(repository, 'createQueryBuilder').mockImplementation(() => createQueryBuilderMock); + const forestClientServiceConvertEntitySpy = jest.spyOn(forestClientService, 'convertEntity'); + const fspIdWithNoFom = 11; + const result = await service.findByFspId(fspIdWithNoFom); + expect(result.length).toEqual(getSimpleProjectResponseData().length); + expect(result[0]).toBeInstanceOf(ProjectByFspResponse) + expect(createQueryBuilderSpy).toHaveBeenCalled(); + expect(forestClientServiceConvertEntitySpy).toHaveBeenCalled(); + expect(result[0].fspId).toEqual(foundProjects[0].fspId) + }); + }); + +}); + +export class ProjectRepositoryFake { + public createQueryBuilder(): void { + // This is intentional for empty body. + } +} + +function provideDependencyMock(): Array { + const dependencyMock = + [ ProjectsByFspService, + { + provide: getRepositoryToken(Project), + useClass: ProjectRepositoryFake + }, + { + provide: PinoLogger, + useValue: { + info: jest.fn((x) => x), + debug: jest.fn((x) => x), + setContext: jest.fn((x) => x), + } + }, + { + provide: ForestClientService, + useValue: { + convertEntity: jest.fn() + } + }, + { + provide: DataSource, + useValue: { + getRepository: jest.fn() + } + } + ]; + return dependencyMock; +} + +function getSimpleProjectResponseData(): Partial[] { + const data = [ + { + "id": 1, + "name": "Project #1", + "fspId": 11, + "forestClient": { + "id": "00001012", + "name": "CLIENT #1" + } + }, + { + "id": 2, + "name": "Project #2", + "fspId": 11, + "forestClient": { + "id": "00001015", + "name": "CLIENT #2" + } + } + ] + return data; +} From 5e1a6d40225d445bbbf80d3bb117129e7aa558c0 Mon Sep 17 00:00:00 2001 From: Ian Liu Date: Thu, 5 Sep 2024 09:45:50 -0700 Subject: [PATCH 8/8] Fix data return type. Minor variable renamed. --- .../projects-by-fsp/projects-by-fsp.service.spec.ts | 8 ++++---- api/src/core/utils.ts | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts index 8e999d7a..5448ca8d 100644 --- a/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts +++ b/api/src/app/modules/external/projects-by-fsp/projects-by-fsp.service.spec.ts @@ -3,8 +3,8 @@ import { getRepositoryToken } from "@nestjs/typeorm"; import { ProjectByFspResponse } from "@src/app/modules/external/projects-by-fsp/projects-by-fsp.dto"; import { ProjectsByFspService } from "@src/app/modules/external/projects-by-fsp/projects-by-fsp.service"; import { ForestClientService } from "@src/app/modules/forest-client/forest-client.service"; -import { ProjectResponse } from "@src/app/modules/project/project.dto"; import { Project } from "@src/app/modules/project/project.entity"; +import { RecursivePartial } from "@src/core/utils"; import { PinoLogger } from "nestjs-pino"; import { DataSource, Repository } from "typeorm"; @@ -61,8 +61,8 @@ describe('ProjectsByFspService', () => { createQueryBuilderMock.getMany = jest.fn().mockReturnValue(foundProjects); const createQueryBuilderSpy = jest.spyOn(repository, 'createQueryBuilder').mockImplementation(() => createQueryBuilderMock); const forestClientServiceConvertEntitySpy = jest.spyOn(forestClientService, 'convertEntity'); - const fspIdWithNoFom = 11; - const result = await service.findByFspId(fspIdWithNoFom); + const fspIdWithFom = 11; + const result = await service.findByFspId(fspIdWithFom); expect(result.length).toEqual(getSimpleProjectResponseData().length); expect(result[0]).toBeInstanceOf(ProjectByFspResponse) expect(createQueryBuilderSpy).toHaveBeenCalled(); @@ -110,7 +110,7 @@ function provideDependencyMock(): Array { return dependencyMock; } -function getSimpleProjectResponseData(): Partial[] { +function getSimpleProjectResponseData(): RecursivePartial[] { const data = [ { "id": 1, diff --git a/api/src/core/utils.ts b/api/src/core/utils.ts index 2bfdac7b..f2881a92 100644 --- a/api/src/core/utils.ts +++ b/api/src/core/utils.ts @@ -66,3 +66,8 @@ export const flatDeep = (arr, d = 1) => { return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), []) : arr.slice(); }; + +// Partial type with nested properties also as Partial. +export type RecursivePartial = { + [P in keyof T]?: RecursivePartial; +}; \ No newline at end of file