diff --git a/docs/hooks/useDataQuery.md b/docs/hooks/useDataQuery.md index dd6b4a161..6d14bea94 100644 --- a/docs/hooks/useDataQuery.md +++ b/docs/hooks/useDataQuery.md @@ -73,6 +73,51 @@ export const IndicatorList = () => { } ``` +#### Typescript + +```tsx +import React from 'react' +import { useDataQuery, PaginatedQueryResult } from '@dhis2/app-runtime' +import { CircularLoader } from '@dhis2/ui' + +const query = { + dataElements: { + resource: 'dataElements', + params: { + fields: 'id,displayName', + pageSize: 10, + }, + }, +} + +type DataElementsResult = PaginatedQueryResult<{ + dataElements: { + dataElements: { + id: string + displayName: string + }[] + } +}> + +export const DataElementList = () => { + const { loading, error, data } = useDataQuery(query) + return ( +
+

Data elements (first 10)

+ {loading && } + {error && {`ERROR: ${error.message}`}} + {data && ( +
+                    {data.dataElements.dataElements
+                        .map((de) => de.displayName)
+                        .join('\n')}
+                
+ )} +
+ ) +} +``` + ### Dynamic Query This example is similar to the previous one but builds on top of it by showing how to fetch new pages of data using dynamic variables. A similar approach can be used implement dynamic filtering, ordering, etc. @@ -134,3 +179,130 @@ export const IndicatorList = () => { ) } ``` + +### Multiple resources in one Query + +```tsx +import React from 'react' +import { + useDataQuery, + PaginatedQueryResult, + Pager, + PaginatedData, +} from '@dhis2/app-runtime' +import { CircularLoader } from '@dhis2/ui' + +const query = { + dataElements: { + resource: 'dataElements', + params: ({ dataElementsPage }: { dataElementsPage?: number }) => ({ + fields: 'id,displayName', + pageSize: 10, + page: dataElementsPage, + }), + }, + dataSets: { + resource: 'dataSets', + params: { + fields: 'id,displayName', + paging: false, + }, + }, + indicators: { + resource: 'indicators', + params: ({ indicatorsPage }: { indicatorsPage?: number }) => ({ + fields: 'id,displayName', + pageSize: 10, + page: indicatorsPage, + }), + }, +} as const + +type QueryResult = { + dataElements: PaginatedData<{ + //can wrap your data-type in utility type PaginatedData + dataElements: { + id: string + displayName: string + }[] + }> + dataSets: { + // no pagination + dataSets: { + id: string + displayName: string + }[] + } + indicators: { + pager: Pager // or you can specifiy the pager manually + indicators: { + id: string + displayName: string + }[] + } +} +export const Component = () => { + const { error, data, refetch } = useDataQuery(query) + + // keep previous data, so list does not disappear when refetching/fetching new page + const prevData = React.useRef(data) + const stableData = data || prevData.current + React.useEffect(() => { + if (data) { + prevData.current = data + } + }, [data]) + + if (error) { + return {`ERROR: ${error.message}`} + } + if (!stableData) { + // initial fetch + return + } + + const indicators = stableData?.indicators.indicators + const indicatorsPager = stableData?.indicators.pager + const dataElements = stableData?.dataElements.dataElements + const dataElementPager = stableData?.dataElements.pager + const dataSets = stableData?.dataSets.dataSets + + return ( +
+

Data elements (first 10)

+
{dataElements?.map((de) => de.displayName).join('\n')}
+ + {dataElements && dataElementPager && ( + refetch({ dataElementsPage: page })} + hidePageSizeSelect={true} + pageLength={dataElements.length} + /> + )} + +

Data Sets

+
{dataSets?.map((de) => de.displayName).join('\n')}
+ +

Indicators Sets

