@asymmetrik/fhir-qb-mongo
Advanced tools
Comparing version 0.10.2 to 0.10.3
124
index.js
@@ -10,3 +10,3 @@ let supportedSearchTransformations = { | ||
*/ | ||
let buildAndQuery = function({ queries }) { | ||
let buildAndQuery = function(queries) { | ||
return { $and: queries }; | ||
@@ -18,3 +18,3 @@ }; | ||
*/ | ||
let buildOrQuery = function({ queries, invert }) { | ||
let buildOrQuery = function({ queries, invert = false }) { | ||
return { [invert ? '$nor' : '$or']: queries }; | ||
@@ -119,7 +119,83 @@ }; | ||
/** | ||
* Takes in 2 lists, joinsToPerform and matchesToPerform. Constructs a mongo aggregation query that first performs | ||
* any necessary joins as dictated by joinsToPerform, and then filters the results them down using matchesToPerform. | ||
* | ||
* Returns a mongo aggregate query. | ||
* TODO - WORK IN PROGRESS | ||
* Apply search result transformations | ||
* @param query | ||
* @param searchResultTransformations | ||
*/ | ||
let applySearchResultTransformations = function({ | ||
query, | ||
searchResultTransformations, | ||
}) { | ||
Object.keys(searchResultTransformations).forEach(transformation => { | ||
query.push( | ||
supportedSearchTransformations[transformation]( | ||
searchResultTransformations[transformation], | ||
), | ||
); | ||
}); | ||
return query; | ||
}; | ||
/** | ||
* If we should not include archived, add a filter to remove them from the results | ||
* @param query | ||
* @param archivedParamPath | ||
* @param includeArchived | ||
* @returns {*} | ||
*/ | ||
let applyArchivedFilter = function({ | ||
query, | ||
archivedParamPath, | ||
includeArchived, | ||
}) { | ||
if (!includeArchived) { | ||
query.push({ $match: { [archivedParamPath]: false } }); | ||
} | ||
return query; | ||
}; | ||
/** | ||
* Apply paging | ||
* @param query | ||
* @param pageNumber | ||
* @param resultsPerPage | ||
* @returns {*} | ||
*/ | ||
let applyPaging = function({ query, pageNumber, resultsPerPage }) { | ||
// If resultsPerPage is defined, skip to the appropriate page and limit the number of results that appear per page. | ||
// Otherwise just insert a filler (to keep mongo happy) that skips no entries. | ||
let pageSelection = resultsPerPage | ||
? [{ $skip: (pageNumber - 1) * resultsPerPage }, { $limit: resultsPerPage }] | ||
: [{ $skip: 0 }]; | ||
// If resultsPerPage is defined, calculate the total number of pages as the total number of records | ||
// divided by the results per page rounded up to the nearest integer. | ||
// Otherwise if resultsPerPage is not defined, all of the results will be on one page. | ||
let numberOfPages = resultsPerPage | ||
? { $ceil: { $divide: ['$total', resultsPerPage] } } | ||
: 1; | ||
query.push({ | ||
$facet: { | ||
metadata: [ | ||
{ $count: 'total' }, | ||
{ $addFields: { numberOfPages: numberOfPages } }, | ||
{ $addFields: { page: pageNumber } }, // TODO may need some additional validation on this. | ||
], | ||
data: pageSelection, | ||
}, | ||
}); | ||
return query; | ||
}; | ||
/** | ||
* Assembles a mongo aggregation pipeline | ||
* @param joinsToPerform - List of joins to perform first through lookups | ||
* @param matchesToPerform - List of matches to perform | ||
* @param searchResultTransformations | ||
* @param implementationParameters | ||
* @param includeArchived | ||
* @param pageNumber | ||
* @param resultsPerPage | ||
* @returns {Array} | ||
*/ | ||
let assembleSearchQuery = function({ | ||
@@ -129,6 +205,16 @@ joinsToPerform, | ||
searchResultTransformations, | ||
implementationParameters, | ||
includeArchived, | ||
pageNumber, | ||
resultsPerPage, | ||
}) { | ||
let aggregatePipeline = []; | ||
let query = []; | ||
let toSuppress = {}; | ||
// Check that the necessary implementation parameters were passed through | ||
let {archivedParamPath} = implementationParameters; | ||
if (!archivedParamPath) { | ||
throw new Error('Missing required implementation parameter \'archivedParamPath\''); | ||
} | ||
// Construct the necessary joins and add them to the aggregate pipeline. Also follow each $lookup with an $unwind | ||
@@ -139,3 +225,3 @@ // for ease of use. | ||
let { from, localKey, foreignKey } = join; | ||
aggregatePipeline.push({ | ||
query.push({ | ||
$lookup: { | ||
@@ -148,3 +234,3 @@ from: from, | ||
}); | ||
aggregatePipeline.push({ $unwind: `$${from}` }); | ||
query.push({ $unwind: `$${from}` }); | ||
toSuppress[from] = 0; | ||
@@ -163,3 +249,3 @@ } | ||
} | ||
aggregatePipeline.push({ $match: buildAndQuery({ queries: listOfOrs }) }); | ||
query.push({ $match: buildAndQuery(listOfOrs) }); | ||
} | ||
@@ -169,15 +255,13 @@ | ||
if (Object.keys(toSuppress).length > 0) { | ||
aggregatePipeline.push({ $project: toSuppress }); | ||
query.push({ $project: toSuppress }); | ||
} | ||
// TODO - WORK IN PROGRESS - handling search result transformations | ||
// Handle search result parameters | ||
Object.keys(searchResultTransformations).forEach(transformation => { | ||
aggregatePipeline.push( | ||
supportedSearchTransformations[transformation]( | ||
searchResultTransformations[transformation], | ||
), | ||
); | ||
query = applyArchivedFilter({ query, archivedParamPath, includeArchived }); | ||
query = applySearchResultTransformations({ | ||
query, | ||
searchResultTransformations, | ||
}); | ||
return aggregatePipeline; | ||
query = applyPaging({ query, pageNumber, resultsPerPage }); | ||
return query; | ||
}; | ||
@@ -184,0 +268,0 @@ |
@@ -196,4 +196,20 @@ const mongoQB = require('./index'); | ||
describe('assembleSearchQuery Tests', () => { | ||
test('Should return empty pipeline if no matches or joins to perform', () => { | ||
const expectedResult = []; | ||
test('Should return empty pipeline (except for archival and paging) if no matches or joins to perform', () => { | ||
const expectedResult = [ | ||
{ $match: { 'meta._isArchived': false } }, | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }, { $limit: 10 }], | ||
metadata: [ | ||
{ $count: 'total' }, | ||
{ | ||
$addFields: { | ||
numberOfPages: { $ceil: { $divide: ['$total', 10] } }, | ||
}, | ||
}, | ||
{ $addFields: { page: 1 } }, | ||
], | ||
}, | ||
}, | ||
]; | ||
let observedResult = mongoQB.assembleSearchQuery({ | ||
@@ -203,2 +219,6 @@ joinsToPerform: [], | ||
searchResultTransformations: {}, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
resultsPerPage: 10, | ||
}); | ||
@@ -219,2 +239,17 @@ expect(observedResult).toEqual(expectedResult); | ||
{ $project: { foo: 0 } }, | ||
{ $match: { 'meta._isArchived': false } }, | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }, { $limit: 10 }], | ||
metadata: [ | ||
{ $count: 'total' }, | ||
{ | ||
$addFields: { | ||
numberOfPages: { $ceil: { $divide: ['$total', 10] } }, | ||
}, | ||
}, | ||
{ $addFields: { page: 1 } }, | ||
], | ||
}, | ||
}, | ||
]; | ||
@@ -225,2 +260,6 @@ let observedResult = mongoQB.assembleSearchQuery({ | ||
searchResultTransformations: {}, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
resultsPerPage: 10, | ||
}); | ||
@@ -230,3 +269,20 @@ expect(observedResult).toEqual(expectedResult); | ||
test('Should fill in empty matches with empty objects to keep queries valid', () => { | ||
const expectedResult = [{ $match: { $and: [{ $or: [{}] }] } }]; | ||
const expectedResult = [ | ||
{ $match: { $and: [{ $or: [{}] }] } }, | ||
{ $match: { 'meta._isArchived': false } }, | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }, { $limit: 10 }], | ||
metadata: [ | ||
{ $count: 'total' }, | ||
{ | ||
$addFields: { | ||
numberOfPages: { $ceil: { $divide: ['$total', 10] } }, | ||
}, | ||
}, | ||
{ $addFields: { page: 1 } }, | ||
], | ||
}, | ||
}, | ||
]; | ||
let observedResult = mongoQB.assembleSearchQuery({ | ||
@@ -236,2 +292,6 @@ joinsToPerform: [], | ||
searchResultTransformations: {}, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
resultsPerPage: 10, | ||
}); | ||
@@ -243,2 +303,17 @@ expect(observedResult).toEqual(expectedResult); | ||
{ $match: { $and: [{ $or: [{ foo: { $gte: 1, $lte: 10 } }] }] } }, | ||
{ $match: { 'meta._isArchived': false } }, | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }, { $limit: 10 }], | ||
metadata: [ | ||
{ $count: 'total' }, | ||
{ | ||
$addFields: { | ||
numberOfPages: { $ceil: { $divide: ['$total', 10] } }, | ||
}, | ||
}, | ||
{ $addFields: { page: 1 } }, | ||
], | ||
}, | ||
}, | ||
]; | ||
@@ -249,2 +324,6 @@ let observedResult = mongoQB.assembleSearchQuery({ | ||
searchResultTransformations: {}, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
resultsPerPage: 10, | ||
}); | ||
@@ -256,3 +335,20 @@ expect(observedResult).toEqual(expectedResult); | ||
test('Should add $limit to the end of the pipeline when given _count parameter', () => { | ||
const expectedResult = [{ $limit: 3 }]; | ||
const expectedResult = [ | ||
{ $match: { 'meta._isArchived': false } }, | ||
{ $limit: 3 }, | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }, { $limit: 10 }], | ||
metadata: [ | ||
{ $count: 'total' }, | ||
{ | ||
$addFields: { | ||
numberOfPages: { $ceil: { $divide: ['$total', 10] } }, | ||
}, | ||
}, | ||
{ $addFields: { page: 1 } }, | ||
], | ||
}, | ||
}, | ||
]; | ||
let observedResult = mongoQB.assembleSearchQuery({ | ||
@@ -262,2 +358,6 @@ joinsToPerform: [], | ||
searchResultTransformations: { _count: 3 }, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
resultsPerPage: 10, | ||
}); | ||
@@ -267,2 +367,94 @@ expect(observedResult).toEqual(expectedResult); | ||
}); | ||
describe('Paging Tests', () => { | ||
test('Should default to page 1 with no limits if resultsPerPage is undefined', () => { | ||
const expectedResult = [ | ||
{ | ||
$match: { | ||
'meta._isArchived': false, | ||
}, | ||
}, | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }], | ||
metadata: [ | ||
{ | ||
$count: 'total', | ||
}, | ||
{ | ||
$addFields: { | ||
numberOfPages: 1, | ||
}, | ||
}, | ||
{ | ||
$addFields: { | ||
page: 1, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
]; | ||
let observedResult = mongoQB.assembleSearchQuery({ | ||
joinsToPerform: [], | ||
matchesToPerform: [], | ||
searchResultTransformations: {}, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
}); | ||
expect(observedResult).toEqual(expectedResult); | ||
}); | ||
}); | ||
describe('Apply Archived Filter Tests', () => { | ||
test('Should throw an error if missing the required archivedParamPath from the implementation parameters', () => { | ||
let error; | ||
try { | ||
mongoQB.assembleSearchQuery({ | ||
joinsToPerform: [], | ||
matchesToPerform: [], | ||
searchResultTransformations: {}, | ||
implementationParameters: {}, | ||
includeArchived: false, | ||
pageNumber: 1, | ||
}); | ||
} catch (err) { | ||
error = err; | ||
} | ||
expect(error.message).toContain('Missing required implementation parameter \'archivedParamPath\''); | ||
}); | ||
test('Should return input query as is if we are not filtering out archived results', () => { | ||
const expectedResult = [ | ||
{ | ||
$facet: { | ||
data: [{ $skip: 0 }, {$limit: 10}], | ||
metadata: [ | ||
{ | ||
$count: 'total', | ||
}, | ||
{ | ||
$addFields: { | ||
numberOfPages: {$ceil: {$divide:['$total',10]}}, | ||
}, | ||
}, | ||
{ | ||
$addFields: { | ||
page: 1, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
]; | ||
let observedResult = mongoQB.assembleSearchQuery({ | ||
joinsToPerform: [], | ||
matchesToPerform: [], | ||
searchResultTransformations: {}, | ||
implementationParameters: {archivedParamPath: 'meta._isArchived'}, | ||
includeArchived: true, | ||
pageNumber: 1, | ||
resultsPerPage: 10 | ||
}); | ||
expect(observedResult).toEqual(expectedResult); | ||
}); | ||
}); | ||
}); |
{ | ||
"name": "@asymmetrik/fhir-qb-mongo", | ||
"version": "0.10.2", | ||
"version": "0.10.3", | ||
"description": "FHIR query builder for Mongo DB", | ||
@@ -35,3 +35,3 @@ "main": "index.js", | ||
}, | ||
"gitHead": "d5334845f7d5f5d2e00c3826016757bbfef9c4f4" | ||
"gitHead": "0f6aeb28427f9a5574a5a6bbe0ead0cdf0ad8af8" | ||
} |
@@ -23,3 +23,2 @@ # FHIR-Query-Builder-Mongo | ||
``` | ||
These are used by the fhir-qb to build a query that will work in the mongo | ||
aggregation pipeline. | ||
These are used by the fhir-qb to build a query that will work in the mongo aggregation pipeline. |
23762
699
24