Skip to content

Commit 5e1b1f9

Browse files
Merge pull request #118 from costateixeira/jct-search-params
add support for _summary and _total
2 parents f2ced1c + 7f8cf90 commit 5e1b1f9

4 files changed

Lines changed: 168 additions & 15 deletions

File tree

tests/tx/search.test.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,123 @@ describe('Search Worker', () => {
200200
}
201201
});
202202
});
203+
describe('_summary parameter', () => {
204+
test('should return only summary elements with _summary=true', async () => {
205+
const response = await request(app)
206+
.get('/tx/r5/CodeSystem')
207+
.query({ _summary: 'true', _count: 5 })
208+
.set('Accept', 'application/json');
209+
210+
expect(response.status).toBe(200);
211+
expect(response.body.resourceType).toBe('Bundle');
212+
213+
if (response.body.entry && response.body.entry.length > 0) {
214+
const resource = response.body.entry[0].resource;
215+
// Summary elements should be present
216+
expect(resource.resourceType).toBe('CodeSystem');
217+
expect(resource.id).toBeDefined();
218+
// Non-summary elements should be absent
219+
expect(resource.concept).toBeUndefined();
220+
expect(resource.property).toBeUndefined();
221+
}
222+
});
223+
224+
test('should return only count with _summary=count', async () => {
225+
const response = await request(app)
226+
.get('/tx/r5/CodeSystem')
227+
.query({ _summary: 'count' })
228+
.set('Accept', 'application/json');
229+
230+
expect(response.status).toBe(200);
231+
expect(response.body.resourceType).toBe('Bundle');
232+
expect(response.body.type).toBe('searchset');
233+
expect(response.body.total).toBeGreaterThan(0);
234+
// No entries when _summary=count
235+
expect(response.body.entry).toBeUndefined();
236+
expect(response.body.link).toBeUndefined();
237+
});
238+
239+
test('should return full resources with _summary=false', async () => {
240+
const response = await request(app)
241+
.get('/tx/r5/CodeSystem')
242+
.query({ _summary: 'false', url: 'http://hl7.org/fhir/administrative-gender' })
243+
.set('Accept', 'application/json');
244+
245+
expect(response.status).toBe(200);
246+
247+
if (response.body.entry && response.body.entry.length > 0) {
248+
const resource = response.body.entry[0].resource;
249+
expect(resource.resourceType).toBe('CodeSystem');
250+
// Full resource should include concept
251+
expect(resource.concept).toBeDefined();
252+
}
253+
});
254+
});
255+
256+
describe('_total parameter', () => {
257+
test('should include total with _total=accurate (default)', async () => {
258+
const response = await request(app)
259+
.get('/tx/r5/CodeSystem')
260+
.query({ _count: 5 })
261+
.set('Accept', 'application/json');
262+
263+
expect(response.status).toBe(200);
264+
expect(response.body.total).toBeDefined();
265+
expect(typeof response.body.total).toBe('number');
266+
});
267+
268+
test('should not include total with _total=none', async () => {
269+
const response = await request(app)
270+
.get('/tx/r5/CodeSystem')
271+
.query({ _total: 'none', _count: 5 })
272+
.set('Accept', 'application/json');
273+
274+
expect(response.status).toBe(200);
275+
expect(response.body.resourceType).toBe('Bundle');
276+
expect(response.body.total).toBeUndefined();
277+
});
278+
});
279+
280+
describe('_format parameter', () => {
281+
test('should return JSON with _format=json', async () => {
282+
const response = await request(app)
283+
.get('/tx/r5/CodeSystem')
284+
.query({ _format: 'json', _count: 2 });
285+
286+
expect(response.status).toBe(200);
287+
expect(response.headers['content-type']).toContain('application/fhir+json');
288+
expect(response.body.resourceType).toBe('Bundle');
289+
});
290+
291+
test('should return XML with _format=xml', async () => {
292+
const response = await request(app)
293+
.get('/tx/r5/CodeSystem')
294+
.query({ _format: 'xml', _count: 2 });
295+
296+
expect(response.status).toBe(200);
297+
expect(response.headers['content-type']).toContain('application/fhir+xml');
298+
expect(response.text).toContain('<Bundle');
299+
expect(response.text).toContain('xmlns="http://hl7.org/fhir"');
300+
});
301+
302+
test('should return JSON with _format=application/fhir+json', async () => {
303+
const response = await request(app)
304+
.get('/tx/r5/CodeSystem')
305+
.query({ _format: 'application/fhir+json', _count: 2 });
306+
307+
expect(response.status).toBe(200);
308+
expect(response.headers['content-type']).toContain('application/fhir+json');
309+
});
310+
311+
test('_format should override Accept header', async () => {
312+
const response = await request(app)
313+
.get('/tx/r5/CodeSystem')
314+
.query({ _format: 'xml', _count: 2 })
315+
.set('Accept', 'application/fhir+json');
316+
317+
expect(response.status).toBe(200);
318+
// _format=xml should override Accept: application/fhir+json
319+
expect(response.headers['content-type']).toContain('application/fhir+xml');
320+
});
321+
});
203322
});

