Skip to content

Commit

Permalink
SIMSBIOHUB-638: Add Vantage to Techniques (Backend) (#1423)
Browse files Browse the repository at this point in the history
* A new vantage tables
* Update technique qualitative tables, migrate existing data.
* Add migration to remove duplicate constraint from previous PR.

---------

Co-authored-by: Nick Phura <[email protected]>
  • Loading branch information
mauberti-bc and NickPhura authored Nov 26, 2024
1 parent 0a9a57f commit f0cb44e
Show file tree
Hide file tree
Showing 17 changed files with 983 additions and 29 deletions.
24 changes: 24 additions & 0 deletions api/src/openapi/schemas/technique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,27 @@ export const techniqueViewSchema: OpenAPIV3.SchemaObject = {
}
}
};

export const vantageModeSchema: OpenAPIV3.SchemaObject = {
type: 'object',
description: 'Vantage modes allowed for method lookup options that can be applied to a technique',
required: ['vantage_modes'],
additionalProperties: false,
properties: {
vantage_modes: {
type: 'array',
description: 'Possible vantage modes',
items: {
type: 'object',
required: ['vantage_mode_id', 'name', 'vantage_id', 'description'],
additionalProperties: false,
properties: {
vantage_mode_id: { type: 'string', description: 'The primary key of the vantage mode option.' },
name: { type: 'string', description: 'The name of the vantage mode option.' },
vantage_id: { type: 'string', description: 'The vantage of the mode.' },
description: { type: 'string', description: 'The description of the mode option.' }
}
}
}
}
};
24 changes: 23 additions & 1 deletion api/src/paths/codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ GET.apiDoc = {
'site_selection_strategies',
'survey_progress',
'method_response_metrics',
'attractants'
'attractants',
'vantages'
],
properties: {
management_action_type: {
Expand Down Expand Up @@ -403,6 +404,27 @@ GET.apiDoc = {
}
}
}
},
vantages: {
type: 'array',
description: 'Vantages that vantage modes belong to.',
items: {
type: 'object',
additionalProperties: false,
required: ['id', 'name', 'description'],
properties: {
id: {
type: 'integer',
minimum: 1
},
name: {
type: 'string'
},
description: {
type: 'string'
}
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/paths/reference/get/technique-attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getAPIUserDBConnection } from '../../../database/db';
import { TechniqueAttributeService } from '../../../services/technique-attributes-service';
import { getLogger } from '../../../utils/logger';

const defaultLog = getLogger('paths/reference');
const defaultLog = getLogger('paths/reference/get/technique-attribute');

export const GET: Operation = [getTechniqueAttributes()];

Expand Down
81 changes: 81 additions & 0 deletions api/src/paths/reference/get/vantage-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import * as db from '../../../database/db';
import { HTTPError } from '../../../errors/http-error';
import { VantageModeService } from '../../../services/vantage-mode-service';
import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db';
import { getVantageModes } from './vantage-mode';

chai.use(sinonChai);

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

it('should return vantage modes for method lookup ids', async () => {
const mockVantageModeResponse = [
{ vantage_mode_id: 1, vantage_id: 101, name: 'Mode A', description: 'Description for Mode A' },
{ vantage_mode_id: 2, vantage_id: 102, name: 'Mode B', description: 'Description for Mode B' }
];

const mockDBConnection = getMockDBConnection({
open: sinon.stub(),
commit: sinon.stub(),
release: sinon.stub()
});

sinon.stub(db, 'getDBConnection').returns(mockDBConnection);

const getVantageModesByMethodLookupIdsStub = sinon
.stub(VantageModeService.prototype, 'getVantageModesByMethodLookupIds')
.resolves(mockVantageModeResponse);

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
mockReq.query = { methodLookupId: ['1', '2'] };

const requestHandler = getVantageModes();

await requestHandler(mockReq, mockRes, mockNext);

expect(getVantageModesByMethodLookupIdsStub).to.have.been.calledOnceWith([1, 2]);
expect(mockRes.jsonValue).to.eql(mockVantageModeResponse);

expect(mockDBConnection.open).to.have.been.calledOnce;
expect(mockDBConnection.commit).to.have.been.calledOnce;
expect(mockDBConnection.release).to.have.been.calledOnce;
});

it('should catch and handle errors', async () => {
const mockDBConnection = getMockDBConnection({
open: sinon.stub(),
commit: sinon.stub(),
release: sinon.stub(),
rollback: sinon.stub()
});

sinon.stub(db, 'getDBConnection').returns(mockDBConnection);

const getVantageModesByMethodLookupIdsStub = sinon
.stub(VantageModeService.prototype, 'getVantageModesByMethodLookupIds')
.rejects(new Error('Test database error'));

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
mockReq.query = { methodLookupId: ['1', '2'] };

const requestHandler = getVantageModes();

try {
await requestHandler(mockReq, mockRes, mockNext);
expect.fail('Expected method to throw');
} catch (error) {
expect(mockDBConnection.open).to.have.been.calledOnce;
expect(mockDBConnection.rollback).to.have.been.calledOnce;
expect(mockDBConnection.release).to.have.been.calledOnce;
expect(getVantageModesByMethodLookupIdsStub).to.have.been.calledOnceWith([1, 2]);
expect((error as HTTPError).message).to.equal('Test database error');
}
});
});
85 changes: 85 additions & 0 deletions api/src/paths/reference/get/vantage-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { getAPIUserDBConnection } from '../../../database/db';
import { vantageModeSchema } from '../../../openapi/schemas/technique';
import { VantageModeService } from '../../../services/vantage-mode-service';
import { getLogger } from '../../../utils/logger';

