From 08352639fc6f3e4d75f19f2cba1fa77a3db633f5 Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Mon, 7 Jun 2021 13:29:14 +0200 Subject: [PATCH 1/4] refactor(api): scenarios: crud --- .../modules/projects/projects-crud.service.ts | 6 +++--- .../scenarios/dto/scenario.serializer.ts | 4 ++++ .../src/modules/scenarios/scenario.service.ts | 21 +++++++++++++++++++ ...s.service.ts => scenarios-crud.service.ts} | 2 +- .../modules/scenarios/scenarios.controller.ts | 15 ++++--------- .../src/modules/scenarios/scenarios.module.ts | 14 ++++++++++--- 6 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts create mode 100644 api/apps/api/src/modules/scenarios/scenario.service.ts rename api/apps/api/src/modules/scenarios/{scenarios.service.ts => scenarios-crud.service.ts} (99%) diff --git a/api/apps/api/src/modules/projects/projects-crud.service.ts b/api/apps/api/src/modules/projects/projects-crud.service.ts index b849986cb4..7b56bd688c 100644 --- a/api/apps/api/src/modules/projects/projects-crud.service.ts +++ b/api/apps/api/src/modules/projects/projects-crud.service.ts @@ -6,7 +6,7 @@ import { Project } from './project.api.entity'; import { CreateProjectDTO } from './dto/create.project.dto'; import { UpdateProjectDTO } from './dto/update.project.dto'; import { UsersService } from '@marxan-api/modules/users/users.service'; -import { ScenariosService } from '@marxan-api/modules/scenarios/scenarios.service'; +import { ScenariosCrudService } from '@marxan-api/modules/scenarios/scenarios-crud.service'; import { PlanningUnitsService } from '@marxan-api/modules/planning-units/planning-units.service'; import { AppBaseService, @@ -41,8 +41,8 @@ export class ProjectsCrudService extends AppBaseService< constructor( @InjectRepository(Project) protected readonly repository: Repository, - @Inject(forwardRef(() => ScenariosService)) - protected readonly scenariosService: ScenariosService, + @Inject(forwardRef(() => ScenariosCrudService)) + protected readonly scenariosService: ScenariosCrudService, @Inject(UsersService) protected readonly usersService: UsersService, @Inject(AdminAreasService) protected readonly adminAreasService: AdminAreasService, diff --git a/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts b/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts new file mode 100644 index 0000000000..696101d6ee --- /dev/null +++ b/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class ScenarioSerializer {} diff --git a/api/apps/api/src/modules/scenarios/scenario.service.ts b/api/apps/api/src/modules/scenarios/scenario.service.ts new file mode 100644 index 0000000000..1b2da33b0e --- /dev/null +++ b/api/apps/api/src/modules/scenarios/scenario.service.ts @@ -0,0 +1,21 @@ +import { HttpService, Injectable } from '@nestjs/common'; +import { AppConfig } from '@marxan-api/utils/config.utils'; +import { ScenariosCrudService } from '@marxan-api/modules/scenarios/scenarios-crud.service'; +import { ScenarioFeaturesService } from '@marxan-api/modules/scenarios-features'; +import { AdjustPlanningUnits } from '@marxan-api/modules/analysis/entry-points/adjust-planning-units'; +import { CostSurfaceFacade } from '@marxan-api/modules/scenarios/cost-surface/cost-surface.facade'; + +@Injectable() +export class ScenarioService { + private readonly geoprocessingUrl: string = AppConfig.get( + 'geoprocessing.url', + ) as string; + + constructor( + public readonly service: ScenariosCrudService, + private readonly scenarioFeatures: ScenarioFeaturesService, + private readonly updatePlanningUnits: AdjustPlanningUnits, + private readonly costSurface: CostSurfaceFacade, + private readonly httpService: HttpService, + ) {} +} diff --git a/api/apps/api/src/modules/scenarios/scenarios.service.ts b/api/apps/api/src/modules/scenarios/scenarios-crud.service.ts similarity index 99% rename from api/apps/api/src/modules/scenarios/scenarios.service.ts rename to api/apps/api/src/modules/scenarios/scenarios-crud.service.ts index 0e0ec4a5b5..60b29d0c19 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.service.ts +++ b/api/apps/api/src/modules/scenarios/scenarios-crud.service.ts @@ -29,7 +29,7 @@ type ScenarioFilterKeys = keyof Pick< type ScenarioFilters = Record; @Injectable() -export class ScenariosService extends AppBaseService< +export class ScenariosCrudService extends AppBaseService< Scenario, CreateScenarioDTO, UpdateScenarioDTO, diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index 3501837d66..ce7fed9427 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -20,7 +20,7 @@ import { ScenarioResult, } from './scenario.api.entity'; import { Request } from 'express'; -import { ScenariosService } from './scenarios.service'; +import { ScenariosCrudService } from './scenarios-crud.service'; import { FetchSpecification, ProcessFetchSpecification, @@ -56,22 +56,14 @@ import { ApiConsumesShapefile } from '@marxan-api/decorators/shapefile.decorator import { CostSurfaceFacade } from './cost-surface/cost-surface.facade'; import { FileInterceptor } from '@nestjs/platform-express'; import { AppConfig } from '@marxan-api/utils/config.utils'; +import { ScenarioService } from '@marxan-api/modules/scenarios/scenario.service'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(scenarioResource.className) @Controller(`${apiGlobalPrefixes.v1}/scenarios`) export class ScenariosController { - private readonly geoprocessingUrl: string = AppConfig.get( - 'geoprocessing.url', - ) as string; - constructor( - public readonly service: ScenariosService, - private readonly scenarioFeatures: ScenarioFeaturesService, - private readonly updatePlanningUnits: AdjustPlanningUnits, - private readonly costSurface: CostSurfaceFacade, - private readonly httpService: HttpService, - ) {} + constructor(private readonly scenariosService: ScenarioService) {} @ApiOperation({ description: 'Find all scenarios', @@ -165,6 +157,7 @@ export class ScenariosController { .toPromise(); return geoJson; } + @ApiOperation({ description: 'Update scenario' }) @ApiOkResponse({ type: ScenarioResult }) @Patch(':id') diff --git a/api/apps/api/src/modules/scenarios/scenarios.module.ts b/api/apps/api/src/modules/scenarios/scenarios.module.ts index 9709880711..630dfd9356 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.module.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.module.ts @@ -4,7 +4,7 @@ import { CqrsModule } from '@nestjs/cqrs'; import { ScenariosController } from './scenarios.controller'; import { Scenario } from './scenario.api.entity'; -import { ScenariosService } from './scenarios.service'; +import { ScenariosCrudService } from './scenarios-crud.service'; import { UsersModule } from '@marxan-api/modules/users/users.module'; import { Project } from '@marxan-api/modules/projects/project.api.entity'; import { ProtectedAreasModule } from '@marxan-api/modules/protected-areas/protected-areas.module'; @@ -14,6 +14,8 @@ import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { WdpaAreaCalculationService } from './wdpa-area-calculation.service'; import { AnalysisModule } from '../analysis/analysis.module'; import { CostSurfaceModule } from './cost-surface/cost-surface.module'; +import { ScenarioService } from './scenario.service'; +import { ScenarioSerializer } from './dto/scenario.serializer'; @Module({ imports: [ @@ -27,8 +29,14 @@ import { CostSurfaceModule } from './cost-surface/cost-surface.module'; CostSurfaceModule, HttpModule, ], - providers: [ScenariosService, ProxyService, WdpaAreaCalculationService], + providers: [ + ScenarioService, + ScenariosCrudService, + ProxyService, + WdpaAreaCalculationService, + ScenarioSerializer, + ], controllers: [ScenariosController], - exports: [ScenariosService], + exports: [ScenariosCrudService, ScenarioService], }) export class ScenariosModule {} From 49e72e66f26aa6504af0e3e5776552233c84945d Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Mon, 7 Jun 2021 14:37:35 +0200 Subject: [PATCH 2/4] feature(api): scenarios: enable scenario-id guard --- .../dto/scenario-feature.serializer.ts | 16 +++ .../scenarios/dto/scenario.serializer.ts | 14 ++- .../src/modules/scenarios/scenario.service.ts | 102 +++++++++++++++++- .../modules/scenarios/scenarios.controller.ts | 86 ++++----------- .../src/modules/scenarios/scenarios.module.ts | 2 + 5 files changed, 151 insertions(+), 69 deletions(-) create mode 100644 api/apps/api/src/modules/scenarios/dto/scenario-feature.serializer.ts diff --git a/api/apps/api/src/modules/scenarios/dto/scenario-feature.serializer.ts b/api/apps/api/src/modules/scenarios/dto/scenario-feature.serializer.ts new file mode 100644 index 0000000000..901dc84e28 --- /dev/null +++ b/api/apps/api/src/modules/scenarios/dto/scenario-feature.serializer.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { PaginationMeta } from '@marxan-api/utils/app-base.service'; +import { ScenarioFeaturesService } from '@marxan-api/modules/scenarios-features'; +import { GeoFeature } from '@marxan-api/modules/geo-features/geo-feature.api.entity'; + +@Injectable() +export class ScenarioFeatureSerializer { + constructor(private readonly featuresCrud: ScenarioFeaturesService) {} + + async serialize( + entities: Partial | (Partial | undefined)[], + paginationMeta?: PaginationMeta, + ): Promise { + return this.featuresCrud.serialize(entities, paginationMeta); + } +} diff --git a/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts b/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts index 696101d6ee..04bb28c6f0 100644 --- a/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts +++ b/api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts @@ -1,4 +1,16 @@ import { Injectable } from '@nestjs/common'; +import { PaginationMeta } from '@marxan-api/utils/app-base.service'; +import { Scenario } from '../scenario.api.entity'; +import { ScenariosCrudService } from '../scenarios-crud.service'; @Injectable() -export class ScenarioSerializer {} +export class ScenarioSerializer { + constructor(private readonly scenariosCrudService: ScenariosCrudService) {} + + async serialize( + entities: Partial | (Partial | undefined)[], + paginationMeta?: PaginationMeta, + ): Promise { + return this.scenariosCrudService.serialize(entities, paginationMeta); + } +} diff --git a/api/apps/api/src/modules/scenarios/scenario.service.ts b/api/apps/api/src/modules/scenarios/scenario.service.ts index 1b2da33b0e..50b8be1c86 100644 --- a/api/apps/api/src/modules/scenarios/scenario.service.ts +++ b/api/apps/api/src/modules/scenarios/scenario.service.ts @@ -1,9 +1,19 @@ import { HttpService, Injectable } from '@nestjs/common'; +import { FetchSpecification } from 'nestjs-base-service'; + +import { AppInfoDTO } from '@marxan-api/dto/info.dto'; import { AppConfig } from '@marxan-api/utils/config.utils'; -import { ScenariosCrudService } from '@marxan-api/modules/scenarios/scenarios-crud.service'; import { ScenarioFeaturesService } from '@marxan-api/modules/scenarios-features'; import { AdjustPlanningUnits } from '@marxan-api/modules/analysis/entry-points/adjust-planning-units'; -import { CostSurfaceFacade } from '@marxan-api/modules/scenarios/cost-surface/cost-surface.facade'; +import { apiGlobalPrefixes } from '@marxan-api/api.config'; + +import { CostSurfaceFacade } from './cost-surface/cost-surface.facade'; +import { ScenariosCrudService } from './scenarios-crud.service'; +import { JobStatus } from './scenario.api.entity'; + +import { CreateScenarioDTO } from './dto/create.scenario.dto'; +import { UpdateScenarioDTO } from './dto/update.scenario.dto'; +import { UpdateScenarioPlanningUnitLockStatusDto } from './dto/update-scenario-planning-unit-lock-status.dto'; @Injectable() export class ScenarioService { @@ -12,10 +22,96 @@ export class ScenarioService { ) as string; constructor( - public readonly service: ScenariosCrudService, + private readonly crudService: ScenariosCrudService, private readonly scenarioFeatures: ScenarioFeaturesService, private readonly updatePlanningUnits: AdjustPlanningUnits, private readonly costSurface: CostSurfaceFacade, private readonly httpService: HttpService, ) {} + + async findAll(fetchSpecification: FetchSpecification) { + return this.crudService.findAllPaginated(fetchSpecification); + } + + async getById(scenarioId: string, fetchSpecification?: FetchSpecification) { + return this.crudService.getById(scenarioId, fetchSpecification); + } + + async remove(scenarioId: string): Promise { + await this.assertScenario(scenarioId); + return this.crudService.remove(scenarioId); + } + + async create(input: CreateScenarioDTO, info: AppInfoDTO) { + return this.crudService.create(input, info); + } + + async update(scenarioId: string, input: UpdateScenarioDTO) { + await this.assertScenario(scenarioId); + return this.crudService.update(scenarioId, input); + } + + async getFeatures(scenarioId: string) { + await this.assertScenario(scenarioId); + return ( + await this.scenarioFeatures.findAll(undefined, { + params: { + scenarioId, + }, + }) + )[0]; + } + + async getPendingJobs(_scenarioId: string) { + return { + status: JobStatus.running, + }; + } + + async changeLockStatus( + scenarioId: string, + input: UpdateScenarioPlanningUnitLockStatusDto, + ) { + await this.assertScenario(scenarioId); + // TODO implement more flexible error results to propagate 4xx + await this.updatePlanningUnits.update(scenarioId, { + include: { + geo: input.byGeoJson?.include, + pu: input.byId?.include, + }, + exclude: { + pu: input.byId?.exclude, + geo: input.byGeoJson?.exclude, + }, + }); + return; + } + + processCostSurfaceShapefile(scenarioId: string, file: Express.Multer.File) { + this.costSurface.convert(scenarioId, file); + return; + } + + async uploadLockInShapeFile(scenarioId: string, file: Express.Multer.File) { + await this.assertScenario(scenarioId); + /** + * @validateStatus is required for HttpService to not reject and wrap geoprocessing's response + * in case a shapefile is not validated and a status 4xx is sent back. + */ + const { data: geoJson } = await this.httpService + .post( + `${this.geoprocessingUrl}${apiGlobalPrefixes.v1}/planning-units/planning-unit-shapefile`, + file, + { + headers: { 'Content-Type': 'application/json' }, + validateStatus: (status) => status <= 499, + }, + ) + .toPromise(); + return geoJson; + } + + private async assertScenario(scenarioId: string) { + await this.crudService.getById(scenarioId); + } } diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index ce7fed9427..3a1dcc8cc2 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -3,7 +3,6 @@ import { Controller, Delete, Get, - HttpService, Param, ParseUUIDPipe, Patch, @@ -20,7 +19,6 @@ import { ScenarioResult, } from './scenario.api.entity'; import { Request } from 'express'; -import { ScenariosCrudService } from './scenarios-crud.service'; import { FetchSpecification, ProcessFetchSpecification, @@ -44,26 +42,27 @@ import { import { CreateScenarioDTO } from './dto/create.scenario.dto'; import { UpdateScenarioDTO } from './dto/update.scenario.dto'; import { RequestWithAuthenticatedUser } from '@marxan-api/app.controller'; - -import { ScenarioFeaturesService } from '../scenarios-features'; import { RemoteScenarioFeaturesData } from '../scenarios-features/entities/remote-scenario-features-data.geo.entity'; import { ProcessingStatusDto } from './dto/processing-status.dto'; import { UpdateScenarioPlanningUnitLockStatusDto } from './dto/update-scenario-planning-unit-lock-status.dto'; import { uploadOptions } from '@marxan-api/utils/file-uploads.utils'; import { ShapefileGeoJSONResponseDTO } from './dto/shapefile.geojson.response.dto'; -import { AdjustPlanningUnits } from '../analysis/entry-points/adjust-planning-units'; import { ApiConsumesShapefile } from '@marxan-api/decorators/shapefile.decorator'; -import { CostSurfaceFacade } from './cost-surface/cost-surface.facade'; import { FileInterceptor } from '@nestjs/platform-express'; -import { AppConfig } from '@marxan-api/utils/config.utils'; -import { ScenarioService } from '@marxan-api/modules/scenarios/scenario.service'; +import { ScenarioService } from './scenario.service'; +import { ScenarioSerializer } from './dto/scenario.serializer'; +import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(scenarioResource.className) @Controller(`${apiGlobalPrefixes.v1}/scenarios`) export class ScenariosController { - constructor(private readonly scenariosService: ScenarioService) {} + constructor( + private readonly service: ScenarioService, + private readonly scenarioSerializer: ScenarioSerializer, + private readonly scenarioFeatureSerializer: ScenarioFeatureSerializer, + ) {} @ApiOperation({ description: 'Find all scenarios', @@ -84,8 +83,8 @@ export class ScenariosController { async findAll( @ProcessFetchSpecification() fetchSpecification: FetchSpecification, ): Promise { - const results = await this.service.findAllPaginated(fetchSpecification, {}); - return this.service.serialize(results.data, results.metadata); + const results = await this.service.findAll(fetchSpecification); + return this.scenarioSerializer.serialize(results.data, results.metadata); } @ApiOperation({ description: 'Find scenario by id' }) @@ -98,7 +97,7 @@ export class ScenariosController { @Param('id', ParseUUIDPipe) id: string, @ProcessFetchSpecification() fetchSpecification: FetchSpecification, ): Promise { - return await this.service.serialize( + return await this.scenarioSerializer.serialize( await this.service.getById(id, fetchSpecification), ); } @@ -110,7 +109,7 @@ export class ScenariosController { @Body(new ValidationPipe()) dto: CreateScenarioDTO, @Req() req: RequestWithAuthenticatedUser, ): Promise { - return await this.service.serialize( + return await this.scenarioSerializer.serialize( await this.service.create(dto, { authenticatedUser: req.user }), ); } @@ -122,15 +121,7 @@ export class ScenariosController { @Param('id') scenarioId: string, @Req() request: Request, ): Promise { - // TODO #1 pre-validate scenarioId - /** - * Could be via interceptor - * (would require to not include @Res() and force-ignore http-proxy needs) - * or just ...BaseService - */ - - this.costSurface.convert(scenarioId, request.file); - return; + this.service.processCostSurfaceShapefile(scenarioId, request.file); } @ApiConsumesShapefile() @@ -140,22 +131,7 @@ export class ScenariosController { @Param('id', ParseUUIDPipe) scenarioId: string, @UploadedFile() file: Express.Multer.File, ): Promise { - await this.service.getById(scenarioId); - /** - * @validateStatus is required for HttpService to not reject and wrap geoprocessing's response - * in case a shapefile is not validated and a status 4xx is sent back. - */ - const { data: geoJson } = await this.httpService - .post( - `${this.geoprocessingUrl}${apiGlobalPrefixes.v1}/planning-units/planning-unit-shapefile`, - file, - { - headers: { 'Content-Type': 'application/json' }, - validateStatus: (status) => status <= 499, - }, - ) - .toPromise(); - return geoJson; + return this.service.uploadLockInShapeFile(scenarioId, file); } @ApiOperation({ description: 'Update scenario' }) @@ -165,7 +141,9 @@ export class ScenariosController { @Param('id') id: string, @Body(new ValidationPipe()) dto: UpdateScenarioDTO, ): Promise { - return await this.service.serialize(await this.service.update(id, dto)); + return await this.scenarioSerializer.serialize( + await this.service.update(id, dto), + ); } @ApiOperation({ description: 'Delete scenario' }) @@ -181,31 +159,15 @@ export class ScenariosController { @Param('id', ParseUUIDPipe) id: string, @Body() input: UpdateScenarioPlanningUnitLockStatusDto, ): Promise { - // TODO implement more flexible error results to propagate 4xx - await this.updatePlanningUnits.update(id, { - include: { - geo: input.byGeoJson?.include, - pu: input.byId?.include, - }, - exclude: { - pu: input.byId?.exclude, - geo: input.byGeoJson?.exclude, - }, - }); + await this.service.changeLockStatus(id, input); return; } @Get(':id/planning-units') async planningUnitsStatus( - // eslint-disable-next-line @typescript-eslint/no-unused-vars @Param('id', ParseUUIDPipe) id: string, ): Promise { - // TODO call analysis-module's service - - // TODO where exactly we should look for the status? - return { - status: JobStatus.running, - }; + return this.service.getPendingJobs(id); } @ApiOperation({ description: `Resolve scenario's features pre-gap data.` }) @@ -216,14 +178,8 @@ export class ScenariosController { async getScenarioFeatures( @Param('id', ParseUUIDPipe) id: string, ): Promise[]> { - return this.scenarioFeatures.serialize( - ( - await this.scenarioFeatures.findAll(undefined, { - params: { - scenarioId: id, - }, - }) - )[0], + return this.scenarioFeatureSerializer.serialize( + await this.service.getFeatures(id), ); } } diff --git a/api/apps/api/src/modules/scenarios/scenarios.module.ts b/api/apps/api/src/modules/scenarios/scenarios.module.ts index 630dfd9356..555dba0448 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.module.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.module.ts @@ -16,6 +16,7 @@ import { AnalysisModule } from '../analysis/analysis.module'; import { CostSurfaceModule } from './cost-surface/cost-surface.module'; import { ScenarioService } from './scenario.service'; import { ScenarioSerializer } from './dto/scenario.serializer'; +import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; @Module({ imports: [ @@ -35,6 +36,7 @@ import { ScenarioSerializer } from './dto/scenario.serializer'; ProxyService, WdpaAreaCalculationService, ScenarioSerializer, + ScenarioFeatureSerializer, ], controllers: [ScenariosController], exports: [ScenariosCrudService, ScenarioService], From 7bbb2846ed3c06cec1a5f2ec2dcb1e7030ed5e33 Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Mon, 7 Jun 2021 15:12:33 +0200 Subject: [PATCH 3/4] test(api): scenario: pu-change spec now has existing scenario --- .../scenario-put-change.e2e-spec.ts | 17 +++---- .../test/scenario-pu-change/steps/world.ts | 46 +++++++++---------- api/apps/api/test/steps/given-scenario.ts | 0 3 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 api/apps/api/test/steps/given-scenario.ts diff --git a/api/apps/api/test/scenario-pu-change/scenario-put-change.e2e-spec.ts b/api/apps/api/test/scenario-pu-change/scenario-put-change.e2e-spec.ts index 7bfdf28dc5..3c2108b993 100644 --- a/api/apps/api/test/scenario-pu-change/scenario-put-change.e2e-spec.ts +++ b/api/apps/api/test/scenario-pu-change/scenario-put-change.e2e-spec.ts @@ -1,9 +1,10 @@ import { INestApplication } from '@nestjs/common'; +import { PromiseType } from 'utility-types'; import { bootstrapApplication } from '../utils/api-application'; import { GivenUserIsLoggedIn } from '../steps/given-user-is-logged-in'; -import { WhenRequestingStatus } from './steps/WhenRequestingStatus'; -import { createWorld, World } from './steps/world'; +import { WhenRequestingStatus } from './steps/WhenRequestingStatus'; +import { createWorld } from './steps/world'; import { FakeQueue } from '../utils/queues'; import { QueueToken } from '../../src/modules/queue/queue.tokens'; import { ExpectBadRequest } from './assertions/expect-bad-request'; @@ -13,15 +14,15 @@ import { tearDown } from '../utils/tear-down'; let app: INestApplication; let jwtToken: string; -let scenarioId: string; let queue: FakeQueue; -let world: World; +let world: PromiseType>; beforeAll(async () => { app = await bootstrapApplication(); jwtToken = await GivenUserIsLoggedIn(app); - world = await createWorld(app); + world = await createWorld(app, jwtToken); + await world.GivenScenarioPuDataExists(); queue = app.get(QueueToken); }); @@ -34,13 +35,13 @@ afterAll(async () => { describe(`when requesting to change inclusive options`, () => { it(`denies to request with valid input`, async () => { ExpectBadRequest( - await world.WhenChangingPlanningUnitInclusivityForRandomPu(jwtToken), + await world.WhenChangingPlanningUnitInclusivityForRandomPu(), ); }); describe(`when desired PU ids are available`, () => { it(`triggers the job`, async () => { - await world.WhenChangingPlanningUnitInclusivityWithExistingPu(jwtToken); + await world.WhenChangingPlanningUnitInclusivityWithExistingPu(); const job = Object.values(queue.jobs)[0]; HasExpectedJobDetails(job); HasRelevantJobName(job, world.scenarioId); @@ -52,7 +53,7 @@ describe.skip(`when requesting status of change`, () => { let outcome: unknown; beforeAll(async () => { - outcome = await WhenRequestingStatus(app, scenarioId, jwtToken); + outcome = await WhenRequestingStatus(app, world.scenarioId, jwtToken); }); it(`returns relevant status`, async () => { diff --git a/api/apps/api/test/scenario-pu-change/steps/world.ts b/api/apps/api/test/scenario-pu-change/steps/world.ts index 35c8a34cfc..04d172e4c9 100644 --- a/api/apps/api/test/scenario-pu-change/steps/world.ts +++ b/api/apps/api/test/scenario-pu-change/steps/world.ts @@ -3,26 +3,17 @@ import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { v4 } from 'uuid'; -import { GivenScenarioPuDataExists } from '../../steps/given-scenario-pu-data-exists'; import { ScenariosPlanningUnitGeoEntity } from '@marxan/scenarios-planning-unit'; - -import { WhenChangingPlanningUnitInclusivity } from './WhenChangingPlanningUnitInclusivity'; import { DbConnections } from '@marxan-api/ormconfig.connections'; -export interface World { - scenarioId: string; - GivenScenarioPuDataExists: () => Promise; - WhenChangingPlanningUnitInclusivityForRandomPu: ( - jwToken: string, - ) => Promise; - WhenChangingPlanningUnitInclusivityWithExistingPu: ( - jwToken: string, - ) => Promise; - cleanup: () => Promise; -} +import { GivenScenarioPuDataExists } from '../../steps/given-scenario-pu-data-exists'; +import { WhenChangingPlanningUnitInclusivity } from './WhenChangingPlanningUnitInclusivity'; +import { ScenariosTestUtils } from '../../utils/scenarios.test.utils'; +import { ScenarioType } from '@marxan-api/modules/scenarios/scenario.api.entity'; +import { GivenProjectExists } from '../../steps/given-project'; -export const createWorld = async (app: INestApplication): Promise => { - const scenarioId = v4(); +export const createWorld = async (app: INestApplication, jwt: string) => { + const { cleanup, projectId } = await GivenProjectExists(app, jwt); const scenariosPuData: Repository = await app.get( getRepositoryToken( ScenariosPlanningUnitGeoEntity, @@ -30,13 +21,21 @@ export const createWorld = async (app: INestApplication): Promise => { ), ); + const scenarioId = ( + await ScenariosTestUtils.createScenario(app, jwt, { + name: `scenario-name`, + type: ScenarioType.marxan, + projectId, + }) + ).data.id; + return { scenarioId, GivenScenarioPuDataExists: async () => (await GivenScenarioPuDataExists(scenariosPuData, scenarioId)).rows, - WhenChangingPlanningUnitInclusivityForRandomPu: async (jwt: string) => + WhenChangingPlanningUnitInclusivityForRandomPu: async () => WhenChangingPlanningUnitInclusivity(app, scenarioId, jwt, [v4(), v4()]), - WhenChangingPlanningUnitInclusivityWithExistingPu: async (jwt: string) => + WhenChangingPlanningUnitInclusivityWithExistingPu: async () => WhenChangingPlanningUnitInclusivity( app, scenarioId, @@ -45,11 +44,10 @@ export const createWorld = async (app: INestApplication): Promise => { (entity) => entity.puGeometryId, ), ), - cleanup: () => - scenariosPuData - .delete({ - scenarioId, - }) - .then(() => undefined), + cleanup: async () => { + await scenariosPuData.delete({ scenarioId }); + await ScenariosTestUtils.deleteScenario(app, jwt, scenarioId); + await cleanup(); + }, }; }; diff --git a/api/apps/api/test/steps/given-scenario.ts b/api/apps/api/test/steps/given-scenario.ts deleted file mode 100644 index e69de29bb2..0000000000 From 8e4e9a68d948000c3dda981102d5afb8a224884d Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Tue, 8 Jun 2021 09:33:14 +0200 Subject: [PATCH 4/4] refactor(api): scenarios: rename things --- api/apps/api/src/modules/scenarios/scenarios.controller.ts | 6 +++--- api/apps/api/src/modules/scenarios/scenarios.module.ts | 6 +++--- .../scenarios/{scenario.service.ts => scenarios.service.ts} | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename api/apps/api/src/modules/scenarios/{scenario.service.ts => scenarios.service.ts} (97%) diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index 3a1dcc8cc2..708dfd12ec 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -49,7 +49,7 @@ import { uploadOptions } from '@marxan-api/utils/file-uploads.utils'; import { ShapefileGeoJSONResponseDTO } from './dto/shapefile.geojson.response.dto'; import { ApiConsumesShapefile } from '@marxan-api/decorators/shapefile.decorator'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ScenarioService } from './scenario.service'; +import { ScenariosService } from './scenarios.service'; import { ScenarioSerializer } from './dto/scenario.serializer'; import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; @@ -59,7 +59,7 @@ import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; @Controller(`${apiGlobalPrefixes.v1}/scenarios`) export class ScenariosController { constructor( - private readonly service: ScenarioService, + private readonly service: ScenariosService, private readonly scenarioSerializer: ScenarioSerializer, private readonly scenarioFeatureSerializer: ScenarioFeatureSerializer, ) {} @@ -83,7 +83,7 @@ export class ScenariosController { async findAll( @ProcessFetchSpecification() fetchSpecification: FetchSpecification, ): Promise { - const results = await this.service.findAll(fetchSpecification); + const results = await this.service.findAllPaginated(fetchSpecification); return this.scenarioSerializer.serialize(results.data, results.metadata); } diff --git a/api/apps/api/src/modules/scenarios/scenarios.module.ts b/api/apps/api/src/modules/scenarios/scenarios.module.ts index 555dba0448..d371aaaee8 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.module.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.module.ts @@ -14,7 +14,7 @@ import { ProxyService } from '@marxan-api/modules/proxy/proxy.service'; import { WdpaAreaCalculationService } from './wdpa-area-calculation.service'; import { AnalysisModule } from '../analysis/analysis.module'; import { CostSurfaceModule } from './cost-surface/cost-surface.module'; -import { ScenarioService } from './scenario.service'; +import { ScenariosService } from './scenarios.service'; import { ScenarioSerializer } from './dto/scenario.serializer'; import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; @@ -31,7 +31,7 @@ import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; HttpModule, ], providers: [ - ScenarioService, + ScenariosService, ScenariosCrudService, ProxyService, WdpaAreaCalculationService, @@ -39,6 +39,6 @@ import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; ScenarioFeatureSerializer, ], controllers: [ScenariosController], - exports: [ScenariosCrudService, ScenarioService], + exports: [ScenariosCrudService, ScenariosService], }) export class ScenariosModule {} diff --git a/api/apps/api/src/modules/scenarios/scenario.service.ts b/api/apps/api/src/modules/scenarios/scenarios.service.ts similarity index 97% rename from api/apps/api/src/modules/scenarios/scenario.service.ts rename to api/apps/api/src/modules/scenarios/scenarios.service.ts index 50b8be1c86..2cc1a26638 100644 --- a/api/apps/api/src/modules/scenarios/scenario.service.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.service.ts @@ -16,7 +16,7 @@ import { UpdateScenarioDTO } from './dto/update.scenario.dto'; import { UpdateScenarioPlanningUnitLockStatusDto } from './dto/update-scenario-planning-unit-lock-status.dto'; @Injectable() -export class ScenarioService { +export class ScenariosService { private readonly geoprocessingUrl: string = AppConfig.get( 'geoprocessing.url', ) as string; @@ -29,7 +29,7 @@ export class ScenarioService { private readonly httpService: HttpService, ) {} - async findAll(fetchSpecification: FetchSpecification) { + async findAllPaginated(fetchSpecification: FetchSpecification) { return this.crudService.findAllPaginated(fetchSpecification); }