Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

feat: allow sorting by date type parameters #60

Merged
merged 4 commits into from
Apr 22, 2021
Merged
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
2 changes: 2 additions & 0 deletions src/QueryBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,5 @@ export const buildQueryForAllSearchParameters = (
},
};
};

export { buildSortClause } from './sort';
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
80 changes: 80 additions & 0 deletions src/QueryBuilder/sort.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface';
import { buildSortClause, parseSortParameter } from './sort';
import { FHIRSearchParametersRegistry } from '../FHIRSearchParametersRegistry';

const fhirSearchParametersRegistry = new FHIRSearchParametersRegistry('4.0.1');

describe('parseSortParameter', () => {
test('status,-date,category', () => {
expect(parseSortParameter('status,-date,category')).toMatchInlineSnapshot(`
Array [
Object {
"order": "asc",
"searchParam": "status",
},
Object {
"order": "desc",
"searchParam": "date",
},
Object {
"order": "asc",
"searchParam": "category",
},
]
`);
});
});

describe('buildSortClause', () => {
test('valid date params', () => {
expect(buildSortClause(fhirSearchParametersRegistry, 'Patient', '-_lastUpdated,birthdate'))
.toMatchInlineSnapshot(`
Array [
Object {
"meta.lastUpdated": Object {
"order": "desc",
"unmapped_type": "long",
},
},
Object {
"meta.lastUpdated.end": Object {
"order": "desc",
"unmapped_type": "long",
},
},
Object {
"birthDate": Object {
"order": "asc",
"unmapped_type": "long",
},
},
Object {
"birthDate.start": Object {
"order": "asc",
"unmapped_type": "long",
},
},
]
`);
});

test('invalid params', () => {
[
'notAPatientParam',
'_lastUpdated,notAPatientParam',
'+birthdate',
'#$%/., symbols and stuff',
'valid params must match a param name from fhirSearchParametersRegistry, so most strings are invalid...',
'name', // This is actually a valid param but right now we only allow sorting by date params
].forEach(p =>
expect(() => buildSortClause(fhirSearchParametersRegistry, 'Patient', p)).toThrow(
InvalidSearchParameterError,
),
);
});
});
72 changes: 72 additions & 0 deletions src/QueryBuilder/sort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*
*/

import { InvalidSearchParameterError } from 'fhir-works-on-aws-interface';
import { FHIRSearchParametersRegistry } from '../FHIRSearchParametersRegistry';

interface SortParameter {
order: 'asc' | 'desc';
searchParam: string;
}

export const parseSortParameter = (param: string): SortParameter[] => {
const parts = param.split(',');
return parts.map(s => {
const order = s.startsWith('-') ? 'desc' : 'asc';
return {
order,
searchParam: s.replace(/^-/, ''),
};
});
};

const elasticsearchSort = (field: string, order: 'asc' | 'desc') => ({
[field]: {
order,
// unmapped_type makes queries more fault tolerant. Since we are using dynamic mapping there's no guarantee
// that the mapping exists at query time. This ignores the unmapped field instead of failing
unmapped_type: 'long',
},
});

