diff --git a/api/apps/api/test/upload-feature/upload-feature-csv.e2e-spec.ts b/api/apps/api/test/upload-feature/upload-feature-csv.e2e-spec.ts index d8ab2747bd..85fd5616a7 100644 --- a/api/apps/api/test/upload-feature/upload-feature-csv.e2e-spec.ts +++ b/api/apps/api/test/upload-feature/upload-feature-csv.e2e-spec.ts @@ -18,6 +18,7 @@ test(`custom feature csv upload`, async () => { expect(result.body).toHaveLength(2); await fixtures.ThenNewFeaturesAreCreated(); await fixtures.ThenNewFeaturesAmountsAreCreated(); + await fixtures.ThenFeatureAmountPerPlanningUnitAreCreated(); await fixtures.ThenFeatureUploadRegistryIsCleared(); await fixtures.ThenProjectSourcesIsSetToLegacyProject(); }); diff --git a/api/apps/api/test/upload-feature/upload-feature.fixtures.ts b/api/apps/api/test/upload-feature/upload-feature.fixtures.ts index 00fbb2cb28..10a9e9aa51 100644 --- a/api/apps/api/test/upload-feature/upload-feature.fixtures.ts +++ b/api/apps/api/test/upload-feature/upload-feature.fixtures.ts @@ -14,6 +14,7 @@ import { HttpStatus } from '@nestjs/common'; import { GeoFeatureTag } from '@marxan-api/modules/geo-feature-tags/geo-feature-tag.api.entity'; import { tagMaxlength } from '@marxan-api/modules/geo-feature-tags/dto/update-geo-feature-tag.dto'; import { Project } from '@marxan-api/modules/projects/project.api.entity'; +import { FeatureAmountsPerPlanningUnitEntity } from '@marxan/feature-amounts-per-planning-unit'; export const getFixtures = async () => { const app = await bootstrapApplication(); @@ -54,9 +55,18 @@ export const getFixtures = async () => { getRepositoryToken(Project, DbConnections.default), ); - const featuresAmounsGeoDbRepository: Repository = app.get( - getRepositoryToken(GeoFeatureGeometry, DbConnections.geoprocessingDB), - ); + const featuresAmountsGeoDbRepository: Repository = + app.get( + getRepositoryToken(GeoFeatureGeometry, DbConnections.geoprocessingDB), + ); + + const featureAmountsPerPlanningUnitRepo: Repository = + app.get( + getRepositoryToken( + FeatureAmountsPerPlanningUnitEntity, + DbConnections.geoprocessingDB, + ), + ); const geoEntityManager: EntityManager = app.get( getEntityManagerToken(DbConnections.geoprocessingDB), @@ -322,13 +332,13 @@ export const getFixtures = async () => { featureClassName: 'feat_28135ef', }, }); - const newFeature1Amounts = await featuresAmounsGeoDbRepository.find({ + const newFeature1Amounts = await featuresAmountsGeoDbRepository.find({ where: { featureId: newFeatures1?.id }, order: { amount: 'DESC', }, }); - const newFeature2Amounts = await featuresAmounsGeoDbRepository.find({ + const newFeature2Amounts = await featuresAmountsGeoDbRepository.find({ where: { featureId: newFeatures2?.id }, }); @@ -342,6 +352,37 @@ export const getFixtures = async () => { expect(newFeature2Amounts[1].amount).toBe(0); expect(newFeature2Amounts[2].amount).toBe(0); }, + ThenFeatureAmountPerPlanningUnitAreCreated: async () => { + const feature1 = await featuresRepository.findOne({ + where: { + featureClassName: 'feat_1d666bd', + }, + }); + const feature2 = await featuresRepository.findOne({ + where: { + featureClassName: 'feat_28135ef', + }, + }); + const feature1Amounts = await featureAmountsPerPlanningUnitRepo.find({ + where: { featureId: feature1?.id }, + order: { + amount: 'DESC', + }, + }); + const feature2Amounts = await featureAmountsPerPlanningUnitRepo.find({ + where: { featureId: feature2?.id }, + }); + + expect(feature1Amounts).toHaveLength(3); + expect(feature2Amounts).toHaveLength(3); + expect(feature1Amounts[0].amount).toBe(4.245387225); + expect(feature1Amounts[1].amount).toBe(4.245387225); + expect(feature1Amounts[2].amount).toBe(3.245387225); + + expect(feature2Amounts[0].amount).toBe(0); + expect(feature2Amounts[1].amount).toBe(0); + expect(feature2Amounts[2].amount).toBe(0); + }, ThenFeatureUploadRegistryIsCleared: async () => { const featureImportRegistryRecord = await featureImportRegistry.findOne({ where: { projectId }, diff --git a/api/apps/geoprocessing/src/legacy-project-import/legacy-piece-importers/features.legacy-piece-importer.ts b/api/apps/geoprocessing/src/legacy-project-import/legacy-piece-importers/features.legacy-piece-importer.ts index 264b1c3345..c9d6472ae3 100644 --- a/api/apps/geoprocessing/src/legacy-project-import/legacy-piece-importers/features.legacy-piece-importer.ts +++ b/api/apps/geoprocessing/src/legacy-project-import/legacy-piece-importers/features.legacy-piece-importer.ts @@ -30,6 +30,7 @@ import { PuvsprDatReader, } from './file-readers/puvspr-dat.reader'; import { SpecDatReader, SpecDatRow } from './file-readers/spec-dat.reader'; +import { FeatureAmountsPerPlanningUnitEntity } from '@marxan/feature-amounts-per-planning-unit'; type FeaturesData = { id: string; @@ -302,6 +303,7 @@ export class FeaturesLegacyProjectPieceImporter }; }); + this.logger.log(`Saving features API metadata...`); await Promise.all( featuresInsertValues.map((value) => apiEm @@ -326,6 +328,7 @@ export class FeaturesLegacyProjectPieceImporter projectPusGeomsMap, ); + this.logger.log(`Saving Geo feature entities...`); await Promise.all( chunk( featuresDataInsertValues, @@ -344,6 +347,30 @@ export class FeaturesLegacyProjectPieceImporter ), ); + this.logger.log(`Saving feature amounts...`); + await Promise.all( + chunk( + featuresDataInsertValues, + CHUNK_SIZE_FOR_BATCH_GEODB_OPERATIONS, + ).map((values) => { + this.geoprocessingEntityManager + .createQueryBuilder() + .insert() + .into(FeatureAmountsPerPlanningUnitEntity) + .values( + values.map(({ amount, projectPuId, featureId }) => { + return { amount, projectPuId, featureId, projectId }; + }), + ) + .execute(); + }), + ); + + await this.updateAmountMinMaxForAPIFeatures( + apiEm, + featuresData.map((feature) => feature.id), + ); + return nonExistingPus; }, ); @@ -355,4 +382,39 @@ export class FeaturesLegacyProjectPieceImporter : undefined, }; } + + private async updateAmountMinMaxForAPIFeatures( + apiEntityManager: EntityManager, + featureIds: string[], + ): Promise { + this.logger.log(`Saving min and max amounts for new features...`); + + const minAndMaxAmountsForFeatures = await this.geoprocessingEntityManager + .createQueryBuilder() + .select('feature_id', 'id') + .addSelect('MIN(amount)', 'amountMin') + .addSelect('MAX(amount)', 'amountMax') + .from('feature_amounts_per_planning_unit', 'fappu') + .where('fappu.feature_id IN (:...featureIds)', { featureIds }) + .groupBy('fappu.feature_id') + .getRawMany(); + + const minMaxSqlValueStringForFeatures = minAndMaxAmountsForFeatures + .map( + (feature) => + `(uuid('${feature.id}'), ${feature.amountMin}, ${feature.amountMax})`, + ) + .join(', '); + + const query = ` + update features set + amount_min = minmax.min, + amount_max = minmax.max + from ( + values + ${minMaxSqlValueStringForFeatures} + ) as minmax(feature_id, min, max) + where features.id = minmax.feature_id;`; + await apiEntityManager.query(query); + } } diff --git a/api/apps/geoprocessing/test/integration/legacy-project-import/features.legacy-piece-importer.e2e-spec.ts b/api/apps/geoprocessing/test/integration/legacy-project-import/features.legacy-piece-importer.e2e-spec.ts index 4084399ac8..7f1e6809ea 100644 --- a/api/apps/geoprocessing/test/integration/legacy-project-import/features.legacy-piece-importer.e2e-spec.ts +++ b/api/apps/geoprocessing/test/integration/legacy-project-import/features.legacy-piece-importer.e2e-spec.ts @@ -43,6 +43,7 @@ import { GivenUserExists, } from '../cloning/fixtures'; import { FakeLogger } from '@marxan-geoprocessing/utils/__mocks__/fake-logger'; +import { FeatureAmountsPerPlanningUnitEntity } from '@marxan/feature-amounts-per-planning-unit'; let fixtures: FixtureType; @@ -233,7 +234,11 @@ const getFixtures = async () => { logging: false, }), TypeOrmModule.forFeature( - [ProjectsPuEntity, GeoFeatureGeometry], + [ + ProjectsPuEntity, + GeoFeatureGeometry, + FeatureAmountsPerPlanningUnitEntity, + ], geoprocessingConnections.default, ), TypeOrmModule.forRoot({ @@ -289,6 +294,8 @@ const getFixtures = async () => { const featuresDataRepo = sandbox.get>( getRepositoryToken(GeoFeatureGeometry), ); + const featureAmountsPerPlanningUnitRepo: Repository = + sandbox.get(getRepositoryToken(FeatureAmountsPerPlanningUnitEntity)); const specDatFileType = LegacyProjectImportFileType.SpecDat; const puvsprDatFileType = LegacyProjectImportFileType.PuvsprDat; @@ -515,6 +522,21 @@ const getFixtures = async () => { }, }); + const featureAmounts = await featureAmountsPerPlanningUnitRepo.find({ + where: { featureId: In(insertedFeaturesIds) }, + }); + + const featuresMinMax = await apiEntityManager + .createQueryBuilder() + .select('id') + .addSelect('amount_min', 'amountMin') + .addSelect('amount_max', 'amountMax') + .from('features', 'f') + .where('f.id IN (:...featureIds)', { + featureIds: insertedFeaturesIds, + }) + .execute(); + expect(insertedFeaturesData).toHaveLength( amountOfFeaturesData - amountOfNonExistingPuids, ); @@ -526,6 +548,25 @@ const getFixtures = async () => { pus.map(({ id }) => id).includes(projectPuId), ), ).toEqual(true); + + expect(featureAmounts).toHaveLength( + amountOfFeaturesData - amountOfNonExistingPuids, + ); + expect( + featureAmounts.every( + ({ amount, projectPuId }) => + amount === expectedAmount && + projectPuId && + pus.map(({ id }) => id).includes(projectPuId), + ), + ).toEqual(true); + expect( + featuresMinMax.every( + (featureMinMax: any) => + featureMinMax.amountMin === expectedAmount && + featureMinMax.amountMax === expectedAmount, + ), + ).toBeTruthy(); }, }; },