@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.
- package/build/cjs/engine/helpers/getMutationFetchType.test.js +5 -0
- package/build/cjs/engine/helpers/validate.js +5 -1
- package/build/cjs/engine/helpers/validate.test.js +8 -0
- package/build/cjs/links/RestAPILink/queryToRequestOptions/requestContentType.js +5 -1
- package/build/cjs/links/RestAPILink/queryToRequestOptions/requestContentType.test.js +6 -0
- package/build/cjs/links/RestAPILink/queryToRequestOptions.js +4 -0
- package/build/cjs/links/RestAPILink/queryToRequestOptions.test.js +10 -0
- package/build/cjs/links/RestAPILink/queryToResourcePath.js +15 -2
- package/build/cjs/links/RestAPILink/queryToResourcePath.test.js +48 -15
- package/build/cjs/links/RestAPILink.js +9 -12
- package/build/cjs/react/hooks/mergeAndCompareVariables.js +33 -0
- package/build/cjs/react/hooks/mergeAndCompareVariables.test.js +57 -0
- package/build/cjs/react/hooks/useDataQuery.js +43 -53
- package/build/cjs/react/hooks/useDataQuery.test.js +59 -0
- package/build/cjs/react/hooks/useStaticInput.js +1 -0
- package/build/es/engine/helpers/getMutationFetchType.test.js +5 -0
- package/build/es/engine/helpers/validate.js +5 -1
- package/build/es/engine/helpers/validate.test.js +8 -0
- package/build/es/links/RestAPILink/queryToRequestOptions/requestContentType.js +5 -1
- package/build/es/links/RestAPILink/queryToRequestOptions/requestContentType.test.js +6 -0
- package/build/es/links/RestAPILink/queryToRequestOptions.js +4 -0
- package/build/es/links/RestAPILink/queryToRequestOptions.test.js +10 -0
- package/build/es/links/RestAPILink/queryToResourcePath.js +15 -2
- package/build/es/links/RestAPILink/queryToResourcePath.test.js +48 -15
- package/build/es/links/RestAPILink.js +9 -12
- package/build/es/react/hooks/mergeAndCompareVariables.js +23 -0
- package/build/es/react/hooks/mergeAndCompareVariables.test.js +53 -0
- package/build/es/react/hooks/useDataQuery.js +44 -54
- package/build/es/react/hooks/useDataQuery.test.js +59 -0
- package/build/es/react/hooks/useStaticInput.js +2 -1
- package/build/types/engine/types/ExecuteOptions.d.ts +1 -1
- package/build/types/engine/types/Mutation.d.ts +2 -2
- package/build/types/links/RestAPILink/queryToRequestOptions/requestContentType.d.ts +4 -6
- package/build/types/links/RestAPILink/queryToResourcePath.d.ts +2 -1
- package/build/types/links/RestAPILink.d.ts +5 -8
- package/build/types/react/hooks/mergeAndCompareVariables.d.ts +6 -0
- package/build/types/react/hooks/useDataQuery.d.ts +2 -2
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
68
|
+
expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value¶m=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(
|
|
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(
|
|
87
|
+
expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value%3F%3D42¶m=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(
|
|
97
|
+
expect(queryToResourcePath(link, query, 'read')).toBe("".concat(apiPath, "/test?key=42¶m=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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
10
|
-
apiVersion
|
|
11
|
-
}) {
|
|
12
|
-
_defineProperty(this, "apiPath", void 0);
|
|
8
|
+
constructor(config) {
|
|
9
|
+
_defineProperty(this, "config", void 0);
|
|
13
10
|
|
|
14
|
-
_defineProperty(this, "
|
|
11
|
+
_defineProperty(this, "versionedApiPath", void 0);
|
|
15
12
|
|
|
16
|
-
_defineProperty(this, "
|
|
13
|
+
_defineProperty(this, "unversionedApiPath", void 0);
|
|
17
14
|
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
38
|
+
* Display current query state and refetch count in React DevTools
|
|
35
39
|
*/
|
|
36
40
|
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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') {
|