Skip to content

Commit

Permalink
#1894 create GET /jobs/:jobId/classifications/:value
Browse files Browse the repository at this point in the history
  • Loading branch information
grindarius committed May 2, 2024
1 parent fddbebd commit b9456bf
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 8 deletions.
15 changes: 14 additions & 1 deletion apps/api/src/_services/api-core/__mocks__/api-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { vi } from 'vitest'
import { type Classifier } from '@rfcx-bio/common/api-bio/classifiers/classifiers'
import { type WithTotalCount } from '@rfcx-bio/common/total-count'

import { type CoreClassifierJob, type CoreClassifierJobClassificationSummary, type CoreClassifierJobInformation, type CoreClassifierJobTotalDetections, type CoreDetection, type CoreDetectionsSummary } from '../types'
import { type CoreClassificationLite, type CoreClassifierJob, type CoreClassifierJobClassificationSummary, type CoreClassifierJobInformation, type CoreClassifierJobSummary, type CoreClassifierJobTotalDetections, type CoreDetection, type CoreDetectionsSummary } from '../types'

const randomCoreId = (): string => (Math.random() + 1).toString(36).substring(6)
const randomArbimonId = (): number => Math.floor(Math.random() * 99999)
Expand Down Expand Up @@ -218,3 +218,16 @@ export const getDetectionsSummary = vi.fn(async (): Promise<CoreDetectionsSummar
confirmed: 8
}
})

export const getClassifierJobSummaryByClassification = vi.fn(async (): Promise<CoreClassifierJobSummary & CoreClassificationLite> => {
return {
value: 'sciurus_carolinensis_simple_call_2',
title: 'Sciurus carolinensis, Simple call 2',
image: null,
total: 1732,
unreviewed: 1730,
uncertain: 2,
rejected: 0,
confirmed: 0
}
})
18 changes: 17 additions & 1 deletion apps/api/src/_services/api-core/api-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { isValidToken } from '~/api-helpers/is-valid-token'
import { ApiClient } from '../api-helpers/api-client'
import { unpackAxiosError } from '../api-helpers/axios-errors'
import { env } from '../env'
import { type CoreClassifierJob, type CoreClassifierJobClassificationSummary, type CoreClassifierJobInformation, type CoreClassifierJobTotalDetections, type CoreCreateClassifierJobBody, type CoreDetection, type CoreDetectionsSummary, type CoreGetClassifiersQueryParams, type CoreGetDetectionsQueryParams, type CoreGetDetectionsSummaryQueryParams, type CoreUpdateDetectionStatusBody, type CoreUpdateDetectionStatusParams, type DetectDetectionsQueryParamsCore, type DetectDetectionsResponseCore, type GetClassifierJobClassificationSummaryQueryParams } from './types'
import { type CoreClassificationLite, type CoreClassifierJob, type CoreClassifierJobClassificationSummary, type CoreClassifierJobInformation, type CoreClassifierJobSummary, type CoreClassifierJobTotalDetections, type CoreCreateClassifierJobBody, type CoreDetection, type CoreDetectionsSummary, type CoreGetClassifiersQueryParams, type CoreGetDetectionsQueryParams, type CoreGetDetectionsSummaryQueryParams, type CoreUpdateDetectionStatusBody, type CoreUpdateDetectionStatusParams, type DetectDetectionsQueryParamsCore, type DetectDetectionsResponseCore, type GetClassifierJobClassificationSummaryQueryParams } from './types'

const CORE_API_BASE_URL = env.CORE_API_BASE_URL

Expand Down Expand Up @@ -270,6 +270,22 @@ export async function getClassifierJobSummaries (token: string, jobId: number, p
}
}

export async function getClassifierJobSummaryByClassification (token: string, jobId: number, value: string): Promise<CoreClassifierJobSummary & CoreClassificationLite> {
try {
const resp = await axios.request({
method: 'GET',
url: `${CORE_API_BASE_URL}/classifier-jobs/${jobId}/summary/${value}`,
headers: {
authorization: token
}
})

return resp.data
} catch (e) {
return unpackAxiosError(e)
}
}

/**
* @deprecated because the bll from the route that calls this function will be deprecated
*/
Expand Down
14 changes: 8 additions & 6 deletions apps/api/src/_services/api-core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,14 @@ export interface GetClassifierJobClassificationSummaryQueryParams {
sort?: string
}

export interface CoreClassificationLite {
title: string
value: string
image: string | null
}

export interface CoreClassifierJobClassificationSummary {
classificationsSummary: Array<CoreClassifierJobSummary & { title: string, value: string, image: string | null }>
classificationsSummary: Array<CoreClassifierJobSummary & CoreClassificationLite>
}