+
{indicators?.map((de) => de.displayName).join('\n')}
+ {indicators && indicatorsPager && ( + refetch({ indicatorsPage: page })} + hidePageSizeSelect={true} + pageLength={indicators.length} + /> + )} +
+ ) +} +``` diff --git a/services/data/src/engine/DataEngine.test.ts b/services/data/src/engine/DataEngine.test.ts index 2c47d870c..93e51cbb3 100644 --- a/services/data/src/engine/DataEngine.test.ts +++ b/services/data/src/engine/DataEngine.test.ts @@ -15,6 +15,12 @@ const mockMutation: Mutation = { data: {}, } +type ResultData = { + type: string + resource: string + answer: number +} + const mockLink: DataEngineLink = { executeResourceQuery: jest.fn( async (type: FetchType, query: ResolvedResourceQuery) => { @@ -81,11 +87,19 @@ describe('DataEngine', () => { it('Should call multilple queries in parallel', async () => { const engine = new DataEngine(mockLink) - const result = await engine.query({ + + type Result = { + test: ResultData + test2: ResultData + test3: ResultData + } + const q = { test: { resource: 'test' }, test2: { resource: 'test2' }, test3: { resource: 'test3' }, - }) + } + const result = await engine.query(q) + expect(mockLink.executeResourceQuery).toHaveBeenCalledTimes(3) expect(result).toMatchObject({ test: { @@ -107,11 +121,16 @@ describe('DataEngine', () => { }) it('Should call onComplete callback only once for multiple-query method', async () => { + type Result = { + test: ResultData + test2: ResultData + test3: ResultData + } const options = { onComplete: jest.fn(), } const engine = new DataEngine(mockLink) - await engine.query( + await engine.query( { test: { resource: 'test' }, test2: { resource: 'test2' }, @@ -119,6 +138,7 @@ describe('DataEngine', () => { }, options ) + expect(mockLink.executeResourceQuery).toHaveBeenCalledTimes(3) expect(options.onComplete).toHaveBeenCalledTimes(1) }) diff --git a/services/data/src/engine/DataEngine.ts b/services/data/src/engine/DataEngine.ts index 1c43520dc..0accdcef5 100644 --- a/services/data/src/engine/DataEngine.ts +++ b/services/data/src/engine/DataEngine.ts @@ -6,12 +6,12 @@ import { } from './helpers/validate' import { DataEngineLink } from './types/DataEngineLink' import { QueryExecuteOptions } from './types/ExecuteOptions' -import { JsonMap, JsonValue } from './types/JsonValue' +import { JsonValue } from './types/JsonValue' import { Mutation } from './types/Mutation' -import { Query } from './types/Query' +import { Query, QueryResultData } from './types/Query' const reduceResponses = (responses: JsonValue[], names: string[]) => - responses.reduce((out, response, idx) => { + responses.reduce((out, response, idx) => { out[names[idx]] = response return out }, {}) @@ -22,15 +22,18 @@ export class DataEngine { this.link = link } - public query( - query: Query, + public query< + TQueryResultData extends QueryResultData, + TQuery extends Query = Query + >( + query: TQuery, { variables = {}, signal, onComplete, onError, - }: QueryExecuteOptions = {} - ): Promise { + }: QueryExecuteOptions = {} + ): Promise { const names = Object.keys(query) const queries = names .map((name) => query[name]) @@ -46,7 +49,7 @@ export class DataEngine { }) ) .then((results) => { - const data = reduceResponses(results, names) + const data: TQueryResultData = reduceResponses(results, names) onComplete && onComplete(data) return data }) diff --git a/services/data/src/engine/index.ts b/services/data/src/engine/index.ts index 790c2b74c..3afd5f547 100644 --- a/services/data/src/engine/index.ts +++ b/services/data/src/engine/index.ts @@ -7,3 +7,4 @@ export * from './types/Mutation' export * from './types/PossiblyDynamic' export * from './types/Query' export * from './types/QueryParameters' +export * from './types/Pager' diff --git a/services/data/src/engine/types/ExecuteOptions.ts b/services/data/src/engine/types/ExecuteOptions.ts index c7ddb37e7..d82d0f14a 100644 --- a/services/data/src/engine/types/ExecuteOptions.ts +++ b/services/data/src/engine/types/ExecuteOptions.ts @@ -1,5 +1,5 @@ import { FetchError } from './FetchError' -import { QueryVariables } from './Query' +import { QueryVariables, QueryResultData, QueryResult } from './Query' export type FetchType = | 'create' @@ -8,9 +8,11 @@ export type FetchType = | 'json-patch' | 'replace' | 'delete' -export interface QueryExecuteOptions { +export interface QueryExecuteOptions< + TQueryResultData extends QueryResultData = QueryResult +> { variables?: QueryVariables signal?: AbortSignal - onComplete?: (data: any) => void + onComplete?: (data: TQueryResultData) => void onError?: (error: FetchError) => void } diff --git a/services/data/src/engine/types/Pager.ts b/services/data/src/engine/types/Pager.ts new file mode 100644 index 000000000..e6fb45fa2 --- /dev/null +++ b/services/data/src/engine/types/Pager.ts @@ -0,0 +1,15 @@ +import { QueryResult } from './Query' + +export type Pager = { + page: number + total: number + pageSize: number + pageCount: number + nextPage: string +} + +export type PaginatedData = T & { pager: Pager } + +export type PaginatedQueryResult = { + [K in keyof TQueryResult]: PaginatedData +} diff --git a/services/data/src/engine/types/Query.ts b/services/data/src/engine/types/Query.ts index 5a79c1c67..1a13ec3f0 100644 --- a/services/data/src/engine/types/Query.ts +++ b/services/data/src/engine/types/Query.ts @@ -1,5 +1,4 @@ import { FetchError } from './FetchError' -import { JsonMap } from './JsonValue' import { PossiblyDynamic } from './PossiblyDynamic' import { QueryParameters } from './QueryParameters' @@ -19,7 +18,11 @@ export interface ResolvedResourceQuery extends ResourceQuery { } export type Query = Record -export type QueryResult = JsonMap +export type QueryResult = any + +export type QueryResultData = { + [K in keyof TQuery]: QueryResult +} export interface QueryOptions { variables?: QueryVariables diff --git a/services/data/src/react/hooks/useDataQuery.ts b/services/data/src/react/hooks/useDataQuery.ts index 9b81a145d..864fdd50f 100644 --- a/services/data/src/react/hooks/useDataQuery.ts +++ b/services/data/src/react/hooks/useDataQuery.ts @@ -3,11 +3,11 @@ import { useQuery, setLogger } from 'react-query' import type { Query, QueryOptions, - QueryResult, QueryVariables, + QueryResultData, } from '../../engine' import type { FetchError } from '../../engine/types/FetchError' -import type { QueryRenderInput, QueryRefetchFunction } from '../../types' +import type { DataQueryResult, QueryRefetchFunction } from '../../types' import { mergeAndCompareVariables } from './mergeAndCompareVariables' import { useDataEngine } from './useDataEngine' import { useStaticInput } from './useStaticInput' @@ -33,16 +33,19 @@ type QueryState = { refetchCallback?: (data: any) => void } -export const useDataQuery = ( - query: Query, +export const useDataQuery = < + TQueryResultData extends QueryResultData, + TQuery extends Query = Query +>( + query: TQuery, { onComplete: userOnSuccess, onError: userOnError, variables: initialVariables = {}, lazy: initialLazy = false, }: QueryOptions = {} -): QueryRenderInput => { - const [staticQuery] = useStaticInput(query, { +): DataQueryResult => { + const [staticQuery] = useStaticInput(query, { warn: true, name: 'query', }) diff --git a/services/data/src/types.ts b/services/data/src/types.ts index 3baed1573..8efe70b7b 100644 --- a/services/data/src/types.ts +++ b/services/data/src/types.ts @@ -54,6 +54,8 @@ export interface QueryRenderInput refetch: QueryRefetchFunction } +export type DataQueryResult = QueryRenderInput + export interface MutationState { engine: DataEngine called: boolean