Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion lib/authorization/delaylist.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ function init (env) {

const ipDelayList = {};

const DELAY_ON_FAIL = _.get(env, 'settings.authFailDelay') || 5000;
const rawAuthFailDelay = _.get(env, 'settings.authFailDelay');
const parsedAuthFailDelay = Number(rawAuthFailDelay);
const DELAY_ON_FAIL = Number.isFinite(parsedAuthFailDelay) && parsedAuthFailDelay > 0 ? parsedAuthFailDelay : 5000;
const FAIL_AGE = 60000;

ipDelayList.addFailedRequest = function addFailedRequest (ip) {
Expand Down
2 changes: 1 addition & 1 deletion lib/server/bootevent.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ function boot (env, language) {
ctx.activity = require('./activity')(env, ctx);
ctx.entries = require('./entries')(env, ctx);
ctx.treatments = require('./treatments')(env, ctx);
ctx.devicestatus = require('./devicestatus')(env.devicestatus_collection, ctx);
ctx.devicestatus = require('./devicestatus')(env, ctx);
ctx.profile = require('./profile')(env.profile_collection, ctx);
ctx.food = require('./food')(env, ctx);
ctx.pebble = require('./pebble')(env, ctx);
Expand Down
133 changes: 95 additions & 38 deletions lib/server/devicestatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,110 @@
var moment = require('moment');
var find_options = require('./query');

function storage (collection, ctx) {
/**
* Truncate OpenAPS prediction arrays to a bounded length.
*
* This helper limits the size of `openaps.suggested.predBGs` and
* `openaps.enacted.predBGs` arrays to avoid unbounded growth of
* `devicestatus` documents, which helps keep MongoDB documents
* under the 16 MB size limit.
*
* The function only operates on the following prediction series,
* when present:
* - `IOB` (insulin-on-board-based predictions)
* - `COB` (carbs-on-board-based predictions)
* - `UAM` (unannounced-meal-based predictions)
* - `ZT` (zero-temp-based predictions)
*
* The `maxSize` argument defines the maximum number of prediction
* points retained per series. When not explicitly configured by
* `env.predictionsMaxSize`, this is typically set to 288, which
* corresponds to 24 hours of 5-minute prediction intervals
* (24 * 60 / 5) and provides a practical upper bound to keep
* documents reasonably small for MongoDB storage.
*
* If `maxSize` is falsy or not a positive number, the input
* object is returned unmodified.
*
* @param {Object} obj - The devicestatus-like object potentially containing
* `openaps.suggested.predBGs` and/or `openaps.enacted.predBGs`.
* @param {number} maxSize - Maximum allowed length for each prediction array
* (commonly 288 by default).
* @returns {Object} The same object reference, possibly with truncated
* prediction arrays.
*/
function truncatePredictions (obj, maxSize) {
if (!maxSize || maxSize <= 0) return obj;

var predictionTypes = ['IOB', 'COB', 'UAM', 'ZT'];

if (obj && obj.openaps && obj.openaps.suggested && obj.openaps.suggested.predBGs) {
var suggestedPredBGs = obj.openaps.suggested.predBGs;
predictionTypes.forEach(function(type) {
if (Array.isArray(suggestedPredBGs[type]) && suggestedPredBGs[type].length > maxSize) {
suggestedPredBGs[type] = suggestedPredBGs[type].slice(0, maxSize);
}
});
}

function create (statuses, fn) {
if (obj && obj.openaps && obj.openaps.enacted && obj.openaps.enacted.predBGs) {
var enactedPredBGs = obj.openaps.enacted.predBGs;
predictionTypes.forEach(function(type) {
if (Array.isArray(enactedPredBGs[type]) && enactedPredBGs[type].length > maxSize) {
enactedPredBGs[type] = enactedPredBGs[type].slice(0, maxSize);
}
});
}

if (!Array.isArray(statuses)) { statuses = [statuses]; }
return obj;
}

const r = [];
let errorOccurred = false;
function storage (env, ctx) {

for (let i = 0; i < statuses.length; i++) {
var collection = env.devicestatus_collection;
var predictionsMaxSize = env.predictionsMaxSize || null;

function create (statuses, fn) {

const obj = statuses[i];
if (!Array.isArray(statuses)) { statuses = [statuses]; }

if (errorOccurred) return;
if (statuses.length === 0) {
return fn(null, []);
}

// Normalize all dates to UTC
const d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment();
// Prepare all documents before insert
statuses.forEach(function(obj) {
var d = moment(obj.created_at).isValid() ? moment.parseZone(obj.created_at) : moment();
obj.created_at = d.toISOString();
obj.utcOffset = d.utcOffset();
truncatePredictions(obj, predictionsMaxSize);
});

api().insertOne(obj, function(err, results) {
if (err !== null && err.message) {
console.log('Error inserting the device status object', err.message);
errorOccurred = true;
fn(err.message, null);
return;
}

if (!err) {

if (!obj._id) obj._id = results.insertedIds[0]._id;
r.push(obj);

ctx.bus.emit('data-update', {
type: 'devicestatus'
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime([obj])
});

// Last object! Return results
if (i == statuses.length - 1) {
fn(null, r);
ctx.bus.emit('data-received');
}
}
// Use insertMany for batch insert
api().insertMany(statuses, { ordered: true }, function(err, insertResult) {
if (err) {
console.log('Error inserting device status objects', err.message);
fn(err.message || err, null);
return;
}

// Assign _ids from insertMany result
if (insertResult && insertResult.insertedIds) {
Object.keys(insertResult.insertedIds).forEach(function(index) {
statuses[index]._id = insertResult.insertedIds[index];
});
}

// Emit data-update for all inserted documents
ctx.bus.emit('data-update', {
type: 'devicestatus'
, op: 'update'
, changes: ctx.ddata.processRawDataForRuntime(statuses)
});
};

ctx.bus.emit('data-received');
fn(null, statuses);
});
}

function last (fn) {
Expand Down Expand Up @@ -103,14 +160,14 @@ function storage (collection, ctx) {
ctx.bus.emit('data-update', {
type: 'devicestatus'
, op: 'remove'
, count: stat.result.n
, count: stat.deletedCount
, changes: opts.find._id
});

fn(err, stat);
}

return api().remove(
return api().deleteMany(
query_for(opts), removed);
}

Expand Down
8 changes: 6 additions & 2 deletions lib/server/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,16 @@ function enforceDateFilter (query, opts) {
}
}

// A MongoDB ObjectID is exactly 24 hexadecimal characters.
const OBJECT_ID_PATTERN = /^[a-fA-F0-9]{24}$/;

/**
* Helper to set ObjectID type for `_id` queries.
* Forces anything named `_id` to be the `ObjectID` type.
* Only converts strings that match the 24-character hexadecimal ObjectID
* format; non-ObjectID strings (e.g. UUIDs) are left unchanged.
*/
function updateIdQuery (query) {
if (query._id && query._id.length) {
if (query._id && typeof query._id === 'string' && OBJECT_ID_PATTERN.test(query._id)) {
query._id = ObjectID(query._id);
}
}
Expand Down
14 changes: 14 additions & 0 deletions tests/query.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,18 @@ describe('query', function ( ) {

(typeof opts.date).should.equal('undefined')
});

it('should keep non-ObjectId _id queries as strings', function ( ) {
var uuid = '69F15FD2-8075-4DEB-AEA3-4352F455840D';
var opts = query({ find: { _id: uuid } });

opts._id.should.equal(uuid);
});

it('should convert ObjectId-shaped _id queries', function ( ) {
var objectId = '55cbd4e47e726599048a3f91';
var opts = query({ find: { _id: objectId } });

opts._id.toString().should.equal(objectId);
});
});