@dhis2/app-service-data 3.4.1 → 3.4.4
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/queryToRequestOptions/multipartFormDataMatchers.js +3 -10
- package/build/cjs/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.test.js +0 -17
- package/build/cjs/links/RestAPILink/queryToRequestOptions/requestContentType.js +23 -10
- package/build/cjs/links/RestAPILink/queryToRequestOptions/requestContentType.test.js +22 -1
- package/build/cjs/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.js +13 -0
- package/build/cjs/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.test.js +21 -0
- package/build/cjs/links/RestAPILink/queryToResourcePath.js +1 -1
- package/build/cjs/links/RestAPILink/queryToResourcePath.test.js +6 -0
- 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/queryToRequestOptions/multipartFormDataMatchers.js +1 -5
- package/build/es/links/RestAPILink/queryToRequestOptions/multipartFormDataMatchers.test.js +1 -18
- package/build/es/links/RestAPILink/queryToRequestOptions/requestContentType.js +19 -8
- package/build/es/links/RestAPILink/queryToRequestOptions/requestContentType.test.js +23 -2
- package/build/es/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.js +4 -0
- package/build/es/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.test.js +18 -0
- package/build/es/links/RestAPILink/queryToResourcePath.js +1 -1
- package/build/es/links/RestAPILink/queryToResourcePath.test.js +6 -0
- 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/queryToRequestOptions/multipartFormDataMatchers.d.ts +0 -1
- package/build/types/links/RestAPILink/queryToRequestOptions/requestContentType.d.ts +3 -3
- package/build/types/links/RestAPILink/queryToRequestOptions/xWwwFormUrlencodedMatchers.d.ts +2 -0
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.
|
|
6
|
+
exports.isAppInstall = exports.isStaticContentUpload = exports.isMessageConversationAttachment = exports.isFileResourceUpload = exports.isDataValue = void 0;
|
|
7
7
|
|
|
8
8
|
/*
|
|
9
9
|
* Requests that expect a "multipart/form-data" Content-Type have been collected by scanning
|
|
@@ -45,13 +45,6 @@ exports.isStaticContentUpload = isStaticContentUpload;
|
|
|
45
45
|
|
|
46
46
|
const isAppInstall = (type, {
|
|
47
47
|
resource
|
|
48
|
-
}) => type === 'create' && resource === 'apps';
|
|
48
|
+
}) => type === 'create' && resource === 'apps';
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
exports.isAppInstall = isAppInstall;
|
|
52
|
-
|
|
53
|
-
const isSvgConversion = (type, {
|
|
54
|
-
resource
|
|
55
|
-
}) => type === 'create' && (resource === 'svg.png' || resource === 'svg.pdf');
|
|
56
|
-
|
|
57
|
-
exports.isSvgConversion = isSvgConversion;
|
|
50
|
+
exports.isAppInstall = isAppInstall;
|
|
@@ -71,21 +71,4 @@ describe('isAppInstall', () => {
|
|
|
71
71
|
resource: 'notApps'
|
|
72
72
|
})).toEqual(false);
|
|
73
73
|
});
|
|
74
|
-
});
|
|
75
|
-
describe('isSvgConversion', () => {
|
|
76
|
-
it('returns true for a POST to "svg.png"', () => {
|
|
77
|
-
expect((0, _multipartFormDataMatchers.isSvgConversion)('create', {
|
|
78
|
-
resource: 'svg.png'
|
|
79
|
-
})).toEqual(true);
|
|
80
|
-
});
|
|
81
|
-
it('returns true for a POST to "svg.pdf"', () => {
|
|
82
|
-
expect((0, _multipartFormDataMatchers.isSvgConversion)('create', {
|
|
83
|
-
resource: 'svg.pdf'
|
|
84
|
-
})).toEqual(true);
|
|
85
|
-
});
|
|
86
|
-
it('retuns false for a POST to a different resource', () => {
|
|
87
|
-
expect((0, _multipartFormDataMatchers.isSvgConversion)('create', {
|
|
88
|
-
resource: 'notSvg'
|
|
89
|
-
})).toEqual(false);
|
|
90
|
-
});
|
|
91
74
|
});
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.requestBodyForContentType = exports.requestHeadersForContentType = exports.requestContentType = exports.
|
|
6
|
+
exports.requestBodyForContentType = exports.requestHeadersForContentType = exports.requestContentType = exports.getConversionErrorMessage = void 0;
|
|
7
7
|
|
|
8
8
|
var multipartFormDataMatchers = _interopRequireWildcard(require("./multipartFormDataMatchers"));
|
|
9
9
|
|
|
10
10
|
var textPlainMatchers = _interopRequireWildcard(require("./textPlainMatchers"));
|
|
11
11
|
|
|
12
|
+
var xWwwFormUrlencodedMatchers = _interopRequireWildcard(require("./xWwwFormUrlencodedMatchers"));
|
|
13
|
+
|
|
12
14
|
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
|
|
13
15
|
|
|
14
16
|
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
|
@@ -17,20 +19,23 @@ const resourceExpectsTextPlain = (type, query) => Object.values(textPlainMatcher
|
|
|
17
19
|
|
|
18
20
|
const resourceExpectsMultipartFormData = (type, query) => Object.values(multipartFormDataMatchers).some(multipartFormDataMatcher => multipartFormDataMatcher(type, query));
|
|
19
21
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
const resourceExpectsXWwwFormUrlencoded = (type, query) => Object.values(xWwwFormUrlencodedMatchers).some(xWwwFormUrlencodedMatcher => xWwwFormUrlencodedMatcher(type, query));
|
|
23
|
+
|
|
24
|
+
const getConversionErrorMessage = outputType => "Could not convert data to ".concat(outputType, ": object does not have own enumerable string-keyed properties");
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
exports.getConversionErrorMessage = getConversionErrorMessage;
|
|
27
|
+
|
|
28
|
+
const convertData = (data, initialValue) => {
|
|
24
29
|
const dataEntries = Object.entries(data);
|
|
25
30
|
|
|
26
31
|
if (dataEntries.length === 0) {
|
|
27
|
-
throw new Error(
|
|
32
|
+
throw new Error(getConversionErrorMessage(initialValue.constructor.name));
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
return dataEntries.reduce((
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
},
|
|
35
|
+
return dataEntries.reduce((convertedData, [key, value]) => {
|
|
36
|
+
convertedData.append(key, value);
|
|
37
|
+
return convertedData;
|
|
38
|
+
}, initialValue);
|
|
34
39
|
};
|
|
35
40
|
|
|
36
41
|
const requestContentType = (type, query) => {
|
|
@@ -50,6 +55,10 @@ const requestContentType = (type, query) => {
|
|
|
50
55
|
return 'multipart/form-data';
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
if (resourceExpectsXWwwFormUrlencoded(type, query)) {
|
|
59
|
+
return 'application/x-www-form-urlencoded';
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
return 'application/json';
|
|
54
63
|
};
|
|
55
64
|
|
|
@@ -86,7 +95,11 @@ const requestBodyForContentType = (contentType, {
|
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
if (contentType === 'multipart/form-data') {
|
|
89
|
-
return
|
|
98
|
+
return convertData(data, new FormData());
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
102
|
+
return convertData(data, new URLSearchParams());
|
|
90
103
|
} // 'text/plain'
|
|
91
104
|
|
|
92
105
|
|
|
@@ -88,7 +88,28 @@ describe('requestBodyForContentType', () => {
|
|
|
88
88
|
type: 'text/plain'
|
|
89
89
|
})
|
|
90
90
|
});
|
|
91
|
-
}).toThrow(new Error(
|
|
91
|
+
}).toThrow(new Error('Could not convert data to FormData: object does not have own enumerable string-keyed properties'));
|
|
92
|
+
});
|
|
93
|
+
it('converts to URLSearchParams if contentType is "application/x-www-form-urlencoded"', () => {
|
|
94
|
+
const data = {
|
|
95
|
+
a: 'AAA'
|
|
96
|
+
};
|
|
97
|
+
const result = (0, _requestContentType.requestBodyForContentType)('application/x-www-form-urlencoded', {
|
|
98
|
+
resource: 'test',
|
|
99
|
+
data
|
|
100
|
+
});
|
|
101
|
+
expect(result instanceof URLSearchParams).toEqual(true);
|
|
102
|
+
expect(result.get('a')).toEqual('AAA');
|
|
103
|
+
});
|
|
104
|
+
it('throws an error if contentType is "application/x-www-form-urlencoded" and data does have own string-keyd properties', () => {
|
|
105
|
+
expect(() => {
|
|
106
|
+
(0, _requestContentType.requestBodyForContentType)('application/x-www-form-urlencoded', {
|
|
107
|
+
resource: 'test',
|
|
108
|
+
data: new File(['foo'], 'foo.txt', {
|
|
109
|
+
type: 'text/plain'
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
}).toThrow(new Error('Could not convert data to URLSearchParams: object does not have own enumerable string-keyed properties'));
|
|
92
113
|
});
|
|
93
114
|
it('returns the data as received if contentType is "text/plain"', () => {
|
|
94
115
|
const data = 'Something';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.isSvgConversion = void 0;
|
|
7
|
+
|
|
8
|
+
// POST to convert an SVG file
|
|
9
|
+
const isSvgConversion = (type, {
|
|
10
|
+
resource
|
|
11
|
+
}) => type === 'create' && (resource === 'svg.png' || resource === 'svg.pdf');
|
|
12
|
+
|
|
13
|
+
exports.isSvgConversion = isSvgConversion;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _xWwwFormUrlencodedMatchers = require("./xWwwFormUrlencodedMatchers");
|
|
4
|
+
|
|
5
|
+
describe('isSvgConversion', () => {
|
|
6
|
+
it('returns true for a POST to "svg.png"', () => {
|
|
7
|
+
expect((0, _xWwwFormUrlencodedMatchers.isSvgConversion)('create', {
|
|
8
|
+
resource: 'svg.png'
|
|
9
|
+
})).toEqual(true);
|
|
10
|
+
});
|
|
11
|
+
it('returns true for a POST to "svg.pdf"', () => {
|
|
12
|
+
expect((0, _xWwwFormUrlencodedMatchers.isSvgConversion)('create', {
|
|
13
|
+
resource: 'svg.pdf'
|
|
14
|
+
})).toEqual(true);
|
|
15
|
+
});
|
|
16
|
+
it('retuns false for a POST to a different resource', () => {
|
|
17
|
+
expect((0, _xWwwFormUrlencodedMatchers.isSvgConversion)('create', {
|
|
18
|
+
resource: 'notSvg'
|
|
19
|
+
})).toEqual(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -64,7 +64,7 @@ const isAction = resource => resource.startsWith(actionPrefix);
|
|
|
64
64
|
const makeActionPath = resource => (0, _path.joinPath)('dhis-web-commons', "".concat(resource.substr(actionPrefix.length), ".action"));
|
|
65
65
|
|
|
66
66
|
const skipApiVersion = (resource, config) => {
|
|
67
|
-
if (resource === 'tracker') {
|
|
67
|
+
if (resource === 'tracker' || resource.startsWith('tracker/')) {
|
|
68
68
|
var _config$serverVersion, _config$serverVersion2;
|
|
69
69
|
|
|
70
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) {
|
|
@@ -153,6 +153,12 @@ describe('queryToResourcePath', () => {
|
|
|
153
153
|
};
|
|
154
154
|
expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe("".concat(link.unversionedApiPath, "/tracker"));
|
|
155
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
|
+
});
|
|
156
162
|
it('should return a VERSIONED endpoint for the new tracker importer (in version 2.38)', () => {
|
|
157
163
|
const query = {
|
|
158
164
|
resource: 'tracker'
|
|
@@ -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!"));
|
|
@@ -26,8 +26,4 @@ export const isStaticContentUpload = (type, {
|
|
|
26
26
|
|
|
27
27
|
export const isAppInstall = (type, {
|
|
28
28
|
resource
|
|
29
|
-
}) => type === 'create' && resource === 'apps';
|
|
30
|
-
|
|
31
|
-
export const isSvgConversion = (type, {
|
|
32
|
-
resource
|
|
33
|
-
}) => type === 'create' && (resource === 'svg.png' || resource === 'svg.pdf');
|
|
29
|
+
}) => type === 'create' && resource === 'apps';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isFileResourceUpload, isMessageConversationAttachment, isStaticContentUpload, isAppInstall,
|
|
1
|
+
import { isFileResourceUpload, isMessageConversationAttachment, isStaticContentUpload, isAppInstall, isDataValue } from './multipartFormDataMatchers';
|
|
2
2
|
describe('isDataValue', () => {
|
|
3
3
|
it('returns true for a POST to "dataValues"', () => {
|
|
4
4
|
expect(isDataValue('create', {
|
|
@@ -68,21 +68,4 @@ describe('isAppInstall', () => {
|
|
|
68
68
|
resource: 'notApps'
|
|
69
69
|
})).toEqual(false);
|
|
70
70
|
});
|
|
71
|
-
});
|
|
72
|
-
describe('isSvgConversion', () => {
|
|
73
|
-
it('returns true for a POST to "svg.png"', () => {
|
|
74
|
-
expect(isSvgConversion('create', {
|
|
75
|
-
resource: 'svg.png'
|
|
76
|
-
})).toEqual(true);
|
|
77
|
-
});
|
|
78
|
-
it('returns true for a POST to "svg.pdf"', () => {
|
|
79
|
-
expect(isSvgConversion('create', {
|
|
80
|
-
resource: 'svg.pdf'
|
|
81
|
-
})).toEqual(true);
|
|
82
|
-
});
|
|
83
|
-
it('retuns false for a POST to a different resource', () => {
|
|
84
|
-
expect(isSvgConversion('create', {
|
|
85
|
-
resource: 'notSvg'
|
|
86
|
-
})).toEqual(false);
|
|
87
|
-
});
|
|
88
71
|
});
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import * as multipartFormDataMatchers from './multipartFormDataMatchers';
|
|
2
2
|
import * as textPlainMatchers from './textPlainMatchers';
|
|
3
|
+
import * as xWwwFormUrlencodedMatchers from './xWwwFormUrlencodedMatchers';
|
|
3
4
|
|
|
4
5
|
const resourceExpectsTextPlain = (type, query) => Object.values(textPlainMatchers).some(textPlainMatcher => textPlainMatcher(type, query));
|
|
5
6
|
|
|
6
7
|
const resourceExpectsMultipartFormData = (type, query) => Object.values(multipartFormDataMatchers).some(multipartFormDataMatcher => multipartFormDataMatcher(type, query));
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const resourceExpectsXWwwFormUrlencoded = (type, query) => Object.values(xWwwFormUrlencodedMatchers).some(xWwwFormUrlencodedMatcher => xWwwFormUrlencodedMatcher(type, query));
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
export const getConversionErrorMessage = outputType => "Could not convert data to ".concat(outputType, ": object does not have own enumerable string-keyed properties");
|
|
12
|
+
|
|
13
|
+
const convertData = (data, initialValue) => {
|
|
11
14
|
const dataEntries = Object.entries(data);
|
|
12
15
|
|
|
13
16
|
if (dataEntries.length === 0) {
|
|
14
|
-
throw new Error(
|
|
17
|
+
throw new Error(getConversionErrorMessage(initialValue.constructor.name));
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
return dataEntries.reduce((
|
|
18
|
-
|
|
19
|
-
return
|
|
20
|
-
},
|
|
20
|
+
return dataEntries.reduce((convertedData, [key, value]) => {
|
|
21
|
+
convertedData.append(key, value);
|
|
22
|
+
return convertedData;
|
|
23
|
+
}, initialValue);
|
|
21
24
|
};
|
|
22
25
|
|
|
23
26
|
export const requestContentType = (type, query) => {
|
|
@@ -37,6 +40,10 @@ export const requestContentType = (type, query) => {
|
|
|
37
40
|
return 'multipart/form-data';
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
if (resourceExpectsXWwwFormUrlencoded(type, query)) {
|
|
44
|
+
return 'application/x-www-form-urlencoded';
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
return 'application/json';
|
|
41
48
|
};
|
|
42
49
|
export const requestHeadersForContentType = contentType => {
|
|
@@ -67,7 +74,11 @@ export const requestBodyForContentType = (contentType, {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
if (contentType === 'multipart/form-data') {
|
|
70
|
-
return
|
|
77
|
+
return convertData(data, new FormData());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (contentType === 'application/x-www-form-urlencoded') {
|
|
81
|
+
return convertData(data, new URLSearchParams());
|
|
71
82
|
} // 'text/plain'
|
|
72
83
|
|
|
73
84
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { requestContentType, requestHeadersForContentType, requestBodyForContentType
|
|
1
|
+
import { requestContentType, requestHeadersForContentType, requestBodyForContentType } from './requestContentType';
|
|
2
2
|
describe('requestContentType', () => {
|
|
3
3
|
it('returns "application/json" for a normal resource', () => {
|
|
4
4
|
expect(requestContentType('create', {
|
|
@@ -85,7 +85,28 @@ describe('requestBodyForContentType', () => {
|
|
|
85
85
|
type: 'text/plain'
|
|
86
86
|
})
|
|
87
87
|
});
|
|
88
|
-
}).toThrow(new Error(
|
|
88
|
+
}).toThrow(new Error('Could not convert data to FormData: object does not have own enumerable string-keyed properties'));
|
|
89
|
+
});
|
|
90
|
+
it('converts to URLSearchParams if contentType is "application/x-www-form-urlencoded"', () => {
|
|
91
|
+
const data = {
|
|
92
|
+
a: 'AAA'
|
|
93
|
+
};
|
|
94
|
+
const result = requestBodyForContentType('application/x-www-form-urlencoded', {
|
|
95
|
+
resource: 'test',
|
|
96
|
+
data
|
|
97
|
+
});
|
|
98
|
+
expect(result instanceof URLSearchParams).toEqual(true);
|
|
99
|
+
expect(result.get('a')).toEqual('AAA');
|
|
100
|
+
});
|
|
101
|
+
it('throws an error if contentType is "application/x-www-form-urlencoded" and data does have own string-keyd properties', () => {
|
|
102
|
+
expect(() => {
|
|
103
|
+
requestBodyForContentType('application/x-www-form-urlencoded', {
|
|
104
|
+
resource: 'test',
|
|
105
|
+
data: new File(['foo'], 'foo.txt', {
|
|
106
|
+
type: 'text/plain'
|
|
107
|
+
})
|
|
108
|
+
});
|
|
109
|
+
}).toThrow(new Error('Could not convert data to URLSearchParams: object does not have own enumerable string-keyed properties'));
|
|
89
110
|
});
|
|
90
111
|
it('returns the data as received if contentType is "text/plain"', () => {
|
|
91
112
|
const data = 'Something';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isSvgConversion } from './xWwwFormUrlencodedMatchers';
|
|
2
|
+
describe('isSvgConversion', () => {
|
|
3
|
+
it('returns true for a POST to "svg.png"', () => {
|
|
4
|
+
expect(isSvgConversion('create', {
|
|
5
|
+
resource: 'svg.png'
|
|
6
|
+
})).toEqual(true);
|
|
7
|
+
});
|
|
8
|
+
it('returns true for a POST to "svg.pdf"', () => {
|
|
9
|
+
expect(isSvgConversion('create', {
|
|
10
|
+
resource: 'svg.pdf'
|
|
11
|
+
})).toEqual(true);
|
|
12
|
+
});
|
|
13
|
+
it('retuns false for a POST to a different resource', () => {
|
|
14
|
+
expect(isSvgConversion('create', {
|
|
15
|
+
resource: 'notSvg'
|
|
16
|
+
})).toEqual(false);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -56,7 +56,7 @@ const isAction = resource => resource.startsWith(actionPrefix);
|
|
|
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') {
|
|
59
|
+
if (resource === 'tracker' || resource.startsWith('tracker/')) {
|
|
60
60
|
var _config$serverVersion, _config$serverVersion2;
|
|
61
61
|
|
|
62
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) {
|
|
@@ -150,6 +150,12 @@ describe('queryToResourcePath', () => {
|
|
|
150
150
|
};
|
|
151
151
|
expect(queryToResourcePath(link, query, 'read')).toBe("".concat(link.unversionedApiPath, "/tracker"));
|
|
152
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
|
+
});
|
|
153
159
|
it('should return a VERSIONED endpoint for the new tracker importer (in version 2.38)', () => {
|
|
154
160
|
const query = {
|
|
155
161
|
resource: 'tracker'
|
|
@@ -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!"));
|
|
@@ -4,4 +4,3 @@ export declare const isFileResourceUpload: (type: FetchType, { resource }: Resol
|
|
|
4
4
|
export declare const isMessageConversationAttachment: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
|
|
5
5
|
export declare const isStaticContentUpload: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
|
|
6
6
|
export declare const isAppInstall: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
|
|
7
|
-
export declare const isSvgConversion: (type: FetchType, { resource }: ResolvedResourceQuery) => boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ResolvedResourceQuery, FetchType } from '../../../engine';
|
|
2
|
-
declare type RequestContentType = 'application/json' | 'application/json-patch+json' | 'text/plain' | 'multipart/form-data' | null;
|
|
3
|
-
export declare const
|
|
2
|
+
declare type RequestContentType = 'application/json' | 'application/json-patch+json' | 'text/plain' | 'multipart/form-data' | 'application/x-www-form-urlencoded' | null;
|
|
3
|
+
export declare const getConversionErrorMessage: (outputType: string) => string;
|
|
4
4
|
export declare const requestContentType: (type: FetchType, query: ResolvedResourceQuery) => null | RequestContentType;
|
|
5
5
|
export declare const requestHeadersForContentType: (contentType: RequestContentType) => undefined | Record<'Content-Type', string>;
|
|
6
|
-
export declare const requestBodyForContentType: (contentType: RequestContentType, { data }: ResolvedResourceQuery) => undefined | string | FormData;
|
|
6
|
+
export declare const requestBodyForContentType: (contentType: RequestContentType, { data }: ResolvedResourceQuery) => undefined | string | FormData | URLSearchParams;
|
|
7
7
|
export {};
|
|
@@ -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.4",
|
|
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.4",
|
|
26
26
|
"@dhis2/cli-app-scripts": "^7.1.1",
|
|
27
27
|
"prop-types": "^15.7.2",
|
|
28
28
|
"react": "^16.8",
|