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: import scenario command and handler #916

Merged
merged 4 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { ImportAdaptersModule } from '../adapters/import-adapters.module';
import { CompleteImportPieceHandler } from './complete-import-piece.handler';
import { ExportConfigReader } from './export-config-reader';
import { ImportProjectHandler } from './import-project.handler';
import { ImportScenarioHandler } from './import-scenario.handler';

@Module({
imports: [CqrsModule, ImportAdaptersModule, ArchiveReaderModule],
providers: [
ImportProjectHandler,
ImportScenarioHandler,
CompleteImportPieceHandler,
ExportConfigReader,
Logger,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ArchiveLocation } from '@marxan/cloning/domain';
import { Failure as ArchiveReadError } from '@marxan/cloning/infrastructure/archive-reader.port';
import { Command } from '@nestjs-architects/typed-cqrs';
import { Either } from 'fp-ts/lib/Either';
import { SaveError } from './import.repository.port';

export type ImportScenarioError = SaveError | ArchiveReadError;

export class ImportScenario extends Command<
Either<ImportScenarioError, string>
> {
constructor(public readonly archiveLocation: ArchiveLocation) {
super();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import {
ArchiveLocation,
ClonePiece,
ComponentId,
ResourceId,
ResourceKind,
} from '@marxan/cloning/domain';
import {
Failure as ArchiveFailure,
invalidFiles,
} from '@marxan/cloning/infrastructure/archive-reader.port';
import {
ExportConfigContent,
ProjectExportConfigContent,
ScenarioExportConfigContent,
} from '@marxan/cloning/infrastructure/clone-piece-data/export-config';
import { FixtureType } from '@marxan/utils/tests/fixture-type';
import { CqrsModule, EventBus, IEvent } from '@nestjs/cqrs';
import { Test } from '@nestjs/testing';
import { Either, isLeft, isRight, left, Right, right } from 'fp-ts/Either';
import { PromiseType } from 'utility-types';
import { v4 } from 'uuid';
import { MemoryImportRepository } from '../adapters/memory-import.repository.adapter';
import {
ImportComponent,
ImportId,
ImportRequested,
PieceImportRequested,
} from '../domain';
import { ImportComponentStatuses } from '../domain/import/import-component-status';
import { ExportConfigReader } from './export-config-reader';
import { ImportResourcePieces } from './import-resource-pieces.port';
import { ImportScenario } from './import-scenario.command';
import { ImportScenarioHandler } from './import-scenario.handler';
import { ImportRepository } from './import.repository.port';

let fixtures: FixtureType<typeof getFixtures>;

beforeEach(async () => {
fixtures = await getFixtures();
});

it(`importing invalid archive`, async () => {
fixtures.GivenExtractingArchiveFails();
const result = await fixtures.WhenRequestingImport();
fixtures.ThenImportFails(result);
});

it(`importing archive with sequential components`, async () => {
fixtures.GivenExtractingArchiveHasSequentialComponents();
const result = await fixtures.WhenRequestingImport();

fixtures.ThenRequestImportIsSaved(result);
fixtures.ThenImportRequestedIsEmitted();
fixtures.ThenLowestOrderComponentIsRequested();
});

it(`importing archive with equal order components`, async () => {
fixtures.GivenExtractingArchiveHasEqualComponents();
const result = await fixtures.WhenRequestingImport();
fixtures.ThenRequestImportIsSaved(result);
fixtures.ThenImportRequestedIsEmitted();
fixtures.ThenAllComponentsAreRequested();
});

const getFixtures = async () => {
const sandbox = await Test.createTestingModule({
imports: [CqrsModule],
providers: [
{
provide: ExportConfigReader,
useClass: FakeExportConfigReader,
},
{
provide: ImportRepository,
useClass: MemoryImportRepository,
},
{ provide: ImportResourcePieces, useClass: FakeImportResourcePieces },
ImportScenarioHandler,
],
}).compile();
await sandbox.init();

let resourceId: ResourceId;

const events: IEvent[] = [];
sandbox.get(EventBus).subscribe((event) => events.push(event));

const sut = sandbox.get(ImportScenarioHandler);
const repo: MemoryImportRepository = sandbox.get(ImportRepository);
const exportConfigReader: FakeExportConfigReader = sandbox.get(
ExportConfigReader,
);
const importResourcePieces: FakeImportResourcePieces = sandbox.get(
ImportResourcePieces,
);

return {
GivenExtractingArchiveFails: () => {
exportConfigReader.mock.mockImplementation(async () =>
left(invalidFiles),
);
},
GivenExtractingArchiveHasSequentialComponents: () => {
importResourcePieces.mockSequentialPieces();
},
GivenExtractingArchiveHasEqualComponents: () => {
importResourcePieces.mockEqualPieces();
},
WhenRequestingImport: async () => {
const importResult = await sut.execute(
new ImportScenario(new ArchiveLocation(`whatever`)),
);
if (isRight(importResult))
resourceId = new ResourceId(
repo.entities[importResult.right].resourceId,
);
return importResult;
},
ThenRequestImportIsSaved: (
importResult: PromiseType<ReturnType<ImportScenarioHandler['execute']>>,
) => {
expect(isRight(importResult)).toBeTruthy();
expect(
repo.entities[(importResult as Right<string>).right],
).toBeDefined();
},
ThenImportFails: (
importResult: PromiseType<ReturnType<ImportScenarioHandler['execute']>>,
) => {
expect(isLeft(importResult)).toBeTruthy();
},
ThenImportRequestedIsEmitted: () => {
expect(
events.filter((event) => event instanceof ImportRequested),
).toEqual([
{
importId: expect.any(ImportId),
resourceId,
resourceKind: ResourceKind.Scenario,
},
]);
},
ThenLowestOrderComponentIsRequested: () => {
expect(
events.filter((event) => event instanceof PieceImportRequested),
).toMatchObject([
{
importId: expect.any(ImportId),
componentId: expect.any(ComponentId),
},
]);
},
ThenAllComponentsAreRequested: () => {
expect(
events.filter((event) => event instanceof PieceImportRequested),
).toMatchObject([
{
importId: expect.any(ImportId),
componentId: expect.any(ComponentId),
},
{
importId: expect.any(ImportId),
componentId: expect.any(ComponentId),
},
]);
},
};
};

class FakeExportConfigReader {
mock: jest.MockedFunction<
ExportConfigReader['read']
> = jest.fn().mockResolvedValue(
right({
resourceKind: ResourceKind.Scenario,
projectId: v4(),
} as ExportConfigContent),
);

async read(
location: ArchiveLocation,
): Promise<Either<ArchiveFailure, ExportConfigContent>> {
return this.mock(location);
}
}

class FakeImportResourcePieces extends ImportResourcePieces {
mock: jest.MockedFunction<
ImportResourcePieces['resolveForScenario']
> = jest.fn();

resolveForProject(
id: ResourceId,
archiveLocation: ArchiveLocation,
pieces: ProjectExportConfigContent['pieces'],
): ImportComponent[] {
return [];
}

resolveForScenario(
id: ResourceId,
archiveLocation: ArchiveLocation,
pieces: ScenarioExportConfigContent['pieces'],
kind: ResourceKind,
oldScenarioId: string,
): ImportComponent[] {
return this.mock(id, archiveLocation, pieces, kind, oldScenarioId);
}

mockSequentialPieces() {
this.mock.mockImplementation((resourceId: ResourceId) => [
ImportComponent.fromSnapshot({
status: ImportComponentStatuses.Submitted,
order: 0,
resourceId: resourceId.value,
id: `import component unique id`,
piece: ClonePiece.ScenarioMetadata,
uris: [
{
uri: `/tmp/scenario-metadata-random-uuid.json`,
relativePath: `scenario-metadata.json`,
},
],
}),
ImportComponent.fromSnapshot({
status: ImportComponentStatuses.Submitted,
order: 1,
resourceId: resourceId.value,
id: `some other piece`,
piece: ClonePiece.ScenarioRunResults,
uris: [
{
uri: `/tmp/scenario-run-results-random-uuid.json`,
relativePath: `scenario-run-results.json`,
},
],
}),
]);
}

mockEqualPieces() {
this.mock.mockImplementation((resourceId: ResourceId) => [
ImportComponent.fromSnapshot({
status: ImportComponentStatuses.Submitted,
order: 2,
resourceId: resourceId.value,
id: `import component unique id`,
piece: ClonePiece.ScenarioMetadata,
uris: [
{
uri: `/tmp/scenario-metadata-random-uuid.json`,
relativePath: `scenario-metadata.json`,
},
],
}),
ImportComponent.fromSnapshot({
status: ImportComponentStatuses.Submitted,
order: 2,
resourceId: resourceId.value,
id: `some other piece`,
piece: ClonePiece.ScenarioRunResults,
uris: [
{
uri: `/tmp/scenario-run-results-random-uuid.json`,
relativePath: `scenario-run-results.json`,
},
],
}),
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { ResourceId, ResourceKind } from '@marxan/cloning/domain';
import { ScenarioExportConfigContent } from '@marxan/cloning/infrastructure/clone-piece-data/export-config';
import {
CommandHandler,
EventPublisher,
IInferredCommandHandler,
} from '@nestjs/cqrs';
import { Either, isLeft, right } from 'fp-ts/Either';
import { Import } from '../domain/import/import';
import { ExportConfigReader } from './export-config-reader';
import { ImportResourcePieces } from './import-resource-pieces.port';
import { ImportScenario, ImportScenarioError } from './import-scenario.command';
import { ImportRepository } from './import.repository.port';

@CommandHandler(ImportScenario)
export class ImportScenarioHandler
implements IInferredCommandHandler<ImportScenario> {
constructor(
private readonly exportConfigReader: ExportConfigReader,
private readonly importRepo: ImportRepository,
private readonly eventPublisher: EventPublisher,
private readonly importResourcePieces: ImportResourcePieces,
) {}

async execute({
archiveLocation,
}: ImportScenario): Promise<Either<ImportScenarioError, string>> {
const exportConfigOrError = await this.exportConfigReader.read(
archiveLocation,
);
if (isLeft(exportConfigOrError)) return exportConfigOrError;

const exportConfig = exportConfigOrError.right as ScenarioExportConfigContent;
const importResourceId = ResourceId.create();

const pieces = this.importResourcePieces.resolveForScenario(
importResourceId,
archiveLocation,
exportConfig.pieces,
ResourceKind.Scenario,
exportConfig.resourceId,
);

const importRequest = this.eventPublisher.mergeObjectContext(
Import.newOne(
importResourceId,
ResourceKind.Scenario,
new ResourceId(exportConfig.projectId),
archiveLocation,
pieces,
),
);

importRequest.run();

const result = await this.importRepo.save(importRequest);

if (isLeft(result)) return result;

importRequest.commit();

return right(importRequest.importId.value);
}
}