const defaultLog = getLogger('paths/reference/get/vantage-mode');

export const GET: Operation = [getVantageModes()];

GET.apiDoc = {
description: 'Find vantage modes applicable to method lookup options',
tags: ['reference'],
parameters: [
{
in: 'query',
name: 'methodLookupId',
schema: {
type: 'array',
items: {
type: 'string'
},
minItems: 1
},
required: true
}
],
responses: {
200: {
description: 'Vantages for a method lookup id.',
content: {
'application/json': {
schema: vantageModeSchema
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/403'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

/**
* Get all vantage modes possible for multiple method lookup ids.
*
* @returns {RequestHandler}
*/
export function getVantageModes(): RequestHandler {
return async (req, res) => {
const connection = getAPIUserDBConnection();

try {
const methodLookupIds = (req.query.methodLookupId as string[]).map(Number);

await connection.open();

const vantageModeService = new VantageModeService(connection);

const response = await vantageModeService.getVantageModesByMethodLookupIds(methodLookupIds);

await connection.commit();

return res.status(200).json(response);
} catch (error) {
defaultLog.error({ label: 'getVantageModes', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
25 changes: 24 additions & 1 deletion api/src/repositories/code-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const MethodResponseMetricsCode = ICode.extend({ description: z.string() });
const AttractantCode = ICode.extend({ description: z.string() });
const ObservationSubcountSignCode = ICode.extend({ description: z.string() });
const AlertTypeCode = ICode.extend({ description: z.string() });
const VantageCode = ICode.extend({ description: z.string() });

export const IAllCodeSets = z.object({
management_action_type: CodeSet(),
Expand All @@ -48,7 +49,8 @@ export const IAllCodeSets = z.object({
method_response_metrics: CodeSet(MethodResponseMetricsCode.shape),
attractants: CodeSet(AttractantCode.shape),
observation_subcount_signs: CodeSet(ObservationSubcountSignCode.shape),
alert_types: CodeSet(AlertTypeCode.shape)
alert_types: CodeSet(AlertTypeCode.shape),
vantages: CodeSet(VantageCode.shape)
});
export type IAllCodeSets = z.infer<typeof IAllCodeSets>;

Expand Down Expand Up @@ -490,4 +492,25 @@ export class CodeRepository extends BaseRepository {

return response.rows;
}

/**
* Fetch vantages associated with vantage modes
*
* @return {*}
* @memberof CodeRepository
*/
async getVantages() {
const sqlStatement = SQL`
SELECT
vantage_id AS id,
name,
description
FROM vantage
WHERE record_end_date IS null;
`;

const response = await this.connection.sql(sqlStatement, VantageCode);

return response.rows;
}
}
44 changes: 27 additions & 17 deletions api/src/repositories/technique-attribute-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,20 @@ export class TechniqueAttributeRepository extends BaseRepository {
'method_lookup_attribute_qualitative_id',
knex.raw(`
json_agg(json_build_object(
'method_lookup_attribute_qualitative_option_id', method_lookup_attribute_qualitative_option_id,
'name', name,
'description', description
'method_lookup_attribute_qualitative_option_id', mlaqo.method_lookup_attribute_qualitative_option_id,
'name', taqo.name,
'description', taqo.description
)) as options
`)
)
.from('method_lookup_attribute_qualitative_option')
.where('record_end_date', null)
.groupBy('method_lookup_attribute_qualitative_id')
.from('method_lookup_attribute_qualitative_option as mlaqo')
.join(
'technique_attribute_qualitative_option as taqo',
'taqo.technique_attribute_qualitative_option_id',
'mlaqo.technique_attribute_qualitative_option_id'
)
.where('mlaqo.record_end_date', null)
.groupBy('mlaqo.method_lookup_attribute_qualitative_id')
)
.with(
'w_qualitative_attributes',
Expand Down Expand Up @@ -212,14 +217,19 @@ export class TechniqueAttributeRepository extends BaseRepository {
'method_lookup_attribute_qualitative_id',
knex.raw(`
json_agg(json_build_object(
'method_lookup_attribute_qualitative_option_id', method_lookup_attribute_qualitative_option_id,
'name', name,
'description', description
'method_lookup_attribute_qualitative_option_id', mlaqo.method_lookup_attribute_qualitative_option_id,
'name', taqo.name,
'description', taqo.description
)) as options
`)
)
.from('method_lookup_attribute_qualitative_option')
.where('record_end_date', null)
.from('method_lookup_attribute_qualitative_option as mlaqo')
.join(
'technique_attribute_qualitative_option as taqo',
'taqo.technique_attribute_qualitative_option_id',
'mlaqo.technique_attribute_qualitative_option_id'
)
.where('mlaqo.record_end_date', null)
.groupBy('method_lookup_attribute_qualitative_id')
)
.with(
Expand Down Expand Up @@ -476,13 +486,13 @@ export class TechniqueAttributeRepository extends BaseRepository {
defaultLog.debug({ label: 'deleteQualitativeAttributesForTechnique', methodTechniqueId });

const queryBuilder = getKnex()
.del()
.delete()
.from('method_technique_attribute_qualitative as mtaq')
.leftJoin('method_technique as mt', 'mt.method_technique_id', 'mtaq.method_technique_id')
.whereIn('method_technique_attribute_qualitative_id', methodTechniqueAttributeQualitativeIds)
.whereIn('mtaq.method_technique_attribute_qualitative_id', methodTechniqueAttributeQualitativeIds)
.andWhere('mtaq.method_technique_id', methodTechniqueId)
.andWhere('mt.survey_id', surveyId)
.returning('method_technique_attribute_qualitative.method_technique_attribute_qualitative_id');
.returning('mtaq.method_technique_attribute_qualitative_id');

const response = await this.connection.knex(
queryBuilder,
Expand Down Expand Up @@ -518,13 +528,13 @@ export class TechniqueAttributeRepository extends BaseRepository {
defaultLog.debug({ label: 'deleteQuantitativeAttributesForTechnique', methodTechniqueId });

const queryBuilder = getKnex()
.del()
.delete()
.from('method_technique_attribute_quantitative as mtaq')
.leftJoin('method_technique as mt', 'mt.method_technique_id', 'mtaq.method_technique_id')
.whereIn('method_technique_attribute_quantitative_id', methodTechniqueAttributeQuantitativeIds)
.whereIn('mtaq.method_technique_attribute_quantitative_id', methodTechniqueAttributeQuantitativeIds)
.andWhere('mtaq.method_technique_id', methodTechniqueId)
.andWhere('mt.survey_id', surveyId)
.returning('method_technique_attribute_quantitative.method_technique_attribute_quantitative_id');
.returning('mtaq.method_technique_attribute_quantitative_id');

const response = await this.connection.knex(
queryBuilder,
Expand Down
Loading

0 comments on commit f0cb44e

Please sign in to comment.