diff --git a/api/src/openapi/schemas/technique.ts b/api/src/openapi/schemas/technique.ts index fb0fd24dc3..6eee864bd6 100644 --- a/api/src/openapi/schemas/technique.ts +++ b/api/src/openapi/schemas/technique.ts @@ -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.' } + } + } + } + } +}; diff --git a/api/src/paths/codes.ts b/api/src/paths/codes.ts index 0f60bb0406..ebbf556be8 100644 --- a/api/src/paths/codes.ts +++ b/api/src/paths/codes.ts @@ -37,7 +37,8 @@ GET.apiDoc = { 'site_selection_strategies', 'survey_progress', 'method_response_metrics', - 'attractants' + 'attractants', + 'vantages' ], properties: { management_action_type: { @@ -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' + } + } + } } } } diff --git a/api/src/paths/reference/get/technique-attribute.ts b/api/src/paths/reference/get/technique-attribute.ts index 67119bf603..7a59eee333 100644 --- a/api/src/paths/reference/get/technique-attribute.ts +++ b/api/src/paths/reference/get/technique-attribute.ts @@ -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()]; diff --git a/api/src/paths/reference/get/vantage-mode.test.ts b/api/src/paths/reference/get/vantage-mode.test.ts new file mode 100644 index 0000000000..69e73adb96 --- /dev/null +++ b/api/src/paths/reference/get/vantage-mode.test.ts @@ -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'); + } + }); +}); diff --git a/api/src/paths/reference/get/vantage-mode.ts b/api/src/paths/reference/get/vantage-mode.ts new file mode 100644 index 0000000000..1214e239e6 --- /dev/null +++ b/api/src/paths/reference/get/vantage-mode.ts @@ -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(); + } + }; +} diff --git a/api/src/repositories/code-repository.ts b/api/src/repositories/code-repository.ts index 228bd6bdee..53e9df3c85 100644 --- a/api/src/repositories/code-repository.ts +++ b/api/src/repositories/code-repository.ts @@ -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(), @@ -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; @@ -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; + } } diff --git a/api/src/repositories/technique-attribute-repository.ts b/api/src/repositories/technique-attribute-repository.ts index 41a34a0ae7..bec5542959 100644 --- a/api/src/repositories/technique-attribute-repository.ts +++ b/api/src/repositories/technique-attribute-repository.ts @@ -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', @@ -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( @@ -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, @@ -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, diff --git a/api/src/repositories/vantage-mode-repository.test.ts b/api/src/repositories/vantage-mode-repository.test.ts new file mode 100644 index 0000000000..14f64babaf --- /dev/null +++ b/api/src/repositories/vantage-mode-repository.test.ts @@ -0,0 +1,60 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMockDBConnection } from '../__mocks__/db'; +import { VantageMode, VantageModeRepository } from './vantage-mode-repository'; // Adjust paths as necessary + +chai.use(sinonChai); + +describe('VantageModeRepository', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getVantageModesByMethodLookupIds', () => { + it('should successfully return vantage modes for provided method lookup ids', async () => { + const mockVantageMode: VantageMode = { + vantage_mode_id: 1, + vantage_id: 101, + name: 'Mode A', + description: 'Description for mode A' + }; + + const mockResponse = { + rows: [mockVantageMode], + rowCount: 1 + } as any as Promise>; + + const dbConnection = getMockDBConnection({ + knex: () => mockResponse + }); + + const repository = new VantageModeRepository(dbConnection); + const methodLookupIds = [1, 2, 3]; + + const response = await repository.getVantageModesByMethodLookupIds(methodLookupIds); + + expect(response).to.eql([mockVantageMode]); + }); + + it('should return an empty array if no vantage modes are found for provided method lookup ids', async () => { + const mockResponse = { + rows: [], + rowCount: 0 + } as any as Promise>; + + const dbConnection = getMockDBConnection({ + knex: () => mockResponse + }); + + const repository = new VantageModeRepository(dbConnection); + const methodLookupIds = [10, 20, 30]; + + const response = await repository.getVantageModesByMethodLookupIds(methodLookupIds); + + expect(response).to.eql([]); + }); + }); +}); diff --git a/api/src/repositories/vantage-mode-repository.ts b/api/src/repositories/vantage-mode-repository.ts new file mode 100644 index 0000000000..256fddd4c9 --- /dev/null +++ b/api/src/repositories/vantage-mode-repository.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; +import { getKnex } from '../database/db'; +import { getLogger } from '../utils/logger'; +import { BaseRepository } from './base-repository'; + +const defaultLog = getLogger('repositories/technique-vantage-repository'); + +const VantageMode = z.object({ + vantage_mode_id: z.number(), + vantage_id: z.number(), + name: z.string(), + description: z.string() +}); + +export type VantageMode = z.infer; + +export class VantageModeRepository extends BaseRepository { + /** + * Get vantage modes for a set of method lookup ids + * + * @param {number[]} methodLookupIds + * @return {*} {Promise} + * @memberof VantageModeRepository + */ + async getVantageModesByMethodLookupIds(methodLookupIds: number[]): Promise { + defaultLog.debug({ label: 'getVantageModesByMethodLookupIds', methodLookupIds }); + + const knex = getKnex(); + + const queryBuilder = knex + .select('vantage_mode_id', 'vantage_id', 'description') + .from('vantage_mode as vm') + .join('method_vantage_mode as mvm', 'mvm.vantage_mode_id', 'vm.vantage_mode_id') + .whereIn('mvm.method_lookup_id', methodLookupIds); + + const response = await this.connection.knex(queryBuilder, VantageMode); + + return response.rows; + } +} diff --git a/api/src/services/code-service.test.ts b/api/src/services/code-service.test.ts index 12237b08bc..d00351f3cc 100644 --- a/api/src/services/code-service.test.ts +++ b/api/src/services/code-service.test.ts @@ -46,7 +46,8 @@ describe('CodeService', () => { 'survey_progress', 'method_response_metrics', 'observation_subcount_signs', - 'alert_types' + 'alert_types', + 'vantages' ); }); }); diff --git a/api/src/services/code-service.ts b/api/src/services/code-service.ts index 9975056138..f8c828d741 100644 --- a/api/src/services/code-service.ts +++ b/api/src/services/code-service.ts @@ -45,7 +45,8 @@ export class CodeService extends DBService { method_response_metrics, attractants, observation_subcount_signs, - alert_types + alert_types, + vantages ] = await Promise.all([ await this.codeRepository.getManagementActionType(), await this.codeRepository.getFirstNations(), @@ -67,7 +68,8 @@ export class CodeService extends DBService { await this.codeRepository.getMethodResponseMetrics(), await this.codeRepository.getAttractants(), await this.codeRepository.getObservationSubcountSigns(), - await this.codeRepository.getAlertTypes() + await this.codeRepository.getAlertTypes(), + await this.codeRepository.getVantages() ]); return { @@ -91,7 +93,8 @@ export class CodeService extends DBService { method_response_metrics, attractants, observation_subcount_signs, - alert_types + alert_types, + vantages }; } diff --git a/api/src/services/technique-attributes-service.ts b/api/src/services/technique-attributes-service.ts index 961c77ef5b..4a66b76c81 100644 --- a/api/src/services/technique-attributes-service.ts +++ b/api/src/services/technique-attributes-service.ts @@ -207,17 +207,13 @@ export class TechniqueAttributeService extends DBService { // If the incoming data does have method_technique_attribute_quantitative_id, record is for update const attributesForUpdate = attributes.filter((attribute) => attribute.method_technique_attribute_quantitative_id); - const promises = []; - if (attributesForUpdate.length > 0) { - promises.push( + await Promise.all( attributesForUpdate.map((attribute) => this.techniqueAttributeRepository.updateQuantitativeAttributeForTechnique(methodTechniqueId, attribute) ) ); } - - await Promise.all(promises); } /** diff --git a/api/src/services/vantage-mode-service.test.ts b/api/src/services/vantage-mode-service.test.ts new file mode 100644 index 0000000000..c8ff4494c6 --- /dev/null +++ b/api/src/services/vantage-mode-service.test.ts @@ -0,0 +1,63 @@ +import chai, { expect } from 'chai'; +import { describe } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { ApiGeneralError } from '../errors/api-error'; +import { VantageModeRepository } from '../repositories/vantage-mode-repository'; +import { getMockDBConnection } from '../__mocks__/db'; +import { VantageModeService } from './vantage-mode-service'; + +chai.use(sinonChai); + +describe('VantageModeService', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getVantageModesByMethodLookupIds', () => { + it('should run successfully and return vantage modes for the provided method lookup ids', async () => { + const mockVantageMode = { + vantage_mode_id: 1, + vantage_id: 101, + name: 'Mode A', + description: 'Description for Mode A' + }; + + sinon.stub(VantageModeRepository.prototype, 'getVantageModesByMethodLookupIds').resolves([mockVantageMode]); + + const dbConnection = getMockDBConnection(); + const service = new VantageModeService(dbConnection); + + const methodLookupIds = [1, 2, 3]; + const response = await service.getVantageModesByMethodLookupIds(methodLookupIds); + + expect(response).to.eql([mockVantageMode]); + }); + + it('should return an empty array when no vantage modes are found for the provided method lookup ids', async () => { + sinon.stub(VantageModeRepository.prototype, 'getVantageModesByMethodLookupIds').resolves([]); + + const dbConnection = getMockDBConnection(); + const service = new VantageModeService(dbConnection); + + const methodLookupIds = [10, 20, 30]; + const response = await service.getVantageModesByMethodLookupIds(methodLookupIds); + + expect(response).to.eql([]); + }); + + it('should handle errors gracefully when repository method fails', async () => { + sinon.stub(VantageModeRepository.prototype, 'getVantageModesByMethodLookupIds').rejects(new Error('Query error')); + + const dbConnection = getMockDBConnection(); + const service = new VantageModeService(dbConnection); + + try { + await service.getVantageModesByMethodLookupIds([1, 2, 3]); + expect.fail('Expected method to throw an error'); + } catch (actualError) { + expect((actualError as ApiGeneralError).message).to.equal('Query error'); + } + }); + }); +}); diff --git a/api/src/services/vantage-mode-service.ts b/api/src/services/vantage-mode-service.ts new file mode 100644 index 0000000000..c94ef5af76 --- /dev/null +++ b/api/src/services/vantage-mode-service.ts @@ -0,0 +1,31 @@ +import { IDBConnection } from '../database/db'; +import { VantageMode, VantageModeRepository } from '../repositories/vantage-mode-repository'; +import { DBService } from './db-service'; + +/** + * Service layer for vantage mode related information + * + * @export + * @class VantageModeService + * @extends {DBService} + */ +export class VantageModeService extends DBService { + VantageModeRepository: VantageModeRepository; + + constructor(connection: IDBConnection) { + super(connection); + + this.VantageModeRepository = new VantageModeRepository(connection); + } + + /** + * Get vantage modes for a set of method lookup ids + * + * @param {number[]} methodLookupIds + * @return {*} {Promise} + * @memberof VantageModeService + */ + async getVantageModesByMethodLookupIds(methodLookupIds: number[]): Promise { + return this.VantageModeRepository.getVantageModesByMethodLookupIds(methodLookupIds); + } +} diff --git a/database/src/migrations/20241116000001_remove_duplicate_constraint.ts b/database/src/migrations/20241116000001_remove_duplicate_constraint.ts new file mode 100644 index 0000000000..7cad1ac3fd --- /dev/null +++ b/database/src/migrations/20241116000001_remove_duplicate_constraint.ts @@ -0,0 +1,29 @@ +import { Knex } from 'knex'; + +/** + * In the migration `20240722000000_method_technique` a duplicate constraint was created. + * + * This migration removes the duplicate constraint. + * + * Constraints: + * - method_technique_attribute_qualitative_fk4 + * - method_technique_attribute_qualitative_fk5 + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + ---------------------------------------------------------------------------------------- + -- DROP DUPLICATE CONSTRAINT + ---------------------------------------------------------------------------------------- + SET SEARCH_PATH=biohub; + + ALTER TABLE method_technique_attribute_qualitative DROP CONSTRAINT IF EXISTS method_technique_attribute_qualitative_fk5; + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +} diff --git a/database/src/migrations/20241116000002_update_technique_qualitative_tables.ts b/database/src/migrations/20241116000002_update_technique_qualitative_tables.ts new file mode 100644 index 0000000000..fdf117251a --- /dev/null +++ b/database/src/migrations/20241116000002_update_technique_qualitative_tables.ts @@ -0,0 +1,123 @@ +import { Knex } from 'knex'; + +/** + * UPDATES TO EXISTING CONCEPTS: + * + * - Adds a table for storing options for qualitative attributes of techniques, making the options reusable + * - (ie. avoid duplicate records for qualitative attributes with the same options) + * + * - eg. If camera trap and dip net both have a "material" attribute with "plastic" as an option, there should be one "plastic" record that gets reused. + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + ---------------------------------------------------------------------------------------- + -- DROP EXISTING VIEW + ---------------------------------------------------------------------------------------- + SET SEARCH_PATH=biohub_dapi_v1; + + -- Drop view to allow name and description column to be deleted + DROP VIEW IF EXISTS method_lookup_attribute_qualitative_option; + + ---------------------------------------------------------------------------------------- + -- CREATE NEW TABLE + ---------------------------------------------------------------------------------------- + SET SEARCH_PATH=biohub; + + CREATE TABLE technique_attribute_qualitative_option ( + technique_attribute_qualitative_option_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), + name varchar(50) NOT NULL, + description varchar(3000), + record_end_date date, + create_date timestamptz(6) DEFAULT now() NOT NULL, + create_user integer NOT NULL, + update_date timestamptz(6), + update_user integer, + revision_count integer DEFAULT 0 NOT NULL, + CONSTRAINT technique_attribute_qualitative_option_pk PRIMARY KEY (technique_attribute_qualitative_option_id) + ); + + COMMENT ON TABLE technique_attribute_qualitative_option IS 'Options to be selected for a technique_attribute_qualitative record, representing values for categorical attributes.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.technique_attribute_qualitative_option_id IS 'System generated surrogate primary key identifier.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.name IS 'The name of the record.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.description IS 'The description of the record.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.record_end_date IS 'Record level end date.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.create_date IS 'The datetime the record was created.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.create_user IS 'The id of the user who created the record as identified in the system user table.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.update_date IS 'The datetime the record was updated.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.update_user IS 'The id of the user who updated the record as identified in the system user table.'; + COMMENT ON COLUMN technique_attribute_qualitative_option.revision_count IS 'Revision count used for concurrency control.'; + + -- Add unique end-date key constraint (don't allow 2 records with the same name and a NULL record_end_date) + CREATE UNIQUE INDEX technique_attribute_qualitative_option_nuk1 ON technique_attribute_qualitative_option(name, (record_end_date is NULL)) where record_end_date is null; + + -- Add audit/journal triggers + CREATE TRIGGER audit_technique_attribute_qualitative_option BEFORE INSERT OR UPDATE OR DELETE ON technique_attribute_qualitative_option for each ROW EXECUTE PROCEDURE tr_audit_trigger(); + CREATE TRIGGER journal_technique_attribute_qualitative_option AFTER INSERT OR UPDATE OR DELETE ON technique_attribute_qualitative_option for each ROW EXECUTE PROCEDURE tr_journal_trigger(); + + ---------------------------------------------------------------------------------------- + -- ADD COLUMN TO EXISTING TABLE + ---------------------------------------------------------------------------------------- + + CREATE INDEX method_lookup_attribute_qualitative_option_idx2 ON method_lookup_attribute_qualitative_option(method_lookup_attribute_qualitative_option_id); + + -- Alter the qualitative options table to include a reference to the new table + + -- Add new column + ALTER TABLE method_lookup_attribute_qualitative_option ADD COLUMN technique_attribute_qualitative_option_id INTEGER; + + COMMENT ON COLUMN method_lookup_attribute_qualitative_option.technique_attribute_qualitative_option_id IS 'Foreign key to a technique attribute option.'; + + -- Add foreign key constraint + ALTER TABLE method_lookup_attribute_qualitative_option ADD CONSTRAINT method_lookup_attribute_qualitative_option_fk2 + FOREIGN KEY (technique_attribute_qualitative_option_id) + REFERENCES technique_attribute_qualitative_option(technique_attribute_qualitative_option_id); + + -- add indexes for foreign keys + CREATE INDEX method_lookup_attribute_qualitative_option_idx3 ON method_lookup_attribute_qualitative_option(technique_attribute_qualitative_option_id); + + ---------------------------------------------------------------------------------------- + -- MIGRATE EXISTING DATA + ---------------------------------------------------------------------------------------- + + -- Populate new table from existing options + WITH w_insert AS ( + INSERT INTO technique_attribute_qualitative_option (name, description) + SELECT name, description + FROM ( + SELECT name, description, + ROW_NUMBER() OVER (PARTITION BY name ORDER BY description) AS rn + FROM method_lookup_attribute_qualitative_option mla + ) subquery + WHERE rn = 1 + RETURNING name, description, technique_attribute_qualitative_option_id + ) + UPDATE method_lookup_attribute_qualitative_option + SET technique_attribute_qualitative_option_id = w_insert.technique_attribute_qualitative_option_id + FROM w_insert + WHERE method_lookup_attribute_qualitative_option.name = w_insert.name; + + -- Add not null constraints and drop name and description, which are replaced with the foreign key reference to technique_attribute_qualitative_option + ALTER TABLE method_lookup_attribute_qualitative_option ALTER COLUMN technique_attribute_qualitative_option_id SET NOT NULL; + + ALTER TABLE method_lookup_attribute_qualitative_option DROP COLUMN name; + ALTER TABLE method_lookup_attribute_qualitative_option DROP COLUMN description; + + ---------------------------------------------------------------------------------------- + -- ADD/UPDATE VIEWS + ---------------------------------------------------------------------------------------- + + SET SEARCH_PATH=biohub_dapi_v1; + + CREATE OR REPLACE VIEW technique_attribute_qualitative_option AS SELECT * FROM biohub.technique_attribute_qualitative_option; + CREATE OR REPLACE VIEW method_lookup_attribute_qualitative_option AS SELECT * FROM biohub.method_lookup_attribute_qualitative_option; + + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +} diff --git a/database/src/migrations/20241116000003_add_method_vantage_tables.ts b/database/src/migrations/20241116000003_add_method_vantage_tables.ts new file mode 100644 index 0000000000..fa21a84768 --- /dev/null +++ b/database/src/migrations/20241116000003_add_method_vantage_tables.ts @@ -0,0 +1,363 @@ +import { Knex } from 'knex'; + +/** + * NEW CONCEPT: Vantage + * + * - Adds tables for vantage and vantage modes + * - Adds a join table to assign vantage modes to method lookup options, setting which vantage modes can be used for a method lookup option + * - Adds a join table to assign vantage modes to techniques + * + * @export + * @param {Knex} knex + * @return {*} {Promise} + */ +export async function up(knex: Knex): Promise { + await knex.raw(`--sql + ---------------------------------------------------------------------------------------- + -- ADD NEW VANTAGE-RELATED TABLES + ---------------------------------------------------------------------------------------- + SET SEARCH_PATH=biohub; + + CREATE TABLE vantage ( + vantage_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), + name varchar(50) NOT NULL, + description varchar(1000), + record_end_date date, + create_date timestamptz(6) DEFAULT now() NOT NULL, + create_user integer NOT NULL, + update_date timestamptz(6), + update_user integer, + revision_count integer DEFAULT 0 NOT NULL, + CONSTRAINT vantage_pk PRIMARY KEY (vantage_id) + ); + + COMMENT ON TABLE vantage IS 'Vantages that vantage_mode records belong to, like categories of modes.'; + COMMENT ON COLUMN vantage.vantage_id IS 'System generated surrogate primary key identifier.'; + COMMENT ON COLUMN vantage.name IS 'The name of the record.'; + COMMENT ON COLUMN vantage.description IS 'The description of the record.'; + COMMENT ON COLUMN vantage.record_end_date IS 'Record level end date.'; + COMMENT ON COLUMN vantage.create_date IS 'The datetime the record was created.'; + COMMENT ON COLUMN vantage.create_user IS 'The id of the user who created the record as identified in the system user table.'; + COMMENT ON COLUMN vantage.update_date IS 'The datetime the record was updated.'; + COMMENT ON COLUMN vantage.update_user IS 'The id of the user who updated the record as identified in the system user table.'; + COMMENT ON COLUMN vantage.revision_count IS 'Revision count used for concurrency control.'; + + -- Triggers, indexes + + -- Add unique end-date key constraint (don't allow 2 records with the same name and a NULL record_end_date) + CREATE UNIQUE INDEX vantage_nuk1 ON vantage(name, (record_end_date is NULL)) where record_end_date is null; + + -- Add audit/journal triggers + CREATE TRIGGER audit_vantage BEFORE INSERT OR UPDATE OR DELETE ON vantage for each ROW EXECUTE PROCEDURE tr_audit_trigger(); + CREATE TRIGGER journal_vantage AFTER INSERT OR UPDATE OR DELETE ON vantage for each ROW EXECUTE PROCEDURE tr_journal_trigger(); + + ------- + + CREATE TABLE vantage_mode ( + vantage_mode_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), + vantage_id integer NOT NULL, + name varchar(50) NOT NULL, + description varchar(1000), + record_end_date date, + create_date timestamptz(6) DEFAULT now() NOT NULL, + create_user integer NOT NULL, + update_date timestamptz(6), + update_user integer, + revision_count integer DEFAULT 0 NOT NULL, + CONSTRAINT vantage_mode_pk PRIMARY KEY (vantage_mode_id) + ); + + COMMENT ON TABLE vantage_mode IS 'Vantage mode options that can be made available for method lookup options.'; + COMMENT ON COLUMN vantage_mode.vantage_mode_id IS 'System generated surrogate primary key identifier.'; + COMMENT ON COLUMN vantage_mode.vantage_id IS 'The vantage of the record.'; + COMMENT ON COLUMN vantage_mode.name IS 'The name of the record.'; + COMMENT ON COLUMN vantage_mode.description IS 'The description of the record.'; + COMMENT ON COLUMN vantage_mode.record_end_date IS 'Record level end date.'; + COMMENT ON COLUMN vantage_mode.create_date IS 'The datetime the record was created.'; + COMMENT ON COLUMN vantage_mode.create_user IS 'The id of the user who created the record as identified in the system user table.'; + COMMENT ON COLUMN vantage_mode.update_date IS 'The datetime the record was updated.'; + COMMENT ON COLUMN vantage_mode.update_user IS 'The id of the user who updated the record as identified in the system user table.'; + COMMENT ON COLUMN vantage_mode.revision_count IS 'Revision count used for concurrency control.'; + + -- Add unique end-date key constraint (don't allow 2 records with the same name and a NULL record_end_date) + CREATE UNIQUE INDEX vantage_mode_nuk1 ON vantage_mode(vantage_id, name, (record_end_date is NULL)) where record_end_date is null; + + -- Add indexes for foreign keys + ALTER TABLE vantage_mode ADD CONSTRAINT vantage_mode_fk1 + FOREIGN KEY (vantage_id) + REFERENCES vantage(vantage_id); + + -- Add foreign key index + CREATE INDEX vantage_mode_idx1 ON vantage_mode(vantage_id); + + -- Add audit/journal triggers + CREATE TRIGGER audit_vantage_mode BEFORE INSERT OR UPDATE OR DELETE ON vantage_mode for each ROW EXECUTE PROCEDURE tr_audit_trigger(); + CREATE TRIGGER journal_vantage_mode AFTER INSERT OR UPDATE OR DELETE ON vantage_mode for each ROW EXECUTE PROCEDURE tr_journal_trigger(); + + ------- + + CREATE TABLE vantage_mode_method ( + vantage_mode_method_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), + vantage_mode_id integer NOT NULL, + method_lookup_id integer NOT NULL, + description varchar(1000), + record_end_date date, + create_date timestamptz(6) DEFAULT now() NOT NULL, + create_user integer NOT NULL, + update_date timestamptz(6), + update_user integer, + revision_count integer DEFAULT 0 NOT NULL, + CONSTRAINT vantage_mode_method_pk PRIMARY KEY (vantage_mode_method_id) + ); + + COMMENT ON TABLE vantage_mode_method IS 'Join table indicating which vantage modes apply to which method lookup options.'; + COMMENT ON COLUMN vantage_mode_method.vantage_mode_method_id IS 'System generated surrogate primary key identifier.'; + COMMENT ON COLUMN vantage_mode_method.vantage_mode_id IS 'The vantage mode option of the record.'; + COMMENT ON COLUMN vantage_mode_method.method_lookup_id IS 'The method lookup option of the record.'; + COMMENT ON COLUMN vantage_mode_method.description IS 'The description of the record.'; + COMMENT ON COLUMN vantage_mode_method.record_end_date IS 'Record level end date.'; + COMMENT ON COLUMN vantage_mode_method.create_date IS 'The datetime the record was created.'; + COMMENT ON COLUMN vantage_mode_method.create_user IS 'The id of the user who created the record as identified in the system user table.'; + COMMENT ON COLUMN vantage_mode_method.update_date IS 'The datetime the record was updated.'; + COMMENT ON COLUMN vantage_mode_method.update_user IS 'The id of the user who updated the record as identified in the system user table.'; + COMMENT ON COLUMN vantage_mode_method.revision_count IS 'Revision count used for concurrency control.'; + + -- Add unique end-date key constraint (don't allow 2 records with the same vantage_mode_id, method_lookup_id and a NULL record_end_date) + CREATE UNIQUE INDEX vantage_mode_method_nuk1 ON vantage_mode_method (vantage_mode_id, method_lookup_id, (record_end_date is NULL)) where record_end_date is null; + + -- Add foreign key constraints + ALTER TABLE vantage_mode_method ADD CONSTRAINT vantage_mode_method_fk1 + FOREIGN KEY (method_lookup_id) + REFERENCES method_lookup (method_lookup_id); + + ALTER TABLE vantage_mode_method ADD CONSTRAINT vantage_mode_method_fk2 + FOREIGN KEY (vantage_mode_id) + REFERENCES vantage_mode (vantage_mode_id); + + -- Add indexes for foreign keys + CREATE INDEX vantage_mode_method_idx1 ON vantage_mode_method(method_lookup_id); + + CREATE INDEX vantage_mode_method_idx2 ON vantage_mode_method(vantage_mode_id); + + -- Add audit/journal triggers + CREATE TRIGGER audit_vantage_mode_method BEFORE INSERT OR UPDATE OR DELETE ON vantage_mode_method for each ROW EXECUTE PROCEDURE tr_audit_trigger(); + CREATE TRIGGER journal_vantage_mode_method AFTER INSERT OR UPDATE OR DELETE ON vantage_mode_method for each ROW EXECUTE PROCEDURE tr_journal_trigger(); + + ------- + + CREATE TABLE method_technique_vantage_mode ( + method_technique_vantage_mode_id integer GENERATED ALWAYS AS IDENTITY (START WITH 1 INCREMENT BY 1), + method_technique_id integer NOT NULL, + vantage_mode_method_id integer NOT NULL, + description varchar(1000), + record_end_date date, + create_date timestamptz(6) DEFAULT now() NOT NULL, + create_user integer NOT NULL, + update_date timestamptz(6), + update_user integer, + revision_count integer DEFAULT 0 NOT NULL, + CONSTRAINT method_technique_vantage_mode_pk PRIMARY KEY (method_technique_vantage_mode_id) + ); + + COMMENT ON TABLE method_technique_vantage_mode IS 'Join table applying vantage modes to techniques.'; + COMMENT ON COLUMN method_technique_vantage_mode.method_technique_vantage_mode_id IS 'System generated surrogate primary key identifier.'; + COMMENT ON COLUMN method_technique_vantage_mode.method_technique_id IS 'The method technique of the record.'; + COMMENT ON COLUMN method_technique_vantage_mode.vantage_mode_method_id IS 'The vantage mode of the record.'; + COMMENT ON COLUMN method_technique_vantage_mode.description IS 'The description of the record.'; + COMMENT ON COLUMN method_technique_vantage_mode.record_end_date IS 'Record level end date.'; + COMMENT ON COLUMN method_technique_vantage_mode.create_date IS 'The datetime the record was created.'; + COMMENT ON COLUMN method_technique_vantage_mode.create_user IS 'The id of the user who created the record as identified in the system user table.'; + COMMENT ON COLUMN method_technique_vantage_mode.update_date IS 'The datetime the record was updated.'; + COMMENT ON COLUMN method_technique_vantage_mode.update_user IS 'The id of the user who updated the record as identified in the system user table.'; + COMMENT ON COLUMN method_technique_vantage_mode.revision_count IS 'Revision count used for concurrency control.'; + + -- Add unique index + CREATE UNIQUE INDEX method_technique_vantage_mode_uk1 ON method_technique_vantage_mode (method_technique_id, vantage_mode_method_id); + + -- Add foreign key constraints + ALTER TABLE method_technique_vantage_mode ADD CONSTRAINT method_technique_vantage_mode_fk1 + FOREIGN KEY (method_technique_id) + REFERENCES method_technique (method_technique_id); + + ALTER TABLE method_technique_vantage_mode ADD CONSTRAINT method_technique_vantage_mode_fk2 + FOREIGN KEY (vantage_mode_method_id) + REFERENCES vantage_mode_method (vantage_mode_method_id); + + -- Add indexes for foreign keys + CREATE INDEX method_technique_vantage_mode_idx1 ON method_technique_vantage_mode(method_technique_id); + + CREATE INDEX method_technique_vantage_mode_idx2 ON method_technique_vantage_mode(vantage_mode_method_id); + + -- Add audit/journal triggers + CREATE TRIGGER audit_method_technique_vantage_mode BEFORE INSERT OR UPDATE OR DELETE ON method_technique_vantage_mode for each ROW EXECUTE PROCEDURE tr_audit_trigger(); + CREATE TRIGGER journal_method_technique_vantage_mode AFTER INSERT OR UPDATE OR DELETE ON method_technique_vantage_mode for each ROW EXECUTE PROCEDURE tr_journal_trigger(); + + ---------------------------------------------------------------------------------------- + -- POPULATE INITIAL VATAGE TABLE VALUES + ---------------------------------------------------------------------------------------- + + INSERT INTO + vantage (name, description) + VALUES + ('air', 'View from an aircraft or drone.'), + ('arboreal', 'View from the tree canopy.'), + ('water', 'View from a body of water.'), + ('benthic', 'View from the bottom of a waterbody.'), + ('ground', 'View from the ground.'); + + INSERT INTO + vantage_mode (vantage_id, name, description) + VALUES + -- Air Vantage Modes + ((SELECT vantage_id FROM vantage WHERE name = 'air'), 'helicopter', 'View from a helicopter.'), + ((SELECT vantage_id FROM vantage WHERE name = 'air'), 'plane', 'View from a plane.'), + ((SELECT vantage_id FROM vantage WHERE name = 'air'), 'drone', 'View from a drone.'), + + -- Arboreal Vantage Modes + ((SELECT vantage_id FROM vantage WHERE name = 'arboreal'), 'stationary fixture', 'View from a stationary fixture in the tree canopy.'), + ((SELECT vantage_id FROM vantage WHERE name = 'arboreal'), 'climbing', 'View from climbing in the tree canopy.'), + + -- Water Vantage Modes + ((SELECT vantage_id FROM vantage WHERE name = 'water'), 'stationary fixture', 'At a fixed position in or under the water.'), + ((SELECT vantage_id FROM vantage WHERE name = 'water'), 'boat', 'View from a boat or canoe.'), + ((SELECT vantage_id FROM vantage WHERE name = 'water'), 'kayak or canoe', 'View from a kayak or canoe.'), + ((SELECT vantage_id FROM vantage WHERE name = 'water'), 'submersible', 'View from an underwater submersible.'), + + -- Ground Vantage Modes + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'stationary fixture', 'At a fixed position on the ground.'), + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'foot', 'On foot.'), + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'on-road vehicle', 'In a truck, car, or similar on-road vehicle.'), + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'off-road vehicle', 'On a quad, dirtbike, or similar all-terrain vehicle.'), + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'horseback', 'On horseback.'), + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'snowmobile', 'On a snowmobile.'), + ((SELECT vantage_id FROM vantage WHERE name = 'ground'), 'bike', 'On a bicycle.'), + + -- Benthic Vantage Modes + ((SELECT vantage_id FROM vantage WHERE name = 'benthic'), 'stationary fixture', 'At a fixed position on the bottom of a waterbody.'), + ((SELECT vantage_id FROM vantage WHERE name = 'benthic'), 'submersible', 'View from a submersible on the bottom of a waterbody.'); + + INSERT INTO + vantage_mode_method (method_lookup_id, vantage_mode_id) + VALUES + -- Visual Encounter Method + -- Air Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'helicopter' AND v.name = 'air')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'plane' AND v.name = 'air')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'drone' AND v.name = 'air')), + + -- Arboreal Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'stationary fixture' AND v.name = 'arboreal')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'climbing' AND v.name = 'arboreal')), + + -- Water Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'boat' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'kayak or canoe' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'submersible' AND v.name = 'water')), + + -- Ground Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'foot' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'on-road vehicle' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'off-road vehicle' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'horseback' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'snowmobile' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'visual encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'bike' AND v.name = 'ground')), + + -- Audio Encounter Method + -- Air Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'helicopter' AND v.name = 'air')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'plane' AND v.name = 'air')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'drone' AND v.name = 'air')), + + -- Arboreal Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'stationary fixture' AND v.name = 'arboreal')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'climbing' AND v.name = 'arboreal')), + + -- Water Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'boat' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'kayak or canoe' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'submersible' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'stationary fixture' AND v.name = 'water')), + + -- Ground Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'foot' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'on-road vehicle' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'off-road vehicle' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'horseback' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'snowmobile' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'audio encounter'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'bike' AND v.name = 'ground')), + + -- Radar Method + -- Air Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'helicopter' AND v.name = 'air')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'plane' AND v.name = 'air')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'drone' AND v.name = 'air')), + + -- Ground Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'foot' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'on-road vehicle' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'off-road vehicle' AND v.name = 'ground')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'stationary fixture' AND v.name = 'ground')), + + -- Water Vantage Modes + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'boat' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'kayak or canoe' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'submersible' AND v.name = 'water')), + ((SELECT method_lookup_id FROM method_lookup WHERE LOWER(name) = 'radar'), + (SELECT vantage_mode_id FROM vantage_mode vm JOIN vantage v ON v.vantage_id = vm.vantage_id WHERE vm.name = 'stationary fixture' AND v.name = 'water')); + + ---------------------------------------------------------------------------------------- + -- ADD/UPDATE VIEWS + ---------------------------------------------------------------------------------------- + SET SEARCH_PATH=biohub_dapi_v1; + + CREATE OR REPLACE VIEW vantage AS SELECT * FROM biohub.vantage; + CREATE OR REPLACE VIEW vantage_mode AS SELECT * FROM biohub.vantage_mode; + CREATE OR REPLACE VIEW vantage_mode_method AS SELECT * FROM biohub.vantage_mode_method; + CREATE OR REPLACE VIEW method_technique AS SELECT * FROM biohub.method_technique; + CREATE OR REPLACE VIEW method_technique_vantage_mode AS SELECT * FROM biohub.method_technique_vantage_mode; + + `); +} + +export async function down(knex: Knex): Promise { + await knex.raw(``); +}