Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(marxan-run): zip input & output directories #316

Merged
merged 9 commits into from
Jul 9, 2021
32 changes: 32 additions & 0 deletions api/apps/api/src/modules/scenarios/dto/zip-files.serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { Either, isRight } from 'fp-ts/Either';
import {
metadataNotFound,
outputZipNotYetAvailable,
OutputZipFailure,
} from '../output-files/output-files.service';

@Injectable()
export class ZipFilesSerializer {
serialize(response: Either<OutputZipFailure, Buffer>): Buffer {
if (isRight(response)) {
return response.right;
}
const error = response.left;
switch (error) {
case metadataNotFound:
throw new NotFoundException(`Marxan was not yet executed.`);
case outputZipNotYetAvailable:
throw new NotFoundException(
`Marxan has not yet finished or finished with error.`,
);
default:
const _exhaustiveCheck: never = error;
throw _exhaustiveCheck;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MarxanExecutionMetadataGeoEntity } from '@marxan/marxan-output';
import { DbConnections } from '@marxan-api/ormconfig.connections';

import { OutputFilesService } from './output-files.service';

@Module({
imports: [
TypeOrmModule.forFeature(
[MarxanExecutionMetadataGeoEntity],
DbConnections.geoprocessingDB,
),
],
providers: [OutputFilesService],
exports: [OutputFilesService],
})
export class OutputFilesModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Repository } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { MarxanExecutionMetadataGeoEntity } from '@marxan/marxan-output';
import { DbConnections } from '@marxan-api/ormconfig.connections';
import { Either, left, right } from 'fp-ts/Either';

export const metadataNotFound = Symbol(
`marxan output file - metadata not found`,
);
export const outputZipNotYetAvailable = Symbol(
`marxan output file - output file not available, possibly error`,
);

export type OutputZipFailure =
| typeof metadataNotFound
| typeof outputZipNotYetAvailable;

@Injectable()
export class OutputFilesService {
constructor(
@InjectRepository(
MarxanExecutionMetadataGeoEntity,
DbConnections.geoprocessingDB,
)
private readonly executionMetadataRepo: Repository<MarxanExecutionMetadataGeoEntity>,
) {}

async get(scenarioId: string): Promise<Either<OutputZipFailure, Buffer>> {
const latest = await this.executionMetadataRepo.findOne({
where: {
scenarioId,
},
order: {
createdAt: 'DESC',
},
});
if (!latest) {
return left(metadataNotFound);
}
if (!latest.outputZip) {
return left(outputZipNotYetAvailable);
}
return right(latest.outputZip);
}
}
35 changes: 27 additions & 8 deletions api/apps/api/src/modules/scenarios/scenarios.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ import {
} from 'nestjs-base-service';

import {
ApiAcceptedResponse,
ApiBearerAuth,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiNoContentResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
ApiParam,
ApiProduces,
ApiQuery,
ApiTags,
ApiUnauthorizedResponse,
ApiForbiddenResponse,
ApiParam,
ApiAcceptedResponse,
} from '@nestjs/swagger';
import { apiGlobalPrefixes } from '@marxan-api/api.config';
import { JwtAuthGuard } from '@marxan-api/guards/jwt-auth.guard';
Expand All @@ -61,12 +61,13 @@ import { ScenarioFeatureResultDto } from './dto/scenario-feature-result.dto';
import { ScenarioSolutionResultDto } from './dto/scenario-solution-result.dto';
import { ScenarioSolutionSerializer } from './dto/scenario-solution.serializer';
import { ProxyService } from '@marxan-api/modules/proxy/proxy.service';
import { ZipFilesSerializer } from './dto/zip-files.serializer';

const basePath = `${apiGlobalPrefixes.v1}/scenarios`;
const solutionsSubPath = `:id/marxan/run/:runId/solutions`;

const marxanRunTag = 'Marxan Run';
const marxanFilesTag = 'Marxan Run - Files';
const marxanRunFiles = 'Marxan Run - Files';

@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
Expand All @@ -79,6 +80,7 @@ export class ScenariosController {
private readonly scenarioFeatureSerializer: ScenarioFeatureSerializer,
private readonly scenarioSolutionSerializer: ScenarioSolutionSerializer,
private readonly proxyService: ProxyService,
private readonly zipFilesSerializer: ZipFilesSerializer,
) {}

@ApiOperation({
Expand Down Expand Up @@ -249,7 +251,7 @@ export class ScenariosController {
);
}

@ApiTags(marxanFilesTag)
@ApiTags(marxanRunFiles)
@ApiOperation({ description: `Resolve scenario's input parameter file.` })
@Get(':id/marxan/dat/input.dat')
@ApiProduces('text/plain')
Expand All @@ -260,7 +262,7 @@ export class ScenariosController {
return await this.service.getInputParameterFile(id);
}

@ApiTags(marxanFilesTag)
@ApiTags(marxanRunFiles)
@ApiOperation({ description: `Resolve scenario's spec file.` })
@Get(':id/marxan/dat/spec.dat')
@ApiProduces('text/plain')
Expand All @@ -271,6 +273,23 @@ export class ScenariosController {
return await this.service.getSpecDatCsv(id);
}

@ApiTags(marxanRunFiles)
@ApiOperation({
description: `Get archived output files`,
})
@Get(`:id/marxan/output`)
@Header(`Content-Type`, `application/zip`)
@Header('Content-Disposition', 'attachment; filename="output.zip"')
async getOutputArchive(
@Param(`id`, ParseUUIDPipe) scenarioId: string,
@Res() response: Response,
) {
const result = await this.service.getMarxanExecutionOutputArchive(
scenarioId,
);
response.send(this.zipFilesSerializer.serialize(result));
}

@ApiOkResponse({
type: ScenarioSolutionResultDto,
})
Expand Down Expand Up @@ -387,7 +406,7 @@ export class ScenariosController {
);
}

@ApiTags(marxanFilesTag)
@ApiTags(marxanRunFiles)
@Header('Content-Type', 'text/csv')
@ApiOkResponse({
schema: {
Expand Down
4 changes: 4 additions & 0 deletions api/apps/api/src/modules/scenarios/scenarios.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { SpecDatModule } from './input-files/spec.dat.module';

import { MarxanRunService } from './marxan-run/marxan-run.service';
import { MarxanRunController } from './marxan-run/marxan-run.controller';
import { OutputFilesModule } from './output-files/output-files.module';
import { ZipFilesSerializer } from './dto/zip-files.serializer';

@Module({
imports: [
Expand All @@ -56,6 +58,7 @@ import { MarxanRunController } from './marxan-run/marxan-run.controller';
CostSurfaceViewModule,
SpecDatModule,
PlanningUnitsProtectionLevelModule,
OutputFilesModule,
],
providers: [
ScenariosService,
Expand All @@ -69,6 +72,7 @@ import { MarxanRunController } from './marxan-run/marxan-run.controller';
MarxanInput,
InputParameterFileProvider,
MarxanRunService,
ZipFilesSerializer,
{
provide: ioSettingsToken,
useFactory: () => {
Expand Down
7 changes: 7 additions & 0 deletions api/apps/api/src/modules/scenarios/scenarios.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { SolutionResultCrudService } from './solutions-result/solution-result-cr
import { CostSurfaceViewService } from './cost-surface-readmodel/cost-surface-view.service';
import { InputParameterFileProvider } from './input-parameter-file.provider';
import { SpecDatService } from './input-files/spec.dat.service';
import { OutputFilesService } from './output-files/output-files.service';

@Injectable()
export class ScenariosService {
Expand All @@ -46,6 +47,7 @@ export class ScenariosService {
private readonly marxanInputValidator: MarxanInput,
private readonly inputParameterFileProvider: InputParameterFileProvider,
private readonly specDatService: SpecDatService,
private readonly outputFilesService: OutputFilesService,
) {}

async findAllPaginated(
Expand Down Expand Up @@ -223,4 +225,9 @@ export class ScenariosService {
(withValidatedMetadata.metadata ??= {}).marxanInputParameterFile = marxanInput;
return withValidatedMetadata;
}

async getMarxanExecutionOutputArchive(scenarioId: string) {
await this.assertScenario(scenarioId);
return this.outputFilesService.get(scenarioId);
}
}
1 change: 1 addition & 0 deletions api/apps/api/test/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@marxan/scenario-cost-surface": "<rootDir>/../../../libs/scenario-cost-surface/src",
"@marxan/marxan-input/(.*)": "<rootDir>/../../../libs/marxan-input/src/$1",
"@marxan/marxan-input": "<rootDir>/../../../libs/marxan-input/src",
"@marxan/marxan-output": "<rootDir>/../../../libs/marxan-output/src",
"@marxan/planning-area-repository/(.*)": "<rootDir>/../../../libs/planning-area-repository/src/$1",
"@marxan/planning-area-repository": "<rootDir>/../../../libs/planning-area-repository/src"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Response } from 'supertest';
import { PromiseType } from 'utility-types';
import { createWorld } from './world';

let world: PromiseType<ReturnType<typeof createWorld>>;

beforeEach(async () => {
world = await createWorld();
});

afterEach(async () => {
await world?.cleanup();
});

describe(`given output zip is available`, () => {
let zip: Response;
beforeEach(async () => {
// given
await world.GivenOutputZipIsAvailable();

// when
zip = await world.WhenGettingZipArchive();
});

it(`allows to get zip archive`, async () => {
await world.ThenZipContainsOutputFiles(zip);
});
});

describe(`given metadata is not available`, () => {
let response: any;
beforeEach(async () => {
// when
response = await world.WhenGettingZipArchive();
});

it(`returns NotFound`, () => {
world.ThenReturns404(response);
});
});
Loading