@dhis2/app-service-data 3.17.0-beta.4 → 3.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitignore +5 -0
- package/d2.config.js +9 -0
- package/jest.config.js +14 -0
- package/package.json +4 -4
- package/src/__tests__/integration.test.tsx +80 -0
- package/src/__tests__/mutations.test.tsx +71 -0
- package/src/index.ts +5 -0
- package/src/react/components/CustomDataProvider.tsx +33 -0
- package/src/react/components/DataMutation.tsx +24 -0
- package/src/react/components/DataProvider.test.tsx +22 -0
- package/src/react/components/DataProvider.tsx +58 -0
- package/src/react/components/DataQuery.tsx +26 -0
- package/src/react/components/index.ts +4 -0
- package/src/react/context/DataContext.tsx +5 -0
- package/src/react/context/defaultDataContext.test.ts +46 -0
- package/src/react/context/defaultDataContext.ts +9 -0
- package/src/react/hooks/index.ts +3 -0
- package/src/react/hooks/mergeAndCompareVariables.test.ts +65 -0
- package/src/react/hooks/mergeAndCompareVariables.ts +32 -0
- package/src/react/hooks/stableVariablesHash.test.ts +53 -0
- package/src/react/hooks/stableVariablesHash.ts +56 -0
- package/src/react/hooks/useDataEngine.ts +8 -0
- package/src/react/hooks/useDataMutation.test.tsx +296 -0
- package/src/react/hooks/useDataMutation.ts +42 -0
- package/src/react/hooks/useDataQuery.test.tsx +1029 -0
- package/src/react/hooks/useDataQuery.ts +170 -0
- package/src/react/hooks/useQueryExecutor.test.tsx +195 -0
- package/src/react/hooks/useQueryExecutor.ts +99 -0
- package/src/react/hooks/useStaticInput.test.ts +101 -0
- package/src/react/hooks/useStaticInput.ts +27 -0
- package/src/react/index.ts +2 -0
- package/src/setupRTL.ts +5 -0
- package/src/types.ts +72 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateMutation,
|
|
3
|
+
UpdateMutation,
|
|
4
|
+
QueryVariables,
|
|
5
|
+
} from '@dhis2/data-engine'
|
|
6
|
+
import { renderHook, act, waitFor } from '@testing-library/react'
|
|
7
|
+
import * as React from 'react'
|
|
8
|
+
import { CustomDataProvider } from '../components/CustomDataProvider'
|
|
9
|
+
import { useDataEngine } from './useDataEngine'
|
|
10
|
+
import { useDataMutation } from './useDataMutation'
|
|
11
|
+
|
|
12
|
+
describe('useDataMutation', () => {
|
|
13
|
+
it('should render without failing', async () => {
|
|
14
|
+
const mutation: CreateMutation = {
|
|
15
|
+
type: 'create',
|
|
16
|
+
resource: 'answer',
|
|
17
|
+
data: { answer: '?' },
|
|
18
|
+
}
|
|
19
|
+
const data = { answer: 42 }
|
|
20
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
21
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const { result } = renderHook(() => useDataMutation(mutation), {
|
|
25
|
+
wrapper,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const [mutate, beforeMutation] = result.current
|
|
29
|
+
expect(beforeMutation).toMatchObject({
|
|
30
|
+
loading: false,
|
|
31
|
+
called: false,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
act(() => {
|
|
35
|
+
mutate()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await waitFor(() => {
|
|
39
|
+
const [, duringMutation] = result.current
|
|
40
|
+
expect(duringMutation).toMatchObject({
|
|
41
|
+
loading: true,
|
|
42
|
+
called: true,
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await waitFor(() => {
|
|
47
|
+
const [, afterMutation] = result.current
|
|
48
|
+
expect(afterMutation).toMatchObject({
|
|
49
|
+
loading: false,
|
|
50
|
+
called: true,
|
|
51
|
+
data: 42,
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should run immediately with lazy: false', async () => {
|
|
57
|
+
const mutation: CreateMutation = {
|
|
58
|
+
type: 'create',
|
|
59
|
+
resource: 'answer',
|
|
60
|
+
data: { answer: '?' },
|
|
61
|
+
}
|
|
62
|
+
const data = { answer: 42 }
|
|
63
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
64
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const { result } = renderHook(
|
|
68
|
+
() => useDataMutation(mutation, { lazy: false }),
|
|
69
|
+
{ wrapper }
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const [, duringMutation] = result.current
|
|
73
|
+
expect(duringMutation).toMatchObject({
|
|
74
|
+
loading: true,
|
|
75
|
+
called: true,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
const [, afterMutation] = result.current
|
|
80
|
+
expect(afterMutation).toMatchObject({
|
|
81
|
+
loading: false,
|
|
82
|
+
called: true,
|
|
83
|
+
data: 42,
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should call onComplete on success', async () => {
|
|
89
|
+
const onComplete = jest.fn()
|
|
90
|
+
const mutation: CreateMutation = {
|
|
91
|
+
type: 'create',
|
|
92
|
+
resource: 'answer',
|
|
93
|
+
data: { answer: '?' },
|
|
94
|
+
}
|
|
95
|
+
const data = { answer: 42 }
|
|
96
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
97
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const { result } = renderHook(
|
|
101
|
+
() => useDataMutation(mutation, { onComplete }),
|
|
102
|
+
{ wrapper }
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(onComplete).toHaveBeenCalledTimes(0)
|
|
106
|
+
const [mutate] = result.current
|
|
107
|
+
act(() => {
|
|
108
|
+
mutate()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
await waitFor(() => {
|
|
112
|
+
const [, state] = result.current
|
|
113
|
+
expect(state).toMatchObject({
|
|
114
|
+
loading: false,
|
|
115
|
+
called: true,
|
|
116
|
+
data: 42,
|
|
117
|
+
})
|
|
118
|
+
expect(onComplete).toHaveBeenCalledTimes(1)
|
|
119
|
+
expect(onComplete).toHaveBeenLastCalledWith(42)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should call onError on error', async () => {
|
|
124
|
+
const error = new Error('Something went wrong')
|
|
125
|
+
const onError = jest.fn()
|
|
126
|
+
const mutation: CreateMutation = {
|
|
127
|
+
type: 'create',
|
|
128
|
+
resource: 'answer',
|
|
129
|
+
data: { answer: 42 },
|
|
130
|
+
}
|
|
131
|
+
const data = {
|
|
132
|
+
answer: () => {
|
|
133
|
+
throw error
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
137
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const { result } = renderHook(
|
|
141
|
+
() => useDataMutation(mutation, { onError }),
|
|
142
|
+
{ wrapper }
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
expect(onError).toHaveBeenCalledTimes(0)
|
|
146
|
+
const [mutate] = result.current
|
|
147
|
+
|
|
148
|
+
act(() => {
|
|
149
|
+
mutate()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
const [, state] = result.current
|
|
154
|
+
expect(state).toMatchObject({
|
|
155
|
+
loading: false,
|
|
156
|
+
called: true,
|
|
157
|
+
error,
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
161
|
+
expect(onError).toHaveBeenLastCalledWith(error)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should resolve variables', async () => {
|
|
165
|
+
const mutation = {
|
|
166
|
+
type: 'update',
|
|
167
|
+
resource: 'answer',
|
|
168
|
+
id: ({ id }: QueryVariables) => id as string,
|
|
169
|
+
data: { answer: '?' },
|
|
170
|
+
} as unknown as UpdateMutation
|
|
171
|
+
const answerSpy = jest.fn(() => Promise.resolve(42))
|
|
172
|
+
const data = { answer: answerSpy }
|
|
173
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
174
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
const { result } = renderHook(
|
|
178
|
+
() =>
|
|
179
|
+
useDataMutation(mutation, {
|
|
180
|
+
lazy: false,
|
|
181
|
+
variables: { id: '1' },
|
|
182
|
+
}),
|
|
183
|
+
{ wrapper }
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
await waitFor(() => {
|
|
187
|
+
expect(answerSpy).toHaveBeenLastCalledWith(
|
|
188
|
+
expect.any(String),
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
id: '1',
|
|
191
|
+
}),
|
|
192
|
+
expect.any(Object)
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const [mutate] = result.current
|
|
197
|
+
act(() => {
|
|
198
|
+
mutate({ id: '2' })
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(answerSpy).toHaveBeenLastCalledWith(
|
|
203
|
+
expect.any(String),
|
|
204
|
+
expect.objectContaining({
|
|
205
|
+
id: '2',
|
|
206
|
+
}),
|
|
207
|
+
expect.any(Object)
|
|
208
|
+
)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should return a reference to the engine', async () => {
|
|
213
|
+
const mutation: CreateMutation = {
|
|
214
|
+
type: 'create',
|
|
215
|
+
resource: 'answer',
|
|
216
|
+
data: { answer: '?' },
|
|
217
|
+
}
|
|
218
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
219
|
+
<CustomDataProvider data={{}}>{children}</CustomDataProvider>
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
const engineHook = renderHook(() => useDataEngine(), { wrapper })
|
|
223
|
+
const mutationHook = renderHook(() => useDataMutation(mutation), {
|
|
224
|
+
wrapper,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Ideally we'd check referential equality here with .toBe, but since
|
|
229
|
+
* both hooks run in a different context that doesn't work.
|
|
230
|
+
*/
|
|
231
|
+
expect(mutationHook.result.current[1].engine).toStrictEqual(
|
|
232
|
+
engineHook.result.current
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should return a stable mutate function', async () => {
|
|
237
|
+
const mutation: CreateMutation = {
|
|
238
|
+
type: 'create',
|
|
239
|
+
resource: 'answer',
|
|
240
|
+
data: { answer: '?' },
|
|
241
|
+
}
|
|
242
|
+
const data = { answer: 42 }
|
|
243
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
244
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
const { result } = renderHook(() => useDataMutation(mutation), {
|
|
248
|
+
wrapper,
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const [firstMutate] = result.current
|
|
252
|
+
|
|
253
|
+
await act(async () => {
|
|
254
|
+
await firstMutate({ variable: 'variable' })
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
const [secondMutate, state] = result.current
|
|
258
|
+
expect(state).toMatchObject({
|
|
259
|
+
loading: false,
|
|
260
|
+
called: true,
|
|
261
|
+
})
|
|
262
|
+
expect(firstMutate).toBe(secondMutate)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should resolve with the data from mutate on success', async () => {
|
|
266
|
+
const mutation: CreateMutation = {
|
|
267
|
+
type: 'create',
|
|
268
|
+
resource: 'answer',
|
|
269
|
+
data: { answer: '?' },
|
|
270
|
+
}
|
|
271
|
+
const data = { answer: 42 }
|
|
272
|
+
const wrapper = ({ children }: { children?: React.ReactNode }) => (
|
|
273
|
+
<CustomDataProvider data={data}>{children}</CustomDataProvider>
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const { result } = renderHook(() => useDataMutation(mutation), {
|
|
277
|
+
wrapper,
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
let mutatePromise!: Promise<unknown>
|
|
281
|
+
const [mutate] = result.current
|
|
282
|
+
act(() => {
|
|
283
|
+
mutatePromise = mutate()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
await waitFor(() => {
|
|
287
|
+
const [, state] = result.current
|
|
288
|
+
expect(state).toMatchObject({
|
|
289
|
+
loading: false,
|
|
290
|
+
called: true,
|
|
291
|
+
data: 42,
|
|
292
|
+
})
|
|
293
|
+
expect(mutatePromise).resolves.toBe(42)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
QueryOptions,
|
|
3
|
+
Mutation,
|
|
4
|
+
QueryExecuteOptions,
|
|
5
|
+
} from '@dhis2/data-engine'
|
|
6
|
+
import { useCallback } from 'react'
|
|
7
|
+
import { MutationRenderInput } from '../../types'
|
|
8
|
+
import { useDataEngine } from './useDataEngine'
|
|
9
|
+
import { useQueryExecutor } from './useQueryExecutor'
|
|
10
|
+
import { useStaticInput } from './useStaticInput'
|
|
11
|
+
|
|
12
|
+
const empty = {}
|
|
13
|
+
export const useDataMutation = (
|
|
14
|
+
mutation: Mutation,
|
|
15
|
+
{ onComplete, onError, variables = empty, lazy = true }: QueryOptions = {}
|
|
16
|
+
): MutationRenderInput => {
|
|
17
|
+
const engine = useDataEngine()
|
|
18
|
+
const [theMutation] = useStaticInput<Mutation>(mutation, {
|
|
19
|
+
warn: true,
|
|
20
|
+
name: 'mutation',
|
|
21
|
+
})
|
|
22
|
+
const execute = useCallback(
|
|
23
|
+
(options: QueryExecuteOptions) => engine.mutate(theMutation, options),
|
|
24
|
+
[engine, theMutation]
|
|
25
|
+
)
|
|
26
|
+
const {
|
|
27
|
+
refetch: mutate,
|
|
28
|
+
called,
|
|
29
|
+
loading,
|
|
30
|
+
error,
|
|
31
|
+
data,
|
|
32
|
+
} = useQueryExecutor({
|
|
33
|
+
execute,
|
|
34
|
+
variables,
|
|
35
|
+
singular: false,
|
|
36
|
+
immediate: !lazy,
|
|
37
|
+
onComplete,
|
|
38
|
+
onError,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
return [mutate, { engine, called, loading, error, data }]
|
|
42
|
+
}
|