Skip to content

Commit

Permalink
feature(GeoFeatures): Adds scenario usage count to the find all resul…
Browse files Browse the repository at this point in the history
…t of GeoFeatures
  • Loading branch information
KevSanchez committed Jul 28, 2023
1 parent 07d51c3 commit 8e3a589
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ export class GeoFeature extends BaseEntity {

@ApiPropertyOptional()
tag?: string;

@ApiPropertyOptional()
scenarioUsageCount?: number;
}

export class JSONAPIGeoFeaturesData {
Expand Down
64 changes: 63 additions & 1 deletion api/apps/api/src/modules/geo-features/geo-features.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import {
InjectDataSource,
InjectEntityManager,
InjectRepository,
} from '@nestjs/typeorm';
import { DeepReadonly } from 'utility-types';
import {
DataSource,
Expand Down Expand Up @@ -94,6 +98,8 @@ export class GeoFeaturesService extends AppBaseService<
private readonly geoDataSource: DataSource,
@InjectRepository(GeoFeatureGeometry, DbConnections.geoprocessingDB)
private readonly geoFeaturesGeometriesRepository: Repository<GeoFeatureGeometry>,
@InjectEntityManager(DbConnections.geoprocessingDB)
private readonly geoEntityManager: EntityManager,
@InjectRepository(GeoFeature)
private readonly geoFeaturesRepository: Repository<GeoFeature>,
@InjectRepository(Project)
Expand Down Expand Up @@ -129,6 +135,7 @@ export class GeoFeaturesService extends AppBaseService<
'properties',
'isCustom',
'tag',
'scenarioUsageCount',
],
keyForAttribute: 'camelCase',
};
Expand Down Expand Up @@ -313,6 +320,12 @@ export class GeoFeaturesService extends AppBaseService<
);
}

if (!(omitFields && omitFields.includes('scenarioUsageCount'))) {
extendedResults[0] = await this.extendFindAllGeoFeatureWithScenarioUsage(
extendedResults[0],
);
}

return extendedResults;
}

Expand All @@ -334,6 +347,10 @@ export class GeoFeaturesService extends AppBaseService<
);
}

if (!(omitFields && omitFields.includes('scenarioUsageCount'))) {
extendedResult = await this.extendFindGeoFeatureWithScenarioUsage(entity);
}

return extendedResult;
}

Expand Down Expand Up @@ -676,4 +693,49 @@ export class GeoFeaturesService extends AppBaseService<
}
return newFeatures;
}

private async extendFindAllGeoFeatureWithScenarioUsage(
geoFeatures: GeoFeature[],
): Promise<GeoFeature[]> {
const featureIds = geoFeatures.map((i) => i.id);

const scenarioUsages: {
id: string;
usage: number;
}[] = await this.geoEntityManager
.createQueryBuilder()
.select('api_feature_id', 'id')
.addSelect('COUNT(DISTINCT sfd.scenario_id)', 'usage')
.from('scenario_features_data', 'sfd')
.where('sfd.api_feature_id IN (:...featureIds)', { featureIds })
.groupBy('api_feature_id')
.execute();

return geoFeatures.map((feature) => {
const scenarioUsage = scenarioUsages.find((el) => el.id === feature.id);

return {
...feature,
scenarioUsageCount: scenarioUsage ? Number(scenarioUsage.usage) : 0,
} as GeoFeature;
});
}

private async extendFindGeoFeatureWithScenarioUsage(
feature: GeoFeature,
): Promise<GeoFeature> {
const [usage]: {
count: number;
}[] = await this.geoEntityManager
.createQueryBuilder()
.select('COUNT(DISTINCT sfd.scenario_id', 'count')
.from('scenario_features_data', 'sfd')
.where('sfd.api_feature_id = featureId', { featureId: feature.id })
.execute();

return {
...feature,
scenarioUsageCount: usage ? Number(usage.count) : 0,
} as GeoFeature;
}
}
80 changes: 80 additions & 0 deletions api/apps/api/test/geo-features.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import { bootstrapApplication } from './utils/api-application';
import { GivenUserIsLoggedIn } from './steps/given-user-is-logged-in';

import { createWorld } from './project/projects-world';
import { Repository } from 'typeorm';
import { ScenarioFeaturesData } from '@marxan/features';
import { v4 } from 'uuid';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { getEntityManagerToken, getRepositoryToken } from '@nestjs/typeorm';
import { DbConnections } from '@marxan-api/ormconfig.connections';
import { range } from 'lodash';
import { GeoFeatureGeometry } from '@marxan/geofeatures';

let world: PromiseType<ReturnType<typeof createWorld>>;

Expand Down Expand Up @@ -52,6 +60,38 @@ describe('GeoFeaturesModule (e2e)', () => {
expect(response.body.data[0].type).toBe(geoFeatureResource.name.plural);
});

test('should include scenarioUsageCount', async () => {
//ARRANGE
await GivenScenarioFeaturesData(app, 'demo_panthera_pardus', 3);
await GivenScenarioFeaturesData(app, 'demo_kobus_leche', 5);

//ACT
const multipleResponse = await request(app.getHttpServer())
.get(`/api/v1/projects/${world.projectWithCountry}/features`)
.set('Authorization', `Bearer ${jwtToken}`)
.expect(HttpStatus.OK);

// endpoint for retrieving a single feature by id is not implmenented, however the usageCount functionality is still there

//ASSERT
const geoFeaturesForProject = multipleResponse.body.data;
expect(geoFeaturesForProject.length).toBeGreaterThan(0);
expect(multipleResponse.body.data[0].type).toBe(
geoFeatureResource.name.plural,
);
for (const geoFeature of geoFeaturesForProject) {
if (geoFeature.attributes.featureClassName === 'demo_panthera_pardus') {
expect(geoFeature.attributes.scenarioUsageCount).toBe(3);
} else if (
geoFeature.attributes.featureClassName === 'demo_kobus_leche'
) {
expect(geoFeature.attributes.scenarioUsageCount).toBe(5);
} else {
expect(geoFeature.attributes.scenarioUsageCount).toBe(0);
}
}
});

test.todo(
'As a user, when I upload feature shapefiles, I should see the related features in the list of those available within a project',
);
Expand Down Expand Up @@ -110,3 +150,43 @@ describe('GeoFeaturesModule (e2e)', () => {
});
});
});

export async function GivenScenarioFeaturesData(
app: INestApplication,
featureClassName: string,
amountOfScenariosForFeature: number,
) {
const geoEM = app.get(getEntityManagerToken(DbConnections.geoprocessingDB));
const featureRepo: Repository<GeoFeature> = app.get(
getRepositoryToken(GeoFeature),
);
const featureGeoRepo: Repository<GeoFeatureGeometry> = app.get(
getRepositoryToken(GeoFeatureGeometry, DbConnections.geoprocessingDB),
);

const feature = await featureRepo.findOneOrFail({
where: { featureClassName },
});
const featureGeo = await featureGeoRepo.findOneOrFail({
where: { featureId: feature.id },
});

const insertValues = range(1, amountOfScenariosForFeature + 1).map((data) => {
return {
id: v4(),
featureDataId: featureGeo.id,
scenarioId: v4(), //there's no relational integratity with the API,
apiFeatureId: feature.id,
featureId: data,
};
});

await geoEM
.createQueryBuilder()
.insert()
.into(ScenarioFeaturesData)
.values(insertValues as QueryDeepPartialEntity<ScenarioFeaturesData>[])
.execute();

return insertValues;
}

0 comments on commit 8e3a589

Please sign in to comment.