@dhis2/app-service-data 3.3.0 → 3.4.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 (38) hide show
  1. package/build/cjs/engine/helpers/getMutationFetchType.test.js +5 -0
  2. package/build/cjs/engine/helpers/validate.js +5 -1
  3. package/build/cjs/engine/helpers/validate.test.js +8 -0
  4. package/build/cjs/links/RestAPILink/queryToRequestOptions/requestContentType.js +5 -1
  5. package/build/cjs/links/RestAPILink/queryToRequestOptions/requestContentType.test.js +6 -0
  6. package/build/cjs/links/RestAPILink/queryToRequestOptions.js +4 -0
  7. package/build/cjs/links/RestAPILink/queryToRequestOptions.test.js +10 -0
  8. package/build/cjs/links/RestAPILink/queryToResourcePath.js +15 -2
  9. package/build/cjs/links/RestAPILink/queryToResourcePath.test.js +48 -15
  10. package/build/cjs/links/RestAPILink.js +9 -12
  11. package/build/cjs/react/hooks/mergeAndCompareVariables.js +33 -0
  12. package/build/cjs/react/hooks/mergeAndCompareVariables.test.js +57 -0
  13. package/build/cjs/react/hooks/useDataQuery.js +43 -53
  14. package/build/cjs/react/hooks/useDataQuery.test.js +59 -0
  15. package/build/cjs/react/hooks/useStaticInput.js +1 -0
  16. package/build/es/engine/helpers/getMutationFetchType.test.js +5 -0
  17. package/build/es/engine/helpers/validate.js +5 -1
  18. package/build/es/engine/helpers/validate.test.js +8 -0
  19. package/build/es/links/RestAPILink/queryToRequestOptions/requestContentType.js +5 -1
  20. package/build/es/links/RestAPILink/queryToRequestOptions/requestContentType.test.js +6 -0
  21. package/build/es/links/RestAPILink/queryToRequestOptions.js +4 -0
  22. package/build/es/links/RestAPILink/queryToRequestOptions.test.js +10 -0
  23. package/build/es/links/RestAPILink/queryToResourcePath.js +15 -2
  24. package/build/es/links/RestAPILink/queryToResourcePath.test.js +48 -15
  25. package/build/es/links/RestAPILink.js +9 -12
  26. package/build/es/react/hooks/mergeAndCompareVariables.js +23 -0
  27. package/build/es/react/hooks/mergeAndCompareVariables.test.js +53 -0
  28. package/build/es/react/hooks/useDataQuery.js +44 -54
  29. package/build/es/react/hooks/useDataQuery.test.js +59 -0
  30. package/build/es/react/hooks/useStaticInput.js +2 -1
  31. package/build/types/engine/types/ExecuteOptions.d.ts +1 -1
  32. package/build/types/engine/types/Mutation.d.ts +2 -2
  33. package/build/types/links/RestAPILink/queryToRequestOptions/requestContentType.d.ts +4 -6
  34. package/build/types/links/RestAPILink/queryToResourcePath.d.ts +2 -1
  35. package/build/types/links/RestAPILink.d.ts +5 -8
  36. package/build/types/react/hooks/mergeAndCompareVariables.d.ts +6 -0
  37. package/build/types/react/hooks/useDataQuery.d.ts +2 -2
  38. package/package.json +2 -2
