Skip to content

Commit 0cccc4b

Browse files
Added support for paging when expanding value sets, fixed "uri" for the "itemWeight" property
LF-3081
1 parent 2320dfd commit 0cccc4b

File tree

4 files changed

+572
-92
lines changed

4 files changed

+572
-92
lines changed

package-lock.json

+6-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/sdc-ig-supplements.js

+173-65
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
let engine = {};
55

6+
/**
7+
* Maximum number of codes to load when loading a paginated value set.
8+
* @type {number}
9+
*/
10+
const MAX_VS_CODES = 500;
11+
612
/**
713
* Returns numeric values from the score extension associated with the input
814
* collection of Questionnaire items. See the description of the ordinal()
@@ -211,77 +217,28 @@ function addWeightFromCorrespondingResourcesToResult(res, ctx, questionnaire,
211217

212218
if (containedVS) {
213219
if (!containedVS.expansion) {
214-
const parameters = [{
215-
"name": "valueSet",
216-
"resource": containedVS
217-
}];
218-
if (ctx.model?.version === 'r5') {
219-
parameters.push({
220-
"name": "property",
221-
"valueString": "itemWeight"
222-
});
223-
}
224-
score = fetchWithCache(`${getTerminologyUrl(ctx)}/ValueSet/$expand`, {
225-
method: 'POST',
226-
headers: {
227-
'Accept': 'application/fhir+json',
228-
'Content-Type': 'application/fhir+json'
229-
},
230-
body: JSON.stringify({
231-
"resourceType": "Parameters",
232-
"parameter": parameters
233-
})
234-
})
235-
.then(r => r.ok ? r.json() : Promise.reject(r.json()))
236-
.then((terminologyVS) => {
237-
return getScoreFromVS(ctx.model?.version, terminologyVS,
238-
checkExtUrl, code, system);
239-
});
220+
score = getWeightFromContainedVS(ctx, containedVS, checkExtUrl, code, system);
240221
} else {
241-
score = getScoreFromVS(ctx.model?.version, containedVS, checkExtUrl,
222+
score = getWeightFromVS(ctx.model?.version, containedVS, checkExtUrl,
242223
code, system);
243224
}
225+
} else if (vsId) {
226+
throw new Error(
227+
`Cannot find a contained value set with id: ` + vsId + '.');
244228
} else {
245-
const parameters = ctx.model?.version === 'dstu2' ?
246-
{identifier: vsURL} : { url: vsURL };
247-
if (ctx.model?.version === 'r5') {
248-
parameters.property = 'itemWeight';
249-
}
250-
score = fetchWithCache(`${getTerminologyUrl(ctx)}/ValueSet/$expand?` +
251-
new URLSearchParams(parameters).toString(), {
252-
headers: {
253-
'Accept': 'application/fhir+json'
254-
}
255-
})
256-
.then(r => r.ok ? r.json() : Promise.reject(r.json()))
257-
.then((terminologyVS) => {
258-
return getScoreFromVS(ctx.model?.version, terminologyVS,
259-
checkExtUrl, code, system);
260-
});
229+
score = getWeightFromTerminologyVS(ctx, vsURL, checkExtUrl, code, system);
261230
}
262231
} // end if (vsURL)
263232

264233
if (system && ctx.model?.version !== 'dstu2') {
265-
if (score === undefined) {
266-
const isCodeSystem = (r) => r.url === system && r.resourceType === 'CodeSystem';
267-
const containedCS = getContainedResources(elem)?.find(isCodeSystem)
268-
|| questionnaire?.contained?.find(isCodeSystem);
269-
270-
if (containedCS) {
271-
if (checkIfItemWeightExists(containedCS?.property)) {
272-
score = getItemWeightFromProperty(
273-
getCodeSystemItem(containedCS?.concept, code)
274-
);
275-
}
276-
} else {
277-
score = getWeightFromTerminologyCodeSet(ctx, code, system);
278-
}
234+
if (score === undefined || score === null) {
235+
score = getWeightFromCS(ctx, questionnaire, elem, checkExtUrl, code, system);
279236
} else if (score instanceof Promise) {
280237
score = score.then(weightFromVS => {
281238
if (weightFromVS !== undefined) {
282239
return weightFromVS;
283240
}
284-
return getWeightFromTerminologyCodeSet(ctx, code, system);
241+
return getWeightFromCS(ctx, questionnaire, elem, checkExtUrl, code, system);
285242
});
286243
}
287244
}
@@ -297,6 +254,153 @@ function addWeightFromCorrespondingResourcesToResult(res, ctx, questionnaire,
297254
return score instanceof Promise;
298255
}
299256

257+
/**
258+
* Returns the promised score value from the expanded contained value set
259+
* obtained from the terminology server.
260+
* @param {Object} ctx - object describing the context of expression
261+
* evaluation (see the "applyParsedPath" function).
262+
* @param {string} containedVS - contained value set.
263+
* @param {Function} checkExtUrl - function to check if the extension passed as
264+
* a parameter has a score URL.
265+
* @param {string} code - symbol in syntax defined by the system.
266+
* @param {string} system - code system.
267+
* @param {number} offset - Paging support - where to start if a subset is
268+
* desired (default = 0). Offset is number of records (not number of pages).
269+
* Paging only applies to flat expansions - servers ignore paging if the
270+
* expansion is not flat (I think if the server doesn't return the
271+
* "ValueSet.expansion.offset" field, it means there is no paging for the
272+
* ValueSet).
273+
* @returns {Promise<number|undefined>}
274+
*/
275+
function getWeightFromContainedVS(ctx, containedVS, checkExtUrl, code, system, offset = 0) {
276+
const parameters = [{
277+
"name": "valueSet",
278+
"resource": containedVS
279+
}];
280+
if (offset) {
281+
parameters.push({
282+
"name": "offset",
283+
"valueInteger": offset
284+
});
285+
}
286+
if (ctx.model?.version === 'r5') {
287+
parameters.push({
288+
"name": "property",
289+
"valueString": "itemWeight"
290+
});
291+
}
292+
return fetchWithCache(`${getTerminologyUrl(ctx)}/ValueSet/$expand`, {
293+
method: 'POST',
294+
headers: {
295+
'Accept': 'application/fhir+json',
296+
'Content-Type': 'application/fhir+json'
297+
},
298+
body: JSON.stringify({
299+
"resourceType": "Parameters",
300+
"parameter": parameters
301+
})
302+
})
303+
.then(r => r.ok ? r.json() : Promise.reject(r.json()))
304+
.then((terminologyVS) => {
305+
let score = getWeightFromVS(ctx.model?.version, terminologyVS,
306+
checkExtUrl, code, system);
307+
if (score === null) {
308+
if (terminologyVS?.expansion?.offset === offset) {
309+
const newOffset = terminologyVS?.expansion?.offset +
310+
terminologyVS?.expansion?.contains?.length;
311+
if (newOffset < MAX_VS_CODES) {
312+
return getWeightFromContainedVS(ctx, containedVS, checkExtUrl, code, system, newOffset);
313+
}
314+
}
315+
score = undefined;
316+
}
317+
return score;
318+
});
319+
}
320+
321+
/**
322+
* Returns the promised score value from the expanded value set obtained from the
323+
* terminology server.
324+
* @param {Object} ctx - object describing the context of expression
325+
* evaluation (see the "applyParsedPath" function).
326+
* @param {string} vsURL - value set URL specified in the Questionnaire item.
327+
* @param {Function} checkExtUrl - function to check if the extension passed as
328+
* a parameter has a score URL.
329+
* @param {string} code - symbol in syntax defined by the system.
330+
* @param {string} system - code system.
331+
* @param {number} offset - Paging support - where to start if a subset is
332+
* desired (default = 0). Offset is number of records (not number of pages).
333+
* Paging only applies to flat expansions - servers ignore paging if the
334+
* expansion is not flat (I think if the server doesn't return the
335+
* "ValueSet.expansion.offset" field, it means there is no paging for the
336+
* ValueSet).
337+
* @returns {Promise<number|undefined>}
338+
*/
339+
function getWeightFromTerminologyVS(ctx, vsURL, checkExtUrl, code, system, offset = 0) {
340+
const parameters = ctx.model?.version === 'dstu2' ?
341+
{identifier: vsURL} : { url: vsURL };
342+
if (offset) {
343+
parameters.offset = offset;
344+
}
345+
if (ctx.model?.version === 'r5') {
346+
parameters.property = 'itemWeight';
347+
}
348+
return fetchWithCache(`${getTerminologyUrl(ctx)}/ValueSet/$expand?` +
349+
new URLSearchParams(parameters).toString(), {
350+
headers: {
351+
'Accept': 'application/fhir+json'
352+
}
353+
})
354+
.then(r => r.ok ? r.json() : Promise.reject(r.json()))
355+
.then((terminologyVS) => {
356+
const score = getWeightFromVS(ctx.model?.version, terminologyVS,
357+
checkExtUrl, code, system);
358+
if (score === null) {
359+
if (terminologyVS?.expansion?.offset === offset) {
360+
const newOffset = terminologyVS?.expansion?.offset +
361+
terminologyVS?.expansion?.contains?.length;
362+
if (newOffset < MAX_VS_CODES) {
363+
return getWeightFromTerminologyVS(ctx, vsURL, checkExtUrl, code, system, newOffset);
364+
}
365+
}
366+
return undefined;
367+
}
368+
return score;
369+
});
370+
}
371+
372+
/**
373+
* Returns the value (or its promise) of the itemWeight property or score
374+
* extension for the specified system and code from a CodeSystem.
375+
* @param {Object} ctx - object describing the context of expression
376+
* evaluation (see the "applyParsedPath" function). * @param ctx
377+
* @param {Object} questionnaire - object containing questionnaire resource data
378+
* @param {ResourceNode|any} elem - source collection item for which we obtain
379+
* the score value.
380+
* @param {Function} checkExtUrl - function to check if the extension passed as
381+
* a parameter has a score URL.
382+
* @param {string} code - symbol in syntax defined by the system.
383+
* @param {string} system - code system.
384+
* @return {number|undefined|Promise<number|undefined>}
385+
*/
386+
function getWeightFromCS(ctx, questionnaire, elem, checkExtUrl, code, system) {
387+
const isCodeSystem = (r) => r.url === system && r.resourceType === 'CodeSystem';
388+
const containedCS = getContainedResources(elem)?.find(isCodeSystem)
389+
|| questionnaire?.contained?.find(isCodeSystem);
390+
let score;
391+
392+
if (containedCS) {
393+
const item = getCodeSystemItem(containedCS?.concept, code);
394+
score = ctx.model?.version === 'r5' &&
395+
checkIfItemWeightExists(containedCS?.property) &&
396+
getItemWeightFromProperty(item) ||
397+
item?.extension?.find(checkExtUrl)?.valueDecimal;
398+
} else {
399+
score = getWeightFromTerminologyCodeSet(ctx, code, system);
400+
}
401+
402+
return score;
403+
}
300404

