Skip to content

Commit

Permalink
Merge pull request #491 from lifeomic/more-endpoints
Browse files Browse the repository at this point in the history
Migrate useMe to be powered by useRestQuery
  • Loading branch information
swain authored Dec 5, 2023
2 parents 3aea3e1 + 7a84d40 commit 577436c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 51 deletions.
6 changes: 6 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
45 changes: 23 additions & 22 deletions src/hooks/useMe.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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 }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
<QueryClientProvider client={queryClient}>
<ActiveAccountProvider account="mockaccount">
<HttpClientContextProvider>{children}</HttpClientContextProvider>
</ActiveAccountProvider>
</QueryClientProvider>
),
});
};

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: [
Expand All @@ -47,7 +42,8 @@ test('fetches and parses $me', async () => {
],
},
};
const subject2 = {
const subject2: Patient & { id: string } = {
resourceType: 'Patient',
id: 'patientId2',
meta: {
tag: [
Expand All @@ -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 },
Expand Down
45 changes: 17 additions & 28 deletions src/hooks/useMe.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<MeResponse>('/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;
}
20 changes: 19 additions & 1 deletion src/types/fhir-api-types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import {
Appointment,
Bundle,
BundleEntry,
Observation,
Patient,
Practitioner,
Questionnaire,
QuestionnaireResponse,
} from 'fhir/r3';

// These "better" types strongly type the bundle
// to match the LifeOmic FHIR API guarantees.
type BetterEntry<T> = Omit<BundleEntry<T>, 'resource'> & {
// `resource` is guaranteed to be defined by the API
resource: T;
};

type BetterBundle<T> = Omit<Bundle<T>, 'entry'> & {
// `entry` is guaranteed to be defined by the API
entry: BetterEntry<T>[];
};

/**
* Add entries to this type to support them in the hooks.
*/
Expand Down Expand Up @@ -73,6 +86,11 @@ export type FhirAPIEndpoints = {
// GET /<resource>
[Name in keyof FhirResourcesByName as `GET /v1/fhir/dstu3/${Name}`]: {
Request: SearchParamsForResourceType<Name>;
Response: Bundle<WithIdDefined<FhirResourcesByName[Name]>>;
Response: BetterBundle<WithIdDefined<FhirResourcesByName[Name]>>;
};
} & {
'GET /v1/fhir/dstu3/$me': {
Request: {};
Response: BetterBundle<WithIdDefined<Patient>>;
};
};

0 comments on commit 577436c

Please sign in to comment.