@@ -13,6 +13,7 @@ const useStaticInput = (staticValue, {
13
13
  } = {}) => {
14
14
  const originalValue = (0, _react.useRef)(staticValue);
15
15
  const [value, setValue] = (0, _react.useState)(() => originalValue.current);
16
+ (0, _react.useDebugValue)(value, debugValue => "".concat(name, ": ").concat(JSON.stringify(debugValue)));
16
17
  (0, _react.useEffect)(() => {
17
18
  if (warn && originalValue.current !== staticValue) {
18
19
  console.warn("The ".concat(name, " should be static, don't create it within the render loop!"));
@@ -11,6 +11,11 @@ describe('getMutationFetchType', () => {
11
11
  resource: 'test',
12
12
  id: 'id'
13
13
  })).toBe('delete');
14
+ expect(getMutationFetchType({
15
+ type: 'json-patch',
16
+ resource: 'test',
17
+ data: {}
18
+ })).toBe('json-patch');
14
19
  });
15
20
  it('should return `replace` for non-partial `update`', () => {
16
21
  expect(getMutationFetchType({
@@ -1,6 +1,6 @@
1
1
  import { InvalidQueryError } from '../types/InvalidQueryError';
2
2
  const validQueryKeys = ['resource', 'id', 'params', 'data'];
3
- const validTypes = ['read', 'create', 'update', 'replace', 'delete'];
3
+ const validTypes = ['read', 'create', 'update', 'replace', 'delete', 'json-patch'];
4
4
  export const getResourceQueryErrors = (type, query) => {
5
5
  if (!validTypes.includes(type)) {
6
6
  return ["Unknown query or mutation type ".concat(type)];
@@ -32,6 +32,10 @@ export const getResourceQueryErrors = (type, query) => {
32
32
  errors.push("Mutation type 'delete' does not support property 'data'");
33
33
  }
34
34
 
35
+ if (type === 'json-patch' && !Array.isArray(query.data)) {
36
+ errors.push("Mutation type 'json-patch' requires property 'data' to be of type Array");
37
+ }
38
+
35
39
  const invalidKeys = Object.keys(query).filter(k => !validQueryKeys.includes(k));
36
40
  invalidKeys.forEach(k => {
37
41
  errors.push("Property ".concat(k, " is not supported"));
@@ -73,6 +73,14 @@ describe('query validation', () => {
73
73
  expect(errors).toHaveLength(1);
74
74
  expect(errors).toMatchInlineSnapshot("\n Array [\n \"Mutation type 'delete' does not support property 'data'\",\n ]\n ");
75
75
  });
76
+ it('should fail if query is json-patch mutation with non-array data prop', () => {
77
+ const errors = getResourceQueryErrors('json-patch', {
78
+ resource: 'metadata',
79
+ data: {}
80
+ });
81
+ expect(errors).toHaveLength(1);
82
+ expect(errors).toMatchInlineSnapshot("\n Array [\n \"Mutation type 'json-patch' requires property 'data' to be of type Array\",\n ]\n ");
83
+ });
76
84
  it('should fail if unrecognized keys are passed to query', () => {
77
85
  const errors = getResourceQueryErrors('update', {
78
86
  resource: 'indicators',
@@ -25,6 +25,10 @@ export const requestContentType = (type, query) => {
25
25
  return null;
26
26
  }
27
27
 
28
+ if (type === 'json-patch') {
29
+ return 'application/json-patch+json';
30
+ }
31
+
28
32
  if (resourceExpectsTextPlain(type, query)) {
29
33
  return 'text/plain';
30
34
  }
@@ -58,7 +62,7 @@ export const requestBodyForContentType = (contentType, {
58
62
  return undefined;
59
63
  }
60
64
 
61
- if (contentType === 'application/json') {
65
+ if (contentType === 'application/json' || contentType === 'application/json-patch+json') {
62
66
  return JSON.stringify(data);
63
67
  }
64
68
 
@@ -6,6 +6,12 @@ describe('requestContentType', () => {
6
6
  data: 'test'
7
7
  })).toEqual('application/json');
8
8
  });
9
+ it('returns "application/json-patch+json" when the fetch type is "json-patch"', () => {
10
+ expect(requestContentType('json-patch', {
11
+ resource: 'test',
12
+ data: 'test'
13
+ })).toEqual('application/json-patch+json');
14
+ });
9
15
  it('returns "multipart/form-data" for a specific resource that expects it', () => {
10
16
  expect(requestContentType('create', {
11
17
  resource: 'fileResources',
@@ -9,6 +9,7 @@ const getMethod = type => {
9
9
  return 'GET';
10
10
 
11
11
  case 'update':
12
+ case 'json-patch':
12
13
  return 'PATCH';
13
14
 
14
15
  case 'replace':
@@ -16,6 +17,9 @@ const getMethod = type => {
16
17
 
17
18
  case 'delete':
18
19
  return 'DELETE';
20
+
21
+ default:
22
+ throw new Error("Unknown type ".concat(type));
19
23
  }
20
24
  };
21
25
 
@@ -26,6 +26,16 @@ describe('queryToRequestOptions', () => {
26
26
  });
27
27
  expect(options).toMatchInlineSnapshot("\n Object {\n \"body\": \"{\\\"answer\\\":42,\\\"foo\\\":\\\"bar\\\"}\",\n \"headers\": Object {\n \"Content-Type\": \"application/json\",\n },\n \"method\": \"PATCH\",\n \"signal\": undefined,\n }\n ");
28
28
  });
29
+ it('should return a valid Fetch option object for json-patch request', () => {
30
+ const options = queryToRequestOptions('json-patch', {
31
+ resource: 'test',
32
+ data: {
33
+ answer: 42,
34
+ foo: 'bar'
35
+ }
36
+ });
37
+ expect(options).toMatchInlineSnapshot("\n Object {\n \"body\": \"{\\\"answer\\\":42,\\\"foo\\\":\\\"bar\\\"}\",\n \"headers\": Object {\n \"Content-Type\": \"application/json-patch+json\",\n },\n \"method\": \"PATCH\",\n \"signal\": undefined,\n }\n ");
38
+ });
29
39
  it('should return a valid Fetch option object for replace request', () => {
30
40
  const options = queryToRequestOptions('replace', {
31
41
  resource: 'test',
@@ -55,13 +55,26 @@ const isAction = resource => resource.startsWith(actionPrefix);
55
55
 
56
56
  const makeActionPath = resource => joinPath('dhis-web-commons', "".concat(resource.substr(actionPrefix.length), ".action"));
57
57
 
58
- export const queryToResourcePath = (apiPath, query, type) => {
58
+ const skipApiVersion = (resource, config) => {
59
+ if (resource === 'tracker') {
60
+ var _config$serverVersion, _config$serverVersion2;
61
+
62
+ if (!((_config$serverVersion = config.serverVersion) !== null && _config$serverVersion !== void 0 && _config$serverVersion.minor) || ((_config$serverVersion2 = config.serverVersion) === null || _config$serverVersion2 === void 0 ? void 0 : _config$serverVersion2.minor) < 38) {
63
+ return true;
64
+ }
65
+ }
66
+
67
+ return false;
68
+ };
69
+
70
+ export const queryToResourcePath = (link, query, type) => {
59
71
  const {
60
72
  resource,
61
73
  id,
62
74
  params = {}
63
75
  } = query;
64
- const base = isAction(resource) ? makeActionPath(resource) : joinPath(apiPath, resource, id);
76
+ const apiBase = skipApiVersion(resource, link.config) ? link.unversionedApiPath : link.versionedApiPath;
77
+ const base = isAction(resource) ? makeActionPath(resource) : joinPath(apiBase, resource, id);
65
78
  validateResourceQuery(query, type);
66
79
 
67
80
  if (Object.keys(params).length) {
@@ -1,5 +1,19 @@
1
+ import { RestAPILink } from '../RestAPILink';
1
2
  import { queryToResourcePath } from './queryToResourcePath';
2
- const apiPath = '<api>';
3
+
4
+ const createLink = config => new RestAPILink(config);
5
+
6
+ const defaultConfig = {
7
+ basePath: '<base>',
8
+ apiVersion: '37',
9
+ serverVersion: {
10
+ major: 2,
11
+ minor: 37,
12
+ patch: 11
13
+ }
14
+ };
15
+ const link = createLink(defaultConfig);
16
+ const apiPath = link.versionedApiPath;
3
17
  const actionPrefix = "dhis-web-commons/";
4
18
  const actionPostfix = '.action';
5
19
  describe('queryToResourcePath', () => {
@@ -8,7 +22,7 @@ describe('queryToResourcePath', () => {
8
22
  const query = {
9
23
  resource: 'action::test'
10
24
  };
11
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(actionPrefix, "test").concat(actionPostfix));
25
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(actionPrefix, "test").concat(actionPostfix));
12
26
  });
13
27
  it('should return action URL with a simple querystring if query parameters are passed', () => {
14
28
  const query = {
@@ -17,7 +31,7 @@ describe('queryToResourcePath', () => {
17
31
  key: 'value'
18
32
  }
19
33
  };
20
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(actionPrefix, "test").concat(actionPostfix, "?key=value"));
34
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(actionPrefix, "test").concat(actionPostfix, "?key=value"));
21
35
  });
22
36
  });
23
37
  describe('resource with dot', () => {
@@ -25,14 +39,14 @@ describe('queryToResourcePath', () => {
25
39
  const query = {
26
40
  resource: 'svg.pdf'
27
41
  };
28
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/svg.pdf"));
42
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/svg.pdf"));
29
43
  });
30
44
  });
31
45
  it('should return resource url with no querystring if not query parameters are passed', () => {
32
46
  const query = {
33
47
  resource: 'test'
34
48
  };
35
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test"));
49
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test"));
36
50
  });
37
51
  it('should return resource url and singular parameter separated by ?', () => {
38
52
  const query = {
@@ -41,7 +55,7 @@ describe('queryToResourcePath', () => {
41
55
  key: 'value'
42
56
  }
43
57
  };
44
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key=value"));
58
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value"));
45
59
  });
46
60
  it('should return resource url and multiple parameters separated by ? and &', () => {
47
61
  const query = {
@@ -51,7 +65,7 @@ describe('queryToResourcePath', () => {
51
65
  param: 'value2'
52
66
  }
53
67
  };
54
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key=value&param=value2"));
68
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value&param=value2"));
55
69
  });
56
70
  it('should url encode special characters in query keys', () => {
57
71
  const query = {
@@ -60,7 +74,7 @@ describe('queryToResourcePath', () => {
60
74
  'key=42&val': 'value'
61
75
  }
62
76
  };
63
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key%3D42%26val=value"));
77
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key%3D42%26val=value"));
64
78
  });
65
79
  it('should url encode special characters in string parameters', () => {
66
80
  const query = {
@@ -70,7 +84,7 @@ describe('queryToResourcePath', () => {
70
84
  param: 'value2&& 53'
71
85
  }
72
86
  };
73
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key=value%3F%3D42&param=value2%26%26%2053"));
87
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value%3F%3D42&param=value2%26%26%2053"));
74
88
  });
75
89
  it('should support numeric (integer and float) parameters', () => {
76
90
  const query = {
@@ -80,7 +94,7 @@ describe('queryToResourcePath', () => {
80
94
  param: 193.75
81
95
  }
82
96
  };
83
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key=42&param=193.75"));
97
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=42&param=193.75"));
84
98
  });
85
99
  it('should support boolean parameters', () => {
86
100
  const query = {
@@ -90,7 +104,7 @@ describe('queryToResourcePath', () => {
90
104
  someflag: true
91
105
  }
92
106
  };
93
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key=42&someflag=true"));
107
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=42&someflag=true"));
94
108
  });
95
109
  it('should join array parameters with commas', () => {
96
110
  const query = {
@@ -99,7 +113,7 @@ describe('queryToResourcePath', () => {
99
113
  key: ['asdf', 123]
100
114
  }
101
115
  };
102
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?key=asdf,123"));
116
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=asdf,123"));
103
117
  });
104
118
  it('should include multiple filter parameters when array of filters provided', () => {
105
119
  const query = {
@@ -108,7 +122,7 @@ describe('queryToResourcePath', () => {
108
122
  filter: ['asdf', 123]
109
123
  }
110
124
  };
111
- expect(queryToResourcePath(apiPath, query)).toBe("".concat(apiPath, "/test?filter=asdf&filter=123"));
125
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?filter=asdf&filter=123"));
112
126
  });
113
127
  it('should NOT YET support name-aliased parameters', () => {
114
128
  const query = {
@@ -119,7 +133,7 @@ describe('queryToResourcePath', () => {
119
133
  }
120
134
  }
121
135
  };
122
- expect(() => queryToResourcePath(apiPath, query)).toThrow();
136
+ expect(() => queryToResourcePath(link, query, 'read')).toThrow();
123
137
  });
124
138
  it('should throw if passed something crazy like a function', () => {
125
139
  const query = {
@@ -128,6 +142,25 @@ describe('queryToResourcePath', () => {
128
142
  key: a => a
129
143
  }
130
144
  };
131
- expect(() => queryToResourcePath(apiPath, query)).toThrow();
145
+ expect(() => queryToResourcePath(link, query, 'read')).toThrow();
146
+ });
147
+ it('should return an unversioned endpoint for the new tracker importer (in version 2.37)', () => {
148
+ const query = {
149
+ resource: 'tracker'
150
+ };
151
+ expect(queryToResourcePath(link, query, 'read')).toBe("".concat(link.unversionedApiPath, "/tracker"));
152
+ });
153
+ it('should return a VERSIONED endpoint for the new tracker importer (in version 2.38)', () => {
154
+ const query = {
155
+ resource: 'tracker'
156
+ };
157
+ const v38config = { ...defaultConfig,
158
+ serverVersion: {
159
+ major: 2,
160
+ minor: 38,
161
+ patch: 0
162
+ }
163
+ };
164
+ expect(queryToResourcePath(createLink(v38config), query, 'read')).toBe("".concat(link.versionedApiPath, "/tracker"));
132
165
  });
133
166
  });
@@ -5,29 +5,26 @@ import { joinPath } from './RestAPILink/path';
5
5
  import { queryToRequestOptions } from './RestAPILink/queryToRequestOptions';
6
6
  import { queryToResourcePath } from './RestAPILink/queryToResourcePath';
7
7
  export class RestAPILink {
8
- constructor({
9
- baseUrl,
10
- apiVersion
11
- }) {
12
- _defineProperty(this, "apiPath", void 0);
8
+ constructor(config) {
9
+ _defineProperty(this, "config", void 0);
13
10
 
14
- _defineProperty(this, "baseUrl", void 0);
11
+ _defineProperty(this, "versionedApiPath", void 0);
15
12
 
16
- _defineProperty(this, "apiVersion", void 0);
13
+ _defineProperty(this, "unversionedApiPath", void 0);
17
14
 
18
- this.baseUrl = baseUrl;
19
- this.apiVersion = apiVersion;
20
- this.apiPath = joinPath('api', String(apiVersion));
15
+ this.config = config;
16
+ this.versionedApiPath = joinPath('api', String(config.apiVersion));
17
+ this.unversionedApiPath = joinPath('api');
21
18
  }
22
19
 
23
20
  fetch(path, options) {
24
- return fetchData(joinPath(this.baseUrl, path), options);
21
+ return fetchData(joinPath(this.config.baseUrl, path), options);
25
22
  }
26
23
 
27
24
  executeResourceQuery(type, query, {
28
25
  signal
29
26
  }) {
30
- return this.fetch(queryToResourcePath(this.apiPath, query, type), queryToRequestOptions(type, query, signal));
27
+ return this.fetch(queryToResourcePath(this, query, type), queryToRequestOptions(type, query, signal));
31
28
  }
32
29
 
33
30
  }
@@ -0,0 +1,23 @@
1
+ import { stableVariablesHash } from './stableVariablesHash';
2
+ export const mergeAndCompareVariables = (previousVariables, newVariables, previousHash) => {
3
+ if (!newVariables) {
4
+ return {
5
+ identical: true,
6
+ mergedVariablesHash: previousHash,
7
+ mergedVariables: previousVariables
8
+ };
9
+ } // Use cached hash if it exists
10
+
11
+
12
+ const currentHash = previousHash || stableVariablesHash(previousVariables);
13
+ const mergedVariables = { ...previousVariables,
14
+ ...newVariables
15
+ };
16
+ const mergedVariablesHash = stableVariablesHash(mergedVariables);
17
+ const identical = currentHash === mergedVariablesHash;
18
+ return {
19
+ identical,
20
+ mergedVariablesHash,
21
+ mergedVariables
22
+ };
23
+ };
@@ -0,0 +1,53 @@
1
+ import { mergeAndCompareVariables } from './mergeAndCompareVariables';
2
+ import { stableVariablesHash } from './stableVariablesHash';
3
+ jest.mock('./stableVariablesHash', () => ({
4
+ stableVariablesHash: object => JSON.stringify(object)
5
+ }));
6
+ const testVariables = {
7
+ question: 'What do you get when you multiply six by nine?',
8
+ answer: 42
9
+ };
10
+ const testHash = stableVariablesHash(testVariables);
11
+ describe('mergeAndCompareVariables', () => {
12
+ it('Should return previous variables and hash when no new variables are provided', () => {
13
+ expect(mergeAndCompareVariables(testVariables, undefined, undefined)).toMatchObject({
14
+ identical: true,
15
+ mergedVariables: testVariables,
16
+ mergedVariablesHash: undefined
17
+ });
18
+ });
19
+ it('Should return identical: true when merged variables are identical to old variables (without prev hash)', () => {
20
+ const newVariables = {
21
+ answer: testVariables.answer
22
+ };
23
+ expect(mergeAndCompareVariables(testVariables, newVariables, undefined)).toMatchObject({
24
+ identical: true,
25
+ mergedVariables: testVariables,
26
+ mergedVariablesHash: testHash
27
+ });
28
+ });
29
+ it('Should return identical: false with incorrect previous hash', () => {
30
+ const incorrectPreviousHash = 'IAmAHash';
31
+ const newVariables = {
32
+ answer: 42
33
+ };
34
+ expect(mergeAndCompareVariables(testVariables, newVariables, incorrectPreviousHash)).toMatchObject({
35
+ identical: false,
36
+ mergedVariables: testVariables,
37
+ mergedVariablesHash: testHash
38
+ });
39
+ });
40
+ it('Should return identical: false when merged variables are different than old variables', () => {
41
+ const newVariables = {
42
+ answer: 43
43
+ };
44
+ const expectedMergedVariables = { ...testVariables,
45
+ ...newVariables
46
+ };
47
+ expect(mergeAndCompareVariables(testVariables, newVariables, testHash)).toMatchObject({
48
+ identical: false,
49
+ mergedVariables: expectedMergedVariables,
50
+ mergedVariablesHash: stableVariablesHash(expectedMergedVariables)
51
+ });
52
+ });
53
+ });
@@ -1,6 +1,6 @@
1
- import { useState, useRef, useCallback } from 'react';
1
+ import { useState, useRef, useCallback, useDebugValue } from 'react';
2
2
  import { useQuery, setLogger } from 'react-query';
3
- import { stableVariablesHash } from './stableVariablesHash';
3
+ import { mergeAndCompareVariables } from './mergeAndCompareVariables';
4
4
  import { useDataEngine } from './useDataEngine';
5
5
  import { useStaticInput } from './useStaticInput';
6
6
 
@@ -23,24 +23,35 @@ export const useDataQuery = (query, {
23
23
  variables: initialVariables = {},
24
24
  lazy: initialLazy = false
25
25
  } = {}) => {
26
- const variablesHash = useRef(null);
27
- const [variables, setVariables] = useState(initialVariables);
28
- const [enabled, setEnabled] = useState(!initialLazy);
29
26
  const [staticQuery] = useStaticInput(query, {
30
27
  warn: true,
31
28
  name: 'query'
32
29
  });
30
+ const [variablesUpdateCount, setVariablesUpdateCount] = useState(0);
31
+ const queryState = useRef({
32
+ variables: initialVariables,
33
+ variablesHash: undefined,
34
+ enabled: !initialLazy,
35
+ refetchCallback: undefined
36
+ });
33
37
  /**
34
- * User callbacks and refetch handling
38
+ * Display current query state and refetch count in React DevTools
35
39
  */
36
40
 
37
- const refetchCallback = useRef(null);
41
+ useDebugValue({
42
+ variablesUpdateCount,
43
+ enabled: queryState.current.enabled,
44
+ variables: queryState.current.variables
45
+ }, debugValue => JSON.stringify(debugValue));
46
+ /**
47
+ * User callbacks and refetch handling
48
+ */
38
49
 
39
50
  const onSuccess = data => {
40
- if (refetchCallback.current) {
41
- refetchCallback.current(data);
42
- refetchCallback.current = null;
43
- }
51
+ var _queryState$current$r, _queryState$current;
52
+
53
+ (_queryState$current$r = (_queryState$current = queryState.current).refetchCallback) === null || _queryState$current$r === void 0 ? void 0 : _queryState$current$r.call(_queryState$current, data);
54
+ queryState.current.refetchCallback = undefined;
44
55
 
45
56
  if (userOnSuccess) {
46
57
  userOnSuccess(data);
@@ -49,9 +60,7 @@ export const useDataQuery = (query, {
49
60
 
50
61
  const onError = error => {
51
62
  // If we'd want to reject on errors we'd call the cb with the error here
52
- if (refetchCallback.current) {
53
- refetchCallback.current = null;
54
- }
63
+ queryState.current.refetchCallback = undefined;
55
64
 
56
65
  if (userOnError) {
57
66
  userOnError(error);
@@ -63,10 +72,10 @@ export const useDataQuery = (query, {
63
72
 
64
73
 
65
74
  const engine = useDataEngine();
66
- const queryKey = [staticQuery, variables];
75
+ const queryKey = [staticQuery, queryState.current.variables];
67
76
 
68
77
  const queryFn = () => engine.query(staticQuery, {
69
- variables
78
+ variables: queryState.current.variables
70
79
  });
71
80
 
72
81
  const {
@@ -77,7 +86,7 @@ export const useDataQuery = (query, {
77
86
  data,
78
87
  refetch: queryRefetch
79
88
  } = useQuery(queryKey, queryFn, {
80
- enabled,
89
+ enabled: queryState.current.enabled,
81
90
  onSuccess,
82
91
  onError
83
92
  });
@@ -91,11 +100,17 @@ export const useDataQuery = (query, {
91
100
  */
92
101
 
93
102
  const refetch = useCallback(newVariables => {
103
+ const {
104
+ identical,
105
+ mergedVariables,
106
+ mergedVariablesHash
107
+ } = mergeAndCompareVariables(queryState.current.variables, newVariables, queryState.current.variablesHash);
94
108
  /**
95
109
  * If there are no updates that will trigger an automatic refetch
96
110
  * we'll need to call react-query's refetch directly
97
111
  */
98
- if (enabled && !newVariables) {
112
+
113
+ if (queryState.current.enabled && identical) {
99
114
  return queryRefetch({
100
115
  cancelRefetch: true,
101
116
  throwOnError: false
@@ -104,44 +119,19 @@ export const useDataQuery = (query, {
104
119
  }) => data);
105
120
  }
106
121
 
107
- if (newVariables) {
108
- // Use cached hash if it exists
109
- const currentHash = variablesHash.current || stableVariablesHash(variables);
110
- const mergedVariables = { ...variables,
111
- ...newVariables
112
- };
113
- const mergedHash = stableVariablesHash(mergedVariables);
114
- const identical = currentHash === mergedHash;
115
-
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
- */
121
- return queryRefetch({
122
- cancelRefetch: true,
123
- throwOnError: false
124
- }).then(({
125
- data
126
- }) => data);
127
- } else {
128
- variablesHash.current = mergedHash;
129
- setVariables(mergedVariables);
130
- }
131
- } // Enable the query after the variables have been set to prevent extra request
132
-
133
-
134
- if (!enabled) {
135
- setEnabled(true);
136
- } // This promise does not currently reject on errors
137
-
138
-
139
- return new Promise(resolve => {
140
- refetchCallback.current = data => {
122
+ queryState.current.variables = mergedVariables;
123
+ queryState.current.variablesHash = mergedVariablesHash;
124
+ queryState.current.enabled = true; // This promise does not currently reject on errors
125
+
126
+ const refetchPromise = new Promise(resolve => {
127
+ queryState.current.refetchCallback = data => {
141
128
  resolve(data);
142
129
  };
143
- });
144
- }, [enabled, queryRefetch, variables]);
130
+ }); // Trigger a react-query refetch by incrementing variablesUpdateCount state
131
+
132
+ setVariablesUpdateCount(prevCount => prevCount + 1);
133
+ return refetchPromise;
134
+ }, [queryRefetch]);
145
135
  /**
146
136
  * react-query returns null or an error, but we return undefined
147
137
  * or an error, so this ensures consistency with the other types.
@@ -497,6 +497,65 @@ describe('useDataQuery', () => {
497
497
  });
498
498
  });
499
499
  describe('return values: refetch', () => {
500
+ it('Should be stable if the query variables change', async () => {
501
+ let count = 0;
502
+ const spy = jest.fn(() => {
503
+ count++;
504
+ return count;
505
+ });
506
+ const data = {
507
+ answer: spy
508
+ };
509
+ const query = {
510
+ x: {
511
+ resource: 'answer'
512
+ }
513
+ };
514
+
515
+ const wrapper = ({
516
+ children
517
+ }) => /*#__PURE__*/React.createElement(CustomDataProvider, {
518
+ data: data
519
+ }, children);
520
+
521
+ const {
522
+ result,
523
+ waitFor
524
+ } = renderHook(() => useDataQuery(query, {
525
+ lazy: true
526
+ }), {
527
+ wrapper
528
+ });
529
+ expect(spy).not.toHaveBeenCalled();
530
+ const initialRefetch = result.current.refetch;
531
+ act(() => {
532
+ initialRefetch();
533
+ });
534
+ await waitFor(() => {
535
+ expect(result.current).toMatchObject({
536
+ loading: false,
537
+ called: true,
538
+ data: {
539
+ x: 1
540
+ }
541
+ });
542
+ });
543
+ expect(spy).toHaveBeenCalledTimes(1);
544
+ act(() => {
545
+ initialRefetch();
546
+ });
547
+ await waitFor(() => {
548
+ expect(result.current).toMatchObject({
549
+ loading: false,
550
+ called: true,
551
+ data: {
552
+ x: 2
553
+ }
554
+ });
555
+ });
556
+ expect(spy).toHaveBeenCalledTimes(2);
557
+ expect(initialRefetch).toBe(result.current.refetch);
558
+ });
500
559
  it('Should only trigger a single request when refetch is called on a lazy query with new variables', async () => {
501
560
  const spy = jest.fn((type, query) => {
502
561
  if (query.id === '1') {