@dhis2/app-service-data 3.17.0 → 3.17.2

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.
Files changed (34) hide show
  1. package/.gitignore +5 -0
  2. package/d2.config.js +9 -0
  3. package/jest.config.js +14 -0
  4. package/package.json +4 -4
  5. package/src/__tests__/integration.test.tsx +80 -0
  6. package/src/__tests__/mutations.test.tsx +71 -0
  7. package/src/index.ts +5 -0
  8. package/src/react/components/CustomDataProvider.tsx +33 -0
  9. package/src/react/components/DataMutation.tsx +24 -0
  10. package/src/react/components/DataProvider.test.tsx +22 -0
  11. package/src/react/components/DataProvider.tsx +58 -0
  12. package/src/react/components/DataQuery.tsx +26 -0
  13. package/src/react/components/index.ts +4 -0
  14. package/src/react/context/DataContext.tsx +5 -0
  15. package/src/react/context/defaultDataContext.test.ts +46 -0
  16. package/src/react/context/defaultDataContext.ts +9 -0
  17. package/src/react/hooks/index.ts +3 -0
  18. package/src/react/hooks/mergeAndCompareVariables.test.ts +65 -0
  19. package/src/react/hooks/mergeAndCompareVariables.ts +32 -0
  20. package/src/react/hooks/stableVariablesHash.test.ts +53 -0
  21. package/src/react/hooks/stableVariablesHash.ts +56 -0
  22. package/src/react/hooks/useDataEngine.ts +8 -0
  23. package/src/react/hooks/useDataMutation.test.tsx +296 -0
  24. package/src/react/hooks/useDataMutation.ts +42 -0
  25. package/src/react/hooks/useDataQuery.test.tsx +1029 -0
  26. package/src/react/hooks/useDataQuery.ts +170 -0
  27. package/src/react/hooks/useQueryExecutor.test.tsx +195 -0
  28. package/src/react/hooks/useQueryExecutor.ts +99 -0
  29. package/src/react/hooks/useStaticInput.test.ts +101 -0
  30. package/src/react/hooks/useStaticInput.ts +27 -0
  31. package/src/react/index.ts +2 -0
  32. package/src/setupRTL.ts +5 -0
  33. package/src/types.ts +72 -0
  34. package/tsconfig.json +10 -0
@@ -0,0 +1,8 @@
1
+ import { useContext } from 'react'
2
+ import { DataContext } from '../context/DataContext'
3
+
4
+ export const useDataEngine = () => {
5
+ const context = useContext(DataContext)
6
+
7
+ return context.engine
8
+ }
@@ -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
+ }