From 7a84d40f72c600cdae894a69f45b2a4976c52810 Mon Sep 17 00:00:00 2001 From: Swain Molster Date: Tue, 5 Dec 2023 13:11:33 -0500 Subject: [PATCH] fix!: migrate useMe to be powered by useRestQuery --- UPGRADING.md | 6 +++++ src/hooks/useMe.test.tsx | 45 +++++++++++++++++++------------------ src/hooks/useMe.ts | 45 ++++++++++++++----------------------- src/types/fhir-api-types.ts | 20 ++++++++++++++++- 4 files changed, 65 insertions(+), 51 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index 8736ed06..7bbeae17 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -292,3 +292,9 @@ const MyComponent = () => { - `usePendingInvite` no longer returns the "last accepted id". - The `react-query` cache is now cleared when an invite is accepted. + +### 10.x -> 11.x + +- The `react-query` query key for the `useMe()` data changed. The hook is now + powered by `useRestQuery`. If needing to interact with the cached data, use + the `useRestCache` hook. diff --git a/src/hooks/useMe.test.tsx b/src/hooks/useMe.test.tsx index 09015402..197730f7 100644 --- a/src/hooks/useMe.test.tsx +++ b/src/hooks/useMe.test.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { renderHook, waitFor } from '@testing-library/react-native'; import { useMe } from './useMe'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import MockAdapter from 'axios-mock-adapter'; -import axios from 'axios'; -import { useHttpClient } from './useHttpClient'; +import { HttpClientContextProvider } from './useHttpClient'; +import { ActiveAccountProvider } from './useActiveAccount'; +import { createRestAPIMock } from '../test-utils/rest-api-mocking'; +import { Patient } from 'fhir/r3'; const queryClient = new QueryClient({ defaultOptions: { @@ -14,29 +15,23 @@ const queryClient = new QueryClient({ }, }); -jest.mock('./useHttpClient', () => ({ - useHttpClient: jest.fn(), -})); - -const useHttpClientMock = useHttpClient as jest.Mock; +const api = createRestAPIMock(); const renderHookInContext = async () => { return renderHook(() => useMe(), { wrapper: ({ children }) => ( - {children} + + + {children} + + ), }); }; -const axiosInstance = axios.create(); -const axiosMock = new MockAdapter(axiosInstance); - -beforeEach(() => { - useHttpClientMock.mockReturnValue({ httpClient: axiosInstance }); -}); - test('fetches and parses $me', async () => { - const subject1 = { + const subject1: Patient & { id: string } = { + resourceType: 'Patient', id: 'patientId1', meta: { tag: [ @@ -47,7 +42,8 @@ test('fetches and parses $me', async () => { ], }, }; - const subject2 = { + const subject2: Patient & { id: string } = { + resourceType: 'Patient', id: 'patientId2', meta: { tag: [ @@ -58,12 +54,17 @@ test('fetches and parses $me', async () => { ], }, }; - axiosMock.onGet('/v1/fhir/dstu3/$me').reply(200, { - entry: [{ resource: subject1 }, { resource: subject2 }], + + api.mock('GET /v1/fhir/dstu3/$me', { + status: 200, + data: { + resourceType: 'Bundle', + type: 'searchset', + entry: [{ resource: subject1 }, { resource: subject2 }], + }, }); + const { result } = await renderHookInContext(); - await waitFor(() => result.current.isSuccess); - expect(axiosMock.history.get[0].url).toBe('/v1/fhir/dstu3/$me'); await waitFor(() => { expect(result.current.data).toEqual([ { subjectId: 'patientId1', projectId: 'projectId1', subject: subject1 }, diff --git a/src/hooks/useMe.ts b/src/hooks/useMe.ts index ca4b8195..17b7cb4c 100644 --- a/src/hooks/useMe.ts +++ b/src/hooks/useMe.ts @@ -1,6 +1,5 @@ -import { useQuery } from '@tanstack/react-query'; -import { useHttpClient } from './useHttpClient'; import { Patient } from 'fhir/r3'; +import { useRestQuery } from './rest-api'; export interface Subject { subjectId: string; @@ -9,33 +8,23 @@ export interface Subject { subject: Patient; } -interface Entry { - resource: Patient; -} - -interface MeResponse { - resourceType: 'Bundle'; - entry: Entry[]; -} - export function useMe() { - const { httpClient } = useHttpClient(); + return useRestQuery( + 'GET /v1/fhir/dstu3/$me', + {}, + { + select: (data) => { + const subjects: Subject[] = data.entry.map((entry) => ({ + subjectId: entry.resource.id, + projectId: entry.resource.meta?.tag?.find( + (t) => t.system === 'http://lifeomic.com/fhir/dataset', + )?.code!, + name: entry.resource.name, + subject: entry.resource, + })); - const useMeQuery = useQuery(['fhir/dstu3/$me'], () => - httpClient.get('/v1/fhir/dstu3/$me').then((res) => - res.data.entry?.map( - (entry) => - ({ - subjectId: entry.resource.id, - projectId: entry.resource.meta?.tag?.find( - (t) => t.system === 'http://lifeomic.com/fhir/dataset', - )?.code, - name: entry.resource.name, - subject: entry.resource, - } as Subject), - ), - ), + return subjects; + }, + }, ); - - return useMeQuery; } diff --git a/src/types/fhir-api-types.ts b/src/types/fhir-api-types.ts index 1e76040b..563f26aa 100644 --- a/src/types/fhir-api-types.ts +++ b/src/types/fhir-api-types.ts @@ -1,6 +1,7 @@ import { Appointment, Bundle, + BundleEntry, Observation, Patient, Practitioner, @@ -8,6 +9,18 @@ import { QuestionnaireResponse, } from 'fhir/r3'; +// These "better" types strongly type the bundle +// to match the LifeOmic FHIR API guarantees. +type BetterEntry = Omit, 'resource'> & { + // `resource` is guaranteed to be defined by the API + resource: T; +}; + +type BetterBundle = Omit, 'entry'> & { + // `entry` is guaranteed to be defined by the API + entry: BetterEntry[]; +}; + /** * Add entries to this type to support them in the hooks. */ @@ -73,6 +86,11 @@ export type FhirAPIEndpoints = { // GET / [Name in keyof FhirResourcesByName as `GET /v1/fhir/dstu3/${Name}`]: { Request: SearchParamsForResourceType; - Response: Bundle>; + Response: BetterBundle>; + }; +} & { + 'GET /v1/fhir/dstu3/$me': { + Request: {}; + Response: BetterBundle>; }; };