Skip to content

Commit

Permalink
feat(api): include async job status in all relevant endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
kgajowy committed Sep 28, 2021
1 parent aae6015 commit f5f56e8
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 44 deletions.
4 changes: 4 additions & 0 deletions api/apps/api/src/dto/async-job-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum AsyncJobType {
Project = 'project',
Scenario = 'scenario',
}
61 changes: 61 additions & 0 deletions api/apps/api/src/dto/async-job.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
ApiHideProperty,
ApiProperty,
ApiPropertyOptional,
} from '@nestjs/swagger';
import { Exclude, plainToClass } from 'class-transformer';
import { AsyncJobType } from './async-job-type';

export class AsyncJobDto {
@ApiProperty({
enum: AsyncJobType,
})
type!: AsyncJobType;

@ApiPropertyOptional({
isArray: true,
type: String,
})
ids?: string[];

@ApiProperty()
started!: boolean;

@ApiProperty()
isoDate!: string;

static forScenario(): AsyncJobDto {
return plainToClass(AsyncJobDto, {
type: AsyncJobType.Scenario,
isoDate: new Date().toISOString(),
started: true,
});
}

static forProject(ids?: string[]): AsyncJobDto {
return plainToClass(AsyncJobDto, {
type: AsyncJobType.Project,
started: true,
isoDate: new Date().toISOString(),
ids,
});
}

asJsonApiMetadata(): JsonApiAsyncJobMeta {
return plainToClass(JsonApiAsyncJobMeta, {
meta: plainToClass(AsyncJobDto, {
started: this.started,
ids: this.ids,
isoDate: this.isoDate,
type: this.type,
}),
});
}
}