tx/tx-html.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ class TxHtmlRenderer {
8181
* Check if request accepts HTML
8282
*/
8383
acceptsHtml(req) {
84-
let _fmt = req.query._format;
84+
let _fmt = req.query._format || req.query.format || req.body?._format;
8585
if (_fmt && typeof _fmt !== 'string') {
86-
_fmt = null
86+
_fmt = null;
8787
}
8888
if (_fmt && _fmt == 'html') {
8989
return true;

tx/tx.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,10 @@ class TXModule {
8282
}
8383

8484
acceptsXml(req) {
85-
let _fmt = req.query._format;
85+
let _fmt = req.query._format || req.query.format || req.body?._format;
8686
if (_fmt && typeof _fmt !== 'string') {
87-
_fmt = null
87+
_fmt = null;
8888
}
89-
9089
if (_fmt && _fmt == 'xml') {
9190
return 'application/fhir+xml';
9291
}
@@ -105,9 +104,9 @@ class TXModule {
105104
}
106105

107106
acceptsJson(req) {
108-
let _fmt = req.query._format;
107+
let _fmt = req.query._format || req.query.format || req.body?._format;
109108
if (_fmt && typeof _fmt !== 'string') {
110-
_fmt = null
109+
_fmt = null;
111110
}
112111
if (_fmt && _fmt == 'json') {
113112
return 'application/fhir+json';

tx/workers/search.js

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ class SearchWorker extends TerminologyWorker {
3131

3232
// Allowed search parameters
3333
static ALLOWED_PARAMS = [
34-
'_offset', '_count', '_elements', '_sort',
34+
'_offset', '_count', '_elements', '_sort', '_summary', '_total',
3535
'url', 'version', 'content-mode', 'date', 'description',
3636
'supplements', 'identifier', 'jurisdiction', 'name',
3737
'publisher', 'status', 'system', 'title', 'text'
3838
];
3939

40+
// Summary elements for _summary=true (common metadata fields)
41+
static SUMMARY_ELEMENTS = ['resourceType', 'id', 'meta', 'url', 'version',
42+
'name', 'title', 'status', 'date', 'publisher', 'description'];
43+
4044
// Sortable fields
4145
static SORT_FIELDS = ['id', 'url', 'version', 'date', 'name', 'vurl'];
4246

@@ -55,8 +59,27 @@ class SearchWorker extends TerminologyWorker {
5559
try {
5660
// Parse pagination parameters
5761
const offset = Math.max(0, parseInt(params._offset) || 0);
58-
const elements = params._elements ? decodeURIComponent(params._elements).split(',').map(e => e.trim()) : null;
59-
const count = Math.min(elements ? 2000 : 200, params._count && Utilities.isInteger(params._count) ? parseInt(params._count) : 20);
62+
const summary = params._summary || 'false';
63+
const totalMode = params._total || 'accurate';
64+
65+
// Determine elements based on _summary parameter
66+
let elements;
67+
switch (summary) {
68+
case 'true':
69+
elements = SearchWorker.SUMMARY_ELEMENTS;
70+
break;
71+
case 'text':
72+
elements = ['resourceType', 'id', 'meta', 'text'];
73+
break;
74+
case 'data':
75+
elements = null; // no filter for terminology
76+
break;
77+
default:
78+
elements = params._elements ? decodeURIComponent(params._elements).split(',').map(e => e.trim()) : null;
79+
break;
80+
}
81+
82+
const count = summary === 'count' ? 0 : Math.min(elements ? 2000 : 200, params._count && Utilities.isInteger(params._count) ? parseInt(params._count) : 20);
6083
const sort = params._sort || "id";
6184

6285
// Get matching resources
@@ -84,9 +107,9 @@ class SearchWorker extends TerminologyWorker {
84107

85108
// Build and return the bundle
86109
const bundle = this.buildSearchBundle(
87-
req, resourceType, matches, offset, count, elements
110+
req, resourceType, matches, offset, count, elements, summary, totalMode
88111
);
89-
req.logInfo = `${bundle.entry.length} matches`;
112+
req.logInfo = summary === 'count' ? `count: ${bundle.total}` : `${bundle.entry.length} matches`;
90113
return res.json(bundle);
91114

92115
} catch (error) {
@@ -268,9 +291,18 @@ class SearchWorker extends TerminologyWorker {
268291
/**
269292
* Build a FHIR search Bundle with pagination
270293
*/
271-
buildSearchBundle(req, resourceType, allMatches, offset, count, elements) {
294+
buildSearchBundle(req, resourceType, allMatches, offset, count, elements, summary = 'false', totalMode = 'accurate') {
272295
const total = allMatches.length;
273296

297+
// For _summary=count, return just the count
298+
if (summary === 'count') {
299+
return {
300+
resourceType: 'Bundle',
301+
type: 'searchset',
302+
total: total
303+
};
304+
}
305+
274306
// Get the slice for this page
275307
const pageResults = allMatches.slice(offset, offset + count);
276308

@@ -354,13 +386,16 @@ class SearchWorker extends TerminologyWorker {
354386
};
355387
});
356388

357-
return {
389+
const bundle = {
358390
resourceType: 'Bundle',
359391
type: 'searchset',
360-
total: total,
361392
link: links,
362393
entry: entries
363394
};
395+
if (totalMode !== 'none') {
396+
bundle.total = total;
397+
}
398+
return bundle;
364399
}
365400

366401
/**

0 commit comments

Comments
 (0)