@dhis2/app-service-data 3.2.4 → 3.2.8

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.
@@ -1,72 +1,337 @@
1
1
  import { renderHook, act } from '@testing-library/react-hooks';
2
- import React from 'react';
2
+ import * as React from 'react';
3
3
  import { CustomDataProvider } from '../components/CustomDataProvider';
4
+ import { useDataEngine } from './useDataEngine';
4
5
  import { useDataMutation } from './useDataMutation';
5
- const customData = {
6
- answer: 42
7
- };
8
-
9
- const wrapper = ({
10
- children
11
- }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
12
- data: customData
13
- }, children);
14
-
15
- const mutation = {
16
- type: 'create',
17
- resource: 'answer',
18
- data: {
19
- answer: 42
20
- }
21
- };
22
- describe('useDataMustation', () => {
23
- const originalError = console.error;
24
- afterEach(() => {
25
- console.error = originalError;
6
+ describe('useDataMutation', () => {
7
+ it('should render without failing', async () => {
8
+ const mutation = {
9
+ type: 'create',
10
+ resource: 'answer',
11
+ data: {
12
+ answer: '?'
13
+ }
14
+ };
15
+ const data = {
16
+ answer: 42
17
+ };
18
+
19
+ const wrapper = ({
20
+ children
21
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
22
+ data: data
23
+ }, children);
24
+
25
+ const {
26
+ result,
27
+ waitFor
28
+ } = renderHook(() => useDataMutation(mutation), {
29
+ wrapper
30
+ });
31
+ const [mutate, beforeMutation] = result.current;
32
+ expect(beforeMutation).toMatchObject({
33
+ loading: false,
34
+ called: false
35
+ });
36
+ act(() => {
37
+ mutate();
38
+ });
39
+ await waitFor(() => {
40
+ const [, duringMutation] = result.current;
41
+ expect(duringMutation).toMatchObject({
42
+ loading: true,
43
+ called: true
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
+ it('should run immediately with lazy: false', async () => {
56
+ const mutation = {
57
+ type: 'create',
58
+ resource: 'answer',
59
+ data: {
60
+ answer: '?'
61
+ }
62
+ };
63
+ const data = {
64
+ answer: 42
65
+ };
66
+
67
+ const wrapper = ({
68
+ children
69
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
70
+ data: data
71
+ }, children);
72
+
73
+ const {
74
+ result,
75
+ waitFor
76
+ } = renderHook(() => useDataMutation(mutation, {
77
+ lazy: false
78
+ }), {
79
+ wrapper
80
+ });
81
+ const [, duringMutation] = result.current;
82
+ expect(duringMutation).toMatchObject({
83
+ loading: true,
84
+ called: true
85
+ });
86
+ await waitFor(() => {
87
+ const [, afterMutation] = result.current;
88
+ expect(afterMutation).toMatchObject({
89
+ loading: false,
90
+ called: true,
91
+ data: 42
92
+ });
93
+ });
26
94
  });
27
- it('Should render without failing', async () => {
28
- let hookState;
29
- console.error = jest.fn();
95
+ it('should call onComplete on success', async () => {
96
+ const onComplete = jest.fn();
97
+ const mutation = {
98
+ type: 'create',
99
+ resource: 'answer',
100
+ data: {
101
+ answer: '?'
102
+ }
103
+ };
104
+ const data = {
105
+ answer: 42
106
+ };
107
+
108
+ const wrapper = ({
109
+ children
110
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
111
+ data: data
112
+ }, children);
113
+
114
+ const {
115
+ result,
116
+ waitFor
117
+ } = renderHook(() => useDataMutation(mutation, {
118
+ onComplete
119
+ }), {
120
+ wrapper
121
+ });
122
+ expect(onComplete).toHaveBeenCalledTimes(0);
123
+ const [mutate] = result.current;
30
124
  act(() => {
31
- hookState = renderHook(() => useDataMutation(mutation), {
32
- wrapper
125
+ mutate();
126
+ });
127
+ await waitFor(() => {
128
+ const [, state] = result.current;
129
+ expect(state).toMatchObject({
130
+ loading: false,
131
+ called: true,
132
+ data: 42
33
133
  });
134
+ expect(onComplete).toHaveBeenCalledTimes(1);
135
+ expect(onComplete).toHaveBeenLastCalledWith(42);
34
136
  });
35
- let [mutate, state] = hookState.result.current;
36
- expect(state).toMatchObject({
37
- called: false,
38
- loading: false
137
+ });
138
+ it('should call onError on error', async () => {
139
+ const error = new Error('Something went wrong');
140
+ const onError = jest.fn();
141
+ const mutation = {
142
+ type: 'create',
143
+ resource: 'answer',
144
+ data: {
145
+ answer: 42
146
+ }
147
+ };
148
+ const data = {
149
+ answer: () => {
150
+ throw error;
151
+ }
152
+ };
153
+
154
+ const wrapper = ({
155
+ children
156
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
157
+ data: data
158
+ }, children);
159
+
160
+ const {
161
+ result,
162
+ waitFor
163
+ } = renderHook(() => useDataMutation(mutation, {
164
+ onError
165
+ }), {
166
+ wrapper
39
167
  });
168
+ expect(onError).toHaveBeenCalledTimes(0);
169
+ const [mutate] = result.current;
40
170
  act(() => {
41
171
  mutate();
42
172
  });
43
- mutate = hookState.result.current[0];
44
- state = hookState.result.current[1];
45
- expect(state).toMatchObject({
46
- called: true,
47
- loading: true
173
+ await waitFor(() => {
174
+ const [, state] = result.current;
175
+ expect(state).toMatchObject({
176
+ loading: false,
177
+ called: true,
178
+ error
179
+ });
48
180
  });
181
+ expect(onError).toHaveBeenCalledTimes(1);
182
+ expect(onError).toHaveBeenLastCalledWith(error);
49
183
  });
50
- it('Should run immediately with lazy: false', async () => {
51
- let hookState;
52
- console.error = jest.fn();
184
+ it('should resolve variables', async () => {
185
+ const mutation = {
186
+ type: 'update',
187
+ resource: 'answer',
188
+ id: ({
189
+ id
190
+ }) => id,
191
+ data: {
192
+ answer: '?'
193
+ }
194
+ };
195
+ const answerSpy = jest.fn(() => 42);
196
+ const data = {
197
+ answer: answerSpy
198
+ };
199
+
200
+ const wrapper = ({
201
+ children
202
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
203
+ data: data
204
+ }, children);
205
+
206
+ const {
207
+ result,
208
+ waitFor
209
+ } = renderHook(() => useDataMutation(mutation, {
210
+ lazy: false,
211
+ variables: {
212
+ id: '1'
213
+ }
214
+ }), {
215
+ wrapper
216
+ });
217
+ await waitFor(() => {
218
+ expect(answerSpy).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
219
+ id: '1'
220
+ }), expect.any(Object));
221
+ });
222
+ const [mutate] = result.current;
53
223
  act(() => {
54
- hookState = renderHook(() => useDataMutation(mutation, {
55
- lazy: false
56
- }), {
57
- wrapper
224
+ mutate({
225
+ id: '2'
58
226
  });
59
227
  });
60
- let [, state] = hookState.result.current;
61
- expect(state).toMatchObject({
62
- called: true,
63
- loading: true
228
+ await waitFor(() => {
229
+ expect(answerSpy).toHaveBeenLastCalledWith(expect.any(String), expect.objectContaining({
230
+ id: '2'
231
+ }), expect.any(Object));
64
232
  });
65
- await hookState.waitForNextUpdate();
66
- state = hookState.result.current[1];
233
+ });
234
+ it('should return a reference to the engine', async () => {
235
+ const mutation = {
236
+ type: 'create',
237
+ resource: 'answer',
238
+ data: {
239
+ answer: '?'
240
+ }
241
+ };
242
+
243
+ const wrapper = ({
244
+ children
245
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
246
+ data: {}
247
+ }, children);
248
+
249
+ const engineHook = renderHook(() => useDataEngine(), {
250
+ wrapper
251
+ });
252
+ const mutationHook = renderHook(() => useDataMutation(mutation), {
253
+ wrapper
254
+ });
255
+ /**
256
+ * Ideally we'd check referential equality here with .toBe, but since
257
+ * both hooks run in a different context that doesn't work.
258
+ */
259
+
260
+ expect(mutationHook.result.current[1].engine).toStrictEqual(engineHook.result.current);
261
+ });
262
+ it('should return a stable mutate function', async () => {
263
+ const mutation = {
264
+ type: 'create',
265
+ resource: 'answer',
266
+ data: {
267
+ answer: '?'
268
+ }
269
+ };
270
+ const data = {
271
+ answer: 42
272
+ };
273
+
274
+ const wrapper = ({
275
+ children
276
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
277
+ data: data
278
+ }, children);
279
+
280
+ const {
281
+ result
282
+ } = renderHook(() => useDataMutation(mutation), {
283
+ wrapper
284
+ });
285
+ const [firstMutate] = result.current;
286
+ await act(async () => {
287
+ await firstMutate({
288
+ variable: 'variable'
289
+ });
290
+ });
291
+ const [secondMutate, state] = result.current;
67
292
  expect(state).toMatchObject({
68
293
  loading: false,
69
- data: 42
294
+ called: true
295
+ });
296
+ expect(firstMutate).toBe(secondMutate);
297
+ });
298
+ it('should resolve with the data from mutate on success', async () => {
299
+ const mutation = {
300
+ type: 'create',
301
+ resource: 'answer',
302
+ data: {
303
+ answer: '?'
304
+ }
305
+ };
306
+ const data = {
307
+ answer: 42
308
+ };
309
+
310
+ const wrapper = ({
311
+ children
312
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
313
+ data: data
314
+ }, children);
315
+
316
+ const {
317
+ result,
318
+ waitFor
319
+ } = renderHook(() => useDataMutation(mutation), {
320
+ wrapper
321
+ });
322
+ let mutatePromise;
323
+ const [mutate] = result.current;
324
+ act(() => {
325
+ mutatePromise = mutate();
326
+ });
327
+ await waitFor(() => {
328
+ const [, state] = result.current;
329
+ expect(state).toMatchObject({
330
+ loading: false,
331
+ called: true,
332
+ data: 42
333
+ });
334
+ expect(mutatePromise).resolves.toBe(42);
70
335
  });
71
336
  });
72
337
  });
@@ -1,4 +1,4 @@
1
- import { useState, useRef } from 'react';
1
+ import { useState, useRef, useCallback } from 'react';
2
2
  import { useQuery, setLogger } from 'react-query';
3
3
  import { stableVariablesHash } from './stableVariablesHash';
4
4
  import { useDataEngine } from './useDataEngine';
@@ -84,9 +84,13 @@ export const useDataQuery = (query, {
84
84
  /**
85
85
  * Refetch allows a user to update the variables or just
86
86
  * trigger a refetch of the query with the current variables.
87
+ *
88
+ * We're using useCallback to make the identity of the function
89
+ * as stable as possible, so that it won't trigger excessive
90
+ * rerenders when used for side-effects.
87
91
  */
88
92
 
89
- const refetch = newVariables => {
93
+ const refetch = useCallback(newVariables => {
90
94
  /**
91
95
  * If there are no updates that will trigger an automatic refetch
92
96
  * we'll need to call react-query's refetch directly
@@ -100,10 +104,6 @@ export const useDataQuery = (query, {
100
104
  }) => data);
101
105
  }
102
106
 
103
- if (!enabled) {
104
- setEnabled(true);
105
- }
106
-
107
107
  if (newVariables) {
108
108
  // Use cached hash if it exists
109
109
  const currentHash = variablesHash.current || stableVariablesHash(variables);
@@ -113,8 +113,11 @@ export const useDataQuery = (query, {
113
113
  const mergedHash = stableVariablesHash(mergedVariables);
114
114
  const identical = currentHash === mergedHash;
115
115
 
116
- if (identical) {
117
- // If the variables are identical we'll need to trigger the refetch manually
116
+ if (identical && enabled) {
117
+ /**
118
+ * If the variables are identical and the query is enabled
119
+ * we'll need to trigger the refetch manually
120
+ */
118
121
  return queryRefetch({
119
122
  cancelRefetch: true,
120
123
  throwOnError: false
@@ -125,6 +128,11 @@ export const useDataQuery = (query, {
125
128
  variablesHash.current = mergedHash;
126
129
  setVariables(mergedVariables);
127
130
  }
131
+ } // Enable the query after the variables have been set to prevent extra request
132
+
133
+
134
+ if (!enabled) {
135
+ setEnabled(true);
128
136
  } // This promise does not currently reject on errors
129
137
 
130
138
 
@@ -133,13 +141,12 @@ export const useDataQuery = (query, {
133
141
  resolve(data);
134
142
  };
135
143
  });
136
- };
144
+ }, [enabled, queryRefetch, variables]);
137
145
  /**
138
146
  * react-query returns null or an error, but we return undefined
139
147
  * or an error, so this ensures consistency with the other types.
140
148
  */
141
149
 
142
-
143
150
  const ourError = error || undefined;
144
151
  return {
145
152
  engine,
@@ -497,6 +497,149 @@ describe('useDataQuery', () => {
497
497
  });
498
498
  });
499
499
  describe('return values: refetch', () => {
500
+ it('Should only trigger a single request when refetch is called on a lazy query with new variables', async () => {
501
+ const spy = jest.fn((type, query) => {
502
+ if (query.id === '1') {
503
+ return 42;
504
+ }
505
+
506
+ return 0;
507
+ });
508
+ const data = {
509
+ answer: spy
510
+ };
511
+ const query = {
512
+ x: {
513
+ resource: 'answer',
514
+ id: ({
515
+ id
516
+ }) => id
517
+ }
518
+ };
519
+
520
+ const wrapper = ({
521
+ children
522
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
523
+ data: data
524
+ }, children);
525
+
526
+ const {
527
+ result,
528
+ waitFor
529
+ } = renderHook(() => useDataQuery(query, {
530
+ lazy: true
531
+ }), {
532
+ wrapper
533
+ });
534
+ expect(spy).not.toHaveBeenCalled();
535
+ act(() => {
536
+ result.current.refetch({
537
+ id: '1'
538
+ });
539
+ });
540
+ await waitFor(() => {
541
+ expect(result.current).toMatchObject({
542
+ loading: false,
543
+ called: true,
544
+ data: {
545
+ x: 42
546
+ }
547
+ });
548
+ });
549
+ expect(spy).toHaveBeenCalledTimes(1);
550
+ });
551
+ it('Should only trigger a single request when refetch is called on a lazy query with identical variables', async () => {
552
+ const spy = jest.fn((type, query) => {
553
+ if (query.id === '1') {
554
+ return 42;
555
+ }
556
+
557
+ return 0;
558
+ });
559
+ const data = {
560
+ answer: spy
561
+ };
562
+ const query = {
563
+ x: {
564
+ resource: 'answer',
565
+ id: ({
566
+ id
567
+ }) => id
568
+ }
569
+ };
570
+
571
+ const wrapper = ({
572
+ children
573
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
574
+ data: data
575
+ }, children);
576
+
577
+ const {
578
+ result,
579
+ waitFor
580
+ } = renderHook(() => useDataQuery(query, {
581
+ lazy: true,
582
+ variables: {
583
+ id: '1'
584
+ }
585
+ }), {
586
+ wrapper
587
+ });
588
+ expect(spy).not.toHaveBeenCalled();
589
+ act(() => {
590
+ result.current.refetch({
591
+ id: '1'
592
+ });
593
+ });
594
+ await waitFor(() => {
595
+ expect(result.current).toMatchObject({
596
+ loading: false,
597
+ called: true,
598
+ data: {
599
+ x: 42
600
+ }
601
+ });
602
+ });
603
+ expect(spy).toHaveBeenCalledTimes(1);
604
+ });
605
+ it('Should have a stable identity if the variables have not changed', async () => {
606
+ const data = {
607
+ answer: () => 42
608
+ };
609
+ const query = {
610
+ x: {
611
+ resource: 'answer'
612
+ }
613
+ };
614
+
615
+ const wrapper = ({
616
+ children
617
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
618
+ data: data
619
+ }, children);
620
+
621
+ const {
622
+ result,
623
+ waitForNextUpdate,
624
+ rerender
625
+ } = renderHook(() => useDataQuery(query), {
626
+ wrapper
627
+ });
628
+ const firstRefetch = result.current.refetch;
629
+ await waitForNextUpdate();
630
+ act(() => {
631
+ result.current.refetch();
632
+ /**
633
+ * FIXME: https://github.com/tannerlinsley/react-query/issues/2481
634
+ * This forced rerender is not necessary in the app, just when testing.
635
+ * It is unclear why.
636
+ */
637
+
638
+ rerender();
639
+ });
640
+ await waitForNextUpdate();
641
+ expect(result.current.refetch).toBe(firstRefetch);
642
+ });
500
643
  it('Should return stale data and set loading to true on refetch', async () => {
501
644
  const answers = [42, 43];
502
645
  const mockSpy = jest.fn(() => Promise.resolve(answers.shift()));
@@ -1,7 +1,8 @@
1
1
  import { ResolvedResourceQuery, FetchType } from '../../../engine';
2
2
  export declare const isReplyToMessageConversation: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
3
3
  export declare const isCreateFeedbackMessage: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
4
- export declare const isCreateOrUpdateInterpretation: (type: FetchType, { resource, id }: ResolvedResourceQuery) => boolean;
4
+ export declare const isCreateInterpretation: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
5
+ export declare const isUpdateInterpretation: (type: FetchType, { resource, id }: ResolvedResourceQuery) => boolean;
5
6
  export declare const isCommentOnInterpretation: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
6
7
  export declare const isInterpretationCommentUpdate: (type: FetchType, { resource, id }: ResolvedResourceQuery) => boolean;
7
8
  export declare const isAddOrUpdateSystemOrUserSetting: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dhis2/app-service-data",
3
- "version": "3.2.4",
3
+ "version": "3.2.8",
4
4
  "main": "./build/cjs/index.js",
5
5
  "module": "./build/es/index.js",
6
6
  "types": "build/types/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "build/**"
23
23
  ],
24
24
  "peerDependencies": {
25
- "@dhis2/app-service-config": "3.2.4",
25
+ "@dhis2/app-service-config": "3.2.8",
26
26
  "@dhis2/cli-app-scripts": "^7.1.1",
27
27
  "prop-types": "^15.7.2",
28
28
  "react": "^16.8",