diff --git a/lib/server/query.js b/lib/server/query.js index 0e9750ad..bccf8a84 100644 --- a/lib/server/query.js +++ b/lib/server/query.js @@ -3,6 +3,7 @@ const traverse = require('traverse'); const ObjectID = require('mongodb-legacy').ObjectId; const moment = require('moment'); +const OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/; const TWO_DAYS = 172800000; /** @@ -90,9 +91,30 @@ function enforceDateFilter (query, opts) { * Forces anything named `_id` to be the `ObjectID` type. */ function updateIdQuery (query) { - if (query._id && query._id.length) { - query._id = ObjectID(query._id); + if (!Object.prototype.hasOwnProperty.call(query, '_id')) { + return; } + + if (typeof query._id === 'string') { + query._id = normalizeIdValue(query._id); + return; + } + + if (query._id && typeof query._id === 'object') { + traverse(query._id).forEach(function (x) { + if (this.isLeaf) { + this.update(normalizeIdValue(x)); + } + }); + } +} + +function normalizeIdValue (value) { + if (typeof value === 'string' && OBJECT_ID_HEX_RE.test(value)) { + return new ObjectID(value); + } + + return value; } /** @@ -213,7 +235,7 @@ walker.walk_prop = walk_prop; create.walker = walker; create.parseRegEx = parseRegEx; create.default_options = default_options; +create.normalizeIdValue = normalizeIdValue; // expose module as single high level function exports = module.exports = create; - diff --git a/lib/server/treatments.js b/lib/server/treatments.js index 2257afbb..22229023 100644 --- a/lib/server/treatments.js +++ b/lib/server/treatments.js @@ -7,6 +7,7 @@ var find_options = require('./query'); function storage (env, ctx) { var ObjectID = require('mongodb-legacy').ObjectId; + var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/; function create (objOrArray, fn) { @@ -49,10 +50,11 @@ function storage (env, ctx) { // Build bulkWrite operations for regular docs (no preBolus) // Prepare data and build bulk ops together var bulkOps = objOrArray.map(function(obj) { + normalizeTreatmentId(obj); var results = prepareData(obj); return { replaceOne: { - filter: { created_at: results.created_at, eventType: obj.eventType }, + filter: upsertQueryFor(obj, results), replacement: obj, upsert: true } @@ -90,13 +92,10 @@ function storage (env, ctx) { } function upsert (obj, fn) { + normalizeTreatmentId(obj); var results = prepareData(obj); - - var query = { - created_at: results.created_at - , eventType: obj.eventType - }; + var query = upsertQueryFor(obj, results); api( ).replaceOne(query, obj, {upsert: true}, function complete (err, updateResults) { @@ -121,8 +120,11 @@ function storage (env, ctx) { pbTreat.notes = obj.notes; } - query.created_at = pbTreat.created_at; - api( ).replaceOne(query, pbTreat, {upsert: true}, function pbComplete (err, updateResults) { + var pbQuery = { + created_at: pbTreat.created_at, + eventType: pbTreat.eventType + }; + api( ).replaceOne(pbQuery, pbTreat, {upsert: true}, function pbComplete (err, updateResults) { if (updateResults) { if (updateResults.upsertedCount == 1) { @@ -191,11 +193,14 @@ function storage (env, ctx) { } function save (obj, fn) { - obj._id = new ObjectID(obj._id); + normalizeTreatmentId(obj); prepareData(obj); - function saved (err, created) { + function saved (err, updateResults) { if (!err) { + if (updateResults && updateResults.upsertedCount == 1) { + obj._id = updateResults.upsertedId; + } // console.log('Treatment updated', created); ctx.ddata.processRawDataForRuntime(obj); @@ -209,10 +214,10 @@ function storage (env, ctx) { } if (err) console.error('Problem saving treating', err); - fn(err, created); + fn(err, obj); } - api().save(obj, saved); + api().replaceOne(upsertQueryFor(obj, { created_at: obj.created_at }), obj, {upsert: true}, saved); ctx.bus.emit('data-received'); } @@ -221,6 +226,27 @@ function storage (env, ctx) { return ctx.store.collection(env.treatments_collection); } + function upsertQueryFor (obj, results) { + if (Object.prototype.hasOwnProperty.call(obj, '_id') && obj._id !== null && obj._id !== '') { + return { _id: obj._id }; + } + + return { + created_at: results.created_at + , eventType: obj.eventType + }; + } + + function normalizeTreatmentId (obj) { + if (!Object.prototype.hasOwnProperty.call(obj, '_id') || obj._id === null || obj._id === '') { + return; + } + + if (typeof obj._id === 'string' && OBJECT_ID_HEX_RE.test(obj._id)) { + obj._id = new ObjectID(obj._id); + } + } + api.list = list; api.create = create; api.query_for = query_for; diff --git a/tests/api.treatments.test.js b/tests/api.treatments.test.js index b4afcd57..534b12c1 100644 --- a/tests/api.treatments.test.js +++ b/tests/api.treatments.test.js @@ -246,4 +246,113 @@ describe('Treatment API', function ( ) { } }); }); + + it('supports UUID treatment ids for post, put, and delete', function (done) { + var treatmentId = '69F15FD2-8075-4DEB-AEA3-4352F455840D'; + var originalCreatedAt = '2026-02-17T02:00:16.000Z'; + var repostedCreatedAt = '2026-02-17T02:05:16.000Z'; + var updatedCreatedAt = '2026-02-17T02:10:16.000Z'; + + self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () { + request(self.app) + .post('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send({ + _id: treatmentId, + eventType: 'Temporary Override', + created_at: originalCreatedAt, + durationType: 'indefinite', + correctionRange: [90, 110], + insulinNeedsScaleFactor: 1.2, + reason: 'test override' + }) + .expect(200) + .end(function (err) { + if (err) { + return done(err); + } + + request(self.app) + .post('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send({ + _id: treatmentId, + eventType: 'Temporary Override', + created_at: repostedCreatedAt, + duration: 60, + correctionRange: [90, 110], + insulinNeedsScaleFactor: 1.2, + reason: 'reposted override' + }) + .expect(200) + .end(function (err) { + if (err) { + return done(err); + } + + self.ctx.treatments.list({ find: { _id: treatmentId } }, function (err, list) { + if (err) { + return done(err); + } + + list.length.should.equal(1); + list[0]._id.should.equal(treatmentId); + list[0].created_at.should.equal(repostedCreatedAt); + list[0].duration.should.equal(60); + should.not.exist(list[0].durationType); + + request(self.app) + .put('/api/treatments/') + .set('api-secret', api_secret_hash || '') + .send({ + _id: treatmentId, + eventType: 'Temporary Override', + created_at: updatedCreatedAt, + duration: 30, + correctionRange: [90, 110], + insulinNeedsScaleFactor: 1.2, + reason: 'updated override' + }) + .expect(200) + .end(function (err) { + if (err) { + return done(err); + } + + self.ctx.treatments.list({ find: { _id: treatmentId } }, function (err, updatedList) { + if (err) { + return done(err); + } + + updatedList.length.should.equal(1); + updatedList[0]._id.should.equal(treatmentId); + updatedList[0].created_at.should.equal(updatedCreatedAt); + updatedList[0].duration.should.equal(30); + updatedList[0].reason.should.equal('updated override'); + + request(self.app) + .delete('/api/treatments/' + encodeURIComponent(treatmentId)) + .set('api-secret', api_secret_hash || '') + .expect(200) + .end(function (err) { + if (err) { + return done(err); + } + + self.ctx.treatments.list({ find: { _id: treatmentId } }, function (err, deletedList) { + if (err) { + return done(err); + } + + deletedList.length.should.equal(0); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); }); diff --git a/tests/query.test.js b/tests/query.test.js index 92a8a806..9e20ad40 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); + }); });