Skip to content

Commit

Permalink
SIMSBIOHUB-106: Saving regions on project/ survey create/ update (#1055)
Browse files Browse the repository at this point in the history
Regions are now saved on project/ survey creation/ edit
  • Loading branch information
al-rosenthal authored Jul 20, 2023
1 parent 2f48df0 commit 28c4836
Show file tree
Hide file tree
Showing 18 changed files with 687 additions and 64 deletions.
2 changes: 1 addition & 1 deletion api/src/openapi/schemas/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const projectFundingSourceAgency = {
export const projectCreatePostRequestObject = {
title: 'Project post request object',
type: 'object',
required: ['coordinator', 'project', 'location', 'iucn', 'funding'],
required: ['coordinator', 'project', 'location', 'iucn', 'funding', 'regions'],
properties: {
coordinator: {
title: 'Project coordinator',
Expand Down
8 changes: 7 additions & 1 deletion api/src/paths/project/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ GET.apiDoc = {
properties: {
projectData: {
type: 'object',
required: ['id', 'name', 'project_type', 'start_date', 'end_date', 'completion_status'],
required: ['id', 'name', 'project_type', 'start_date', 'end_date', 'completion_status', 'regions'],
properties: {
id: {
type: 'number'
Expand All @@ -122,6 +122,12 @@ GET.apiDoc = {
},
completion_status: {
type: 'string'
},
regions: {
type: 'array',
items: {
type: 'string'
}
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion api/src/paths/project/{projectId}/update.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { Feature } from 'geojson';
import { PROJECT_ROLE, SYSTEM_ROLE } from '../../../constants/roles';
import { getDBConnection } from '../../../database/db';
import { HTTP400 } from '../../../errors/http-error';
Expand Down Expand Up @@ -432,7 +433,7 @@ export interface IUpdateProject {
coordinator: object | null;
project: object | null;
objectives: object | null;
location: object | null;
location: { geometry: Feature[]; location_description: string } | null;
iucn: object | null;
funding: object | null;
partnerships: object | null;
Expand Down
18 changes: 4 additions & 14 deletions api/src/paths/spatial/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,13 @@ export function getRegions(): RequestHandler {

await connection.open();

for (const feature of features) {
const result = await bcgwLayerService.getRegionsForFeature(feature, connection);
regionsDetails = regionsDetails.concat(result);
}
regionsDetails = await bcgwLayerService.getUniqueRegionsForFeatures(features, connection);

await connection.commit();

// Convert array first into JSON, then into Set, then back to array in order to
// remove duplicate region information.
const regionDetailsJson = regionsDetails.map((value) => JSON.stringify(value));
const response = {
regions: Array.from(new Set<string>(regionDetailsJson)).map(
(value: string) => JSON.parse(value) as RegionDetails
)
};

return res.status(200).json(response);
return res.status(200).json({
regions: regionsDetails
});
} catch (error) {
defaultLog.error({ label: 'getRegions', message: 'error', error });
await connection.rollback();
Expand Down
7 changes: 6 additions & 1 deletion api/src/repositories/project-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ export class ProjectRepository extends BaseRepository {
p.start_date,
p.end_date,
p.coordinator_agency_name as coordinator_agency,
pt.name as project_type
pt.name as project_type,
array_remove(array_agg(DISTINCT rl.region_name), null) as regions
from
project as p
left outer join project_type as pt
Expand All @@ -204,6 +205,10 @@ export class ProjectRepository extends BaseRepository {
on s.project_id = p.project_id
left outer join study_species as sp
on sp.survey_id = s.survey_id
left join project_region pr
on p.project_id = pr.project_id
left join region_lookup rl
on pr.region_id = rl.region_id
where 1 = 1
`;

Expand Down
182 changes: 182 additions & 0 deletions api/src/repositories/region-repository.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import { QueryResult } from 'pg';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { ApiExecuteSQLError } from '../errors/api-error';
import { getMockDBConnection } from '../__mocks__/db';
import { RegionRepository } from './region-repository';

chai.use(sinonChai);

describe('RegionRepository', () => {
afterEach(() => {
sinon.restore();
});

describe('addRegionsToAProject', () => {
it('should return early when no regions passed in', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToProject(1, []);
expect(insertSQL).to.not.be.called;
});

it('should throw issue when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws('SQL FAILED');

try {
await repo.addRegionsToProject(1, [1]);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute insert SQL for project_region');
}
});

it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToProject(1, [1]);
expect(insertSQL).to.be.called;
});
});

describe('addRegionsToASurvey', () => {
it('should return early when no regions passed in', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToSurvey(1, []);
expect(insertSQL).to.not.be.called;
});

it('should throw issue when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws('SQL FAILED');

try {
await repo.addRegionsToSurvey(1, [1]);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute insert SQL for survey_region');
}
});

it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const insertSQL = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.addRegionsToSurvey(1, [1]);
expect(insertSQL).to.be.called;
});
});

describe('deleteRegionsForProject', () => {
it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const sqlStub = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.deleteRegionsForProject(1);
expect(sqlStub).to.be.called;
});

it('should throw an error when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws();

try {
await repo.deleteRegionsForProject(1);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute delete SQL for project_regions');
}
});
});

describe('deleteRegionsForSurvey', () => {
it('should run without issue', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
const sqlStub = sinon.stub(mockDBConnection, 'sql').returns(({} as unknown) as any);

await repo.deleteRegionsForSurvey(1);
expect(sqlStub).to.be.called;
});

it('should throw an error when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'sql').throws();

try {
await repo.deleteRegionsForSurvey(1);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute delete SQL for survey_regions');
}
});
});

describe('deleteRegionsFromASurvey', () => {
it('should return list of regions', async () => {
const mockDBConnection = getMockDBConnection({
knex: async () =>
(({
rowCount: 1,
rows: [
{
region_id: 1,
region_name: 'region name',
org_unit: '1',
org_unit_name: 'org unit name',
feature_code: '11_code',
feature_name: 'source_layer',
object_id: 1234,
geojson: '{}',
geography: '{}'
}
]
} as any) as Promise<QueryResult<any>>)
});
const repo = new RegionRepository(mockDBConnection);

const response = await repo.searchRegionsWithDetails([
{
regionName: 'regionName',
sourceLayer: 'source_layer'
}
]);
expect(response[0].region_name).to.be.eql('region name');
expect(response[0].feature_name).to.be.eql('source_layer');
});

it('should throw an error when SQL fails', async () => {
const mockDBConnection = getMockDBConnection();
const repo = new RegionRepository(mockDBConnection);
sinon.stub(mockDBConnection, 'knex').throws();

try {
await repo.searchRegionsWithDetails([
{
regionName: 'regionName',
sourceLayer: 'source_layer'
}
]);
expect.fail();
} catch (error) {
expect((error as ApiExecuteSQLError).message).to.be.eql('Failed to execute search region SQL');
}
});
});
});
Loading

0 comments on commit 28c4836

Please sign in to comment.