export interface CoreClassifierJobTotalDetections {
Expand Down Expand Up @@ -121,11 +127,7 @@ export interface CoreDetection {
end: string
confidence: number
review_status: CoreRawReviewStatus
classification: {
title: string
value: string
image: string | null
}
classification: CoreClassificationLite
}

export interface CoreGetClassifiersQueryParams {
Expand Down
25 changes: 25 additions & 0 deletions apps/api/src/cnn/get-classifier-job-info-by-classification-bll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getClassifierJobInformation as coreGetClassifierJobInformation, getClassifierJobSummaryByClassification as coreGetClassifierJobSummaryByClassification } from '~/api-core/api-core'
import { BioInvalidPathParamError } from '~/errors'

export interface ClassifierJobByClassificationInformation {
title: string
total: number
streams: Array<{ id: string, name: string }>
}

export const getClassifierJobInfoByClassification = async (token: string, jobId: string, classificationValue: string): Promise<ClassifierJobByClassificationInformation> => {
if (jobId === undefined || jobId === '' || Number.isNaN(Number(jobId))) {
throw BioInvalidPathParamError({ jobId })
}

const [jobInfo, classificationInfo] = await Promise.all([
coreGetClassifierJobInformation(token, Number(jobId)),
coreGetClassifierJobSummaryByClassification(token, Number(jobId), classificationValue)
])

return {
title: classificationInfo.title,
total: classificationInfo.total,
streams: jobInfo.streams
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type GetClassifierJobInfoByClassificationParams, type GetClassifierJobInfoByClassificationResponse } from '@rfcx-bio/common/api-bio/cnn/classifier-job-classification'

import { type Handler } from '~/api-helpers/types'
import { getClassifierJobInfoByClassification } from './get-classifier-job-info-by-classification-bll'

export const getClassifierJobInfoByClassificationHandler: Handler<GetClassifierJobInfoByClassificationResponse, GetClassifierJobInfoByClassificationParams> = async (req) => {
const info = await getClassifierJobInfoByClassification(req.headers.authorization ?? '', req.params.jobId, req.params.classificationValue)
return info
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, expect, test, vi } from 'vitest'

import { makeApp } from '@rfcx-bio/testing/handlers'

import { routesCnn } from './index'

const jobId = 101
const classificationValue = 'sciurus_carolinensis_simple_call_2'

vi.mock('../_services/api-core/api-core')

describe('GET /jobs/:jobId/classifications/:classificationValue', async () => {
test('successfully get classification information by classification value', async () => {
// Arrange
const app = await makeApp(routesCnn, {
projectRole: 'user',
userToken: {
email: 'rubygem1934@rfcx.org'
}
})

// Act
const response = await app.inject({
method: 'GET',
url: `/jobs/${jobId}/classifications/${classificationValue}`
})

// Assert
expect(response.statusCode).toEqual(200)
const json = response.json()
expect(json).toHaveProperty('title', 'Sciurus carolinensis, Simple call 2')
expect(json).toHaveProperty('total', 1732)
expect(json).toHaveProperty('streams', [
{
id: 'kdivmdkfogie',
name: 'GU01'
}
])
})

test('non-rfcx member will get a 403', async () => {
// Arrange
const app = await makeApp(routesCnn, {
projectRole: 'user',
userToken: {
email: 'rubygem1218@gmail.com'
}
})

// Act
const response = await app.inject({
method: 'GET',
url: `/jobs/${jobId}/classifications/${classificationValue}`
})

// Assert
expect(response.statusCode).toEqual(403)
})

test('an invalid jobId (string) will result in 400', async () => {
// Arrange
const app = await makeApp(routesCnn, {
projectRole: 'user',
userToken: {
email: 'rubygeme@rfcx.org'
}
})

// Act
const response = await app.inject({
method: 'GET',
url: `/jobs/who/classifications/${classificationValue}`
})

console.info(response.body)

// Assert
expect(response.statusCode).toEqual(400)
})
})
8 changes: 8 additions & 0 deletions apps/api/src/cnn/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getClassifierJobInfoByClassificationRoute } from '@rfcx-bio/common/api-bio/cnn/classifier-job-classification'
import { getClassifierJobInformationRoute, updateClassifierJobRoute } from '@rfcx-bio/common/api-bio/cnn/classifier-job-information'
import { getClassifierJobSpeciesRoute } from '@rfcx-bio/common/api-bio/cnn/classifier-job-species'
import { getClassifierJobsRoute } from '@rfcx-bio/common/api-bio/cnn/classifier-jobs'
Expand All @@ -10,6 +11,7 @@ import { updateDetectionStatusRoute } from '@rfcx-bio/common/api-bio/cnn/reviews
import { requireRfcxEmail } from '@/_hooks/require-rfcx-user'
import { type RouteRegistration, GET, PATCH, POST } from '~/api-helpers/types'
import { createClassifierJobHandler } from './create-classifier-job-handler'
import { getClassifierJobInfoByClassificationHandler } from './get-classifier-job-info-by-classification-handler'
import { getClassifierJobInformationHandler } from './get-classifier-job-information-handler'
import { getClassifierJobSpeciesHandler } from './get-classifier-job-species-handler'
import { getClassifierJobsHandler } from './get-classifier-jobs-handler'
Expand Down Expand Up @@ -39,6 +41,12 @@ export const routesCnn: RouteRegistration[] = [
preHandler: [requireRfcxEmail],
handler: getClassifierJobInformationHandler
},
{
method: GET,
url: getClassifierJobInfoByClassificationRoute,
preHandler: [requireRfcxEmail],
handler: getClassifierJobInfoByClassificationHandler
},
{
method: GET,
url: getClassifierJobSpeciesRoute,
Expand Down
25 changes: 25 additions & 0 deletions packages/common/src/api-bio/cnn/classifier-job-classification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { type AxiosInstance } from 'axios'

import { type GetClassifierJobInformationResponse } from './classifier-job-information'

// Request types
export interface GetClassifierJobInfoByClassificationParams {
jobId: string
classificationValue: string
}

// Response types
export interface GetClassifierJobInfoByClassificationResponse {
title: string
total: number
streams: GetClassifierJobInformationResponse['streams']
}

// Route
export const getClassifierJobInfoByClassificationRoute = '/jobs/:jobId/classifications/:classificationValue'

// Service
export const apiBioGetClassifierJobInfoByClassification = async (apiClient: AxiosInstance, jobId: number, classificationValue: string): Promise<GetClassifierJobInfoByClassificationResponse> => {
const response = await apiClient.get(`/jobs/${jobId}/classifications/${classificationValue}`)
return response.data
}

0 comments on commit b9456bf

Please sign in to comment.