Skip to content

Commit 4ce191e

Browse files
Changes as per review
LF-2985
1 parent 365df10 commit 4ce191e

File tree

2 files changed

+268
-8
lines changed

2 files changed

+268
-8
lines changed

src/terminologies.js

+45-5
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Terminologies {
3131
* operation.
3232
*/
3333
static validateVS(self, valueset, coded, params = '') {
34+
checkParams(params);
3435
const httpHeaders = {
3536
"Accept": "application/fhir+json; charset=utf-8",
3637
};
@@ -74,11 +75,11 @@ class Terminologies {
7475
)
7576
.then(r => r.json())
7677
.then((bundle) => {
77-
const system = bundle?.entry?.length === 1 && (
78-
bundle.entry[0].resource.compose?.include?.length === 1
79-
&& bundle.entry[0].resource.compose.include[0].system
80-
|| bundle.entry[0].resource.expansion?.contains?.length === 1
81-
&& bundle.entry[0].resource.expansion?.contains[0].system);
78+
const system = bundle?.entry?.length === 1
79+
&& getSystemFromArrayItems(
80+
bundle.entry[0].resource.expansion?.contains,
81+
getSystemFromArrayItems(bundle.entry[0].resource.compose?.include)
82+
);
8283
if (system) {
8384
const queryParams2 = new URLSearchParams({
8485
url: valueset,
@@ -148,4 +149,43 @@ function createIndexKeyMemberOf(value, valueset) {
148149
return undefined;
149150
}
150151

152+
/**
153+
* Throws an exception if the params parameter is not empty and is not a valid
154+
* URL-encoded string.
155+
* @param {string|undefined} params - a URL encoded string with parameters
156+
* (e.g. 'date=2011-03-04&displayLanguage=en').
157+
*/
158+
function checkParams(params) {
159+
if (params?.split('&').find(
160+
p => {
161+
const v = p.split('=');
162+
return v.length <= 2 && v.find(i => encodeURIComponent(decodeURIComponent(i)) !== i);
163+
}
164+
)) {
165+
throw new Error(`"${params}" should be a valid URL encoded string`);
166+
}
167+
}
168+
169+
/**
170+
* Returns the "system" property from an array of items if it is the same for all
171+
* items and equal to the initial value if the initial value is defined.
172+
* @param {Object[]|undefined} arr - array of items
173+
* @param {string|undefined} [system] - optional initial value
174+
* @return {string|undefined}
175+
*/
176+
function getSystemFromArrayItems(arr, system = undefined) {
177+
if (arr) {
178+
for (let i = 0; i < arr.length; ++i) {
179+
if (!system) {
180+
system = arr[i].system;
181+
} else if (system !== arr[i].system) {
182+
system = undefined;
183+
break;
184+
}
185+
}
186+
}
187+
188+
return system;
189+
}
190+
151191
module.exports = Terminologies;

test/async-functions.test.js

+223-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,47 @@ const fhirpath = require('../src/fhirpath');
22
const model = require('../fhir-context/r4');
33
const resource = require('./resources/observation-example.json');
44

5+
let fetchSpy;
6+
7+
/**
8+
* Mocks fetch requests.
9+
* @param {Array} results - an array of fetch response descriptions, each item
10+
* of which is an array with the RegExp URL as the first item and the response
11+
* JSON object as the second.
12+
*/
13+
function mockFetchResults(results) {
14+
fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(
15+
(url) => new Promise((resolve, _) => {
16+
const mockedResult = results?.find(r => r[0].test(url))?.[1]
17+
if(mockedResult) {
18+
resolve({ json: () => mockedResult });
19+
} else {
20+
console.error(`"${url}" is not mocked.`)
21+
}
22+
})
23+
);
24+
}
25+
526
describe('Async functions', () => {
627

7-
describe('%terminologies', () => {
8-
it('should support validateVS', (done) => {
28+
describe('%terminologies.validateVS', () => {
29+
afterEach(() => {
30+
fetchSpy?.mockRestore();
31+
})
32+
33+
it('should work ', (done) => {
34+
mockFetchResults([
35+
[/code=29463-7/, {
36+
"resourceType": "Parameters",
37+
"parameter": [
38+
{
39+
"name": "result",
40+
"valueBoolean": true
41+
}
42+
]
43+
}]
44+
]);
45+
946
let result = fhirpath.evaluate(
1047
resource,
1148
"%terminologies.validateVS('http://hl7.org/fhir/ValueSet/observation-vitalsignresult', Observation.code.coding[0]).parameter.value",
@@ -19,10 +56,45 @@ describe('Async functions', () => {
1956
done();
2057
})
2158
});
59+
60+
it('should throw an error if the params parameter is not a valid URL encoded string', () => {
61+
let result = () => fhirpath.evaluate(
62+
resource,
63+
"%terminologies.validateVS('http://hl7.org/fhir/ValueSet/observation-vitalsignresult', Observation.code.coding[0], 'something=???').parameter.value",
64+
{},
65+
model,
66+
{ async: true, terminologyUrl: "https://lforms-fhir.nlm.nih.gov/baseR4" }
67+
);
68+
expect(result).toThrowError('should be a valid URL encoded string');
69+
});
2270
});
2371

2472
describe('memberOf', () => {
2573
it('should work with Codings when async functions are enabled', (done) => {
74+
mockFetchResults([
75+
[/code=29463-7/, {
76+
"resourceType": "Parameters",
77+
"parameter": [
78+
{
79+
"name": "result",
80+
"valueBoolean": true
81+
}
82+
]
83+
}],
84+
[/.*/, {
85+
"resourceType": "Parameters",
86+
"parameter": [
87+
{
88+
"name": "result",
89+
"valueBoolean": false
90+
},
91+
{
92+
"name": "message",
93+
"valueString": "Unknown code"
94+
}
95+
]
96+
}]
97+
]);
2698

2799
let result = fhirpath.evaluate(
28100
resource,
@@ -40,6 +112,17 @@ describe('Async functions', () => {
40112

41113
it('should work with CodeableConcept when async functions are enabled', (done) => {
42114

115+
mockFetchResults([
116+
[/ValueSet\/\$validate-code/, {
117+
"resourceType": "Parameters",
118+
"parameter": [
119+
{
120+
"name": "result",
121+
"valueBoolean": true
122+
}
123+
]
124+
}]
125+
]);
43126
let result = fhirpath.evaluate(
44127
resource,
45128
"Observation.code.memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')",
@@ -54,7 +137,35 @@ describe('Async functions', () => {
54137
})
55138
});
56139

57-
it('should work with Code when async functions are enabled', (done) => {
140+
it('should work with "code" when async functions are enabled', (done) => {
141+
mockFetchResults([
142+
[/ValueSet\?url=http%3A%2F%2Fhl7\.org%2Ffhir%2FValueSet%2Fobservation-vitalsignresult/, {
143+
"resourceType": "Bundle",
144+
"entry": [
145+
{
146+
"resource": {
147+
"resourceType": "ValueSet",
148+
"compose": {
149+
"include": [
150+
{
151+
"system": "http://loinc.org",
152+
}
153+
]
154+
}
155+
}
156+
}
157+
]
158+
}],
159+
[/code=29463-7&system=http%3A%2F%2Floinc\.org/, {
160+
"resourceType": "Parameters",
161+
"parameter": [
162+
{
163+
"name": "result",
164+
"valueBoolean": true
165+
}
166+
]
167+
}]
168+
]);
58169
let result = fhirpath.evaluate(
59170
resource,
60171
"Observation.code.coding.code[0].memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')",
@@ -70,6 +181,12 @@ describe('Async functions', () => {
70181
});
71182

72183
it('should return an empty result when the ValueSet cannot be resolved', (done) => {
184+
mockFetchResults([
185+
[/ValueSet\?url=http%3A%2F%2Funknown-valueset/, {
186+
"resourceType": "Bundle",
187+
"total": 0,
188+
}]
189+
]);
73190
let result = fhirpath.evaluate(
74191
resource,
75192
"Observation.code.coding.code[0].memberOf('http://unknown-valueset')",
@@ -84,6 +201,109 @@ describe('Async functions', () => {
84201
})
85202
});
86203

204+
it('should work with "code" when there is more than one identical coding system in the ValueSet', (done) => {
205+
mockFetchResults([
206+
[/ValueSet\?url=http%3A%2F%2Fhl7\.org%2Ffhir%2FValueSet%2Fobservation-vitalsignresult/, {
207+
"resourceType": "Bundle",
208+
"entry": [
209+
{
210+
"resource": {
211+
"resourceType": "ValueSet",
212+
"compose": {
213+
"include": [
214+
{
215+
"system": "http://loinc.org",
216+
},
217+
{
218+
"system": "http://loinc.org",
219+
}
220+
]
221+
},
222+
"expansion": {
223+
"contains": [
224+
{
225+
"system": "http://loinc.org",
226+
},
227+
{
228+
"system": "http://loinc.org",
229+
}
230+
]
231+
}
232+
}
233+
}
234+
]
235+
}],
236+
[/code=29463-7&system=http%3A%2F%2Floinc\.org/, {
237+
"resourceType": "Parameters",
238+
"parameter": [
239+
{
240+
"name": "result",
241+
"valueBoolean": true
242+
}
243+
]
244+
}]
245+
]);
246+
let result = fhirpath.evaluate(
247+
resource,
248+
"Observation.code.coding.code[0].memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')",
249+
{},
250+
model,
251+
{ async: true, terminologyUrl: "https://lforms-fhir.nlm.nih.gov/baseR4" }
252+
);
253+
expect(result instanceof Promise).toBe(true);
254+
result.then((r) => {
255+
expect(r).toEqual([true]);
256+
done();
257+
})
258+
});
259+
260+
it('should return an empty result when there is more than one different coding system in the ValueSet', (done) => {
261+
mockFetchResults([
262+
[/ValueSet\?url=http%3A%2F%2Fhl7\.org%2Ffhir%2FValueSet%2Fobservation-vitalsignresult/, {
263+
"resourceType": "Bundle",
264+
"entry": [
265+
{
266+
"resource": {
267+
"resourceType": "ValueSet",
268+
"compose": {
269+
"include": [
270+
{
271+
"system": "http://loinc.org",
272+
},
273+
{
274+
"system": "http://loinc.org",
275+
}
276+
]
277+
},
278+
"expansion": {
279+
"contains": [
280+
{
281+
"system": "http://loinc.org",
282+
},
283+
{
284+
"system": "http://something-else",
285+
}
286+
]
287+
}
288+
}
289+
}
290+
]
291+
}]
292+
]);
293+
let result = fhirpath.evaluate(
294+
resource,
295+
"Observation.code.coding.code[0].memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')",
296+
{},
297+
model,
298+
{ async: true, terminologyUrl: "https://lforms-fhir.nlm.nih.gov/baseR4" }
299+
);
300+
expect(result instanceof Promise).toBe(true);
301+
result.then((r) => {
302+
expect(r).toEqual([]);
303+
done();
304+
})
305+
});
306+
87307
it('should throw an exception when async functions are disabled', () => {
88308

89309
let result = () => fhirpath.evaluate(

0 commit comments

Comments
 (0)