UNPKG

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
'use strict'; 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;