From e6a282e01d22c4bc1a014cfa92a9d060fda2f616 Mon Sep 17 00:00:00 2001 From: mateuszmiko <dmastah92@gmail.com> Date: Wed, 4 Oct 2023 14:30:32 +0200 Subject: [PATCH] [BLOCKED]Resolve MIN-60 "Feature/ connect search mirna query" --- pages/redux-api-poc.tsx | 2 + src/models/fixtures/mirnasFixture.ts | 10 +++ src/models/mirnaSchema.ts | 8 ++ src/queries/getMirnasStringWithQuery.test.ts | 10 +++ src/queries/getMirnasStringWithQuery.ts | 4 + src/redux/mirnas/mirnas.reducers.test.ts | 79 ++++++++++++++++++++ src/redux/mirnas/mirnas.reducers.ts | 17 +++++ src/redux/mirnas/mirnas.slice.ts | 20 +++++ src/redux/mirnas/mirnas.thunks.test.ts | 39 ++++++++++ src/redux/mirnas/mirnas.thunks.ts | 18 +++++ src/redux/mirnas/mirnas.types.ts | 4 + src/types/models.ts | 2 + 12 files changed, 213 insertions(+) create mode 100644 src/models/fixtures/mirnasFixture.ts create mode 100644 src/models/mirnaSchema.ts create mode 100644 src/queries/getMirnasStringWithQuery.test.ts create mode 100644 src/queries/getMirnasStringWithQuery.ts create mode 100644 src/redux/mirnas/mirnas.reducers.test.ts create mode 100644 src/redux/mirnas/mirnas.reducers.ts create mode 100644 src/redux/mirnas/mirnas.slice.ts create mode 100644 src/redux/mirnas/mirnas.thunks.test.ts create mode 100644 src/redux/mirnas/mirnas.thunks.ts create mode 100644 src/redux/mirnas/mirnas.types.ts diff --git a/pages/redux-api-poc.tsx b/pages/redux-api-poc.tsx index 7e51bc3b..2811cf8a 100644 --- a/pages/redux-api-poc.tsx +++ b/pages/redux-api-poc.tsx @@ -2,6 +2,7 @@ import { useSelector } from 'react-redux'; import { selectSearchValue } from '@/redux/search/search.selectors'; import { useAppDispatch } from '@/redux/hooks/useAppDispatch'; import { getDrugs } from '@/redux/drugs/drugs.thunks'; +import { getMirnas } from '@/redux/mirnas/mirnas.thunks'; const ReduxPage = (): JSX.Element => { const dispatch = useAppDispatch(); @@ -9,6 +10,7 @@ const ReduxPage = (): JSX.Element => { const triggerSyncUpdate = (): void => { dispatch(getDrugs('aspirin')); + dispatch(getMirnas('hsa-miR-302b-3p')); }; return ( diff --git a/src/models/fixtures/mirnasFixture.ts b/src/models/fixtures/mirnasFixture.ts new file mode 100644 index 00000000..d0c33263 --- /dev/null +++ b/src/models/fixtures/mirnasFixture.ts @@ -0,0 +1,10 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { createFixture } from 'zod-fixture'; +import { z } from 'zod'; +import { mirnaSchema } from '@/models/mirnaSchema'; +import { ZOD_SEED } from '@/constants/zodSeed'; + +export const mirnasFixture = createFixture(z.array(mirnaSchema), { + seed: ZOD_SEED, + array: { min: 2, max: 2 }, +}); diff --git a/src/models/mirnaSchema.ts b/src/models/mirnaSchema.ts new file mode 100644 index 00000000..ff4d0fd2 --- /dev/null +++ b/src/models/mirnaSchema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; +import { targetSchema } from './targetSchema'; + +export const mirnaSchema = z.object({ + id: z.string(), + name: z.string(), + targets: z.array(targetSchema), +}); diff --git a/src/queries/getMirnasStringWithQuery.test.ts b/src/queries/getMirnasStringWithQuery.test.ts new file mode 100644 index 00000000..a56ebeed --- /dev/null +++ b/src/queries/getMirnasStringWithQuery.test.ts @@ -0,0 +1,10 @@ +import { PROJECT_ID } from '@/constants/mapId'; +import { getMirnasStringWithQuery } from './getMirnasStringWithQuery'; + +describe('getMirnasStringWithQuery', () => { + it('should return url string', () => { + expect(getMirnasStringWithQuery('hsa-miR-302b-3p')).toBe( + `projects/${PROJECT_ID}/miRnas:search?query=hsa-miR-302b-3p`, + ); + }); +}); diff --git a/src/queries/getMirnasStringWithQuery.ts b/src/queries/getMirnasStringWithQuery.ts new file mode 100644 index 00000000..c5969da4 --- /dev/null +++ b/src/queries/getMirnasStringWithQuery.ts @@ -0,0 +1,4 @@ +import { PROJECT_ID } from '@/constants/mapId'; + +export const getMirnasStringWithQuery = (searchQuery: string): string => + `projects/${PROJECT_ID}/miRnas:search?query=${searchQuery}`; diff --git a/src/redux/mirnas/mirnas.reducers.test.ts b/src/redux/mirnas/mirnas.reducers.test.ts new file mode 100644 index 00000000..c29544db --- /dev/null +++ b/src/redux/mirnas/mirnas.reducers.test.ts @@ -0,0 +1,79 @@ +import { mirnasFixture } from '@/models/fixtures/mirnasFixture'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { getMirnasStringWithQuery } from '@/queries/getMirnasStringWithQuery'; +import { HttpStatusCode } from 'axios'; +import { getMirnas } from './mirnas.thunks'; +import mirnasReducer from './mirnas.slice'; +import { MirnasState } from './mirnas.types'; + +const mockedAxiosClient = mockNetworkResponse(); +const SEARCH_QUERY = 'aspirin'; + +const INITIAL_STATE: MirnasState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +describe('mirnas reducer', () => { + let store = {} as ToolkitStoreWithSingleSlice<MirnasState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('mirnas', mirnasReducer); + }); + + it('should match initial state', () => { + const action = { type: 'unknown' }; + + expect(mirnasReducer(undefined, action)).toEqual(INITIAL_STATE); + }); + it('should update store after succesfull getMirnas query', async () => { + mockedAxiosClient + .onGet(getMirnasStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, mirnasFixture); + + const { type } = await store.dispatch(getMirnas(SEARCH_QUERY)); + const { data, loading, error } = store.getState().mirnas; + + expect(type).toBe('project/getMirnas/fulfilled'); + expect(loading).toEqual('succeeded'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual(mirnasFixture); + }); + + it('should update store after failed getMirnas query', async () => { + mockedAxiosClient + .onGet(getMirnasStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.NotFound, []); + + const { type } = await store.dispatch(getMirnas(SEARCH_QUERY)); + const { data, loading, error } = store.getState().mirnas; + + expect(type).toBe('project/getMirnas/rejected'); + expect(loading).toEqual('failed'); + expect(error).toEqual({ message: '', name: '' }); + expect(data).toEqual([]); + }); + + it('should update store on loading getMirnas query', async () => { + mockedAxiosClient + .onGet(getMirnasStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, mirnasFixture); + + const mirnasPromise = store.dispatch(getMirnas(SEARCH_QUERY)); + + const { data, loading } = store.getState().mirnas; + expect(data).toEqual([]); + expect(loading).toEqual('pending'); + + mirnasPromise.then(() => { + const { data: dataPromiseFulfilled, loading: promiseFulfilled } = store.getState().mirnas; + + expect(dataPromiseFulfilled).toEqual(mirnasFixture); + expect(promiseFulfilled).toEqual('succeeded'); + }); + }); +}); diff --git a/src/redux/mirnas/mirnas.reducers.ts b/src/redux/mirnas/mirnas.reducers.ts new file mode 100644 index 00000000..577fbd97 --- /dev/null +++ b/src/redux/mirnas/mirnas.reducers.ts @@ -0,0 +1,17 @@ +import { ActionReducerMapBuilder } from '@reduxjs/toolkit'; +import { MirnasState } from './mirnas.types'; +import { getMirnas } from './mirnas.thunks'; + +export const getMirnasReducer = (builder: ActionReducerMapBuilder<MirnasState>): void => { + builder.addCase(getMirnas.pending, state => { + state.loading = 'pending'; + }); + builder.addCase(getMirnas.fulfilled, (state, action) => { + state.data = action.payload; + state.loading = 'succeeded'; + }); + builder.addCase(getMirnas.rejected, state => { + state.loading = 'failed'; + // TODO: error management to be discussed in the team + }); +}; diff --git a/src/redux/mirnas/mirnas.slice.ts b/src/redux/mirnas/mirnas.slice.ts new file mode 100644 index 00000000..c64a381c --- /dev/null +++ b/src/redux/mirnas/mirnas.slice.ts @@ -0,0 +1,20 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { MirnasState } from '@/redux/mirnas/mirnas.types'; +import { getMirnasReducer } from './mirnas.reducers'; + +const initialState: MirnasState = { + data: [], + loading: 'idle', + error: { name: '', message: '' }, +}; + +export const mirnasSlice = createSlice({ + name: 'mirnas', + initialState, + reducers: {}, + extraReducers: builder => { + getMirnasReducer(builder); + }, +}); + +export default mirnasSlice.reducer; diff --git a/src/redux/mirnas/mirnas.thunks.test.ts b/src/redux/mirnas/mirnas.thunks.test.ts new file mode 100644 index 00000000..6732a6ab --- /dev/null +++ b/src/redux/mirnas/mirnas.thunks.test.ts @@ -0,0 +1,39 @@ +import { mirnasFixture } from '@/models/fixtures/mirnasFixture'; +import { mockNetworkResponse } from '@/utils/mockNetworkResponse'; +import { + ToolkitStoreWithSingleSlice, + createStoreInstanceUsingSliceReducer, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { getMirnasStringWithQuery } from '@/queries/getMirnasStringWithQuery'; +import { HttpStatusCode } from 'axios'; +import { getMirnas } from './mirnas.thunks'; +import mirnasReducer from './mirnas.slice'; +import { MirnasState } from './mirnas.types'; + +const mockedAxiosClient = mockNetworkResponse(); +const SEARCH_QUERY = 'hsa-miR-302b-3p'; + +describe('mirnas thunks', () => { + let store = {} as ToolkitStoreWithSingleSlice<MirnasState>; + beforeEach(() => { + store = createStoreInstanceUsingSliceReducer('mirnas', mirnasReducer); + }); + describe('getMirnas', () => { + it('should return data when data response from API is valid', async () => { + mockedAxiosClient + .onGet(getMirnasStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, mirnasFixture); + + const { payload } = await store.dispatch(getMirnas(SEARCH_QUERY)); + expect(payload).toEqual(mirnasFixture); + }); + it('should return undefined when data response from API is not valid ', async () => { + mockedAxiosClient + .onGet(getMirnasStringWithQuery(SEARCH_QUERY)) + .reply(HttpStatusCode.Ok, { randomProperty: 'randomValue' }); + + const { payload } = await store.dispatch(getMirnas(SEARCH_QUERY)); + expect(payload).toEqual(undefined); + }); + }); +}); diff --git a/src/redux/mirnas/mirnas.thunks.ts b/src/redux/mirnas/mirnas.thunks.ts new file mode 100644 index 00000000..440fe7a6 --- /dev/null +++ b/src/redux/mirnas/mirnas.thunks.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { axiosInstance } from '@/services/api/utils/axiosInstance'; +import { Mirna } from '@/types/models'; +import { mirnaSchema } from '@/models/mirnaSchema'; +import { validateDataUsingZodSchema } from '@/utils/validateDataUsingZodSchema'; +import { getMirnasStringWithQuery } from '@/queries/getMirnasStringWithQuery'; + +export const getMirnas = createAsyncThunk( + 'project/getMirnas', + async (searchQuery: string): Promise<Mirna[] | undefined> => { + const response = await axiosInstance.get<Mirna[]>(getMirnasStringWithQuery(searchQuery)); + + const isDataValid = validateDataUsingZodSchema(response.data, z.array(mirnaSchema)); + + return isDataValid ? response.data : undefined; + }, +); diff --git a/src/redux/mirnas/mirnas.types.ts b/src/redux/mirnas/mirnas.types.ts new file mode 100644 index 00000000..8d2f66eb --- /dev/null +++ b/src/redux/mirnas/mirnas.types.ts @@ -0,0 +1,4 @@ +import { FetchDataState } from '@/types/fetchDataState'; +import { Mirna } from '@/types/models'; + +export type MirnasState = FetchDataState<Mirna[]>; diff --git a/src/types/models.ts b/src/types/models.ts index a7bc1e1e..12d62515 100644 --- a/src/types/models.ts +++ b/src/types/models.ts @@ -2,9 +2,11 @@ import { disease } from '@/models/disease'; import { drugSchema } from '@/models/drugSchema'; import { organism } from '@/models/organism'; import { projectSchema } from '@/models/project'; +import { mirnaSchema } from '@/models/mirnaSchema'; import { z } from 'zod'; export type Project = z.infer<typeof projectSchema>; export type Organism = z.infer<typeof organism>; export type Disease = z.infer<typeof disease>; export type Drug = z.infer<typeof drugSchema>; +export type Mirna = z.infer<typeof mirnaSchema>; -- GitLab