301405
/**
302406
* Returns the promised score value from the code system obtained from the
@@ -415,7 +519,7 @@ function getCodeSystemItem(concept, code) {
415519
*/
416520
function checkIfItemWeightExists(properties) {
417521
return properties?.find(p => p.code === 'itemWeight')?.uri ===
418-
'http://hl7.org/fhir/concept-properties';
522+
'http://hl7.org/fhir/concept-properties#itemWeight';
419523
}
420524

421525
/**
@@ -430,23 +534,27 @@ function getItemWeightFromProperty(item) {
430534

431535
/**
432536
* Returns the value of the itemWeight property or score extension for the
433-
* specified system and code from a value set.
537+
* specified system and code from a value set. If the item in the value set has
538+
* no score, undefined is returned. If the item does not exist, null is returned.
539+
* The difference between null and undefined values used in paging.
434540
* @param {string} modelVersion - model version, e.g. 'r5', 'r4', 'stu3', or 'dstu2'.
435541
* @param {Object} vs - ValueSet.
436542
* @param {Function} checkExtUrl - function to check if the extension passed as
437543
* a parameter has a score URL.
438544
* @param {string} code - symbol in syntax defined by the system.
439545
* @param {string} system - code system.
440-
* @return {number|undefined}
546+
* @return {number|undefined|null}
441547
*/
442-
function getScoreFromVS(modelVersion, vs, checkExtUrl, code, system) {
548+
function getWeightFromVS(modelVersion, vs, checkExtUrl, code, system) {
443549
const item = getValueSetItem(vs.expansion?.contains, code, system);
444550

445-
return item && (
551+
const score = item ? (
446552
modelVersion === 'r5' && checkIfItemWeightExists(vs.expansion?.property) &&
447553
getItemWeightFromProperty(item) ||
448554
item?.extension?.find(checkExtUrl)?.valueDecimal
449-
);
555+
) : null;
556+
557+
return score;
450558
}
451559

452560

@@ -567,7 +675,7 @@ function getQItemByLinkIds(questionnaire, linkIds) {
567675
}
568676
}
569677

