Skip to content

Commit

Permalink
Merge pull request #1100 from Vizzuality/feat/MARXAN-1594-add-endpoin…
Browse files Browse the repository at this point in the history
…t-for-running-a-legacy-project-import

feat : add endpoint for running a legacy project import
  • Loading branch information
angelhigueraacid authored May 23, 2022
2 parents 4748f2c + 38d5983 commit 7299a6a
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { isLeft } from 'fp-ts/lib/Either';
import { v4 } from 'uuid';
import {
LegacyProjectImport,
legacyProjectImportAlreadyStarted,
legacyProjectImportDuplicateFileType,
legacyProjectImportIsNotAcceptingFiles,
} from '../domain/legacy-project-import/legacy-project-import';
import { LegacyProjectImportComponentSnapshot } from '../domain/legacy-project-import/legacy-project-import-component.snapshot';
import {
Expand Down Expand Up @@ -74,7 +74,7 @@ it('fails if given file cannot be stored', async () => {
.ThenErrorStoringFileShouldBeReturned();
});

it('fails if legacy project import is not accepting more files', async () => {
it('fails if legacy project import has already started', async () => {
const legacyProjectImport = await fixtures.GivenLegacyProjectImportWasRequested(
{ files: [], isAcceptingFiles: false, pieces: [] },
);
Expand All @@ -86,7 +86,7 @@ it('fails if legacy project import is not accepting more files', async () => {
file: Buffer.from('example file'),
fileType: LegacyProjectImportFileType.InputDat,
})
.ThenLegacyProjectImportIsNotAcceptingFileErrorShouldBeReturned();
.ThenLegacyProjectImportHasAlreadyStartedErrorShouldBeReturned();
});

it('fails if legacy project import already has a file of the same type of the provided file', async () => {
Expand Down Expand Up @@ -242,11 +242,11 @@ const getFixtures = async () => {

expect(result).toMatchObject({ left: unknownError });
},
ThenLegacyProjectImportIsNotAcceptingFileErrorShouldBeReturned: async () => {
ThenLegacyProjectImportHasAlreadyStartedErrorShouldBeReturned: async () => {
const result = await sut.execute(command);

expect(result).toMatchObject({
left: legacyProjectImportIsNotAcceptingFiles,
left: legacyProjectImportAlreadyStarted,
});
},
ThenLegacyProjectImportDuplicateFileTypeErrorShouldBeReturned: async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { forbiddenError } from '@marxan-api/modules/access-control';
import { ResourceId } from '@marxan/cloning/domain';
import { UserId } from '@marxan/domain-ids';
import { Command } from '@nestjs-architects/typed-cqrs';
import { Either } from 'fp-ts/lib/Either';
import { GenerateLegacyProjectImportPiecesErrors } from '../domain/legacy-project-import/legacy-project-import';
import { RunLegacyProjectImportErrors } from '../domain/legacy-project-import/legacy-project-import';
import {
LegacyProjectImportRepositoryFindErrors,
LegacyProjectImportRepositorySaveErrors,
} from '../domain/legacy-project-import/legacy-project-import.repository';

export type RunLegacyProjectImportError =
| GenerateLegacyProjectImportPiecesErrors
| RunLegacyProjectImportErrors
| LegacyProjectImportRepositorySaveErrors
| LegacyProjectImportRepositoryFindErrors;
| LegacyProjectImportRepositoryFindErrors
| typeof forbiddenError;

export type RunLegacyProjectImportResponse = Either<
RunLegacyProjectImportError,
true
>;

export class RunLegacyProjectImport extends Command<RunLegacyProjectImportResponse> {
constructor(public readonly projectId: ResourceId) {
constructor(
public readonly projectId: ResourceId,
public readonly userId: UserId,
) {
super();
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { forbiddenError } from '@marxan-api/modules/access-control';
import { UsersProjectsApiEntity } from '@marxan-api/modules/access-control/projects-acl/entity/users-projects.api.entity';
import { ArchiveLocation, ResourceId } from '@marxan/cloning/domain';
import { UserId } from '@marxan/domain-ids';
Expand All @@ -15,6 +16,7 @@ import { LegacyProjectImportPieceRequested } from '../domain/events/legacy-proje
import { LegacyProjectImportRequested } from '../domain/events/legacy-project-import-requested.event';
import {
LegacyProjectImport,
legacyProjectImportAlreadyStarted,
legacyProjectImportMissingRequiredFile,
} from '../domain/legacy-project-import/legacy-project-import';
import {
Expand Down Expand Up @@ -44,6 +46,29 @@ it(`runs a legacy project import`, async () => {
fixtures.ThenLegacyProjectImportPieceRequestedAreRequested();
});

it(`fails to run when a non owner role runs a legacy project import`, async () => {
fixtures.GivenAnExistingLegacyProjectImport();
fixtures.GivenAllFilesAreUploaded();
const result = await fixtures.WhenRunningALegacyProjectImport({
unauthorizedUser: true,
});
fixtures.ThenNoEventsAreEmitted();
await fixtures.ThenLegacyProjectImportIsNotUpdated();
fixtures.ThenForbiddenErrorIsReturned(result);
});

it(`fails to run for a second time`, async () => {
await fixtures.GivenAnExistingLegacyProjectImport();
await fixtures.GivenAllFilesAreUploaded();
const result = await fixtures.WhenRunningALegacyProjectImport();
await fixtures.ThenStartingLegacyProjectImportIsUpdated(result);

const secondTimeRunningResult = await fixtures.WhenRunningALegacyProjectImport();
fixtures.ThenLegacyProjectImportIsAlreadyStartedErrorIsRetured(
secondTimeRunningResult,
);
});

it(`fails to run when missing uploaded files`, async () => {
fixtures.GivenAnExistingLegacyProjectImport();
fixtures.GivenNotAllFilesAreUploaded();
Expand Down Expand Up @@ -88,6 +113,7 @@ const getFixtures = async () => {
await sandbox.init();

const ownerId = UserId.create();
const unauthorizedUserId = UserId.create();
const projectId = v4();
const scenarioId = v4();
const existingLegacyProjectImport = LegacyProjectImport.newOne(
Expand Down Expand Up @@ -130,8 +156,15 @@ const getFixtures = async () => {
GivenUpdatingALegacyProjectImportFails: () => {
repo.saveFailure = true;
},
WhenRunningALegacyProjectImport: () => {
return sut.execute(new RunLegacyProjectImport(new ResourceId(projectId)));
WhenRunningALegacyProjectImport: (
{ unauthorizedUser } = { unauthorizedUser: false },
) => {
return sut.execute(
new RunLegacyProjectImport(
new ResourceId(projectId),
unauthorizedUser ? unauthorizedUserId : ownerId,
),
);
},
ThenNoEventsAreEmitted: () => {
expect(events).toHaveLength(0);
Expand All @@ -151,6 +184,20 @@ const getFixtures = async () => {
}),
);
},
ThenLegacyProjectImportIsAlreadyStartedErrorIsRetured: (
result: RunLegacyProjectImportResponse,
) => {
expect(result).toBeDefined();
if (isRight(result)) throw new Error('the handler should have failed');

expect(result.left).toEqual(legacyProjectImportAlreadyStarted);
},
ThenForbiddenErrorIsReturned: (result: RunLegacyProjectImportResponse) => {
expect(result).toBeDefined();
if (isRight(result)) throw new Error('the handler should have failed');

expect(result.left).toEqual(forbiddenError);
},
ThenMissingLegacyProjectImportErrorIsReturned: (
result: RunLegacyProjectImportResponse,
) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { forbiddenError } from '@marxan-api/modules/access-control';
import { ProjectRoles } from '@marxan-api/modules/access-control/projects-acl/dto/user-role-project.dto';
import { UsersProjectsApiEntity } from '@marxan-api/modules/access-control/projects-acl/entity/users-projects.api.entity';
import { ResourceId } from '@marxan/cloning/domain';
import {
CommandHandler,
EventPublisher,
IInferredCommandHandler,
} from '@nestjs/cqrs';
import { InjectRepository } from '@nestjs/typeorm';
import { isLeft, right } from 'fp-ts/Either';
import { isLeft, right, left } from 'fp-ts/Either';
import { Repository } from 'typeorm';
import { LegacyProjectImportRepository } from '../domain/legacy-project-import/legacy-project-import.repository';
import {
Expand All @@ -27,6 +27,7 @@ export class RunLegacyProjectImportHandler

async execute({
projectId,
userId,
}: RunLegacyProjectImport): Promise<RunLegacyProjectImportResponse> {
const legacyProjectImportOrError = await this.legacyProjectImportRepository.find(
projectId,
Expand All @@ -38,9 +39,11 @@ export class RunLegacyProjectImportHandler
legacyProjectImportOrError.right,
);

const { ownerId: userId } = legacyProjectImport.toSnapshot();
const { ownerId } = legacyProjectImport.toSnapshot();

const result = legacyProjectImport.start();
if (userId.value !== ownerId) return left(forbiddenError);

const result = legacyProjectImport.run();

if (isLeft(result)) return result;

Expand All @@ -52,7 +55,7 @@ export class RunLegacyProjectImportHandler
return legacyProjectImportSaveError;

await this.usersRepo.save({
userId,
userId: ownerId,
projectId: projectId.value,
roleName: ProjectRoles.project_owner,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ export const legacyProjectImportComponentAlreadyCompleted = Symbol(
export const legacyProjectImportComponentAlreadyFailed = Symbol(
`legacy project import component already failed`,
);
export const legacyProjectImportIsNotAcceptingFiles = Symbol(
`legacy project import file is not accepting files`,
);
export const legacyProjectImportDuplicateFile = Symbol(
`legacy project import already has this file`,
);
Expand All @@ -37,6 +34,9 @@ export const legacyProjectImportDuplicateFileType = Symbol(
export const legacyProjectImportMissingRequiredFile = Symbol(
`legacy project import missing required file`,
);
export const legacyProjectImportAlreadyStarted = Symbol(
`legacy project import already started`,
);

export type CompleteLegacyProjectImportPieceSuccess = true;
export type CompleteLegacyProjectImportPieceErrors =
Expand All @@ -47,12 +47,16 @@ export type MarkLegacyProjectImportPieceAsFailedErrors =
| typeof legacyProjectImportComponentAlreadyFailed;

export type AddFileToLegacyProjectImportErrors =
| typeof legacyProjectImportIsNotAcceptingFiles
| typeof legacyProjectImportAlreadyStarted
| typeof legacyProjectImportDuplicateFile
| typeof legacyProjectImportDuplicateFileType;

export type GenerateLegacyProjectImportPiecesErrors = typeof legacyProjectImportMissingRequiredFile;

export type RunLegacyProjectImportErrors =
| typeof legacyProjectImportAlreadyStarted
| GenerateLegacyProjectImportPiecesErrors;

export class LegacyProjectImport extends AggregateRoot {
private constructor(
readonly id: LegacyProjectImportId,
Expand Down Expand Up @@ -123,6 +127,10 @@ export class LegacyProjectImport extends AggregateRoot {
.some((piece) => piece.hasFailed());
}

private importProcessAlreadyStarted() {
return !this.isAcceptingFiles;
}

public areRequiredFilesUploaded(): boolean {
const requiredFilesTypes = [
LegacyProjectImportFileType.PlanningGridShapefile,
Expand Down Expand Up @@ -169,7 +177,10 @@ export class LegacyProjectImport extends AggregateRoot {
return right(pieces);
}

start(): Either<GenerateLegacyProjectImportPiecesErrors, true> {
run(): Either<RunLegacyProjectImportErrors, true> {
if (this.importProcessAlreadyStarted())
return left(legacyProjectImportAlreadyStarted);

this.isAcceptingFiles = false;
const piecesOrError = this.generatePieces();

Expand Down Expand Up @@ -271,8 +282,8 @@ export class LegacyProjectImport extends AggregateRoot {
addFile(
file: LegacyProjectImportFile,
): Either<AddFileToLegacyProjectImportErrors, true> {
if (!this.isAcceptingFiles) {
return left(legacyProjectImportIsNotAcceptingFiles);
if (this.importProcessAlreadyStarted()) {
return left(legacyProjectImportAlreadyStarted);
}

const fileTypeAlreadyPresent = this.files.some(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ export class StartLegacyProjectImportResponseDto {
})
scenarioId!: string;
}

export class RunLegacyProjectImportResponseDto {
@ApiProperty({
description: 'ID of the project',
example: '6fbec34e-04a7-4131-be14-c245f2435a6c',
})
projectId!: string;
}
48 changes: 48 additions & 0 deletions api/apps/api/src/modules/projects/projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,18 @@ import {
unfinishedExport,
} from '../clone/export/application/get-archive.query';
import {
RunLegacyProjectImportResponseDto,
StartLegacyProjectImportBodyDto,
StartLegacyProjectImportResponseDto,
} from './dto/legacy-project-import.dto';
import {
legacyProjectImportAlreadyStarted,
legacyProjectImportMissingRequiredFile,
} from '../legacy-project-import/domain/legacy-project-import/legacy-project-import';
import {
legacyProjectImportNotFound,
legacyProjectImportSaveError,
} from '../legacy-project-import/domain/legacy-project-import/legacy-project-import.repository';

@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
Expand Down Expand Up @@ -210,6 +219,45 @@ export class ProjectsController {
};
}

@ImplementsAcl()
@ApiOperation({
description: 'Runs a legacy project import project',
summary: 'Runs a legacy project import project',
})
@ApiOkResponse({ type: RunLegacyProjectImportResponseDto })
@Post('legacy-project-import/:projectId/run')
async runLegacyProject(
@Param('projectId') projectId: string,
@Req() req: RequestWithAuthenticatedUser,
): Promise<RunLegacyProjectImportResponseDto> {
const result = await this.projectsService.runLegacyProject(
projectId,
req.user.id,
);

if (isLeft(result)) {
switch (result.left) {
case forbiddenError:
throw new ForbiddenException();
case legacyProjectImportNotFound:
throw new NotFoundException();
case legacyProjectImportMissingRequiredFile:
throw new BadRequestException(
'missing required files for running a legacy project import',
);
case legacyProjectImportAlreadyStarted:
throw new BadRequestException(
'a run has already being made on this legacy project import',
);
case legacyProjectImportSaveError:
default:
throw new InternalServerErrorException();
}
}

return { projectId };
}

@ImplementsAcl()
@ApiOperation({ description: 'Create project' })
@ApiOkResponse({ type: ProjectResultSingular })
Expand Down
7 changes: 7 additions & 0 deletions api/apps/api/src/modules/projects/projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
StartLegacyProjectImportResult,
} from '../legacy-project-import/application/start-legacy-project-import.command';
import { string } from 'fp-ts';
import { RunLegacyProjectImport } from '../legacy-project-import/application/run-legacy-project-import.command';

export { validationFailed } from '../planning-areas';

Expand Down Expand Up @@ -466,6 +467,12 @@ export class ProjectsService {
);
}

async runLegacyProject(projectId: string, userId: string) {
return this.commandBus.execute(
new RunLegacyProjectImport(new ResourceId(projectId), new UserId(userId)),
);
}

// TODO add ensureThatProjectIsNotBlocked guard
savePlanningAreaFromShapefile = this.planningAreaService.savePlanningAreaFromShapefile.bind(
this.planningAreaService,
Expand Down

0 comments on commit 7299a6a

Please sign in to comment.