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.
292 lines (231 loc) • 7.17 kB
JavaScript
;
var _ = require('lodash');
var async = require('async');
var moment = require('moment');
var find_options = require('./query');
function storage (env, ctx) {
var ObjectID = require('mongodb').ObjectID;
function create (objOrArray, fn) {
function done (err, result) {
ctx.bus.emit('data-received');
fn(err, result);
}
if (_.isArray(objOrArray)) {
var allDocs = [];
var errs = [];
async.eachSeries(objOrArray, function (obj, callback) {
upsert(obj, function upserted (err, docs) {
allDocs = allDocs.concat(docs);
errs.push(err);
callback(err, docs)
});
}, function () {
errs = _.compact(errs);
done(errs.length > 0 ? errs : null, allDocs);
});
} else {
upsert(objOrArray, function upserted (err, docs) {
done(err, docs);
});
}
}
function upsert (obj, fn) {
var results = prepareData(obj);
var query = {
created_at: results.created_at
, eventType: obj.eventType
};
api( ).update(query, obj, {upsert: true}, function complete (err, updateResults) {
if (err) console.error('Problem upserting treatment', err);
if (!err) {
if (updateResults.result.upserted) {
obj._id = updateResults.result.upserted[0]._id
}
}
// TODO document this feature
if (!err && obj.preBolus) {
//create a new object to insert copying only the needed fields
var pbTreat = {
created_at: (new Date(new Date(results.created_at).getTime() + (obj.preBolus * 60000))).toISOString(),
eventType: obj.eventType,
carbs: results.preBolusCarbs
};
if (obj.notes) {
pbTreat.notes = obj.notes;
}
query.created_at = pbTreat.created_at;
api( ).update(query, pbTreat, {upsert: true}, function pbComplete (err, updateResults) {
if (!err) {
if (updateResults.result.upserted) {
pbTreat._id = updateResults.result.upserted[0]._id
}
}
var treatments = _.compact([obj, pbTreat]);
ctx.bus.emit('data-update', {
type: 'treatments',
op: 'update',
changes: ctx.ddata.processRawDataForRuntime(treatments)
});
fn(err, treatments);
});
} else {
ctx.bus.emit('data-update', {
type: 'treatments',
op: 'update',
changes: ctx.ddata.processRawDataForRuntime([obj])
});
fn(err, [obj]);
}
});
}
function list (opts, fn) {
function limit ( ) {
if (opts && opts.count) {
return this.limit(parseInt(opts.count));
}
return this;
}
return limit.call(api()
.find(query_for(opts))
.sort(opts && opts.sort || {created_at: -1}), opts)
.toArray(fn);
}
function query_for (opts) {
return find_options(opts, storage.queryOpts);
}
function remove (opts, fn) {
return api( ).remove(query_for(opts), function (err, stat) {
//TODO: this is triggering a read from Mongo, we can do better
//console.log('Treatment removed', opts); // , stat);
ctx.bus.emit('data-update', {
type: 'treatments',
op: 'remove',
count: stat.result.n,
changes: opts.find._id
});
ctx.bus.emit('data-received');
fn(err, stat);
});
}
function save (obj, fn) {
obj._id = new ObjectID(obj._id);
prepareData(obj);
function saved (err, created) {
if (!err) {
// console.log('Treatment updated', created);
ctx.ddata.processRawDataForRuntime(obj);
ctx.bus.emit('data-update', {
type: 'treatments',
op: 'update',
changes: ctx.ddata.processRawDataForRuntime([obj])
});
}
if (err) console.error('Problem saving treating', err);
fn(err, created);
}
api().save(obj, saved);
ctx.bus.emit('data-received');
}
function api ( ) {
return ctx.store.collection(env.treatments_collection);
}
api.list = list;
api.create = create;
api.query_for = query_for;
api.indexedFields = [
'created_at'
, 'eventType'
, 'insulin'
, 'carbs'
, 'glucose'
, 'enteredBy'
, 'boluscalc.foods._id'
, 'notes'
, 'NSCLIENT_ID'
, 'percent'
, 'absolute'
, 'duration'
, { 'eventType' : 1, 'duration' : 1, 'created_at' : 1 }
];
api.remove = remove;
api.save = save;
api.aggregate = require('./aggregate')({ }, api);
return api;
}
function prepareData(obj) {
// Convert all dates to UTC dates
// TODO remove this -> must not create new date if missing
const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment();
obj.created_at = d.toISOString();
var results = {
created_at: obj.created_at
, preBolusCarbs: ''
};
const offset = d.utcOffset();
obj.utcOffset = offset;
results.offset = offset;
obj.glucose = Number(obj.glucose);
obj.targetTop = Number(obj.targetTop);
obj.targetBottom = Number(obj.targetBottom);
obj.carbs = Number(obj.carbs);
obj.insulin = Number(obj.insulin);
obj.duration = Number(obj.duration);
obj.percent = Number(obj.percent);
obj.absolute = Number(obj.absolute);
obj.relative = Number(obj.relative);
obj.preBolus = Number(obj.preBolus);
//NOTE: the eventTime is sent by the client, but deleted, we only store created_at
var eventTime;
if (obj.eventTime) {
eventTime = new Date(obj.eventTime).toISOString();
results.created_at = eventTime;
}
obj.created_at = results.created_at;
if (obj.preBolus && obj.preBolus !== 0 && obj.carbs) {
results.preBolusCarbs = obj.carbs;
delete obj.carbs;
}
if (obj.eventType === 'Announcement') {
obj.isAnnouncement = true;
}
// clean data
delete obj.eventTime;
function deleteIfEmpty (field) {
if (!obj[field] || obj[field] === 0) {
delete obj[field];
}
}
function deleteIfNaN (field) {
if (isNaN(obj[field])) {
delete obj[field];
}
}
deleteIfEmpty('targetTop');
deleteIfEmpty('targetBottom');
deleteIfEmpty('carbs');
deleteIfEmpty('insulin');
deleteIfEmpty('percent');
deleteIfEmpty('relative');
deleteIfEmpty('notes');
deleteIfEmpty('preBolus');
deleteIfNaN('absolute');
deleteIfNaN('duration');
if (obj.glucose === 0 || isNaN(obj.glucose)) {
delete obj.glucose;
delete obj.glucoseType;
delete obj.units;
}
return results;
}
storage.queryOpts = {
walker: {
insulin: parseInt
, carbs: parseInt
, glucose: parseInt
, notes: find_options.parseRegEx
, eventType: find_options.parseRegEx
, enteredBy: find_options.parseRegEx
}
, dateField: 'created_at'
};
module.exports = storage;