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