// eslint-disable-next-line import/prefer-default-export
export const buildSortClause = (
fhirSearchParametersRegistry: FHIRSearchParametersRegistry,
resourceType: string,
sortQueryParam: string | string[],
): any[] => {
if (Array.isArray(sortQueryParam)) {
throw new InvalidSearchParameterError('_sort parameter cannot be used multiple times on a search query');
}
const sortParams = parseSortParameter(sortQueryParam);

return sortParams.flatMap(sortParam => {
const searchParameter = fhirSearchParametersRegistry.getSearchParameter(resourceType, sortParam.searchParam);
if (searchParameter === undefined) {
throw new InvalidSearchParameterError(
`Unknown _sort parameter value: ${sortParam.searchParam}. Sort parameters values must use a valid Search Parameter`,
);
}
if (searchParameter.type !== 'date') {
throw new InvalidSearchParameterError(
`Invalid _sort parameter: ${sortParam.searchParam}. Only date type parameters can currently be used for sorting`,
);
}
return searchParameter.compiled.flatMap(compiledParam => {
return [
elasticsearchSort(compiledParam.path, sortParam.order),

// Date search params may target fields of type Period, so we add a sort clause for them.
// The FHIR spec does not fully specify how to sort by Period, but it makes sense that the most recent
// record is the one with the most recent "end" date and vice versa.
elasticsearchSort(
sortParam.order === 'desc' ? `${compiledParam.path}.end` : `${compiledParam.path}.start`,
sortParam.order,
rsmayda marked this conversation as resolved.
Show resolved Hide resolved
),
];
});
});
};
16 changes: 15 additions & 1 deletion src/__snapshots__/elasticSearchService.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1466,7 +1466,7 @@ Array [
]
`;

exports[`typeSearch query snapshots for simple queryParams; with ACTIVE filter queryParams={"_count":10,"_getpagesoffset":2} 1`] = `
exports[`typeSearch query snapshots for simple queryParams; with ACTIVE filter queryParams={"_count":10,"_getpagesoffset":2,"_sort":"_lastUpdated"} 1`] = `
Array [
Array [
Object {
Expand All @@ -1483,6 +1483,20 @@ Array [
"must": Array [],
},
},
"sort": Array [
Object {
"meta.lastUpdated": Object {
"order": "asc",
"unmapped_type": "long",
},
},
Object {
"meta.lastUpdated.start": Object {
"order": "asc",
"unmapped_type": "long",
},
},
],
},
"from": 2,
"index": "patient",
Expand Down
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export const enum SEARCH_PAGINATION_PARAMS {

export const SEPARATOR: string = '_';
export const ITERATIVE_INCLUSION_PARAMETERS = ['_include:iterate', '_revinclude:iterate'];
export const SORT_PARAMETER = '_sort';
export const NON_SEARCHABLE_PARAMETERS = [
SORT_PARAMETER,
SEARCH_PAGINATION_PARAMS.PAGES_OFFSET,
SEARCH_PAGINATION_PARAMS.COUNT,
'_format',
Expand Down
2 changes: 1 addition & 1 deletion src/elasticSearchService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('typeSearch', () => {
describe('query snapshots for simple queryParams; with ACTIVE filter', () => {
each([
[{}],
[{ _count: 10, _getpagesoffset: 2 }],
[{ _count: 10, _getpagesoffset: 2, _sort: '_lastUpdated' }],
[{ gender: 'female', name: 'Emily' }],
[{ gender: 'female', birthdate: 'gt1990' }],
[{ gender: 'female', identifier: 'http://acme.org/patient|2345' }],
Expand Down
20 changes: 17 additions & 3 deletions src/elasticSearchService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@ import {
FhirVersion,
} from 'fhir-works-on-aws-interface';
import { ElasticSearch } from './elasticSearch';
import { DEFAULT_SEARCH_RESULTS_PER_PAGE, SEARCH_PAGINATION_PARAMS, ITERATIVE_INCLUSION_PARAMETERS } from './constants';
import {
DEFAULT_SEARCH_RESULTS_PER_PAGE,
SEARCH_PAGINATION_PARAMS,
ITERATIVE_INCLUSION_PARAMETERS,
SORT_PARAMETER,
} from './constants';
import { buildIncludeQueries, buildRevIncludeQueries } from './searchInclusions';
import { FHIRSearchParametersRegistry } from './FHIRSearchParametersRegistry';
import { buildQueryForAllSearchParameters } from './QueryBuilder';
import { buildQueryForAllSearchParameters, buildSortClause } from './QueryBuilder';

const MAX_INCLUDE_ITERATIVE_DEPTH = 5;

Expand Down Expand Up @@ -84,14 +89,23 @@ export class ElasticSearchService implements Search {
]);
const query = buildQueryForAllSearchParameters(this.fhirSearchParametersRegistry, request, filter);

const params = {
const params: any = {
index: resourceType.toLowerCase(),
from,
size,
body: {
query,
},
};

if (request.queryParams[SORT_PARAMETER]) {
params.body.sort = buildSortClause(
this.fhirSearchParametersRegistry,
resourceType,
request.queryParams[SORT_PARAMETER],
);
}

const { total, hits } = await this.executeQuery(params);
const result: SearchResult = {
numberOfResults: total,
Expand Down