diff --git a/api/src/models/standards-view.ts b/api/src/models/standards-view.ts new file mode 100644 index 0000000000..b611855b15 --- /dev/null +++ b/api/src/models/standards-view.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { + CBQualitativeMeasurementTypeDefinition, + CBQuantitativeMeasurementTypeDefinition +} from '../services/critterbase-service'; + +const QualitativeMeasurementSchema = z.object({ + name: z.string(), + description: z.string().nullable(), + options: z.array( + z.object({ + name: z.string(), + description: z.string().nullable() + }) + ) +}); + +const QuantitativeMeasurementSchema = z.object({ + name: z.string(), + description: z.string().nullable(), + unit: z.string().nullable() +}); + +const MethodAttributesSchema = z.object({ + qualitative: z.array(QualitativeMeasurementSchema), + quantitative: z.array(QuantitativeMeasurementSchema) +}); + +export const EnvironmentStandardsSchema = z.object({ + qualitative: z.array(QualitativeMeasurementSchema), + quantitative: z.array(QuantitativeMeasurementSchema) +}); + +export type EnvironmentStandards = z.infer; + +export const MethodStandardSchema = z.object({ + method_lookup_id: z.number(), + name: z.string(), + description: z.string().nullable(), + attributes: MethodAttributesSchema +}); + +export type MethodStandard = z.infer; + +export interface ISpeciesStandards { + tsn: number; + scientificName: string; + measurements: { + quantitative: CBQuantitativeMeasurementTypeDefinition[]; + qualitative: CBQualitativeMeasurementTypeDefinition[]; + }; + markingBodyLocations: { id: string; key: string; value: string }[]; +} diff --git a/api/src/openapi/schemas/standards.ts b/api/src/openapi/schemas/standards.ts new file mode 100644 index 0000000000..6457fcdba7 --- /dev/null +++ b/api/src/openapi/schemas/standards.ts @@ -0,0 +1,137 @@ +import { OpenAPIV3 } from 'openapi-types'; + +export const EnvironmentStandardsSchema: OpenAPIV3.SchemaObject = { + type: 'object', + description: + 'Environment standards response object showing supported environmental variables and associated information', + additionalProperties: false, + properties: { + qualitative: { + type: 'array', + description: 'Array of qualitative environmental variables', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the environmental variable' + }, + description: { + type: 'string', + description: 'Description of the environmental variable', + nullable: true + }, + options: { + type: 'array', + description: 'Array of options for the qualitative variable', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Description of the environmental variable option' + }, + description: { + type: 'string', + description: 'Description of the environmental variable option', + nullable: true + } + } + } + } + } + } + }, + quantitative: { + type: 'array', + description: 'Array of quantitative environmental variables', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'Name of the quantitative environmental variable' + }, + description: { + type: 'string', + description: 'Description of the quantitative environmental variable', + nullable: true + }, + unit: { + type: 'string', + description: 'Unit of measurement of the quantitative environmental variable', + nullable: true + } + } + } + } + } +}; + +export const MethodStandardSchema: OpenAPIV3.SchemaObject = { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + method_lookup_id: { type: 'number' }, + name: { type: 'string' }, + description: { type: 'string', nullable: true }, + attributes: { + type: 'object', + additionalProperties: false, + properties: { + qualitative: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + description: { + type: 'string', + nullable: true + }, + options: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + description: { + type: 'string', + nullable: true + } + } + } + } + } + } + }, + quantitative: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: { + name: { + type: 'string' + }, + description: { + type: 'string', + nullable: true + }, + unit: { type: 'string', nullable: true } + } + } + } + } + } + } + } +}; diff --git a/api/src/paths/standards/environment/index.test.ts b/api/src/paths/standards/environment/index.test.ts new file mode 100644 index 0000000000..2c7a3c0568 --- /dev/null +++ b/api/src/paths/standards/environment/index.test.ts @@ -0,0 +1,89 @@ +import chai, { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/http-error'; +import { StandardsService } from '../../../services/standards-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; +import { getEnvironmentStandards } from './index'; // Adjust the import path based on your file structure + +chai.use(sinonChai); + +describe('standards/environment', () => { + describe('getEnvironmentStandards', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should retrieve environment standards successfully', async () => { + const mockResponse = { + quantitative: [ + { name: 'Quantitative Standard 1', description: 'Description 1', unit: 'Unit' }, + { name: 'Quantitative Standard 2', description: 'Description 2', unit: 'Unit' } + ], + qualitative: [ + { + name: 'Qualitative Standard 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative Standard 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + }; + + const mockDBConnection = getMockDBConnection(); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon.stub(StandardsService.prototype, 'getEnvironmentStandards').resolves(mockResponse); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + const requestHandler = getEnvironmentStandards(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.status).to.have.been.calledWith(200); + expect(mockRes.json).to.have.been.calledWith(mockResponse); + }); + + it('catches and re-throws error', async () => { + const mockDBConnection = getMockDBConnection({ + open: sinon.stub(), + commit: sinon.stub(), + rollback: sinon.stub(), + release: sinon.stub() + }); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon + .stub(StandardsService.prototype, 'getEnvironmentStandards') + .rejects(new Error('Failed to retrieve environment standards')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getEnvironmentStandards(); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(mockDBConnection.open).to.have.been.calledOnce; + expect(mockDBConnection.rollback).to.have.been.calledOnce; + expect(mockDBConnection.release).to.have.been.calledOnce; + expect((actualError as HTTPError).message).to.equal('Failed to retrieve environment standards'); + } + }); + }); +}); diff --git a/api/src/paths/standards/environment/index.ts b/api/src/paths/standards/environment/index.ts new file mode 100644 index 0000000000..42e2e24176 --- /dev/null +++ b/api/src/paths/standards/environment/index.ts @@ -0,0 +1,83 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection } from '../../../database/db'; +import { EnvironmentStandardsSchema } from '../../../openapi/schemas/standards'; +import { StandardsService } from '../../../services/standards-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/projects'); + +export const GET: Operation = [getEnvironmentStandards()]; + +GET.apiDoc = { + description: 'Gets lookup values for environment variables', + tags: ['standards'], + parameters: [ + { + in: 'query', + name: 'keyword', + required: false, + schema: { + type: 'string', + nullable: true + } + } + ], + security: [], + responses: { + 200: { + description: 'Environment data standards response object.', + content: { + 'application/json': { + schema: EnvironmentStandardsSchema + } + } + }, + 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 species data standards + * + * @returns {RequestHandler} + */ +export function getEnvironmentStandards(): RequestHandler { + return async (req, res) => { + const connection = getAPIUserDBConnection(); + + try { + await connection.open(); + + const standardsService = new StandardsService(connection); + + const keyword = (req.query.keyword as string) ?? ''; + + const response = await standardsService.getEnvironmentStandards(keyword); + + await connection.commit(); + + return res.status(200).json(response); + } catch (error) { + defaultLog.error({ label: 'getEnvironmentStandards', message: 'error', error }); + connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/standards/methods/index.test.ts b/api/src/paths/standards/methods/index.test.ts new file mode 100644 index 0000000000..3a5814a930 --- /dev/null +++ b/api/src/paths/standards/methods/index.test.ts @@ -0,0 +1,96 @@ +import chai, { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMethodStandards } from '.'; +import * as db from '../../../database/db'; +import { HTTPError } from '../../../errors/http-error'; +import { StandardsService } from '../../../services/standards-service'; +import { getMockDBConnection, getRequestHandlerMocks } from '../../../__mocks__/db'; + +chai.use(sinonChai); + +describe('standards/environment', () => { + describe('getMethodStandards', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should retrieve environment standards successfully', async () => { + const mockResponse = [ + { + method_lookup_id: 1, + name: 'Name', + description: 'Description', + attributes: { + quantitative: [ + { name: 'Quantitative Standard 1', description: 'Description 1', unit: 'Unit' }, + { name: 'Quantitative Standard 2', description: 'Description 2', unit: 'Unit' } + ], + qualitative: [ + { + name: 'Qualitative Standard 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative Standard 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + } + } + ]; + + const mockDBConnection = getMockDBConnection(); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon.stub(StandardsService.prototype, 'getMethodStandards').resolves(mockResponse); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + const requestHandler = getMethodStandards(); + + await requestHandler(mockReq, mockRes, mockNext); + + expect(mockRes.status).to.have.been.calledWith(200); + expect(mockRes.json).to.have.been.calledWith(mockResponse); + }); + + it('catches and re-throws error', async () => { + const mockDBConnection = getMockDBConnection({ + open: sinon.stub(), + commit: sinon.stub(), + rollback: sinon.stub(), + release: sinon.stub() + }); + + sinon.stub(db, 'getAPIUserDBConnection').returns(mockDBConnection); + + sinon + .stub(StandardsService.prototype, 'getMethodStandards') + .rejects(new Error('Failed to retrieve environment standards')); + + const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); + + try { + const requestHandler = getMethodStandards(); + await requestHandler(mockReq, mockRes, mockNext); + expect.fail(); + } catch (actualError) { + expect(mockDBConnection.open).to.have.been.calledOnce; + expect(mockDBConnection.rollback).to.have.been.calledOnce; + expect(mockDBConnection.release).to.have.been.calledOnce; + expect((actualError as HTTPError).message).to.equal('Failed to retrieve environment standards'); + } + }); + }); +}); diff --git a/api/src/paths/standards/methods/index.ts b/api/src/paths/standards/methods/index.ts new file mode 100644 index 0000000000..f95e4d6bca --- /dev/null +++ b/api/src/paths/standards/methods/index.ts @@ -0,0 +1,83 @@ +import { RequestHandler } from 'express'; +import { Operation } from 'express-openapi'; +import { getAPIUserDBConnection } from '../../../database/db'; +import { MethodStandardSchema } from '../../../openapi/schemas/standards'; +import { StandardsService } from '../../../services/standards-service'; +import { getLogger } from '../../../utils/logger'; + +const defaultLog = getLogger('paths/standards/methods'); + +export const GET: Operation = [getMethodStandards()]; + +GET.apiDoc = { + description: 'Gets lookup values for method variables', + tags: ['standards'], + parameters: [ + { + in: 'query', + name: 'keyword', + required: false, + schema: { + type: 'string', + nullable: true + } + } + ], + security: [], + responses: { + 200: { + description: 'Method data standards response object.', + content: { + 'application/json': { + schema: MethodStandardSchema + } + } + }, + 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 species data standards + * + * @returns {RequestHandler} + */ +export function getMethodStandards(): RequestHandler { + return async (req, res) => { + const connection = getAPIUserDBConnection(); + + try { + await connection.open(); + + const standardsService = new StandardsService(connection); + + const keyword = (req.query.keyword as string) ?? ''; + + const response = await standardsService.getMethodStandards(keyword); + + await connection.commit(); + + return res.status(200).json(response); + } catch (error) { + defaultLog.error({ label: 'getMethodStandards', message: 'error', error }); + connection.rollback(); + throw error; + } finally { + connection.release(); + } + }; +} diff --git a/api/src/paths/standards/taxon/{tsn}/index.ts b/api/src/paths/standards/taxon/{tsn}/index.ts index a46bb597a1..483233ae06 100644 --- a/api/src/paths/standards/taxon/{tsn}/index.ts +++ b/api/src/paths/standards/taxon/{tsn}/index.ts @@ -1,26 +1,12 @@ import { RequestHandler } from 'express'; import { Operation } from 'express-openapi'; -import { SYSTEM_ROLE } from '../../../../constants/roles'; import { getDBConnection } from '../../../../database/db'; -import { authorizeRequestHandler } from '../../../../request-handlers/security/authorization'; import { StandardsService } from '../../../../services/standards-service'; import { getLogger } from '../../../../utils/logger'; const defaultLog = getLogger('paths/projects'); -export const GET: Operation = [ - authorizeRequestHandler(() => { - return { - and: [ - { - validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.DATA_ADMINISTRATOR], - discriminator: 'SystemRole' - } - ] - }; - }), - getSpeciesStandards() -]; +export const GET: Operation = [getSpeciesStandards()]; GET.apiDoc = { description: 'Gets lookup values for a tsn to describe what information can be uploaded for a given species.', @@ -35,7 +21,7 @@ GET.apiDoc = { required: true } ], - security: [{ Bearer: [] }], + security: [], responses: { 200: { description: 'Species data standards response object.', @@ -199,7 +185,8 @@ GET.apiDoc = { */ export function getSpeciesStandards(): RequestHandler { return async (req, res) => { - // TODO: const connection = getAPIUserDBConnection(); + // API user DB connection does not work, possible because user does not exist in Critterbase? + // const connection = getAPIUserDBConnection(); const connection = getDBConnection(req.keycloak_token); try { @@ -216,6 +203,7 @@ export function getSpeciesStandards(): RequestHandler { return res.status(200).json(getSpeciesStandardsResponse); } catch (error) { defaultLog.error({ label: 'getSpeciesStandards', message: 'error', error }); + connection.rollback(); throw error; } finally { connection.release(); diff --git a/api/src/repositories/standards-repository.test.ts b/api/src/repositories/standards-repository.test.ts new file mode 100644 index 0000000000..c37c5a0b91 --- /dev/null +++ b/api/src/repositories/standards-repository.test.ts @@ -0,0 +1,110 @@ +import chai, { expect } from 'chai'; +import { afterEach, describe, it } from 'mocha'; +import { QueryResult } from 'pg'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import { getMockDBConnection } from '../__mocks__/db'; +import { StandardsRepository } from './standards-repository'; + +chai.use(sinonChai); + +describe('StandardsRepository', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('getEnvironmentStandards', () => { + it('should successfully retrieve environment standards', async () => { + const mockData = { + quantitative: [ + { name: 'Quantitative Standard 1', description: 'Description 1' }, + { name: 'Quantitative Standard 2', description: 'Description 2' } + ], + qualitative: [ + { + name: 'Qualitative Standard 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative Standard 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + }; + + const mockResponse = { + rows: [mockData], + rowCount: 1 + } as any as Promise>; + + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repository = new StandardsRepository(dbConnection); + + const result = await repository.getEnvironmentStandards(); + + expect(result).to.deep.equal(mockData); + }); + }); + + describe('getMethodStandards', () => { + it('should successfully retrieve method standards', async () => { + const mockData = [ + { + method_lookup_id: 1, + name: 'Method 1', + description: ' Description 1', + attributes: { + quantitative: [ + { name: 'Method Standard 1', description: 'Description 1', unit: 'Unit 1' }, + { name: 'Method Standard 2', description: 'Description 2', unit: 'Unit 2' } + ], + qualitative: [ + { + name: 'Qualitative 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + } + } + ]; + + const mockResponse = { + rows: mockData, + rowCount: 1 + } as any as Promise>; + + const dbConnection = getMockDBConnection({ + sql: () => mockResponse + }); + + const repository = new StandardsRepository(dbConnection); + + const result = await repository.getMethodStandards(); + + expect(result).to.deep.equal(mockData); + }); + }); +}); diff --git a/api/src/repositories/standards-repository.ts b/api/src/repositories/standards-repository.ts new file mode 100644 index 0000000000..6e7893df94 --- /dev/null +++ b/api/src/repositories/standards-repository.ts @@ -0,0 +1,145 @@ +import SQL from 'sql-template-strings'; +import { + EnvironmentStandards, + EnvironmentStandardsSchema, + MethodStandard, + MethodStandardSchema +} from '../models/standards-view'; +import { BaseRepository } from './base-repository'; + +/** + * Standards repository + * + * @export + * @class standardsRepository + * @extends {BaseRepository} + */ +export class StandardsRepository extends BaseRepository { + /** + * Gets environment standards + * + * @param {string} keyword - search term for filtering the response based on environmental variable name + * @return {*} + * @memberof StandardsRepository + */ + async getEnvironmentStandards(keyword?: string): Promise { + const sql = SQL` + WITH + quan AS ( + SELECT + eq.name AS quant_name, + eq.description AS quant_description, + eq.unit + FROM + environment_quantitative eq + WHERE + eq.name ILIKE '%' || ${keyword ?? ''} || '%' + ), + qual AS ( + SELECT + eq.name AS qual_name, + eq.description AS qual_description, + json_agg(json_build_object('name', eqo.name, 'description', eqo.description)) as options + FROM + environment_qualitative_option eqo + LEFT JOIN + environment_qualitative eq ON eqo.environment_qualitative_id = eq.environment_qualitative_id + WHERE + eq.name ILIKE '%' || ${keyword ?? ''} || '%' + GROUP BY + eq.name, + eq.description + ) + SELECT + (SELECT json_agg(json_build_object('name', quant_name, 'description', quant_description, 'unit', unit)) FROM quan) as quantitative, + (SELECT json_agg(json_build_object('name', qual_name, 'description', qual_description, 'options', options)) FROM qual) as qualitative; + `; + + const response = await this.connection.sql(sql, EnvironmentStandardsSchema); + + return response.rows[0]; + } + + /** + * Gets method standards + * + * @param {string} keyword - search term for filtering the response based on method lookup name + * @return {*} + * @memberof StandardsRepository + */ + async getMethodStandards(keyword?: string): Promise { + const sql = SQL` + WITH + quan AS ( + SELECT + mlaq.method_lookup_id, + tq.name AS quant_name, + tq.description AS quant_description, + mlaq.unit + FROM + method_lookup_attribute_quantitative mlaq + LEFT JOIN + technique_attribute_quantitative tq ON mlaq.technique_attribute_quantitative_id = tq.technique_attribute_quantitative_id + ), + qual AS ( + SELECT + mlaq.method_lookup_id, + taq.name AS qual_name, + taq.description AS qual_description, + COALESCE(json_agg( + json_build_object( + 'name', mlaqo.name, + 'description', mlaqo.description + ) ORDER BY mlaqo.name + ), '[]'::json) AS options + FROM + method_lookup_attribute_qualitative_option mlaqo + LEFT JOIN + method_lookup_attribute_qualitative mlaq ON mlaqo.method_lookup_attribute_qualitative_id = mlaq.method_lookup_attribute_qualitative_id + LEFT JOIN + technique_attribute_qualitative taq ON mlaq.technique_attribute_qualitative_id = taq.technique_attribute_qualitative_id + GROUP BY + mlaq.method_lookup_id, + taq.name, + taq.description + ), + method_lookup AS ( + SELECT + ml.method_lookup_id, + ml.name, + ml.description, + json_build_object( + 'quantitative', ( + SELECT COALESCE(json_agg( + json_build_object( + 'name', quan.quant_name, + 'description', quan.quant_description, + 'unit', quan.unit + ) ORDER BY quan.quant_name + ), '[]'::json) FROM quan + WHERE quan.method_lookup_id = ml.method_lookup_id + ), + 'qualitative', ( + SELECT COALESCE(json_agg( + json_build_object( + 'name', qual.qual_name, + 'description', qual.qual_description, + 'options', qual.options + ) ORDER BY qual.qual_name + ), '[]'::json) FROM qual + WHERE qual.method_lookup_id = ml.method_lookup_id + ) + ) AS attributes + FROM + method_lookup ml + WHERE + ml.name ILIKE '%' || ${keyword ?? ''} || '%' + ) + SELECT * FROM method_lookup; + `; + + const response = await this.connection.sql(sql, MethodStandardSchema); + + return response.rows; + } +} diff --git a/api/src/services/standards-service.test.ts b/api/src/services/standards-service.test.ts index 7bf7f5e487..60a6d4dae1 100644 --- a/api/src/services/standards-service.test.ts +++ b/api/src/services/standards-service.test.ts @@ -68,4 +68,73 @@ describe('StandardsService', () => { expect(response.measurements.qualitative[0].measurement_desc).to.eql('description'); }); }); + + describe('getEnvironmentStandards', async () => { + const mockData = { + qualitative: [{ name: 'name', description: 'name', options: [{ name: 'name', description: 'description' }] }], + quantitative: [ + { name: 'name', description: 'description', unit: 'unit' }, + { name: 'name', description: 'description', unit: 'unit' } + ] + }; + const mockDbConnection = getMockDBConnection(); + + const standardsService = new StandardsService(mockDbConnection); + + const getEnvironmentStandardsStub = sinon + .stub(standardsService.standardsRepository, 'getEnvironmentStandards') + .resolves(mockData); + + const response = await standardsService.getEnvironmentStandards(); + + expect(getEnvironmentStandardsStub).to.be.calledOnce; + expect(response).to.eql(mockData); + }); + + describe('getMethodStandards', async () => { + const mockData = [ + { + method_lookup_id: 1, + name: 'Method 1', + description: ' Description 1', + attributes: { + quantitative: [ + { name: 'Method Standard 1', description: 'Description 1', unit: 'Unit 1' }, + { name: 'Method Standard 2', description: 'Description 2', unit: 'Unit 2' } + ], + qualitative: [ + { + name: 'Qualitative 1', + description: 'Description 1', + options: [ + { name: 'Option 1', description: 'Option 1 Description' }, + { name: 'Option 2', description: 'Option 2 Description' } + ] + }, + { + name: 'Qualitative 2', + description: 'Description 2', + options: [ + { name: 'Option 3', description: 'Option 3 Description' }, + { name: 'Option 4', description: 'Option 4 Description' } + ] + } + ] + } + } + ]; + + const mockDbConnection = getMockDBConnection(); + + const standardsService = new StandardsService(mockDbConnection); + + const getMethodStandardsStub = sinon + .stub(standardsService.standardsRepository, 'getMethodStandards') + .resolves(mockData); + + const response = await standardsService.getMethodStandards(); + + expect(getMethodStandardsStub).to.be.calledOnce; + expect(response).to.eql(mockData); + }); }); diff --git a/api/src/services/standards-service.ts b/api/src/services/standards-service.ts index 76bea02f87..eed1217efb 100644 --- a/api/src/services/standards-service.ts +++ b/api/src/services/standards-service.ts @@ -1,32 +1,21 @@ import { IDBConnection } from '../database/db'; -import { - CBQualitativeMeasurementTypeDefinition, - CBQuantitativeMeasurementTypeDefinition, - CritterbaseService -} from './critterbase-service'; +import { EnvironmentStandards, ISpeciesStandards, MethodStandard } from '../models/standards-view'; +import { StandardsRepository } from '../repositories/standards-repository'; +import { CritterbaseService } from './critterbase-service'; import { DBService } from './db-service'; import { PlatformService } from './platform-service'; -export interface ISpeciesStandardsResponse { - tsn: number; - scientificName: string; - measurements: { - quantitative: CBQuantitativeMeasurementTypeDefinition[]; - qualitative: CBQualitativeMeasurementTypeDefinition[]; - }; - markingBodyLocations: { id: string; key: string; value: string }[]; -} - /** - * Sample Stratum Repository + * Standards Repository * * @export - * @class SampleStratumService + * @class StandardsService * @extends {DBService} */ export class StandardsService extends DBService { platformService: PlatformService; critterbaseService: CritterbaseService; + standardsRepository: StandardsRepository; constructor(connection: IDBConnection) { super(connection); @@ -35,16 +24,17 @@ export class StandardsService extends DBService { keycloak_guid: this.connection.systemUserGUID(), username: this.connection.systemUserIdentifier() }); + this.standardsRepository = new StandardsRepository(connection); } /** - * Gets all survey Sample Stratums. + * Gets species standards * - * @param {number} surveySampleSiteId - * @return {*} {Promise} + * @param {number} tsn + * @return {ISpeciesStandards} * @memberof standardsService */ - async getSpeciesStandards(tsn: number): Promise { + async getSpeciesStandards(tsn: number): Promise { // Fetch all measurement type definitions from Critterbase for the unique taxon_measurement_ids const response = await Promise.all([ this.platformService.getTaxonomyByTsns([tsn]), @@ -59,4 +49,29 @@ export class StandardsService extends DBService { measurements: response[2] }; } + + /** + * Gets environment standards + * + * @param {string} keyword - search term for filtering the response based on environemntal variable name + * @return {EnvironmentStandard[]} + * @memberof standardsService + */ + async getEnvironmentStandards(keyword?: string): Promise { + const response = await this.standardsRepository.getEnvironmentStandards(keyword); + + return response; + } + + /** + * Gets standards for method lookups + * + * @param {string} keyword - search term for filtering the response based on method lookup name + * @return {MethodStandards} + * @memberof standardsService + */ + async getMethodStandards(keyword?: string): Promise { + const response = await this.standardsRepository.getMethodStandards(keyword); + return response; + } } diff --git a/app/src/AppRouter.tsx b/app/src/AppRouter.tsx index 4728f20175..80bbebf558 100644 --- a/app/src/AppRouter.tsx +++ b/app/src/AppRouter.tsx @@ -6,7 +6,7 @@ import AdminUsersRouter from 'features/admin/AdminUsersRouter'; import FundingSourcesRouter from 'features/funding-sources/FundingSourcesRouter'; import ProjectsRouter from 'features/projects/ProjectsRouter'; import ResourcesPage from 'features/resources/ResourcesPage'; -import SpeciesStandardsPage from 'features/standards/SpeciesStandardsPage'; +import StandardsPage from 'features/standards/StandardsPage'; import SummaryRouter from 'features/summary/SummaryRouter'; import BaseLayout from 'layouts/BaseLayout'; import AccessDenied from 'pages/403/AccessDenied'; @@ -112,7 +112,7 @@ const AppRouter: React.FC = () => { - + diff --git a/app/src/components/layout/Header.tsx b/app/src/components/layout/Header.tsx index 3366c386fa..0e52a07142 100644 --- a/app/src/components/layout/Header.tsx +++ b/app/src/components/layout/Header.tsx @@ -277,11 +277,9 @@ const Header: React.FC = () => { Funding Sources - - - Standards - - + + Standards + Support @@ -354,11 +352,9 @@ const Header: React.FC = () => { Funding Sources - - - Standards - - + + Standards +