export class JsonApiAsyncJobMeta {
@ApiProperty({
type: AsyncJobDto,
})
meta!: AsyncJobDto;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { BaseServiceResource } from '@marxan-api/types/resource.interface';
import { GeoFeatureSetSpecification } from './dto/geo-feature-set-specification.dto';
import { JsonApiAsyncJobMeta } from '@marxan-api/dto/async-job.dto';

export const geoFeatureResource: BaseServiceResource = {
className: 'GeoFeature',
Expand Down Expand Up @@ -31,7 +32,7 @@ export class JSONAPIGeoFeatureSetsData {
attributes!: GeoFeatureSetSpecification;
}

export class GeoFeatureSetResult {
export class GeoFeatureSetResult extends JsonApiAsyncJobMeta {
@ApiProperty()
data!: JSONAPIGeoFeatureSetsData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { PaginationMeta } from '@marxan-api/utils/app-base.service';
import { GeoFeatureSetService } from './geo-feature-set.service';
import { GeoFeatureSetSpecification } from './dto/geo-feature-set-specification.dto';
import { SimpleJobStatus } from '../scenarios/scenario.api.entity';
import { GeoFeatureSetResult } from '@marxan-api/modules/geo-features/geo-feature-set.api.entity';
import { plainToClass } from 'class-transformer';
import { AsyncJobDto } from '@marxan-api/dto/async-job.dto';

@Injectable()
export class GeoFeatureSetSerializer {
Expand All @@ -14,10 +17,17 @@ export class GeoFeatureSetSerializer {
| undefined
| (Partial<GeoFeatureSetSpecification> | undefined)[],
paginationMeta?: PaginationMeta,
): Promise<any> {
return this.geoFeatureSetsService.serialize(entities, paginationMeta);
asyncJobTriggered?: boolean,
): Promise<GeoFeatureSetResult> {
return plainToClass(GeoFeatureSetResult, {
...(await this.geoFeatureSetsService.serialize(entities, paginationMeta)),
meta: asyncJobTriggered ? AsyncJobDto.forScenario() : undefined,
});
}

/**
* @deprecated
*/
emptySpecification() {
return this.geoFeatureSetsService.serialize({
status: SimpleJobStatus.draft,
Expand Down

This file was deleted.

33 changes: 29 additions & 4 deletions api/apps/api/src/modules/projects/dto/project.serializer.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PaginationMeta } from '@marxan-api/utils/app-base.service';
import { ProjectsCrudService } from '../projects-crud.service';
import { Project } from '../project.api.entity';
import {
Project,
ProjectResultPlural,
ProjectResultSingular,
} from '../project.api.entity';
import { isDefined } from '@marxan/utils';
import { AsyncJobDto } from '@marxan-api/dto/async-job.dto';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ProjectSerializer {
constructor(private readonly projectsCrud: ProjectsCrudService) {}

async serialize(
entities: Partial<Project> | (Partial<Project> | undefined)[] | undefined,
entities: Partial<Project> | undefined,
paginationMeta?: PaginationMeta,
): Promise<any> {
asyncJobTriggered?: boolean,
): Promise<ProjectResultSingular> {
if (!isDefined(entities)) {
throw new NotFoundException();
}
return this.projectsCrud.serialize(entities, paginationMeta);
// plainToClass produces CallStackExceeded errors?
const result = await this.projectsCrud.serialize(entities, paginationMeta);
return {
...result,
meta: {
...result.meta,
...(asyncJobTriggered ? AsyncJobDto.forProject() : {}),
},
};
}

async serializeAll(
entities: (Partial<Project> | undefined)[] | undefined,
paginationMeta?: PaginationMeta,
) {
if (!isDefined(entities)) {
throw new NotFoundException();
}
return await this.projectsCrud.serialize(entities, paginationMeta);
}
}
3 changes: 2 additions & 1 deletion api/apps/api/src/modules/projects/project.api.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { TimeUserEntityMetadata } from '../../types/time-user-entity-metadata';
import { BaseServiceResource } from '../../types/resource.interface';
import { BBox } from 'geojson';
import { ProtectedAreaDto } from '@marxan-api/modules/projects/dto/protected-area.dto';
import { JsonApiAsyncJobMeta } from '@marxan-api/dto/async-job.dto';

export const projectResource: BaseServiceResource = {
className: 'Project',
Expand Down Expand Up @@ -199,7 +200,7 @@ export class ProjectResultPlural {
data!: JSONAPIProjectData[];
}

export class ProjectResultSingular {
export class ProjectResultSingular extends JsonApiAsyncJobMeta {
@ApiProperty()
data!: JSONAPIProjectData;
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class ProjectsListingController {
},
authenticatedUser: req.user,
});
return this.projectSerializer.serialize(results.data, results.metadata);
return this.projectSerializer.serializeAll(results.data, results.metadata);
}

@ProjectsListing()
Expand All @@ -64,7 +64,7 @@ export class ProjectsListingController {
namesSearch,
},
});
return this.projectSerializer.serialize(results.data, results.metadata);
return this.projectSerializer.serializeAll(results.data, results.metadata);
}
}

Expand Down
25 changes: 13 additions & 12 deletions api/apps/api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,14 @@ import { PlanningAreaResponseDto } from './dto/planning-area-response.dto';
import { isLeft } from 'fp-ts/Either';
import { ShapefileUploadResponse } from './dto/project-upload-shapefile.dto';
import { UploadShapefileDTO } from './dto/upload-shapefile.dto';
import { ProjectGridRequestDto } from './dto/project-grid-request.dto';
import { GeoFeaturesService } from '../geo-features/geo-features.service';
import { AppConfig } from '@marxan-api/utils/config.utils';
import { ShapefileService } from '@marxan/shapefile-converter';
import { isFeatureCollection } from '@marxan/utils';
import { plainToClass } from 'class-transformer';
import {
AsyncJobDto,
JsonApiAsyncJobMeta,
} from '@marxan-api/dto/async-job.dto';

@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
Expand Down Expand Up @@ -129,6 +131,8 @@ export class ProjectsController {
): Promise<ProjectResultSingular> {
return await this.projectSerializer.serialize(
await this.projectsService.create(dto, { authenticatedUser: req.user }),
undefined,
true,
);
}

Expand All @@ -141,6 +145,8 @@ export class ProjectsController {
): Promise<ProjectResultSingular> {
return await this.projectSerializer.serialize(
await this.projectsService.update(id, dto),
undefined,
true,
);
}

Expand All @@ -156,22 +162,17 @@ export class ProjectsController {
description: 'Upload shapefile for project-specific planning unit grid',
})
@UseInterceptors(FileInterceptor('file', uploadOptions))
@ApiCreatedResponse({ type: ProjectGridRequestDto })
@ApiCreatedResponse({ type: JsonApiAsyncJobMeta })
@Post(`:id/grid`)
async setProjectGrid(
@Param('id') projectId: string,
@UploadedFile() file: Express.Multer.File,
) {
): Promise<JsonApiAsyncJobMeta> {
const result = await this.projectsService.setGrid(projectId, file);
if (isLeft(result)) {
throw new InternalServerErrorException(result.left);
}
return plainToClass<ProjectGridRequestDto, ProjectGridRequestDto>(
ProjectGridRequestDto,
{
id: result.right.value,
},
);
return AsyncJobDto.forProject([result.right.value]).asJsonApiMetadata();
}

@ApiOperation({
Expand Down Expand Up @@ -206,14 +207,14 @@ export class ProjectsController {
@Param('id') projectId: string,
@UploadedFile() file: Express.Multer.File,
@Req() req: RequestWithAuthenticatedUser,
): Promise<void> {
): Promise<JsonApiAsyncJobMeta> {
const outcome = await this.projectsService.addShapeFor(projectId, file, {
authenticatedUser: req.user,
});
if (outcome) {
throw new NotFoundException();
}
return;
return AsyncJobDto.forProject().asJsonApiMetadata();
}

@ApiConsumesShapefile({
Expand Down
12 changes: 9 additions & 3 deletions api/apps/api/src/modules/scenarios/dto/scenario.serializer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common';
import { PaginationMeta } from '@marxan-api/utils/app-base.service';
import { Scenario } from '../scenario.api.entity';
import { Scenario, ScenarioResult } from '../scenario.api.entity';
import { ScenariosCrudService } from '../scenarios-crud.service';
import { plainToClass } from 'class-transformer';
import { AsyncJobDto } from '@marxan-api/dto/async-job.dto';

@Injectable()
export class ScenarioSerializer {
Expand All @@ -10,7 +12,11 @@ export class ScenarioSerializer {
async serialize(
entities: Partial<Scenario> | (Partial<Scenario> | undefined)[],
paginationMeta?: PaginationMeta,
): Promise<any> {
return this.scenariosCrudService.serialize(entities, paginationMeta);
asyncJobTriggered?: boolean,
): Promise<ScenarioResult> {
return plainToClass(ScenarioResult, {
...(await this.scenariosCrudService.serialize(entities, paginationMeta)),
meta: asyncJobTriggered ? AsyncJobDto.forScenario() : undefined,
});
}
}
3 changes: 2 additions & 1 deletion api/apps/api/src/modules/scenarios/scenario.api.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { IsArray, IsOptional } from 'class-validator';
import { TimeUserEntityMetadata } from '../../types/time-user-entity-metadata';
import { BaseServiceResource } from '../../types/resource.interface';
import { GeoFeatureSetSpecification } from '../geo-features/dto/geo-feature-set-specification.dto';
import { JsonApiAsyncJobMeta } from '@marxan-api/dto/async-job.dto';

export const scenarioResource: BaseServiceResource = {
className: 'Scenario',
Expand Down Expand Up @@ -215,7 +216,7 @@ export class JSONAPIScenarioData {
attributes!: Scenario;
}

export class ScenarioResult {
export class ScenarioResult extends JsonApiAsyncJobMeta {
@ApiProperty()
data!: JSONAPIScenarioData;
}
Loading

0 comments on commit f5f56e8

Please sign in to comment.