Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

organizeOutputBy=Patient query parameter #55

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,20 @@ Endpoint: `POST [fhir base]/bulkstatus/[client id]/kickoff-import`

The server supports the following query parameters:

From the [2.0.0 ci-build version of the Bulk Data Access IG](https://build.fhir.org/ig/HL7/bulk-data/export.html#query-parameters):

- `_type`: Filters the response to only include resources of the specified resource type(s)
- If omitted, system-level requests will return all resources supported by the server within the scope of the client authorization
- For Patient- and Group-level requests, the [Patient Compartment](https://www.hl7.org/fhir/compartmentdefinition-patient.html) is used as a point of reference for filtering the resource types that are returned.
- `_outputFormat`: The server supports the following formats: `application/fhir+ndjson`, `application/ndjson+fhir`, `application/ndjson`, `ndjson`
- `_typeFilter`: Filters the response to only include resources that meet the criteria of the specified comma-delimited FHIR REST queries. Returns an error for queries specified by the client that are unsupported by the server. Supports queries on the ValueSets (`type:in`, `code:in`, etc.) of a given resource type.
- `patient`: Only applicable to POST requests for group-level and patient-level requests. When provided, the server SHALL NOT return resources in the patient compartment definition belonging to patients outside the list. Can support multiple patient references in a single request.
- `_bySubject`: Only applicable for group-level and patient-level requests. Creates export results, separating resources into files based on what subject they are associated with (rather than based on type). The only `_bySubject` value supported is `Patient`. This will result in an ndjson file for each patient in the returned data. If the `_type` parameter is used in conjunction with this parameter, `Patient` must be one of the types included in the passed value list.
- `_elements`: Filters the content of the responses to omit unlisted, non-mandatory elements from the resources returned. These elements should be provided in the form `[resource type].[element name]` (e.g., `Patient.id`) which only filters the contents of those specified resources or in the form `[element name]` (e.g., `id`) which filters the contents of all of the returned resources.

From the [2.0.0 ci-build version of the argo24 branch of the Bulk Data Access IG](https://build.fhir.org/ig/HL7/bulk-data/branches/argo24/export.html#query-parameters):

- `organizeOutputBy`: Applicable for all types of export requests. Creates export results, separating resources into files based on what resourceType they are to be organized by. The only `organizeOutputBy` value supported currently is `Patient`. This will result in an ndjson file for each patient in the returned data. If the `_type` parameter is used in conjunction with this parameter, `Patient` must be one of the types included in the passed value lists.

#### `_elements` Query Parameter

The server supports the optional and experimental query parameter `_elements` as defined by the Bulk Data Access IG (here)[https://build.fhir.org/ig/HL7/bulk-data/export.html#query-parameters]. The `_elements` parameter is a string of comma-delimited HL7® FHIR® Elements used to filter the returned resources to only include listed elements and mandatory elements. Mandatory elements are defined as elements in the StructureDefinition of a resource type which have a minimum cardinality of 1. Because this server provides json-formatted data, `resourceType` is also an implied mandatory element for all Resources.
Expand Down
28 changes: 10 additions & 18 deletions src/services/export.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ const bulkExport = async (request, reply) => {
})
);
}
if (parameters._bySubject) {
reply.code(400).send(
createOperationOutcome('The "_bySubject" parameter cannot be used in a system-level export request.', {
issueCode: 400,
severity: 'error'
})
);
}
if (validateExportParams(parameters, reply)) {
request.log.info('Base >>> $export');
const clientEntry = await addPendingBulkExportRequest();
Expand All @@ -44,7 +36,7 @@ const bulkExport = async (request, reply) => {
typeFilter: request.query._typeFilter,
systemLevelExport: true,
elements: elements,
byPatient: parameters._bySubject === 'Patient'
byPatient: parameters.organizeOutputBy === 'Patient'
};
await exportQueue.createJob(job).save();
reply.code(202).header('Content-location', `${process.env.BULK_BASE_URL}/bulkstatus/${clientEntry}`).send();
Expand Down Expand Up @@ -74,7 +66,7 @@ const patientBulkExport = async (request, reply) => {
await validatePatientReferences(parameters.patient, reply);
}
request.log.info('Patient >>> $export');
const clientEntry = await addPendingBulkExportRequest(parameters._bySubject === 'Patient');
const clientEntry = await addPendingBulkExportRequest(parameters.organizeOutputBy === 'Patient');

let types = request.query._type?.split(',') || parameters._type?.split(',');
if (types) {
Expand All @@ -93,7 +85,7 @@ const patientBulkExport = async (request, reply) => {
patient: parameters.patient,
systemLevelExport: false,
elements: elements,
byPatient: parameters._bySubject === 'Patient'
byPatient: parameters.organizeOutputBy === 'Patient'
};
await exportQueue.createJob(job).save();
reply.code(202).header('Content-location', `${process.env.BULK_BASE_URL}/bulkstatus/${clientEntry}`).send();
Expand Down Expand Up @@ -132,7 +124,7 @@ const groupBulkExport = async (request, reply) => {
return splitRef[splitRef.length - 1];
});

const clientEntry = await addPendingBulkExportRequest(parameters._bySubject === 'Patient');
const clientEntry = await addPendingBulkExportRequest(parameters.organizeOutputBy === 'Patient');
let types = request.query._type?.split(',') || parameters._type?.split(',');
if (types) {
types = filterPatientResourceTypes(request, reply, types);
Expand All @@ -151,7 +143,7 @@ const groupBulkExport = async (request, reply) => {
systemLevelExport: false,
patientIds: patientIds,
elements: elements,
byPatient: parameters._bySubject === 'Patient'
byPatient: parameters.organizeOutputBy === 'Patient'
};
await exportQueue.createJob(job).save();
reply.code(202).header('Content-location', `${process.env.BULK_BASE_URL}/bulkstatus/${clientEntry}`).send();
Expand Down Expand Up @@ -183,9 +175,9 @@ function validateExportParams(parameters, reply) {
}
}

if (parameters._bySubject && parameters._bySubject !== 'Patient') {
if (parameters.organizeOutputBy && parameters.organizeOutputBy !== 'Patient') {
reply.code(400).send(
createOperationOutcome(`Server does not support the _bySubject parameter for values other than Patient.`, {
createOperationOutcome(`Server does not support the organizeOutputBy parameter for values other than Patient.`, {
issueCode: 400,
severity: 'error'
})
Expand Down Expand Up @@ -215,12 +207,12 @@ function validateExportParams(parameters, reply) {
);
return false;
}
if (parameters._bySubject === 'Patient' && !requestTypes.includes('Patient')) {
if (parameters.organizeOutputBy === 'Patient' && !requestTypes.includes('Patient')) {
reply
.code(400)
.send(
createOperationOutcome(
`When _type is specified with _bySubject Patient, the Patient type must be included in the _type parameter.`,
`When _type is specified with organizeOutputBy Patient, the Patient type must be included in the _type parameter.`,
{ issueCode: 400, severity: 'error' }
)
);
Expand Down Expand Up @@ -314,7 +306,7 @@ function validateExportParams(parameters, reply) {

let unrecognizedParams = [];
Object.keys(parameters).forEach(param => {
if (!['_outputFormat', '_type', '_typeFilter', 'patient', '_elements', '_bySubject'].includes(param)) {
if (!['_outputFormat', '_type', '_typeFilter', 'patient', '_elements', 'organizeOutputBy'].includes(param)) {
unrecognizedParams.push(param);
}
});
Expand Down
64 changes: 31 additions & 33 deletions test/services/export.service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,28 +293,6 @@ describe('Check barebones bulk export logic (failure)', () => {
});
});

test('throws 400 error when "_bySubject" parameter used in system-level export', async () => {
await supertest(app.server)
.post('/$export')
.send({
resourceType: 'Parameters',
parameter: [
{
name: '_bySubject',
valueString: 'test'
}
]
})
.expect(400)
.then(response => {
expect(response.body.resourceType).toEqual('OperationOutcome');
expect(response.body.issue[0].code).toEqual(400);
expect(response.body.issue[0].details.text).toEqual(
'The "_bySubject" parameter cannot be used in a system-level export request.'
);
});
});

test('throws 400 error when POST request body is not of resourceType "Parameters"', async () => {
await supertest(app.server)
.post('/$export')
Expand Down Expand Up @@ -684,21 +662,41 @@ describe('Check group-level export logic (failure)', () => {
});
});

describe('Check by subject export logic', () => {
describe('Check organizeOutputBy=Patient export logic', () => {
beforeEach(async () => {
await bulkStatusSetup();
await app.ready();
});

test('check 202 for Patient bySubject', async () => {
test('check 202 for system-level organizeOutputBy=Patient', async () => {
const createJobSpy = jest.spyOn(queue, 'createJob');
await supertest(app.server)
.post('/$export')
.send({
resourceType: 'Parameters',
parameter: [
{
name: 'organizeOutputBy',
valueString: 'Patient'
}
]
})
.expect(202)
.then(response => {
expect(response.headers['content-location']).toBeDefined();
expect(createJobSpy).toHaveBeenCalled();
});
});

test('check 202 for Patient organizeOutputBy=Patient', async () => {
const createJobSpy = jest.spyOn(queue, 'createJob');
await supertest(app.server)
.post('/Patient/$export')
.send({
resourceType: 'Parameters',
parameter: [
{
name: '_bySubject',
name: 'organizeOutputBy',
valueString: 'Patient'
}
]
Expand All @@ -710,7 +708,7 @@ describe('Check by subject export logic', () => {
});
});

test('check 202 for Group bySubject', async () => {
test('check 202 for Group organizeOutputBy=Patient', async () => {
await createTestResource(testGroup, 'Group');
const createJobSpy = jest.spyOn(queue, 'createJob');
await supertest(app.server)
Expand All @@ -719,7 +717,7 @@ describe('Check by subject export logic', () => {
resourceType: 'Parameters',
parameter: [
{
name: '_bySubject',
name: 'organizeOutputBy',
valueString: 'Patient'
}
]
Expand All @@ -731,28 +729,28 @@ describe('Check by subject export logic', () => {
});
});

test('returns 400 for a _bySubject call that specifies a subject other than Patient', async () => {
test('returns 400 for a organizeOutputBy call that specifies a resource type other than Patient', async () => {
await supertest(app.server)
.get('/Patient/$export?_bySubject=Other')
.get('/Patient/$export?organizeOutputBy=Other')
.expect(400)
.then(response => {
expect(response.body.resourceType).toEqual('OperationOutcome');
expect(response.body.issue[0].code).toEqual(400);
expect(response.body.issue[0].details.text).toEqual(
'Server does not support the _bySubject parameter for values other than Patient.'
'Server does not support the organizeOutputBy parameter for values other than Patient.'
);
});
});

test('returns 400 for a _bySubject call where _type does not include Patient', async () => {
test('returns 400 for a organizeOutputBy call where _type does not include Patient', async () => {
await supertest(app.server)
.get('/Patient/$export?_bySubject=Patient&_type=Condition')
.get('/Patient/$export?organizeOutputBy=Patient&_type=Condition')
.expect(400)
.then(response => {
expect(response.body.resourceType).toEqual('OperationOutcome');
expect(response.body.issue[0].code).toEqual(400);
expect(response.body.issue[0].details.text).toEqual(
'When _type is specified with _bySubject Patient, the Patient type must be included in the _type parameter.'
'When _type is specified with organizeOutputBy Patient, the Patient type must be included in the _type parameter.'
);
});
});
Expand Down
4 changes: 0 additions & 4 deletions test/util/exportToNDJson.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ describe('check export logic', () => {
await exportToNDJson({ clientEntry: clientId, type: mockType, typeFilter: typeFilterWOValueSet });
expect(fs.existsSync(expectedFileNameWOValueSet)).toBe(false);
});
test('Expect folder created and export successful when _bySubject parameter is retrieved from request', async () => {
await exportToNDJson({ clientEntry: clientId, types: mockType, typeFilter: mockTypeFilter, byPatient: true });
expect(fs.existsSync('./tmp/123456/testPatient.ndjson')).toBe(true);
});
});

describe('patientsQueryForType', () => {
Expand Down