From a7a26c81729af30b30eb50eb3a939071b204f2ac Mon Sep 17 00:00:00 2001 From: mateuszmiko <dmastah92@gmail.com> Date: Tue, 10 Oct 2023 09:06:08 +0200 Subject: [PATCH] feat: add chemical query (MIN-59) --- pages/redux-api-poc.tsx | 2 + src/models/chemicalSchema.ts | 15 ++++ src/models/fixtures/chemicalsFixture.ts | 10 +++ src/models/idSchema.ts | 9 +++ src/redux/apiPath.test.ts | 6 ++ src/redux/apiPath.ts | 2 + .../chemicals/chemicals.reducers.test.ts | 79 +++++++++++++++++++ src/redux/chemicals/chemicals.reducers.ts | 17 ++++ src/redux/chemicals/chemicals.slice.ts | 20 +++++ src/redux/chemicals/chemicals.thunks.test.ts | 39 +++++++++ src/redux/chemicals/chemicals.thunks.ts | 20 +++++ src/redux/chemicals/chemicals.types.ts | 4 + src/types/models.ts | 6 +- 13 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 src/models/chemicalSchema.ts create mode 100644 src/models/fixtures/chemicalsFixture.ts create mode 100644 src/models/idSchema.ts create mode 100644 src/redux/chemicals/chemicals.reducers.test.ts create mode 100644 src/redux/chemicals/chemicals.reducers.ts create mode 100644 src/redux/chemicals/chemicals.slice.ts create mode 100644 src/redux/chemicals/chemicals.thunks.test.ts create mode 100644 src/redux/chemicals/chemicals.thunks.ts create mode 100644 src/redux/chemicals/chemicals.types.ts diff --git a/pages/redux-api-poc.tsx b/pages/redux-api-poc.tsx index 86f36932..f6add11e 100644 --- a/pages/redux-api-poc.tsx +++ b/pages/redux-api-poc.tsx @@ -1,4 +1,5 @@ import { getBioEntityContents } from '@/redux/bioEntityContents/bioEntityContents.thunks'; +import { getChemicals } from '@/redux/chemicals/chemicals.thunks'; import { getDrugs } from '@/redux/drugs/drugs.thunks'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { getMirnas } from '@/redux/mirnas/mirnas.thunks'; @@ -17,6 +18,7 @@ const ReduxPage = (): JSX.Element => { dispatch(getDrugs('aspirin')); dispatch(getMirnas('hsa-miR-302b-3p')); dispatch(getBioEntityContents('park7')); + dispatch(getChemicals('Corticosterone')); }; return ( diff --git a/src/models/chemicalSchema.ts b/src/models/chemicalSchema.ts new file mode 100644 index 00000000..eafa7c25 --- /dev/null +++ b/src/models/chemicalSchema.ts @@ -0,0 +1,15 @@ +import { idSchema } from '@/models/idSchema'; +import { z } from 'zod'; +import { referenceSchema } from './referenceSchema'; +import { targetSchema } from './targetSchema'; + +export const chemicalSchema = z.object({ + id: idSchema, + name: z.string(), + description: z.string(), + directEvidence: z.string().nullable(), + directEvidenceReferences: z.array(referenceSchema), + synonyms: z.array(z.string()), + references: z.array(referenceSchema), + targets: z.array(targetSchema), +}); diff --git a/src/models/fixtures/chemicalsFixture.ts b/src/models/fixtures/chemicalsFixture.ts new file mode 100644 index 00000000..88e21691 --- /dev/null +++ b/src/models/fixtures/chemicalsFixture.ts @@ -0,0 +1,10 @@ +import { ZOD_SEED } from '@/constants'; +import { chemicalSchema } from '@/models/chemicalSchema'; +import { z } from 'zod'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; + +export const chemicalsFixture = createFixture(z.array(chemicalSchema), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/idSchema.ts b/src/models/idSchema.ts new file mode 100644 index 00000000..2ccc4d78 --- /dev/null +++ b/src/models/idSchema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const idSchema = z.object({ + annotatorClassName: z.string(), + id: z.number(), + link: z.string(), + resource: z.string(), + type: z.string(), +}); diff --git a/src/redux/apiPath.test.ts b/src/redux/apiPath.test.ts index ca36caae..e1337f97 100644 --- a/src/redux/apiPath.test.ts +++ b/src/redux/apiPath.test.ts @@ -19,4 +19,10 @@ describe('api path', () => { `projects/${PROJECT_ID}/models/*/bioEntities:search?query=park7`, ); }); + + it('should return url string for bio entity content', () => { + expect(apiPath.getChemicalsStringWithQuery('Corticosterone')).toBe( + `projects/${PROJECT_ID}/chemicals:search?query=Corticosterone`, + ); + }); }); diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index d199de1e..8469a3d1 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -7,4 +7,6 @@ export const apiPath = { `projects/${PROJECT_ID}/drugs:search?query=${searchQuery}`, getMirnasStringWithQuery: (searchQuery: string): string => `projects/${PROJECT_ID}/miRnas:search?query=${searchQuery}`, + getChemicalsStringWithQuery: (searchQuery: string): string => + `projects/${PROJECT_ID}/chemicals:search?query=${searchQuery}`, }; diff --git a/src/redux/chemicals/chemicals.reducers.test.ts b/src/redux/chemicals/chemicals.reducers.test.ts new file mode 100644 index 00000000..e74d8171 --- /dev/null +++ b/src/redux/chemicals/chemicals.reducers.test.ts @@ -0,0 +1,79 @@ +import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; +import { apiPath } from '@/redux/apiPath'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import chemicalsReducer from './chemicals.slice'; +import { getChemicals } from './chemicals.thunks'; +import { ChemicalsState } from './chemicals.types'; + +const mockedAxiosClient = mockNetworkResponse(); +const SEARCH_QUERY = 'Corticosterone'; + +const INITIAL_STATE: ChemicalsState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +describe('chemicals reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<ChemicalsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('chemicals', chemicalsReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(chemicalsReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + it('should update store after succesfull getChemicals query', async () => { + mockedAxiosClient + .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, chemicalsFixture); + + const { type } = await store.dispatch(getChemicals(SEARCH_QUERY)); + const { data, loading, error } = store.getState().chemicals; + + expect(type).toBe('project/getChemicals/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(chemicalsFixture); + }); + + it('should update store after failed getChemicals query', async () => { + mockedAxiosClient + .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.NotFound, chemicalsFixture); + + const { type } = await store.dispatch(getChemicals(SEARCH_QUERY)); + const { data, loading, error } = store.getState().chemicals; + + expect(type).toBe('project/getChemicals/rejected'); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + + it('should update store on loading getChemicals query', async () => { + mockedAxiosClient + .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, chemicalsFixture); + + const chemicalsPromise = store.dispatch(getChemicals(SEARCH_QUERY)); + + const { data, loading } = store.getState().chemicals; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + chemicalsPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().chemicals; + + expect(dataPromiseFulfilled).toEqual(chemicalsFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/chemicals/chemicals.reducers.ts b/src/redux/chemicals/chemicals.reducers.ts new file mode 100644 index 00000000..4ca1b96f --- /dev/null +++ b/src/redux/chemicals/chemicals.reducers.ts @@ -0,0 +1,17 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { getChemicals } from './chemicals.thunks'; +import { ChemicalsState } from './chemicals.types'; + +export const getChemicalsReducer = (builder: ActionReducerMapBuilder<ChemicalsState>): void => { + builder.addCase(getChemicals.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getChemicals.fulfilled, (state, action) => { + state.data = action.payload; + state.loading = 'succeeded'; + }); + builder.addCase(getChemicals.rejected, state => { + state.loading = 'failed'; + // TODO to discuss manage state of failure + }); +}; diff --git a/src/redux/chemicals/chemicals.slice.ts b/src/redux/chemicals/chemicals.slice.ts new file mode 100644 index 00000000..a8dd8e59 --- /dev/null +++ b/src/redux/chemicals/chemicals.slice.ts @@ -0,0 +1,20 @@ +import { ChemicalsState } from '@/redux/chemicals/chemicals.types'; +import { createSlice } from '@reduxjs/toolkit'; +import { getChemicalsReducer } from './chemicals.reducers'; + +const initialState: ChemicalsState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +export const chemicalsSlice = createSlice({ + name: 'chemicals', + initialState, + reducers: {}, + extraReducers: builder => { + getChemicalsReducer(builder); + }, +}); + +export default chemicalsSlice.reducer; diff --git a/src/redux/chemicals/chemicals.thunks.test.ts b/src/redux/chemicals/chemicals.thunks.test.ts new file mode 100644 index 00000000..93945477 --- /dev/null +++ b/src/redux/chemicals/chemicals.thunks.test.ts @@ -0,0 +1,39 @@ +import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture'; +import { apiPath } from '@/redux/apiPath'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { HttpStatusCode } from 'axios'; +import chemicalsReducer from './chemicals.slice'; +import { getChemicals } from './chemicals.thunks'; +import { ChemicalsState } from './chemicals.types'; + +const mockedAxiosClient = mockNetworkResponse(); +const SEARCH_QUERY = 'Corticosterone'; + +describe('chemicals thunks', () => { + let store = {} as ToolkitStoreWithSingleSlice<ChemicalsState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('chemicals', chemicalsReducer); + }); + describe('getChemiclas', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient + .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, chemicalsFixture); + + const { payload } = await store.dispatch(getChemicals(SEARCH_QUERY)); + expect(payload).toEqual(chemicalsFixture); + }); + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet(apiPath.getChemicalsStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch(getChemicals(SEARCH_QUERY)); + expect(payload).toEqual(undefined); + }); + }); +}); diff --git a/src/redux/chemicals/chemicals.thunks.ts b/src/redux/chemicals/chemicals.thunks.ts new file mode 100644 index 00000000..df0f9dec --- /dev/null +++ b/src/redux/chemicals/chemicals.thunks.ts @@ -0,0 +1,20 @@ +import { chemicalSchema } from '@/models/chemicalSchema'; +import { apiPath } from '@/redux/apiPath'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { Chemical } from '@/types/models'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { z } from 'zod'; + +export const getChemicals = createAsyncThunk( + 'project/getChemicals', + async (searchQuery: string): Promise<Chemical[] | undefined> => { + const response = await axiosInstance.get<Chemical[]>( + apiPath.getChemicalsStringWithQuery(searchQuery), + ); + + const isDataValid = validateDataUsingZodSchema(response.data, z.array(chemicalSchema)); + + return isDataValid ? response.data : undefined; + }, +); diff --git a/src/redux/chemicals/chemicals.types.ts b/src/redux/chemicals/chemicals.types.ts new file mode 100644 index 00000000..653e910a --- /dev/null +++ b/src/redux/chemicals/chemicals.types.ts @@ -0,0 +1,4 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { Chemical } from '@/types/models'; + +export type ChemicalsState = FetchDataState<Chemical[]>; diff --git a/src/types/models.ts b/src/types/models.ts index 52aca9ac..abadf02a 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -1,9 +1,10 @@ +import { bioEntityContentSchema } from '@/models/bioEntityContentSchema'; +import { chemicalSchema } from '@/models/chemicalSchema'; import { disease } from '@/models/disease'; import { drugSchema } from '@/models/drugSchema'; +import { mirnaSchema } from '@/models/mirnaSchema'; import { organism } from '@/models/organism'; import { projectSchema } from '@/models/project'; -import { mirnaSchema } from '@/models/mirnaSchema'; -import { bioEntityContentSchema } from '@/models/bioEntityContentSchema'; import { z } from 'zod'; export type Project = z.infer<typeof projectSchema>; @@ -12,3 +13,4 @@ export type Disease = z.infer<typeof disease>; export type Drug = z.infer<typeof drugSchema>; export type Mirna = z.infer<typeof mirnaSchema>; export type BioEntityContent = z.infer<typeof bioEntityContentSchema>; +export type Chemical = z.infer<typeof chemicalSchema>; -- GitLab