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

link scenarios correctly to cost surfaces after cloning [MRXN23-606] #1661

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
@@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddStableIdsToCostSurfaces1710069730000
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE cost_surfaces
ADD COLUMN stable_id uuid;

CREATE INDEX cost_surfaces_stable_id__idx ON cost_surfaces(stable_id);

CREATE UNIQUE INDEX cost_surfaces_unique_stable_ids_within_project__idx ON cost_surfaces(project_id, stable_id);
`);

await queryRunner.query(`
UPDATE cost_surfaces
SET stable_id = id;
`);

await queryRunner.query(`
ALTER TABLE cost_surfaces
ALTER COLUMN stable_id SET NOT NULL;

ALTER TABLE cost_surfaces
ALTER COLUMN stable_id SET DEFAULT gen_random_uuid();
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE cost_surfaces
DROP COLUMN stable_id;
`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class CostSurface {
@PrimaryGeneratedColumn('uuid')
id!: string;

@Column({ name: 'stable_id' })
stableId!: string;

@ApiProperty({ type: () => Project })
@ManyToOne((_type) => Project, (project) => project.costSurfaces)
@JoinColumn({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ async function getFixtures() {
costSurfaceId: '',
costSurface: {
id: '',
stableId: '',
name: 'some cost surface',
projectId: '',
isDefault: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { geoprocessingConnections } from '@marxan-geoprocessing/ormconfig';
import { ClonePiece, ExportJobInput, ExportJobOutput } from '@marxan/cloning';
import { ComponentLocation, ResourceKind } from '@marxan/cloning/domain';
import { ClonePieceRelativePathResolver } from '@marxan/cloning/infrastructure/clone-piece-data';
import {
ProjectCustomFeature,
ProjectCustomFeaturesContent,
} from '@marxan/cloning/infrastructure/clone-piece-data/project-custom-features';
import { ProjectCustomFeature } from '@marxan/cloning/infrastructure/clone-piece-data/project-custom-features';
import { CloningFilesRepository } from '@marxan/cloning-files-repository';
import { GeometrySource } from '@marxan/geofeatures';
import { Injectable, Logger } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { isLeft } from 'fp-ts/lib/Either';
Expand All @@ -24,6 +20,7 @@ type CreationStatus = ProjectCustomFeature['creation_status'];

type ProjectCostSurfacesSelectResult = {
id: string;
stable_id: string;
name: string;
min: number;
max: number;
Expand All @@ -32,6 +29,7 @@ type ProjectCostSurfacesSelectResult = {

type CostSurfaceDataSelectResult = {
cost_surface_id: string;
stable_id: string;
cost: number;
projects_pu_id: number;
};
Expand Down Expand Up @@ -61,7 +59,14 @@ export class ProjectCostSurfacesPieceExporter implements ExportPieceProcessor {
const costSurfaces: ProjectCostSurfacesSelectResult[] =
await this.apiEntityManager
.createQueryBuilder()
.select(['cs.id', 'cs.name', 'cs.min', 'cs.max', 'cs.is_default'])
.select([
'cs.id',
'cs.stable_id',
'cs.name',
'cs.min',
'cs.max',
'cs.is_default',
])
.from('cost_surfaces', 'cs')
.where('cs.project_id = :projectId', { projectId: input.resourceId })
.execute();
Expand Down Expand Up @@ -94,16 +99,10 @@ export class ProjectCostSurfacesPieceExporter implements ExportPieceProcessor {
(data: CostSurfaceDataSelectResult) =>
data.cost_surface_id === costSurface.id,
)
.map(
({
cost_surface_id: _cost_surface_id,
projects_pu_id,
...data
}) => {
const puid = projectPusMap[projects_pu_id];
return { puid, ...data };
},
),
.map(({ projects_pu_id, ...data }) => {
const puid = projectPusMap[projects_pu_id];
return { puid, ...data };
}),
})),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ export class ScenarioMetadataPieceExporter implements ExportPieceProcessor {
return piece === ClonePiece.ScenarioMetadata;
}

private async getStableIdForScenarioCostSurface(
costSurfaceId: string,
): Promise<string> {
return await this.entityManager
.createQueryBuilder()
.select(['stable_id'])
.from('cost_surfaces', 'cs')
.where('cs.id = :costSurfaceId', {
costSurfaceId: costSurfaceId,
})
.execute()
.then((result: { stable_id: string }[]) => result[0]?.stable_id);
}

async run(input: ExportJobInput): Promise<ExportJobOutput> {
const scenarioId = input.resourceId;

Expand Down Expand Up @@ -77,6 +91,9 @@ export class ScenarioMetadataPieceExporter implements ExportPieceProcessor {
throw new Error(errorMessage);
}

const scenarioCostSurfaceStableId: string =
await this.getStableIdForScenarioCostSurface(scenario.cost_surface_id);

const [blmRange]: [SelectScenarioBlmResult] = await this.entityManager
.createQueryBuilder()
.select(['values', 'defaults', 'range'])
Expand All @@ -101,7 +118,7 @@ export class ScenarioMetadataPieceExporter implements ExportPieceProcessor {
solutionsAreLocked: scenario.solutions_are_locked,
type: scenario.type,
status: scenario.status ?? undefined,
cost_surface_id: scenario.cost_surface_id,
cost_surface_id: scenarioCostSurfaceStableId,
};

const relativePath = ClonePieceRelativePathResolver.resolveFor(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { geoprocessingConnections } from '@marxan-geoprocessing/ormconfig';
import { ClonePiece, ImportJobInput, ImportJobOutput } from '@marxan/cloning';
import { ResourceKind } from '@marxan/cloning/domain';
import { ProjectCustomFeaturesContent } from '@marxan/cloning/infrastructure/clone-piece-data/project-custom-features';
import { CloningFilesRepository } from '@marxan/cloning-files-repository';
import { GeoFeatureGeometry } from '@marxan/geofeatures';
import { readableToBuffer } from '@marxan/utils';
import { Injectable, Logger } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
Expand All @@ -14,11 +12,12 @@ import {
ImportPieceProcessor,
PieceImportProvider,
} from '../pieces/import-piece-processor';
import { chunk } from 'lodash';
import { chunk, omit } from 'lodash';
import { ProjectsPuEntity } from '@marxan-jobs/planning-unit-geometry';
import { CHUNK_SIZE_FOR_BATCH_GEODB_OPERATIONS } from '@marxan-geoprocessing/utils/chunk-size-for-batch-geodb-operations';
import {
CostSurfaceData,
ProjectCostSurface,
ProjectCostSurfacesContent,
} from '@marxan/cloning/infrastructure/clone-piece-data/project-cost-surfaces';
import { CostSurfacePuDataEntity } from '@marxan/cost-surfaces';
Expand Down Expand Up @@ -85,15 +84,19 @@ export class ProjectCostSurfacesPieceImporter implements ImportPieceProcessor {
const projectPusMap = await this.getProjectPusMap(projectId);

await this.apiEntityManager.transaction(async (apiEm) => {
const costSurfacesInsertValues: any[] = [];
let costSurfacesDataInsertValues: any[] = [];
const costSurfacesInsertValues: (Omit<ProjectCostSurface, 'data'> & {
project_id: string;
stable_id: string;
})[] = [];
let costSurfacesDataInsertValues: Record<string, unknown>[] = [];
costSurfaces.forEach(({ data, ...costSurface }) => {
const costSurfaceId = v4();

costSurfacesInsertValues.push({
...costSurface,
project_id: projectId,
id: costSurfaceId,
stable_id: costSurface.stable_id,
});

const costSurfaceData = data.map((data: CostSurfaceData) => ({
Expand All @@ -115,12 +118,12 @@ export class ProjectCostSurfacesPieceImporter implements ImportPieceProcessor {
});

await Promise.all(
costSurfacesInsertValues.map((values) =>
costSurfacesInsertValues.map((costSurfaceData) =>
apiEm
.createQueryBuilder()
.insert()
.into('cost_surfaces')
.values(values)
.values(costSurfaceData)
.execute(),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ export class ScenarioMetadataPieceImporter implements ImportPieceProcessor {
.execute();
}

private async mapCostSurfaceStableIdToIdOfClonedCostSurface(
em: EntityManager,
costSurfaceId: string,
projectId: string,
): Promise<string> {
return await em
.createQueryBuilder()
.select('id')
.from('cost_surfaces', 'cs')
.where('cs.stable_id = :costSurfaceId', { costSurfaceId })
.andWhere('cs.project_id = :projectId', { projectId })
.execute()
.then((result) => result[0]?.id);
}

async run(input: ImportJobInput): Promise<ImportJobOutput> {
const {
pieceResourceId: scenarioId,
Expand Down Expand Up @@ -121,6 +136,13 @@ export class ScenarioMetadataPieceImporter implements ImportPieceProcessor {
const scenarioCloning = resourceKind === ResourceKind.Scenario;

await this.entityManager.transaction(async (em) => {
const idOfClonedCostSurfaceLinkedToScenario =
await this.mapCostSurfaceStableIdToIdOfClonedCostSurface(
em,
metadata.cost_surface_id,
projectId,
);

if (scenarioCloning) {
await this.updateScenario(em, scenarioId, metadata, input.ownerId);
} else {
Expand All @@ -135,7 +157,10 @@ export class ScenarioMetadataPieceImporter implements ImportPieceProcessor {
em,
scenarioId,
projectId,
metadata,
{
...metadata,
cost_surface_id: idOfClonedCostSurfaceLinkedToScenario,
},
input.ownerId,
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { geoprocessingConnections } from '@marxan-geoprocessing/ormconfig';
import { ClonePiece, ExportJobInput } from '@marxan/cloning';
import { ResourceKind } from '@marxan/cloning/domain';
import { ProjectCustomFeaturesContent } from '@marxan/cloning/infrastructure/clone-piece-data/project-custom-features';
import { CloningFilesRepository } from '@marxan/cloning-files-repository';
import { GeoFeatureGeometry } from '@marxan/geofeatures';
import { FixtureType } from '@marxan/utils/tests/fixture-type';
import { Test } from '@nestjs/testing';
import { getEntityManagerToken, TypeOrmModule } from '@nestjs/typeorm';
import { isLeft, Right } from 'fp-ts/lib/Either';
import { Readable } from 'stream';
import { EntityManager, In } from 'typeorm';
import { EntityManager } from 'typeorm';
import { v4 } from 'uuid';
import {
DeleteProjectAndOrganization,
GivenCostSurfaceData,
GivenCostSurfaces,
GivenFeatures,
GivenFeaturesData,
GivenProjectExists,
readSavedFile,
} from '../fixtures';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ export type CostSurfaceData = {
};

export type ProjectCostSurface = {
/**
* The stable_id of the cost surface in the original project is stored as part
* of the export, so that it can be used to port over as part of cloned
* projects any links between scenarios and cost surfaces (since the former
* are imported with a cost_surface_id matching the stable_id stored here, but
* the corresponding cost surfaces are created with a new unique id in the
* cloned project).
*/
id: string;
stable_id: string;
name: string;
min: number;
max: number;
Expand Down
Loading