diff --git a/lib/authorization/delaylist.js b/lib/authorization/delaylist.js index fe6c8096153..681531b5308 100644 --- a/lib/authorization/delaylist.js +++ b/lib/authorization/delaylist.js @@ -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) { diff --git a/lib/server/bootevent.js b/lib/server/bootevent.js index 464439afe82..bf7e29dc099 100644 --- a/lib/server/bootevent.js +++ b/lib/server/bootevent.js @@ -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); diff --git a/lib/server/devicestatus.js b/lib/server/devicestatus.js index bf71437d646..50a9c2258b3 100644 --- a/lib/server/devicestatus.js +++ b/lib/server/devicestatus.js @@ -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) { @@ -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); } diff --git a/lib/server/query.js b/lib/server/query.js index 8279d5ad1e8..d8f00f45123 100644 --- a/lib/server/query.js +++ b/lib/server/query.js @@ -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); } } diff --git a/tests/query.test.js b/tests/query.test.js index 92a8a80673a..9e20ad40639 100644 --- a/tests/query.test.js +++ b/tests/query.test.js @@ -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); + }); });