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 3 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 be used for sorting`,
carvantes marked this conversation as resolved.
Show resolved Hide resolved
);
}
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