570-
questionnaire2linkIds.get(questionnaire)[linkIdsKey] = currentNode;
678+
linkIds2items[linkIdsKey] = currentNode;
571679
}
572680

573681
return currentNode;

test/mock-fetch-results.js

+32-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
let fetchSpy;
22

3+
/**
4+
* Checks whether a string (in the first parameter) matches a regular expression
5+
* or substring (in the second parameter).
6+
* If the second parameter is not passed (no condition), returns true.
7+
* @param {string} str - string to check
8+
* @param {RegExp|string|undefined} condition - regular expression or substring.
9+
* @returns {boolean}
10+
*/
11+
function checkString(str, condition) {
12+
if (condition === undefined) {
13+
return true;
14+
} else if (condition instanceof RegExp) {
15+
return condition.test(str);
16+
} else {
17+
return str && (str.indexOf(condition) !== -1)
18+
}
19+
}
320
/**
421
* Mocks fetch requests.
522
* @param {Array} results - an array of fetch response descriptions, each item
6-
* of which is an array with a RegExp URL or URL substring as the first item and
7-
* a JSON object of the successful response as the second item or, if the second
23+
* of which is an array with a RegExp for a URL or a URL substring as the first
24+
* item and a JSON object of the successful response as the second item or, if the second
825
* item is null, a JSON object of the unsuccessful response as the third item.
26+
* For mocking POST requests the first item of a response description with a URL
27+
* condition could be replaced with an object:
28+
* {url: string|Regexp, body: string|RegExp},
29+
* where "url" is a RegExp for a URL or a URL substring,
30+
* "body" is a RegExp for the body content or a substring of the body content.
931
*/
1032
function mockFetchResults(results) {
1133
fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(
12-
(url) => new Promise((resolve, _) => {
34+
(url, options) => new Promise((resolve, reject) => {
1335
const mockedItem = results?.find(
14-
r => r[0] instanceof RegExp ? r[0].test(url) : url.indexOf(r[0]) !== -1
36+
(r) => {
37+
if (typeof r[0] === 'string' || r[0] instanceof RegExp) {
38+
return checkString(url, r[0])
39+
} else {
40+
return checkString(url, r[0]?.url) && checkString(options.body, r[0]?.body);
41+
}
42+
}
1543
);
1644
const okResult = mockedItem?.[1];
1745
const badResult = mockedItem?.[2];

0 commit comments

Comments
 (0)