From 66e90cdcbf5e9ae468b0d8c369b399ad96c6b528 Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Mon, 14 Jun 2021 14:55:54 +0200 Subject: [PATCH 1/4] feat(api): allow to get scenarios solutions results --- .../dto/scenario-solution-result.dto.ts | 46 ++++++++++ .../dto/scenario-solution.serializer.ts | 23 +++++ .../modules/scenarios/scenarios.controller.ts | 56 ++++++++++++ .../src/modules/scenarios/scenarios.module.ts | 10 +++ .../modules/scenarios/scenarios.service.ts | 37 ++++++++ .../solution-result-crud.service.ts | 72 +++++++++++++++ .../get-all-solutions.e2e-spec.ts | 47 ++++++++++ api/apps/api/test/scenario-solutions/world.ts | 58 ++++++++++++ api/libs/scenarios-planning-unit/src/index.ts | 1 + .../scenarios-output-results.geo.entity.ts | 90 +++++++++++++++++++ 10 files changed, 440 insertions(+) create mode 100644 api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts create mode 100644 api/apps/api/src/modules/scenarios/dto/scenario-solution.serializer.ts create mode 100644 api/apps/api/src/modules/scenarios/solutions-result/solution-result-crud.service.ts create mode 100644 api/apps/api/test/scenario-solutions/get-all-solutions.e2e-spec.ts create mode 100644 api/apps/api/test/scenario-solutions/world.ts create mode 100644 api/libs/scenarios-planning-unit/src/scenarios-output-results.geo.entity.ts diff --git a/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts b/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts new file mode 100644 index 0000000000..4e83bd619f --- /dev/null +++ b/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts @@ -0,0 +1,46 @@ +import { + ApiExtraModels, + ApiProperty, + getSchemaPath, + refs, +} from '@nestjs/swagger'; +import { ScenariosOutputResultsGeoEntity } from '@marxan/scenarios-planning-unit'; +import { oneOf } from 'purify-ts'; + +class ScenarioSolutionsDataDto { + @ApiProperty() + type: 'solutions' = 'solutions'; + + @ApiProperty() + id!: string; + + @ApiProperty({ + isArray: true, + type: () => ScenariosOutputResultsGeoEntity, + }) + attributes!: ScenariosOutputResultsGeoEntity[]; +} + +class ScenarioSolutionDataDto { + @ApiProperty() + type: 'solution' = 'solution'; + + @ApiProperty() + id!: string; + + @ApiProperty({ + type: () => ScenariosOutputResultsGeoEntity, + }) + attributes!: ScenariosOutputResultsGeoEntity; +} + +@ApiExtraModels(ScenarioSolutionsDataDto, ScenarioSolutionDataDto) +export class ScenarioSolutionResultDto { + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(ScenarioSolutionsDataDto) }, + { $ref: getSchemaPath(ScenarioSolutionDataDto) }, + ], + }) + data!: ScenarioSolutionsDataDto | ScenarioSolutionDataDto; +} diff --git a/api/apps/api/src/modules/scenarios/dto/scenario-solution.serializer.ts b/api/apps/api/src/modules/scenarios/dto/scenario-solution.serializer.ts new file mode 100644 index 0000000000..e2d8ebce15 --- /dev/null +++ b/api/apps/api/src/modules/scenarios/dto/scenario-solution.serializer.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { PaginationMeta } from '@marxan-api/utils/app-base.service'; +import { ScenariosOutputResultsGeoEntity } from '@marxan/scenarios-planning-unit'; +import { SolutionResultCrudService } from '../solutions-result/solution-result-crud.service'; + +@Injectable() +export class ScenarioSolutionSerializer { + constructor( + private readonly scenariosSolutionsCrudService: SolutionResultCrudService, + ) {} + + async serialize( + entities: + | Partial + | (Partial | undefined)[], + paginationMeta?: PaginationMeta, + ): Promise { + return this.scenariosSolutionsCrudService.serialize( + entities, + paginationMeta, + ); + } +} diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index 4b42bcd026..c2662b9a8a 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -4,9 +4,11 @@ import { Delete, Get, Param, + ParseBoolPipe, ParseUUIDPipe, Patch, Post, + Query, Req, UploadedFile, UseGuards, @@ -47,6 +49,10 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { ScenariosService } from './scenarios.service'; import { ScenarioSerializer } from './dto/scenario.serializer'; import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; +import { ScenarioFeatureResultDto } from './dto/scenario-feature-result.dto'; +import { ScenarioSolutionResultDto } from './dto/scenario-solution-result.dto'; +import { ApiImplicitQuery } from '@nestjs/swagger/dist/decorators/api-implicit-query.decorator'; +import { ScenarioSolutionSerializer } from './dto/scenario-solution.serializer'; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -57,6 +63,7 @@ export class ScenariosController { private readonly service: ScenariosService, private readonly scenarioSerializer: ScenarioSerializer, private readonly scenarioFeatureSerializer: ScenarioFeatureSerializer, + private readonly scenarioSolutionSerializer: ScenarioSolutionSerializer, ) {} @ApiOperation({ @@ -170,4 +177,53 @@ export class ScenariosController { await this.service.getFeatures(id), ); } + + @ApiOkResponse({ + type: ScenarioSolutionResultDto, + }) + @ApiImplicitQuery({ + name: 'best', + required: false, + type: Boolean, + }) + @ApiImplicitQuery({ + name: 'most-different', + required: false, + type: Boolean, + }) + @JSONAPIQueryParams() + @Get(`:id/marxan/run/:runId/solutions`) + async getScenarioRunSolutions( + @Param('id', ParseUUIDPipe) id: string, + @Param('runId', ParseUUIDPipe) runId: string, + @ProcessFetchSpecification() fetchSpecification: FetchSpecification, + @Query('best', ParseBoolPipe) selectOnlyBest?: boolean, + @Query('most-different', ParseBoolPipe) selectMostDifferent?: boolean, + ): Promise { + if (selectOnlyBest) { + return this.scenarioSolutionSerializer.serialize( + await this.service.getBestSolution(id, runId), + ); + } + if (selectMostDifferent) { + const result = await this.service.getMostDifferentSolutions( + id, + runId, + fetchSpecification, + ); + return this.scenarioSolutionSerializer.serialize( + result.data, + result.metadata, + ); + } + const result = await this.service.findAllSolutionsPaginated( + id, + runId, + fetchSpecification, + ); + return this.scenarioSolutionSerializer.serialize( + result.data, + result.metadata, + ); + } } diff --git a/api/apps/api/src/modules/scenarios/scenarios.module.ts b/api/apps/api/src/modules/scenarios/scenarios.module.ts index a6600e3064..aa33ff3f79 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.module.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.module.ts @@ -18,6 +18,10 @@ import { ScenariosService } from './scenarios.service'; import { ScenarioSerializer } from './dto/scenario.serializer'; import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; import { CostSurfaceTemplateModule } from './cost-surface-template'; +import { SolutionResultCrudService } from './solutions-result/solution-result-crud.service'; +import { DbConnections } from '@marxan-api/ormconfig.connections'; +import { ScenariosOutputResultsGeoEntity } from '@marxan/scenarios-planning-unit'; +import { ScenarioSolutionSerializer } from './dto/scenario-solution.serializer'; @Module({ imports: [ @@ -25,6 +29,10 @@ import { CostSurfaceTemplateModule } from './cost-surface-template'; ProtectedAreasModule, forwardRef(() => ProjectsModule), TypeOrmModule.forFeature([Project, Scenario]), + TypeOrmModule.forFeature( + [ScenariosOutputResultsGeoEntity], + DbConnections.geoprocessingDB, + ), UsersModule, ScenarioFeaturesModule, AnalysisModule, @@ -39,6 +47,8 @@ import { CostSurfaceTemplateModule } from './cost-surface-template'; WdpaAreaCalculationService, ScenarioSerializer, ScenarioFeatureSerializer, + SolutionResultCrudService, + ScenarioSolutionSerializer, ], controllers: [ScenariosController], exports: [ScenariosCrudService, ScenariosService], diff --git a/api/apps/api/src/modules/scenarios/scenarios.service.ts b/api/apps/api/src/modules/scenarios/scenarios.service.ts index 3044933d1e..91ff9c67f5 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.service.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.service.ts @@ -13,6 +13,7 @@ import { ScenariosCrudService } from './scenarios-crud.service'; 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'; +import { SolutionResultCrudService } from './solutions-result/solution-result-crud.service'; @Injectable() export class ScenariosService { @@ -26,6 +27,7 @@ export class ScenariosService { private readonly updatePlanningUnits: AdjustPlanningUnits, private readonly costSurface: CostSurfaceFacade, private readonly httpService: HttpService, + private readonly solutionsCrudService: SolutionResultCrudService, ) {} async findAllPaginated(fetchSpecification: FetchSpecification) { @@ -103,7 +105,42 @@ export class ScenariosService { return geoJson; } + async findScenarioResults( + scenarioId: string, + runId: string, + fetchSpecification: FetchSpecification, + ) { + await this.assertScenario(scenarioId); + return this.solutionsCrudService.findAll(fetchSpecification); + } + private async assertScenario(scenarioId: string) { await this.crudService.getById(scenarioId); } + + async getBestSolution(scenarioId: string, runId: string) { + await this.assertScenario(scenarioId); + // TODO correct implementation + return this.solutionsCrudService.getById(runId); + } + + async getMostDifferentSolutions( + scenarioId: string, + runId: string, + fetchSpecification: FetchSpecification, + ) { + await this.assertScenario(scenarioId); + // TODO correct implementation + return this.solutionsCrudService.findAllPaginated(fetchSpecification); + } + + async findAllSolutionsPaginated( + scenarioId: string, + runId: string, + fetchSpecification: FetchSpecification, + ) { + await this.assertScenario(scenarioId); + // TODO correct implementation + return this.solutionsCrudService.findAllPaginated(fetchSpecification); + } } diff --git a/api/apps/api/src/modules/scenarios/solutions-result/solution-result-crud.service.ts b/api/apps/api/src/modules/scenarios/solutions-result/solution-result-crud.service.ts new file mode 100644 index 0000000000..0f76be9dac --- /dev/null +++ b/api/apps/api/src/modules/scenarios/solutions-result/solution-result-crud.service.ts @@ -0,0 +1,72 @@ +import { Repository } from 'typeorm'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + AppBaseService, + JSONAPISerializerConfig, +} from '@marxan-api/utils/app-base.service'; + +import { AppInfoDTO } from '@marxan-api/dto/info.dto'; +import { AppConfig } from '@marxan-api/utils/config.utils'; +import { FetchSpecification } from 'nestjs-base-service'; +import { ScenariosOutputResultsGeoEntity } from '@marxan/scenarios-planning-unit'; +import { DbConnections } from '@marxan-api/ormconfig.connections'; + +@Injectable() +export class SolutionResultCrudService extends AppBaseService< + ScenariosOutputResultsGeoEntity, + never, + never, + AppInfoDTO +> { + constructor( + @InjectRepository( + ScenariosOutputResultsGeoEntity, + DbConnections.geoprocessingDB, + ) + protected readonly repository: Repository, + ) { + super(repository, 'solution', 'solutions', { + logging: { muteAll: AppConfig.get('logging.muteAll', false) }, + }); + } + + get serializerConfig(): JSONAPISerializerConfig { + return { + attributes: [ + 'id', + 'planningUnits', + 'missingValues', + 'cost', + 'score', + 'run', + ], + keyForAttribute: 'camelCase', + }; + } + + async extendFindAllResults( + entitiesAndCount: [ScenariosOutputResultsGeoEntity[], number], + _fetchSpecification?: FetchSpecification, + _info?: AppInfoDTO, + ): Promise<[ScenariosOutputResultsGeoEntity[], number]> { + const extendedEntities: Promise[] = entitiesAndCount[0].map( + (entity) => this.extendGetByIdResult(entity), + ); + return [await Promise.all(extendedEntities), entitiesAndCount[1]]; + } + + async extendGetByIdResult( + entity: ScenariosOutputResultsGeoEntity, + _fetchSpecification?: FetchSpecification, + _info?: AppInfoDTO, + ): Promise { + // TODO implement + entity.planningUnits = 17; + entity.missingValues = 13; + entity.cost = 400; + entity.score = 999; + entity.run = 1; + return entity; + } +} diff --git a/api/apps/api/test/scenario-solutions/get-all-solutions.e2e-spec.ts b/api/apps/api/test/scenario-solutions/get-all-solutions.e2e-spec.ts new file mode 100644 index 0000000000..fa0faecc0b --- /dev/null +++ b/api/apps/api/test/scenario-solutions/get-all-solutions.e2e-spec.ts @@ -0,0 +1,47 @@ +import { PromiseType } from 'utility-types'; +import { createWorld } from './world'; + +let world: PromiseType>; + +beforeAll(async () => { + world = await createWorld(); +}); + +describe(`When getting scenario solution results`, () => { + beforeAll(async () => { + await world.GivenScenarioHasSolutionsReady(); + }); + + it(`should resolve solutions`, async () => { + const response = await world.WhenGettingSolutions(); + expect(response.body.meta).toMatchInlineSnapshot(` + Object { + "page": 1, + "size": 25, + "totalItems": 1, + "totalPages": 1, + } + `); + + expect(response.body.data.length).toEqual(1); + expect(response.body.data[0].attributes).toMatchInlineSnapshot( + { + id: expect.any(String), + }, + ` + Object { + "cost": 400, + "id": Any, + "missingValues": 13, + "planningUnits": 17, + "run": 1, + "score": 999, + } + `, + ); + }); +}); + +afterAll(async () => { + await world?.cleanup(); +}); diff --git a/api/apps/api/test/scenario-solutions/world.ts b/api/apps/api/test/scenario-solutions/world.ts new file mode 100644 index 0000000000..14b22c56c0 --- /dev/null +++ b/api/apps/api/test/scenario-solutions/world.ts @@ -0,0 +1,58 @@ +import { bootstrapApplication } from '../utils/api-application'; +import { GivenUserIsLoggedIn } from '../steps/given-user-is-logged-in'; +import * as request from 'supertest'; +import { ScenariosTestUtils } from '../utils/scenarios.test.utils'; +import { GivenProjectExists } from '../steps/given-project'; +import { E2E_CONFIG } from '../e2e.config'; +import { v4 } from 'uuid'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { ScenariosOutputResultsGeoEntity } from '@marxan/scenarios-planning-unit'; +import { DbConnections } from '@marxan-api/ormconfig.connections'; +import { Repository } from 'typeorm'; + +export const createWorld = async () => { + const app = await bootstrapApplication(); + const jwt = await GivenUserIsLoggedIn(app); + const { projectId, cleanup: cleanupProject } = await GivenProjectExists( + app, + jwt, + ); + const scenario = await ScenariosTestUtils.createScenario(app, jwt, { + ...E2E_CONFIG.scenarios.valid.minimal(), + projectId, + }); + + const marxanOutputRepo: Repository = app.get( + getRepositoryToken( + ScenariosOutputResultsGeoEntity, + DbConnections.geoprocessingDB, + ), + ); + + return { + GivenScenarioHasSolutionsReady: async () => { + // TODO implement again once all stuff is in place + await marxanOutputRepo.save( + marxanOutputRepo.create({ + scenarioId: scenario.data.id, + runId: null, + scoreValue: 4000, + }), + ); + }, + WhenGettingSolutions: async () => + request(app.getHttpServer()) + .get( + `/api/v1/scenarios/${scenario.data.id}/marxan/run/${v4()}/solutions`, + ) + .set('Authorization', `Bearer ${jwt}`), + cleanup: async () => { + await marxanOutputRepo.delete({ + scenarioId: scenario.data.id, + }); + await ScenariosTestUtils.deleteScenario(app, jwt, scenario.data.id); + await cleanupProject(); + await app.close(); + }, + }; +}; diff --git a/api/libs/scenarios-planning-unit/src/index.ts b/api/libs/scenarios-planning-unit/src/index.ts index f44a119ae4..e25696aa62 100644 --- a/api/libs/scenarios-planning-unit/src/index.ts +++ b/api/libs/scenarios-planning-unit/src/index.ts @@ -1,3 +1,4 @@ export { LockStatus } from './lock-status.enum'; export { ScenariosPlanningUnitGeoEntity } from './scenarios-planning-unit.geo.entity'; +export { ScenariosOutputResultsGeoEntity } from './scenarios-output-results.geo.entity'; export * from './domain'; diff --git a/api/libs/scenarios-planning-unit/src/scenarios-output-results.geo.entity.ts b/api/libs/scenarios-planning-unit/src/scenarios-output-results.geo.entity.ts new file mode 100644 index 0000000000..d120a13cda --- /dev/null +++ b/api/libs/scenarios-planning-unit/src/scenarios-output-results.geo.entity.ts @@ -0,0 +1,90 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; + +const tableName = `output_results_data`; + +@Entity(tableName) +export class ScenariosOutputResultsGeoEntity { + @ApiProperty() + @PrimaryGeneratedColumn('uuid') + id!: string; + + /** + * references ScenariosPlanningUnitGeoEntity.planningUnitMarxanId + */ + @Column({ + type: 'int', + nullable: true, + name: `puid`, + }) + scenariosPuDataPlanningUnitMarxanId?: number | null; + + @Column({ + type: `uuid`, + nullable: true, + name: `scenario_id`, + }) + scenarioId?: string | null; + + /** + * TODO describe/change + */ + @Column({ + type: `uuid`, + nullable: true, + name: `run_id`, + }) + runId?: string | null; + + /** + * Score of the run + */ + @Column({ + type: `float8`, + nullable: true, + name: `value`, + }) + scoreValue?: number | null; + + /** + * TODO describe/change + */ + @Column({ + type: `jsonb`, + nullable: true, + name: `missing_values`, + }) + missingValuesJsonb?: unknown | null; + + /** + * API fields + * -------------------- + */ + + @ApiProperty({ + description: `The number of planning units contained in the solution for that run.`, + }) + planningUnits!: number; + + @ApiProperty({ + description: `The number of planning units omitted in the solution for that run.`, + }) + missingValues!: number; + + // TODO describe + @ApiProperty({ + description: `TODO`, + }) + cost!: number; + + @ApiProperty({ + description: `Score value of the solution - the higher, the better.`, + }) + score!: number; + + // TODO describe + @ApiProperty({ + description: ``, + }) + run!: number; +} From 15a54c11958c4ef5c440ad70efe9fd694ab2de11 Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Tue, 15 Jun 2021 15:10:17 +0200 Subject: [PATCH 2/4] refactor(api): remove unused imports from scenario solution dto --- .../modules/scenarios/dto/scenario-solution-result.dto.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts b/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts index 4e83bd619f..bf3d54451f 100644 --- a/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts +++ b/api/apps/api/src/modules/scenarios/dto/scenario-solution-result.dto.ts @@ -1,11 +1,5 @@ -import { - ApiExtraModels, - ApiProperty, - getSchemaPath, - refs, -} from '@nestjs/swagger'; +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; import { ScenariosOutputResultsGeoEntity } from '@marxan/scenarios-planning-unit'; -import { oneOf } from 'purify-ts'; class ScenarioSolutionsDataDto { @ApiProperty() From 78de3b1034837a70b3563efe89283787a28f90ad Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Tue, 15 Jun 2021 15:32:59 +0200 Subject: [PATCH 3/4] refactor(api): replace deprecated decorator --- api/apps/api/src/modules/scenarios/scenarios.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index c2662b9a8a..44e9116a5b 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -28,6 +28,7 @@ import { ApiNoContentResponse, ApiOkResponse, ApiOperation, + ApiQuery, ApiTags, } from '@nestjs/swagger'; import { apiGlobalPrefixes } from '@marxan-api/api.config'; @@ -51,7 +52,6 @@ import { ScenarioSerializer } from './dto/scenario.serializer'; import { ScenarioFeatureSerializer } from './dto/scenario-feature.serializer'; import { ScenarioFeatureResultDto } from './dto/scenario-feature-result.dto'; import { ScenarioSolutionResultDto } from './dto/scenario-solution-result.dto'; -import { ApiImplicitQuery } from '@nestjs/swagger/dist/decorators/api-implicit-query.decorator'; import { ScenarioSolutionSerializer } from './dto/scenario-solution.serializer'; @UseGuards(JwtAuthGuard) @@ -181,12 +181,12 @@ export class ScenariosController { @ApiOkResponse({ type: ScenarioSolutionResultDto, }) - @ApiImplicitQuery({ + @ApiQuery({ name: 'best', required: false, type: Boolean, }) - @ApiImplicitQuery({ + @ApiQuery({ name: 'most-different', required: false, type: Boolean, From bed5b79eb096c9bb0ce11a0f839097d688eb3683 Mon Sep 17 00:00:00 2001 From: Kamil Gajowy Date: Thu, 17 Jun 2021 10:20:15 +0200 Subject: [PATCH 4/4] refactor(scenario-solutions): more expresive separation of best/most-diff solutions --- .../modules/scenarios/scenarios.controller.ts | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/api/apps/api/src/modules/scenarios/scenarios.controller.ts b/api/apps/api/src/modules/scenarios/scenarios.controller.ts index 44e9116a5b..6ed817d3d7 100644 --- a/api/apps/api/src/modules/scenarios/scenarios.controller.ts +++ b/api/apps/api/src/modules/scenarios/scenarios.controller.ts @@ -54,10 +54,13 @@ import { ScenarioFeatureResultDto } from './dto/scenario-feature-result.dto'; import { ScenarioSolutionResultDto } from './dto/scenario-solution-result.dto'; import { ScenarioSolutionSerializer } from './dto/scenario-solution.serializer'; +const basePath = `${apiGlobalPrefixes.v1}/scenarios`; +const solutionsSubPath = `:id/marxan/run/:runId/solutions`; + @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiTags(scenarioResource.className) -@Controller(`${apiGlobalPrefixes.v1}/scenarios`) +@Controller(basePath) export class ScenariosController { constructor( private readonly service: ScenariosService, @@ -192,7 +195,7 @@ export class ScenariosController { type: Boolean, }) @JSONAPIQueryParams() - @Get(`:id/marxan/run/:runId/solutions`) + @Get(solutionsSubPath) async getScenarioRunSolutions( @Param('id', ParseUUIDPipe) id: string, @Param('runId', ParseUUIDPipe) runId: string, @@ -201,21 +204,17 @@ export class ScenariosController { @Query('most-different', ParseBoolPipe) selectMostDifferent?: boolean, ): Promise { if (selectOnlyBest) { - return this.scenarioSolutionSerializer.serialize( - await this.service.getBestSolution(id, runId), - ); + return this.getScenarioRunBestSolutions(id, runId); } + if (selectMostDifferent) { - const result = await this.service.getMostDifferentSolutions( + return this.getScenarioRunMostDifferentSolutions( id, runId, fetchSpecification, ); - return this.scenarioSolutionSerializer.serialize( - result.data, - result.metadata, - ); } + const result = await this.service.findAllSolutionsPaginated( id, runId, @@ -226,4 +225,39 @@ export class ScenariosController { result.metadata, ); } + + @ApiOkResponse({ + type: ScenarioSolutionResultDto, + }) + @JSONAPIQueryParams() + @Get(`${solutionsSubPath}/best`) + async getScenarioRunBestSolutions( + @Param('id', ParseUUIDPipe) id: string, + @Param('runId', ParseUUIDPipe) runId: string, + ): Promise { + return this.scenarioSolutionSerializer.serialize( + await this.service.getBestSolution(id, runId), + ); + } + + @ApiOkResponse({ + type: ScenarioSolutionResultDto, + }) + @JSONAPIQueryParams() + @Get(`${solutionsSubPath}/most-different`) + async getScenarioRunMostDifferentSolutions( + @Param('id', ParseUUIDPipe) id: string, + @Param('runId', ParseUUIDPipe) runId: string, + @ProcessFetchSpecification() fetchSpecification: FetchSpecification, + ): Promise { + const result = await this.service.getMostDifferentSolutions( + id, + runId, + fetchSpecification, + ); + return this.scenarioSolutionSerializer.serialize( + result.data, + result.metadata, + ); + } }