@dhis2/app-service-data 3.4.0 → 3.4.3
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/links/RestAPILink/queryToResourcePath.js +15 -2
- package/build/cjs/links/RestAPILink/queryToResourcePath.test.js +54 -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/links/RestAPILink/queryToResourcePath.js +15 -2
- package/build/es/links/RestAPILink/queryToResourcePath.test.js +54 -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/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
|
@@ -63,13 +63,26 @@ const isAction = resource => resource.startsWith(actionPrefix);
|
|
|
63
63
|
|
|
64
64
|
const makeActionPath = resource => (0, _path.joinPath)('dhis-web-commons', "".concat(resource.substr(actionPrefix.length), ".action"));
|
|
65
65
|
|
|
66
|
-
const
|
|
66
|
+
const skipApiVersion = (resource, config) => {
|
|
67
|
+
if (resource === 'tracker' || resource.startsWith('tracker/')) {
|
|
68
|
+
var _config$serverVersion, _config$serverVersion2;
|
|
69
|
+
|
|
70
|
+
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) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const queryToResourcePath = (link, query, type) => {
|
|
67
79
|
const {
|
|
68
80
|
resource,
|
|
69
81
|
id,
|
|
70
82
|
params = {}
|
|
71
83
|
} = query;
|
|
72
|
-
const
|
|
84
|
+
const apiBase = skipApiVersion(resource, link.config) ? link.unversionedApiPath : link.versionedApiPath;
|
|
85
|
+
const base = isAction(resource) ? makeActionPath(resource) : (0, _path.joinPath)(apiBase, resource, id);
|
|
73
86
|
(0, _validateQuery.validateResourceQuery)(query, type);
|
|
74
87
|
|
|
75
88
|
if (Object.keys(params).length) {
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _RestAPILink = require("../RestAPILink");
|
|
4
|
+
|
|
3
5
|
var _queryToResourcePath = require("./queryToResourcePath");
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const createLink = config => new _RestAPILink.RestAPILink(config);
|
|
8
|
+
|
|
9
|
+
const defaultConfig = {
|
|
10
|
+
basePath: '<base>',
|
|
11
|
+
apiVersion: '37',
|
|
12
|
+
serverVersion: {
|
|
13
|
+
major: 2,
|
|
14
|
+
minor: 37,
|
|
15
|
+
patch: 11
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const link = createLink(defaultConfig);
|
|
19
|
+
const apiPath = link.versionedApiPath;
|
|
6
20
|
const actionPrefix = "dhis-web-commons/";
|
|
7
21
|
const actionPostfix = '.action';
|
|
8
22
|
describe('queryToResourcePath', () => {
|
|
@@ -11,7 +25,7 @@ describe('queryToResourcePath', () => {
|
|
|
11
25
|
const query = {
|
|
12
26
|
resource: 'action::test'
|
|
13
27
|
};
|
|
14
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
28
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(actionPrefix, "test").concat(actionPostfix));
|
|
15
29
|
});
|
|
16
30
|
it('should return action URL with a simple querystring if query parameters are passed', () => {
|
|
17
31
|
const query = {
|
|
@@ -20,7 +34,7 @@ describe('queryToResourcePath', () => {
|
|
|
20
34
|
key: 'value'
|
|
21
35
|
}
|
|
22
36
|
};
|
|
23
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
37
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(actionPrefix, "test").concat(actionPostfix, "?key=value"));
|
|
24
38
|
});
|
|
25
39
|
});
|
|
26
40
|
describe('resource with dot', () => {
|
|
@@ -28,14 +42,14 @@ describe('queryToResourcePath', () => {
|
|
|
28
42
|
const query = {
|
|
29
43
|
resource: 'svg.pdf'
|
|
30
44
|
};
|
|
31
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
45
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/svg.pdf"));
|
|
32
46
|
});
|
|
33
47
|
});
|
|
34
48
|
it('should return resource url with no querystring if not query parameters are passed', () => {
|
|
35
49
|
const query = {
|
|
36
50
|
resource: 'test'
|
|
37
51
|
};
|
|
38
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
52
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test"));
|
|
39
53
|
});
|
|
40
54
|
it('should return resource url and singular parameter separated by ?', () => {
|
|
41
55
|
const query = {
|
|
@@ -44,7 +58,7 @@ describe('queryToResourcePath', () => {
|
|
|
44
58
|
key: 'value'
|
|
45
59
|
}
|
|
46
60
|
};
|
|
47
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
61
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value"));
|
|
48
62
|
});
|
|
49
63
|
it('should return resource url and multiple parameters separated by ? and &', () => {
|
|
50
64
|
const query = {
|
|
@@ -54,7 +68,7 @@ describe('queryToResourcePath', () => {
|
|
|
54
68
|
param: 'value2'
|
|
55
69
|
}
|
|
56
70
|
};
|
|
57
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
71
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value¶m=value2"));
|
|
58
72
|
});
|
|
59
73
|
it('should url encode special characters in query keys', () => {
|
|
60
74
|
const query = {
|
|
@@ -63,7 +77,7 @@ describe('queryToResourcePath', () => {
|
|
|
63
77
|
'key=42&val': 'value'
|
|
64
78
|
}
|
|
65
79
|
};
|
|
66
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
80
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key%3D42%26val=value"));
|
|
67
81
|
});
|
|
68
82
|
it('should url encode special characters in string parameters', () => {
|
|
69
83
|
const query = {
|
|
@@ -73,7 +87,7 @@ describe('queryToResourcePath', () => {
|
|
|
73
87
|
param: 'value2&& 53'
|
|
74
88
|
}
|
|
75
89
|
};
|
|
76
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
90
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key=value%3F%3D42¶m=value2%26%26%2053"));
|
|
77
91
|
});
|
|
78
92
|
it('should support numeric (integer and float) parameters', () => {
|
|
79
93
|
const query = {
|
|
@@ -83,7 +97,7 @@ describe('queryToResourcePath', () => {
|
|
|
83
97
|
param: 193.75
|
|
84
98
|
}
|
|
85
99
|
};
|
|
86
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
100
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key=42¶m=193.75"));
|
|
87
101
|
});
|
|
88
102
|
it('should support boolean parameters', () => {
|
|
89
103
|
const query = {
|
|
@@ -93,7 +107,7 @@ describe('queryToResourcePath', () => {
|
|
|
93
107
|
someflag: true
|
|
94
108
|
}
|
|
95
109
|
};
|
|
96
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
110
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key=42&someflag=true"));
|
|
97
111
|
});
|
|
98
112
|
it('should join array parameters with commas', () => {
|
|
99
113
|
const query = {
|
|
@@ -102,7 +116,7 @@ describe('queryToResourcePath', () => {
|
|
|
102
116
|
key: ['asdf', 123]
|
|
103
117
|
}
|
|
104
118
|
};
|
|
105
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
119
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?key=asdf,123"));
|
|
106
120
|
});
|
|
107
121
|
it('should include multiple filter parameters when array of filters provided', () => {
|
|
108
122
|
const query = {
|
|
@@ -111,7 +125,7 @@ describe('queryToResourcePath', () => {
|
|
|
111
125
|
filter: ['asdf', 123]
|
|
112
126
|
}
|
|
113
127
|
};
|
|
114
|
-
expect((0, _queryToResourcePath.queryToResourcePath)(
|
|
128
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(apiPath, "/test?filter=asdf&filter=123"));
|
|
115
129
|
});
|
|
116
130
|
it('should NOT YET support name-aliased parameters', () => {
|
|
117
131
|
const query = {
|
|
@@ -122,7 +136,7 @@ describe('queryToResourcePath', () => {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
};
|
|
125
|
-
expect(() => (0, _queryToResourcePath.queryToResourcePath)(
|
|
139
|
+
expect(() => (0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toThrow();
|
|
126
140
|
});
|
|
127
141
|
it('should throw if passed something crazy like a function', () => {
|
|
128
142
|
const query = {
|
|
@@ -131,6 +145,31 @@ describe('queryToResourcePath', () => {
|
|
|
131
145
|
key: a => a
|
|
132
146
|
}
|
|
133
147
|
};
|
|
134
|
-
expect(() => (0, _queryToResourcePath.queryToResourcePath)(
|
|
148
|
+
expect(() => (0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toThrow();
|
|
149
|
+
});
|
|
150
|
+
it('should return an unversioned endpoint for the new tracker importer (in version 2.37)', () => {
|
|
151
|
+
const query = {
|
|
152
|
+
resource: 'tracker'
|
|
153
|
+
};
|
|
154
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(link.unversionedApiPath, "/tracker"));
|
|
155
|
+
});
|
|
156
|
+
it('should return an unversioned endpoint sub-resources of the new tracker importer (in version 2.37)', () => {
|
|
157
|
+
const query = {
|
|
158
|
+
resource: 'tracker/test'
|
|
159
|
+
};
|
|
160
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(link.unversionedApiPath, "/tracker/test"));
|
|
161
|
+
});
|
|
162
|
+
it('should return a VERSIONED endpoint for the new tracker importer (in version 2.38)', () => {
|
|
163
|
+
const query = {
|
|
164
|
+
resource: 'tracker'
|
|
165
|
+
};
|
|
166
|
+
const v38config = { ...defaultConfig,
|
|
167
|
+
serverVersion: {
|
|
168
|
+
major: 2,
|
|
169
|
+
minor: 38,
|
|
170
|
+
patch: 0
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
expect((0, _queryToResourcePath.queryToResourcePath)(createLink(v38config), query, 'read')).toBe("".concat(link.versionedApiPath, "/tracker"));
|
|
135
174
|
});
|
|
136
175
|
});
|
|
@@ -16,29 +16,26 @@ var _queryToResourcePath = require("./RestAPILink/queryToResourcePath");
|
|
|
16
16
|
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
17
17
|
|
|
18
18
|
class RestAPILink {
|
|
19
|
-
constructor({
|
|
20
|
-
|
|
21
|
-
apiVersion
|
|
22
|
-
}) {
|
|
23
|
-
_defineProperty(this, "apiPath", void 0);
|
|
19
|
+
constructor(config) {
|
|
20
|
+
_defineProperty(this, "config", void 0);
|
|
24
21
|
|
|
25
|
-
_defineProperty(this, "
|
|
22
|
+
_defineProperty(this, "versionedApiPath", void 0);
|
|
26
23
|
|
|
27
|
-
_defineProperty(this, "
|
|
24
|
+
_defineProperty(this, "unversionedApiPath", void 0);
|
|
28
25
|
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.versionedApiPath = (0, _path.joinPath)('api', String(config.apiVersion));
|
|
28
|
+
this.unversionedApiPath = (0, _path.joinPath)('api');
|
|
32
29
|
}
|
|
33
30
|
|
|
34
31
|
fetch(path, options) {
|
|
35
|
-
return (0, _fetchData.fetchData)((0, _path.joinPath)(this.baseUrl, path), options);
|
|
32
|
+
return (0, _fetchData.fetchData)((0, _path.joinPath)(this.config.baseUrl, path), options);
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
executeResourceQuery(type, query, {
|
|
39
36
|
signal
|
|
40
37
|
}) {
|
|
41
|
-
return this.fetch((0, _queryToResourcePath.queryToResourcePath)(this
|
|
38
|
+
return this.fetch((0, _queryToResourcePath.queryToResourcePath)(this, query, type), (0, _queryToRequestOptions.queryToRequestOptions)(type, query, signal));
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.mergeAndCompareVariables = void 0;
|
|
7
|
+
|
|
8
|
+
var _stableVariablesHash = require("./stableVariablesHash");
|
|
9
|
+
|
|
10
|
+
const mergeAndCompareVariables = (previousVariables, newVariables, previousHash) => {
|
|
11
|
+
if (!newVariables) {
|
|
12
|
+
return {
|
|
13
|
+
identical: true,
|
|
14
|
+
mergedVariablesHash: previousHash,
|
|
15
|
+
mergedVariables: previousVariables
|
|
16
|
+
};
|
|
17
|
+
} // Use cached hash if it exists
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
const currentHash = previousHash || (0, _stableVariablesHash.stableVariablesHash)(previousVariables);
|
|
21
|
+
const mergedVariables = { ...previousVariables,
|
|
22
|
+
...newVariables
|
|
23
|
+
};
|
|
24
|
+
const mergedVariablesHash = (0, _stableVariablesHash.stableVariablesHash)(mergedVariables);
|
|
25
|
+
const identical = currentHash === mergedVariablesHash;
|
|
26
|
+
return {
|
|
27
|
+
identical,
|
|
28
|
+
mergedVariablesHash,
|
|
29
|
+
mergedVariables
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
exports.mergeAndCompareVariables = mergeAndCompareVariables;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _mergeAndCompareVariables = require("./mergeAndCompareVariables");
|
|
4
|
+
|
|
5
|
+
var _stableVariablesHash = require("./stableVariablesHash");
|
|
6
|
+
|
|
7
|
+
jest.mock('./stableVariablesHash', () => ({
|
|
8
|
+
stableVariablesHash: object => JSON.stringify(object)
|
|
9
|
+
}));
|
|
10
|
+
const testVariables = {
|
|
11
|
+
question: 'What do you get when you multiply six by nine?',
|
|
12
|
+
answer: 42
|
|
13
|
+
};
|
|
14
|
+
const testHash = (0, _stableVariablesHash.stableVariablesHash)(testVariables);
|
|
15
|
+
describe('mergeAndCompareVariables', () => {
|
|
16
|
+
it('Should return previous variables and hash when no new variables are provided', () => {
|
|
17
|
+
expect((0, _mergeAndCompareVariables.mergeAndCompareVariables)(testVariables, undefined, undefined)).toMatchObject({
|
|
18
|
+
identical: true,
|
|
19
|
+
mergedVariables: testVariables,
|
|
20
|
+
mergedVariablesHash: undefined
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
it('Should return identical: true when merged variables are identical to old variables (without prev hash)', () => {
|
|
24
|
+
const newVariables = {
|
|
25
|
+
answer: testVariables.answer
|
|
26
|
+
};
|
|
27
|
+
expect((0, _mergeAndCompareVariables.mergeAndCompareVariables)(testVariables, newVariables, undefined)).toMatchObject({
|
|
28
|
+
identical: true,
|
|
29
|
+
mergedVariables: testVariables,
|
|
30
|
+
mergedVariablesHash: testHash
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('Should return identical: false with incorrect previous hash', () => {
|
|
34
|
+
const incorrectPreviousHash = 'IAmAHash';
|
|
35
|
+
const newVariables = {
|
|
36
|
+
answer: 42
|
|
37
|
+
};
|
|
38
|
+
expect((0, _mergeAndCompareVariables.mergeAndCompareVariables)(testVariables, newVariables, incorrectPreviousHash)).toMatchObject({
|
|
39
|
+
identical: false,
|
|
40
|
+
mergedVariables: testVariables,
|
|
41
|
+
mergedVariablesHash: testHash
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('Should return identical: false when merged variables are different than old variables', () => {
|
|
45
|
+
const newVariables = {
|
|
46
|
+
answer: 43
|
|
47
|
+
};
|
|
48
|
+
const expectedMergedVariables = { ...testVariables,
|
|
49
|
+
...newVariables
|
|
50
|
+
};
|
|
51
|
+
expect((0, _mergeAndCompareVariables.mergeAndCompareVariables)(testVariables, newVariables, testHash)).toMatchObject({
|
|
52
|
+
identical: false,
|
|
53
|
+
mergedVariables: expectedMergedVariables,
|
|
54
|
+
mergedVariablesHash: (0, _stableVariablesHash.stableVariablesHash)(expectedMergedVariables)
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -9,7 +9,7 @@ var _react = require("react");
|
|
|
9
9
|
|
|
10
10
|
var _reactQuery = require("react-query");
|
|
11
11
|
|
|
12
|
-
var
|
|
12
|
+
var _mergeAndCompareVariables = require("./mergeAndCompareVariables");
|
|
13
13
|
|
|
14
14
|
var _useDataEngine = require("./useDataEngine");
|
|
15
15
|
|
|
@@ -35,24 +35,35 @@ const useDataQuery = (query, {
|
|
|
35
35
|
variables: initialVariables = {},
|
|
36
36
|
lazy: initialLazy = false
|
|
37
37
|
} = {}) => {
|
|
38
|
-
const variablesHash = (0, _react.useRef)(null);
|
|
39
|
-
const [variables, setVariables] = (0, _react.useState)(initialVariables);
|
|
40
|
-
const [enabled, setEnabled] = (0, _react.useState)(!initialLazy);
|
|
41
38
|
const [staticQuery] = (0, _useStaticInput.useStaticInput)(query, {
|
|
42
39
|
warn: true,
|
|
43
40
|
name: 'query'
|
|
44
41
|
});
|
|
42
|
+
const [variablesUpdateCount, setVariablesUpdateCount] = (0, _react.useState)(0);
|
|
43
|
+
const queryState = (0, _react.useRef)({
|
|
44
|
+
variables: initialVariables,
|
|
45
|
+
variablesHash: undefined,
|
|
46
|
+
enabled: !initialLazy,
|
|
47
|
+
refetchCallback: undefined
|
|
48
|
+
});
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
50
|
+
* Display current query state and refetch count in React DevTools
|
|
47
51
|
*/
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
(0, _react.useDebugValue)({
|
|
54
|
+
variablesUpdateCount,
|
|
55
|
+
enabled: queryState.current.enabled,
|
|
56
|
+
variables: queryState.current.variables
|
|
57
|
+
}, debugValue => JSON.stringify(debugValue));
|
|
58
|
+
/**
|
|
59
|
+
* User callbacks and refetch handling
|
|
60
|
+
*/
|
|
50
61
|
|
|
51
62
|
const onSuccess = data => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
var _queryState$current$r, _queryState$current;
|
|
64
|
+
|
|
65
|
+
(_queryState$current$r = (_queryState$current = queryState.current).refetchCallback) === null || _queryState$current$r === void 0 ? void 0 : _queryState$current$r.call(_queryState$current, data);
|
|
66
|
+
queryState.current.refetchCallback = undefined;
|
|
56
67
|
|
|
57
68
|
if (userOnSuccess) {
|
|
58
69
|
userOnSuccess(data);
|
|
@@ -61,9 +72,7 @@ const useDataQuery = (query, {
|
|
|
61
72
|
|
|
62
73
|
const onError = error => {
|
|
63
74
|
// If we'd want to reject on errors we'd call the cb with the error here
|
|
64
|
-
|
|
65
|
-
refetchCallback.current = null;
|
|
66
|
-
}
|
|
75
|
+
queryState.current.refetchCallback = undefined;
|
|
67
76
|
|
|
68
77
|
if (userOnError) {
|
|
69
78
|
userOnError(error);
|
|
@@ -75,10 +84,10 @@ const useDataQuery = (query, {
|
|
|
75
84
|
|
|
76
85
|
|
|
77
86
|
const engine = (0, _useDataEngine.useDataEngine)();
|
|
78
|
-
const queryKey = [staticQuery, variables];
|
|
87
|
+
const queryKey = [staticQuery, queryState.current.variables];
|
|
79
88
|
|
|
80
89
|
const queryFn = () => engine.query(staticQuery, {
|
|
81
|
-
variables
|
|
90
|
+
variables: queryState.current.variables
|
|
82
91
|
});
|
|
83
92
|
|
|
84
93
|
const {
|
|
@@ -89,7 +98,7 @@ const useDataQuery = (query, {
|
|
|
89
98
|
data,
|
|
90
99
|
refetch: queryRefetch
|
|
91
100
|
} = (0, _reactQuery.useQuery)(queryKey, queryFn, {
|
|
92
|
-
enabled,
|
|
101
|
+
enabled: queryState.current.enabled,
|
|
93
102
|
onSuccess,
|
|
94
103
|
onError
|
|
95
104
|
});
|
|
@@ -103,11 +112,17 @@ const useDataQuery = (query, {
|
|
|
103
112
|
*/
|
|
104
113
|
|
|
105
114
|
const refetch = (0, _react.useCallback)(newVariables => {
|
|
115
|
+
const {
|
|
116
|
+
identical,
|
|
117
|
+
mergedVariables,
|
|
118
|
+
mergedVariablesHash
|
|
119
|
+
} = (0, _mergeAndCompareVariables.mergeAndCompareVariables)(queryState.current.variables, newVariables, queryState.current.variablesHash);
|
|
106
120
|
/**
|
|
107
121
|
* If there are no updates that will trigger an automatic refetch
|
|
108
122
|
* we'll need to call react-query's refetch directly
|
|
109
123
|
*/
|
|
110
|
-
|
|
124
|
+
|
|
125
|
+
if (queryState.current.enabled && identical) {
|
|
111
126
|
return queryRefetch({
|
|
112
127
|
cancelRefetch: true,
|
|
113
128
|
throwOnError: false
|
|
@@ -116,44 +131,19 @@ const useDataQuery = (query, {
|
|
|
116
131
|
}) => data);
|
|
117
132
|
}
|
|
118
133
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const mergedHash = (0, _stableVariablesHash.stableVariablesHash)(mergedVariables);
|
|
126
|
-
const identical = currentHash === mergedHash;
|
|
127
|
-
|
|
128
|
-
if (identical && enabled) {
|
|
129
|
-
/**
|
|
130
|
-
* If the variables are identical and the query is enabled
|
|
131
|
-
* we'll need to trigger the refetch manually
|
|
132
|
-
*/
|
|
133
|
-
return queryRefetch({
|
|
134
|
-
cancelRefetch: true,
|
|
135
|
-
throwOnError: false
|
|
136
|
-
}).then(({
|
|
137
|
-
data
|
|
138
|
-
}) => data);
|
|
139
|
-
} else {
|
|
140
|
-
variablesHash.current = mergedHash;
|
|
141
|
-
setVariables(mergedVariables);
|
|
142
|
-
}
|
|
143
|
-
} // Enable the query after the variables have been set to prevent extra request
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (!enabled) {
|
|
147
|
-
setEnabled(true);
|
|
148
|
-
} // This promise does not currently reject on errors
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
return new Promise(resolve => {
|
|
152
|
-
refetchCallback.current = data => {
|
|
134
|
+
queryState.current.variables = mergedVariables;
|
|
135
|
+
queryState.current.variablesHash = mergedVariablesHash;
|
|
136
|
+
queryState.current.enabled = true; // This promise does not currently reject on errors
|
|
137
|
+
|
|
138
|
+
const refetchPromise = new Promise(resolve => {
|
|
139
|
+
queryState.current.refetchCallback = data => {
|
|
153
140
|
resolve(data);
|
|
154
141
|
};
|
|
155
|
-
});
|
|
156
|
-
|
|
142
|
+
}); // Trigger a react-query refetch by incrementing variablesUpdateCount state
|
|
143
|
+
|
|
144
|
+
setVariablesUpdateCount(prevCount => prevCount + 1);
|
|
145
|
+
return refetchPromise;
|
|
146
|
+
}, [queryRefetch]);
|
|
157
147
|
/**
|
|
158
148
|
* react-query returns null or an error, but we return undefined
|
|
159
149
|
* or an error, so this ensures consistency with the other types.
|
|
@@ -507,6 +507,65 @@ describe('useDataQuery', () => {
|
|
|
507
507
|
});
|
|
508
508
|
});
|
|
509
509
|
describe('return values: refetch', () => {
|
|
510
|
+
it('Should be stable if the query variables change', async () => {
|
|
511
|
+
let count = 0;
|
|
512
|
+
const spy = jest.fn(() => {
|
|
513
|
+
count++;
|
|
514
|
+
return count;
|
|
515
|
+
});
|
|
516
|
+
const data = {
|
|
517
|
+
answer: spy
|
|
518
|
+
};
|
|
519
|
+
const query = {
|
|
520
|
+
x: {
|
|
521
|
+
resource: 'answer'
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const wrapper = ({
|
|
526
|
+
children
|
|
527
|
+
}) => /*#__PURE__*/React.createElement(_CustomDataProvider.CustomDataProvider, {
|
|
528
|
+
data: data
|
|
529
|
+
}, children);
|
|
530
|
+
|
|
531
|
+
const {
|
|
532
|
+
result,
|
|
533
|
+
waitFor
|
|
534
|
+
} = (0, _reactHooks.renderHook)(() => (0, _useDataQuery.useDataQuery)(query, {
|
|
535
|
+
lazy: true
|
|
536
|
+
}), {
|
|
537
|
+
wrapper
|
|
538
|
+
});
|
|
539
|
+
expect(spy).not.toHaveBeenCalled();
|
|
540
|
+
const initialRefetch = result.current.refetch;
|
|
541
|
+
(0, _reactHooks.act)(() => {
|
|
542
|
+
initialRefetch();
|
|
543
|
+
});
|
|
544
|
+
await waitFor(() => {
|
|
545
|
+
expect(result.current).toMatchObject({
|
|
546
|
+
loading: false,
|
|
547
|
+
called: true,
|
|
548
|
+
data: {
|
|
549
|
+
x: 1
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
554
|
+
(0, _reactHooks.act)(() => {
|
|
555
|
+
initialRefetch();
|
|
556
|
+
});
|
|
557
|
+
await waitFor(() => {
|
|
558
|
+
expect(result.current).toMatchObject({
|
|
559
|
+
loading: false,
|
|
560
|
+
called: true,
|
|
561
|
+
data: {
|
|
562
|
+
x: 2
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
567
|
+
expect(initialRefetch).toBe(result.current.refetch);
|
|
568
|
+
});
|
|
510
569
|
it('Should only trigger a single request when refetch is called on a lazy query with new variables', async () => {
|
|
511
570
|
const spy = jest.fn((type, query) => {
|
|
512
571
|
if (query.id === '1') {
|
|
@@ -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!"));
|
|
@@ -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' || resource.startsWith('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,31 @@ 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 an unversioned endpoint sub-resources of the new tracker importer (in version 2.37)', () => {
|
|
154
|
+
const query = {
|
|
155
|
+
resource: 'tracker/test'
|
|
156
|
+
};
|
|
157
|
+
expect(queryToResourcePath(link, query, 'read')).toBe("".concat(link.unversionedApiPath, "/tracker/test"));
|
|
158
|
+
});
|
|
159
|
+
it('should return a VERSIONED endpoint for the new tracker importer (in version 2.38)', () => {
|
|
160
|
+
const query = {
|
|
161
|
+
resource: 'tracker'
|
|
162
|
+
};
|
|
163
|
+
const v38config = { ...defaultConfig,
|
|
164
|
+
serverVersion: {
|
|
165
|
+
major: 2,
|
|
166
|
+
minor: 38,
|
|
167
|
+
patch: 0
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
expect(queryToResourcePath(createLink(v38config), query, 'read')).toBe("".concat(link.versionedApiPath, "/tracker"));
|
|
132
171
|
});
|
|
133
172
|
});
|
|
@@ -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') {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from 'react';
|
|
1
|
+
import { useState, useEffect, useRef, useDebugValue } from 'react';
|
|
2
2
|
export const useStaticInput = (staticValue, {
|
|
3
3
|
warn = false,
|
|
4
4
|
name = 'input'
|
|
5
5
|
} = {}) => {
|
|
6
6
|
const originalValue = useRef(staticValue);
|
|
7
7
|
const [value, setValue] = useState(() => originalValue.current);
|
|
8
|
+
useDebugValue(value, debugValue => "".concat(name, ": ").concat(JSON.stringify(debugValue)));
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
if (warn && originalValue.current !== staticValue) {
|
|
10
11
|
console.warn("The ".concat(name, " should be static, don't create it within the render loop!"));
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { ResolvedResourceQuery, FetchType } from '../../engine';
|
|
2
|
-
|
|
2
|
+
import { RestAPILink } from '../RestAPILink';
|
|
3
|
+
export declare const queryToResourcePath: (link: RestAPILink, query: ResolvedResourceQuery, type: FetchType) => string;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
import type { Config } from '@dhis2/app-service-config';
|
|
1
2
|
import { DataEngineLink, DataEngineLinkExecuteOptions, FetchType, JsonValue, ResolvedResourceQuery } from '../engine/';
|
|
2
|
-
export interface RestAPILinkInput {
|
|
3
|
-
baseUrl: string;
|
|
4
|
-
apiVersion: number;
|
|
5
|
-
}
|
|
6
3
|
export declare class RestAPILink implements DataEngineLink {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
constructor(
|
|
4
|
+
readonly config: Config;
|
|
5
|
+
readonly versionedApiPath: string;
|
|
6
|
+
readonly unversionedApiPath: string;
|
|
7
|
+
constructor(config: Config);
|
|
11
8
|
private fetch;
|
|
12
9
|
executeResourceQuery(type: FetchType, query: ResolvedResourceQuery, { signal }: DataEngineLinkExecuteOptions): Promise<JsonValue>;
|
|
13
10
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { QueryVariables } from '../../engine';
|
|
2
|
+
export declare const mergeAndCompareVariables: (previousVariables?: QueryVariables | undefined, newVariables?: QueryVariables | undefined, previousHash?: string | undefined) => {
|
|
3
|
+
identical: boolean;
|
|
4
|
+
mergedVariablesHash: string | undefined;
|
|
5
|
+
mergedVariables: QueryVariables | undefined;
|
|
6
|
+
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Query, QueryOptions } from '../../engine';
|
|
2
|
-
import { QueryRenderInput } from '../../types';
|
|
1
|
+
import type { Query, QueryOptions } from '../../engine';
|
|
2
|
+
import type { QueryRenderInput } from '../../types';
|
|
3
3
|
export declare const useDataQuery: (query: Query, { onComplete: userOnSuccess, onError: userOnError, variables: initialVariables, lazy: initialLazy, }?: QueryOptions) => QueryRenderInput;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2/app-service-data",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
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.4.
|
|
25
|
+
"@dhis2/app-service-config": "3.4.3",
|
|
26
26
|
"@dhis2/cli-app-scripts": "^7.1.1",
|
|
27
27
|
"prop-types": "^15.7.2",
|
|
28
28
|
"react": "^16.8",
|