nightscout
Version:
Nightscout acts as a web-based CGM (Continuous Glucose Monitor) to allow multiple caregivers to remotely view a patients glucose data in realtime.
220 lines (194 loc) • 7.28 kB
JavaScript
;
const traverse = require('traverse');
const ObjectID = require('mongodb').ObjectID;
const moment = require('moment');
const TWO_DAYS = 172800000;
/**
* @module query utilities
* Assist in translating objects from query-string representation into
* mongo-style queries by performing type translation.
*/
/**
* Options for query.
* Interpret and return the options to use for building our query.
*
* @returns {Object} Options for create, below.
```
* { deltaAgo: <ms> // ms ago to constrain queries missing any query body
, dateField: "date" // name of field to ensure there is a valid query body
, walker: <walker-spec> // a mapping of names to types
}
```
*/
function default_options (opts) {
opts = opts || { };
if (opts) {
var keys = [null].concat(Object.keys(opts));
// default at least TWO_DAYS of data
// TODO: discuss/consensus on right value/ENV?
if (keys.indexOf('deltaAgo') < 1) {
opts.deltaAgo = ( TWO_DAYS * 2 );
}
// default at `date` and `sgv` properties are both int-types.
if (keys.indexOf('walker') < 1) {
opts.walker = { date: parseInt, sgv: parseInt };
}
// The default field to constrain is called 'date' for entries module.
// Allow other models/backends to use other fields names.
opts.dateField = opts.dateField || 'date';
}
return opts;
}
/**
* Enforce rule that says that the query must express some constraint on the
* configured `dateField` or against the field named `dateString`. If the
* configured option `useEpoch` is set, the naive JS epoch is used, otherwise
* ISO 8601 is used. The rule ensures that records must have a date field
* with a date and time greater than or equal to the configured `deltaAgo`
* option, (`opts.deltaAgo`).
*/
function enforceDateFilter (query, opts) {
var dateValue = query[opts.dateField];
// rewrite dates to ISO UTC strings so queries work as expected
if (dateValue) {
Object.keys(dateValue).forEach(function(key) {
let dateString = dateValue[key];
if (isNaN(dateString)) {
dateString = dateString.replace(' ', '+'); // some clients don't excape the plus
const validDate = moment(dateString).isValid();
if (!validDate) {
console.error('API request using an invalid date:', dateString);
throw new Error('Cannot parse ' + dateString + ' as a valid ISO-8601 date');
}
const d = moment.parseZone(dateString);
dateValue[key] = d.toISOString();
}
});
}
if (!dateValue && !query.dateString && true !== opts.noDateFilter) {
var minDate = Date.now( ) - opts.deltaAgo;
query[opts.dateField] = {
$gte: opts.useEpoch ? minDate : new Date(minDate).toISOString()
};
}
}
/**
* Helper to set ObjectID type for `_id` queries.
* Forces anything named `_id` to be the `ObjectID` type.
*/
function updateIdQuery (query) {
if (query._id && query._id.length) {
query._id = ObjectID(query._id);
}
}
/**
* @param QueryParams params Object returned by qs.parse or https://github.com/hapijs/qs
* @param BuilderOpts opts Options for how to translate types.
*
* Allows performing logic described by a model's attributes.
* Specifically, we try to ensure that all queries have some kind of query
* body to filter the rows mongodb will spool. The defaults, such as name and
* representation of a date field can be configured via the `opts` passed in.
*
* @returns Object An object which can be passed to `mongodb.find( )`
*/
function create (params, opts) {
// setup default options for what/how to do things
opts = default_options(opts);
// Build the iterator, pass it our initial params to et the results.
var finder = walker(opts.walker)(params);
// Get the final query to pass to mongodb.
var query = finder && finder.find ? finder.find : { };
// Ensure some kind of sane date constraint tied to an index is expressed in the query.
// unless an ID is provided, in which case assume the user knows what they are doing.
if (! query._id ) {
enforceDateFilter(query, opts);
}
// Help queries for _id.
updateIdQuery(query);
//console.info('query:', query);
// Ready for mongodb.find( ) and friends.
return query;
}
/**
* Configure a single iterator given a specification of named mapped to types.
* @params Object spec A simple mapping of field names to function to create that type.
*
* Example spec: { sgv: parseInt }
* @returns function Function will translate types expressed in query.
*/
function walker (spec) {
// empty queue
var fns = [ ];
// for each key/value pair in the spec
var keys = Object.keys(spec);
keys.forEach(function config (prop) {
var typer = spec[prop];
// add function from walk_prop to the queue
fns.push(walk_prop(prop, typer));
});
/**
* Execute all configured mappings in single step.
* @param Object obj QueryString object
* @returns Object for mongodb queries, with fields set to appropriate type
described by previous mapping.
*/
function exec (obj) {
var fn;
// for each mapping in the queue
while (fns.length > 0) {
fn = fns.shift( );
// do each mapping
obj = fn(obj);
}
return obj;
}
// return a function that can execute the configured queue of translations
return exec;
}
/**
* Given a name and a type, return a function which will transform any value
* on a leaf-node into that type.
* @param String prop Property name to to translate.
* @param function typer Function to convert to type, eg `parseInt`
*/
function walk_prop (prop, typer) {
function iter (opts) {
// This is specifically configured to match the `find` convention in our REST API.
// Query parameters are the ones attached to the `find` object.
if (opts && opts.find && opts.find[prop]) {
if (typeof opts.find[prop] === 'string') {
//simple string property, no need to traverse
opts.find[prop] = typer(opts.find[prop]);
} else {
// Traverse any query elements associated with this property.
traverse(opts.find[prop]).forEach(function (x) {
// In Mongo queries, the leaf nodes are always the values to search for.
// Ignore any interstitial arrays/objects to represent
// greater-than-or-equal-to, etc.
if (this.isLeaf) {
// Leaf nodes should be converted to this type.
this.update(typer(x));
}
});
}
}
// Return opts after modifying in place.
return opts;
}
return iter;
}
function parseRegEx (str) {
var regtest = /\/(.*)\/(.*)/.exec(str);
if (regtest) {
return new RegExp(regtest[1],regtest[2]);
}
return str;
}
// attach helpers and utilities to main function for testing
walker.walk_prop = walk_prop;
create.walker = walker;
create.parseRegEx = parseRegEx;
create.default_options = default_options;
// expose module as single high level function
exports = module.exports = create;