mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Merge pull request #8474 from nightscout/fix/mongo-save-hardening
Fix/mongo save hardening
This commit is contained in:
@@ -6,30 +6,7 @@ var _isArray = require('lodash/isArray');
|
|||||||
|
|
||||||
var consts = require('../../constants');
|
var consts = require('../../constants');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
|
var objectIdValidation = require('../shared/objectid-validation');
|
||||||
/**
|
|
||||||
* Validate MongoDB ObjectId format.
|
|
||||||
* Accepts: undefined, null, or 24-character hex string.
|
|
||||||
* Rejects: anything else (UUIDs, short strings, numbers, objects).
|
|
||||||
*/
|
|
||||||
function isValidObjectId(id) {
|
|
||||||
if (id === undefined || id === null) return true;
|
|
||||||
if (typeof id !== 'string') return false;
|
|
||||||
return /^[a-fA-F0-9]{24}$/.test(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate _id field for each document in an array.
|
|
||||||
* @returns {Object|null} - null if all valid, or {index, id} of first invalid
|
|
||||||
*/
|
|
||||||
function findInvalidId(docs) {
|
|
||||||
for (var i = 0; i < docs.length; i++) {
|
|
||||||
if (!isValidObjectId(docs[i]._id)) {
|
|
||||||
return { index: i, id: docs[i]._id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configure(app, wares, ctx) {
|
function configure(app, wares, ctx) {
|
||||||
var express = require('express')
|
var express = require('express')
|
||||||
@@ -98,7 +75,7 @@ function configure(app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate _id fields before storage (return 400 on invalid)
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
var invalid = findInvalidId(activity);
|
var invalid = objectIdValidation.findInvalidId(activity);
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
||||||
@@ -119,7 +96,7 @@ function configure(app, wares, ctx) {
|
|||||||
|
|
||||||
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) {
|
api.delete('/activity/:_id', ctx.authorization.isPermitted('api:activity:delete'), function(req, res) {
|
||||||
// Validate _id parameter
|
// Validate _id parameter
|
||||||
if (!isValidObjectId(req.params._id)) {
|
if (!objectIdValidation.isValidObjectId(req.params._id)) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
||||||
}
|
}
|
||||||
@@ -133,7 +110,7 @@ function configure(app, wares, ctx) {
|
|||||||
var data = req.body;
|
var data = req.body;
|
||||||
|
|
||||||
// Validate _id if provided
|
// Validate _id if provided
|
||||||
if (!isValidObjectId(data._id)) {
|
if (!objectIdValidation.isValidObjectId(data._id)) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(data._id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(data._id));
|
||||||
}
|
}
|
||||||
@@ -159,4 +136,3 @@ function configure(app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = configure;
|
module.exports = configure;
|
||||||
|
|
||||||
|
|||||||
@@ -5,33 +5,7 @@ const moment = require('moment');
|
|||||||
const { query } = require('express');
|
const { query } = require('express');
|
||||||
const _take = require('lodash/take');
|
const _take = require('lodash/take');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const objectIdValidation = require('../shared/objectid-validation');
|
||||||
/**
|
|
||||||
* Validate MongoDB ObjectId format.
|
|
||||||
* Accepts: undefined, null, or 24-character hex string.
|
|
||||||
* Rejects: anything else (UUIDs, short strings, numbers, objects).
|
|
||||||
* @param {*} id - The _id value to validate
|
|
||||||
* @returns {boolean} - true if valid or empty, false if invalid format
|
|
||||||
*/
|
|
||||||
function isValidObjectId(id) {
|
|
||||||
if (id === undefined || id === null) return true; // Will auto-generate
|
|
||||||
if (typeof id !== 'string') return false;
|
|
||||||
return /^[a-fA-F0-9]{24}$/.test(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate _id field for each document in an array.
|
|
||||||
* @param {Array} docs - Array of documents to validate
|
|
||||||
* @returns {Object|null} - null if all valid, or {index, id} of first invalid
|
|
||||||
*/
|
|
||||||
function findInvalidId(docs) {
|
|
||||||
for (var i = 0; i < docs.length; i++) {
|
|
||||||
if (!isValidObjectId(docs[i]._id)) {
|
|
||||||
return { index: i, id: docs[i]._id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configure (app, wares, ctx, env) {
|
function configure (app, wares, ctx, env) {
|
||||||
var express = require('express')
|
var express = require('express')
|
||||||
@@ -102,7 +76,7 @@ function configure (app, wares, ctx, env) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate _id fields before storage (return 400 on invalid)
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
var invalid = findInvalidId(statuses);
|
var invalid = objectIdValidation.findInvalidId(statuses);
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
||||||
@@ -154,7 +128,7 @@ function configure (app, wares, ctx, env) {
|
|||||||
|
|
||||||
api.delete('/devicestatus/:id', ctx.authorization.isPermitted('api:devicestatus:delete'), function(req, res, next) {
|
api.delete('/devicestatus/:id', ctx.authorization.isPermitted('api:devicestatus:delete'), function(req, res, next) {
|
||||||
// Validate _id parameter (unless wildcard)
|
// Validate _id parameter (unless wildcard)
|
||||||
if (req.params.id !== '*' && !isValidObjectId(req.params.id)) {
|
if (req.params.id !== '*' && !objectIdValidation.isValidObjectId(req.params.id)) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params.id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params.id));
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-28
@@ -2,30 +2,7 @@
|
|||||||
|
|
||||||
var _isArray = require('lodash/isArray');
|
var _isArray = require('lodash/isArray');
|
||||||
var consts = require('../../constants');
|
var consts = require('../../constants');
|
||||||
|
var objectIdValidation = require('../shared/objectid-validation');
|
||||||
/**
|
|
||||||
* Validate MongoDB ObjectId format.
|
|
||||||
* Accepts: undefined, null, or 24-character hex string.
|
|
||||||
* Rejects: anything else (UUIDs, short strings, numbers, objects).
|
|
||||||
*/
|
|
||||||
function isValidObjectId(id) {
|
|
||||||
if (id === undefined || id === null) return true;
|
|
||||||
if (typeof id !== 'string') return false;
|
|
||||||
return /^[a-fA-F0-9]{24}$/.test(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate _id field for each document in an array.
|
|
||||||
* @returns {Object|null} - null if all valid, or {index, id} of first invalid
|
|
||||||
*/
|
|
||||||
function findInvalidId(docs) {
|
|
||||||
for (var i = 0; i < docs.length; i++) {
|
|
||||||
if (!isValidObjectId(docs[i]._id)) {
|
|
||||||
return { index: i, id: docs[i]._id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configure (app, wares, ctx) {
|
function configure (app, wares, ctx) {
|
||||||
var express = require('express'),
|
var express = require('express'),
|
||||||
@@ -75,7 +52,7 @@ function configure (app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate _id fields before storage (return 400 on invalid)
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
var invalid = findInvalidId(data);
|
var invalid = objectIdValidation.findInvalidId(data);
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
||||||
@@ -103,7 +80,7 @@ function configure (app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate _id fields before storage (return 400 on invalid)
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
var invalid = findInvalidId(data);
|
var invalid = objectIdValidation.findInvalidId(data);
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(invalid.id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(invalid.id));
|
||||||
@@ -123,7 +100,7 @@ function configure (app, wares, ctx) {
|
|||||||
// delete record
|
// delete record
|
||||||
api.delete('/food/:_id', ctx.authorization.isPermitted('api:food:delete'), function(req, res) {
|
api.delete('/food/:_id', ctx.authorization.isPermitted('api:food:delete'), function(req, res) {
|
||||||
// Validate _id parameter
|
// Validate _id parameter
|
||||||
if (!isValidObjectId(req.params._id)) {
|
if (!objectIdValidation.isValidObjectId(req.params._id)) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
||||||
}
|
}
|
||||||
@@ -141,4 +118,3 @@ function configure (app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = configure;
|
module.exports = configure;
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var consts = require('../../constants');
|
var consts = require('../../constants');
|
||||||
|
var objectIdValidation = require('../shared/objectid-validation');
|
||||||
/**
|
|
||||||
* Validate MongoDB ObjectId format.
|
|
||||||
* Accepts: undefined, null, or 24-character hex string.
|
|
||||||
* Rejects: anything else (UUIDs, short strings, numbers, objects).
|
|
||||||
* @param {*} id - The _id value to validate
|
|
||||||
* @returns {boolean} - true if valid or empty, false if invalid format
|
|
||||||
*/
|
|
||||||
function isValidObjectId(id) {
|
|
||||||
if (id === undefined || id === null) return true; // Will auto-generate
|
|
||||||
if (typeof id !== 'string') return false;
|
|
||||||
return /^[a-fA-F0-9]{24}$/.test(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate _id field for each document in an array.
|
|
||||||
* @param {Array} docs - Array of documents to validate
|
|
||||||
* @returns {Object|null} - null if all valid, or {index, id} of first invalid
|
|
||||||
*/
|
|
||||||
function findInvalidId(docs) {
|
|
||||||
for (var i = 0; i < docs.length; i++) {
|
|
||||||
if (!isValidObjectId(docs[i]._id)) {
|
|
||||||
return { index: i, id: docs[i]._id };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function configure (app, wares, ctx) {
|
function configure (app, wares, ctx) {
|
||||||
var express = require('express'),
|
var express = require('express'),
|
||||||
@@ -97,7 +71,7 @@ function configure (app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate _id fields before storage (return 400 on invalid)
|
// Validate _id fields before storage (return 400 on invalid)
|
||||||
var invalid = findInvalidId(data);
|
var invalid = objectIdValidation.findInvalidId(data);
|
||||||
if (invalid) {
|
if (invalid) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
'Invalid _id format', 'Must be 24-character hex string or omit for auto-generation. Got: ' + String(invalid.id));
|
||||||
@@ -125,7 +99,7 @@ function configure (app, wares, ctx) {
|
|||||||
var data = req.body;
|
var data = req.body;
|
||||||
|
|
||||||
// Validate _id if provided (required for PUT, must be valid format)
|
// Validate _id if provided (required for PUT, must be valid format)
|
||||||
if (!isValidObjectId(data._id)) {
|
if (!objectIdValidation.isValidObjectId(data._id)) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(data._id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(data._id));
|
||||||
}
|
}
|
||||||
@@ -145,7 +119,7 @@ function configure (app, wares, ctx) {
|
|||||||
|
|
||||||
api.delete('/profile/:_id', ctx.authorization.isPermitted('api:profile:delete'), function(req, res) {
|
api.delete('/profile/:_id', ctx.authorization.isPermitted('api:profile:delete'), function(req, res) {
|
||||||
// Validate _id parameter
|
// Validate _id parameter
|
||||||
if (!isValidObjectId(req.params._id)) {
|
if (!objectIdValidation.isValidObjectId(req.params._id)) {
|
||||||
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
return res.sendJSONStatus(res, consts.HTTP_BAD_REQUEST,
|
||||||
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
'Invalid _id format', 'Must be 24-character hex string. Got: ' + String(req.params._id));
|
||||||
}
|
}
|
||||||
@@ -164,4 +138,3 @@ function configure (app, wares, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = configure;
|
module.exports = configure;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var OBJECT_ID_PATTERN = /^[a-fA-F0-9]{24}$/;
|
||||||
|
|
||||||
|
function isValidObjectId(id) {
|
||||||
|
if (id === undefined || id === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof id === 'string' && OBJECT_ID_PATTERN.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findInvalidId(docs) {
|
||||||
|
for (var i = 0; i < docs.length; i++) {
|
||||||
|
if (!isValidObjectId(docs[i]._id)) {
|
||||||
|
return { index: i, id: docs[i]._id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
findInvalidId: findInvalidId,
|
||||||
|
isValidObjectId: isValidObjectId
|
||||||
|
};
|
||||||
@@ -16,6 +16,14 @@ function toSafeInt (value, defaultValue) {
|
|||||||
return Number.isFinite(parsed) ? parsed : defaultValue;
|
return Number.isFinite(parsed) ? parsed : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDocs (docs, options) {
|
||||||
|
if (!options || options.normalize !== false) {
|
||||||
|
_.each(docs, utils.normalizeDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find single document by identifier
|
* Find single document by identifier
|
||||||
@@ -24,27 +32,15 @@ function toSafeInt (value, defaultValue) {
|
|||||||
* @param {Object} projection
|
* @param {Object} projection
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
function findOne (col, identifier, projection, options) {
|
async function findOne (col, identifier, projection, options) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const filter = utils.filterForOne(identifier);
|
||||||
|
const result = await col.find(filter)
|
||||||
|
.project(projection)
|
||||||
|
.sort({ identifier: -1 }) // document with identifier first (not the fallback one)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
const filter = utils.filterForOne(identifier);
|
return normalizeDocs(result, options);
|
||||||
|
|
||||||
col.find(filter)
|
|
||||||
.project(projection)
|
|
||||||
.sort({ identifier: -1 }) // document with identifier first (not the fallback one)
|
|
||||||
.toArray(function mongoDone (err, result) {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
if (!options || options.normalize !== false) {
|
|
||||||
_.each(result, utils.normalizeDoc);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -55,56 +51,33 @@ function findOne (col, identifier, projection, options) {
|
|||||||
* @param {Object} projection
|
* @param {Object} projection
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
function findOneFilter (col, filter, projection, options) {
|
async function findOneFilter (col, filter, projection, options) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const result = await col.find(filter)
|
||||||
|
.project(projection)
|
||||||
|
.sort({ identifier: -1 }) // document with identifier first (not the fallback one)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
col.find(filter)
|
return normalizeDocs(result, options);
|
||||||
.project(projection)
|
|
||||||
.sort({ identifier: -1 }) // document with identifier first (not the fallback one)
|
|
||||||
.toArray(function mongoDone (err, result) {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
if (!options || options.normalize !== false) {
|
|
||||||
_.each(result, utils.normalizeDoc);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find many documents matching the filtering criteria
|
* Find many documents matching the filtering criteria
|
||||||
*/
|
*/
|
||||||
function findMany (col, args) {
|
async function findMany (col, args) {
|
||||||
const logicalOperator = args.logicalOperator || 'and';
|
const logicalOperator = args.logicalOperator || 'and';
|
||||||
return new Promise(function (resolve, reject) {
|
const filter = utils.parseFilter(args.filter, logicalOperator, args.onlyValid);
|
||||||
|
const safeLimit = toSafeInt(args.limit, 1000);
|
||||||
|
const safeSkip = toSafeInt(args.skip, 0);
|
||||||
|
const result = await col.find(filter)
|
||||||
|
.sort(args.sort)
|
||||||
|
.limit(safeLimit)
|
||||||
|
.skip(safeSkip)
|
||||||
|
.project(args.projection)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
const filter = utils.parseFilter(args.filter, logicalOperator, args.onlyValid);
|
return normalizeDocs(result, args.options);
|
||||||
const safeLimit = toSafeInt(args.limit, 1000);
|
|
||||||
const safeSkip = toSafeInt(args.skip, 0);
|
|
||||||
|
|
||||||
col.find(filter)
|
|
||||||
.sort(args.sort)
|
|
||||||
.limit(safeLimit)
|
|
||||||
.skip(safeSkip)
|
|
||||||
.project(args.projection)
|
|
||||||
.toArray(function mongoDone (err, result) {
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
if (!args.options || args.options.normalize !== false) {
|
|
||||||
_.each(result, utils.normalizeDoc);
|
|
||||||
}
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,44 +46,29 @@ function MongoCollection (ctx, env, colName) {
|
|||||||
/**
|
/**
|
||||||
* Get server version
|
* Get server version
|
||||||
*/
|
*/
|
||||||
self.version = function version () {
|
self.version = async function version () {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const result = await ctx.store.db.admin().buildInfo();
|
||||||
|
|
||||||
ctx.store.db.admin().buildInfo({}, function mongoDone (err, result) {
|
return {
|
||||||
|
storage: 'mongodb',
|
||||||
err
|
version: result.version
|
||||||
? reject(err)
|
};
|
||||||
: resolve({
|
|
||||||
storage: 'mongodb',
|
|
||||||
version: result.version
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get timestamp (e.g. srvModified) of the last modified document
|
* Get timestamp (e.g. srvModified) of the last modified document
|
||||||
*/
|
*/
|
||||||
self.getLastModified = function getLastModified (fieldName) {
|
self.getLastModified = async function getLastModified (fieldName) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const [ result ] = await self.col.find()
|
||||||
|
.sort({ [fieldName]: -1 })
|
||||||
|
.limit(1)
|
||||||
|
.project({ [fieldName]: 1 })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
self.col.find()
|
return result;
|
||||||
|
|
||||||
.sort({ [fieldName]: -1 })
|
|
||||||
|
|
||||||
.limit(1)
|
|
||||||
|
|
||||||
.project({ [fieldName]: 1 })
|
|
||||||
|
|
||||||
.toArray(function mongoDone (err, [ result ]) {
|
|
||||||
err
|
|
||||||
? reject(err)
|
|
||||||
: resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,24 +9,16 @@ const utils = require('./utils')
|
|||||||
* @param {Object} doc
|
* @param {Object} doc
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
*/
|
*/
|
||||||
function insertOne (col, doc, options) {
|
async function insertOne (col, doc, options) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const result = await col.insertOne(doc);
|
||||||
|
const identifier = doc.identifier || result.insertedId.toString();
|
||||||
|
|
||||||
col.insertOne(doc, function mongoDone(err, result) {
|
if (!options || options.normalize !== false) {
|
||||||
|
delete doc._id;
|
||||||
|
}
|
||||||
|
|
||||||
if (err) {
|
return identifier;
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
const identifier = doc.identifier || result.insertedId.toString();
|
|
||||||
|
|
||||||
if (!options || options.normalize !== false) {
|
|
||||||
delete doc._id;
|
|
||||||
}
|
|
||||||
resolve(identifier);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -36,20 +28,12 @@ function insertOne (col, doc, options) {
|
|||||||
* @param {string} identifier
|
* @param {string} identifier
|
||||||
* @param {Object} doc
|
* @param {Object} doc
|
||||||
*/
|
*/
|
||||||
function replaceOne (col, identifier, doc) {
|
async function replaceOne (col, identifier, doc) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const filter = utils.filterForOne(identifier);
|
||||||
|
const result = await col.replaceOne(filter, doc, { upsert: true });
|
||||||
|
|
||||||
const filter = utils.filterForOne(identifier);
|
return result.matchedCount;
|
||||||
|
|
||||||
col.replaceOne(filter, doc, { upsert: true }, function mongoDone(err, result) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(result.matchedCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -59,20 +43,12 @@ function replaceOne (col, identifier, doc) {
|
|||||||
* @param {string} identifier
|
* @param {string} identifier
|
||||||
* @param {object} setFields
|
* @param {object} setFields
|
||||||
*/
|
*/
|
||||||
function updateOne (col, identifier, setFields) {
|
async function updateOne (col, identifier, setFields) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const filter = utils.filterForOne(identifier);
|
||||||
|
const result = await col.updateOne(filter, { $set: setFields });
|
||||||
|
|
||||||
const filter = utils.filterForOne(identifier);
|
return { updated: result.modifiedCount };
|
||||||
|
|
||||||
col.updateOne(filter, { $set: setFields }, function mongoDone(err, result) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve({ updated: result.modifiedCount });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -81,40 +57,24 @@ function updateOne (col, identifier, setFields) {
|
|||||||
* @param {Object} col
|
* @param {Object} col
|
||||||
* @param {string} identifier
|
* @param {string} identifier
|
||||||
*/
|
*/
|
||||||
function deleteOne (col, identifier) {
|
async function deleteOne (col, identifier) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const filter = utils.filterForOne(identifier);
|
||||||
|
const result = await col.deleteOne(filter);
|
||||||
|
|
||||||
const filter = utils.filterForOne(identifier);
|
return { deleted: result.deletedCount };
|
||||||
|
|
||||||
col.deleteOne(filter, function mongoDone(err, result) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve({ deleted: result.deletedCount });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permanently remove many documents matching any of filtering criteria
|
* Permanently remove many documents matching any of filtering criteria
|
||||||
*/
|
*/
|
||||||
function deleteManyOr (col, filterDef) {
|
async function deleteManyOr (col, filterDef) {
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
const filter = utils.parseFilter(filterDef, 'or');
|
||||||
|
const result = await col.deleteMany(filter);
|
||||||
|
|
||||||
const filter = utils.parseFilter(filterDef, 'or');
|
return { deleted: result.deletedCount };
|
||||||
|
|
||||||
col.deleteMany(filter, function mongoDone(err, result) {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve({ deleted: result.deletedCount });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
, checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$")
|
, checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$")
|
||||||
, ObjectID = require('mongodb-legacy').ObjectId
|
, ObjectID = require('mongodb').ObjectId
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
var _ = require('lodash');
|
var _ = require('lodash');
|
||||||
var crypto = require('crypto');
|
var crypto = require('crypto');
|
||||||
var shiroTrie = require('shiro-trie');
|
var shiroTrie = require('shiro-trie');
|
||||||
var ObjectID = require('mongodb-legacy').ObjectId;
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
var find_options = require('../server/query');
|
var find_options = require('../server/query');
|
||||||
|
|
||||||
@@ -22,27 +23,44 @@ function init (env, ctx) {
|
|||||||
return find_options(opts, storage.queryOpts);
|
return find_options(opts, storage.queryOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRequiredObjectId(id) {
|
||||||
|
if (id === undefined || id === null || id === '') {
|
||||||
|
return { error: 'Missing _id for update' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return { value: new ObjectID(id) };
|
||||||
|
} catch (err) {
|
||||||
|
return { error: 'Invalid _id format: ' + String(id) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function create (collection) {
|
function create (collection) {
|
||||||
function doCreate(obj, fn) {
|
function doCreate(obj, fn) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) {
|
if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) {
|
||||||
obj.created_at = (new Date()).toISOString();
|
obj.created_at = (new Date()).toISOString();
|
||||||
}
|
}
|
||||||
collection.insertOne(obj, function (err, doc) {
|
|
||||||
if (err != null && err.message) {
|
return runWithCallback(async function () {
|
||||||
console.log('Data insertion error', err.message);
|
try {
|
||||||
fn(err.message, null);
|
await collection.insertOne(obj);
|
||||||
return;
|
} catch (err) {
|
||||||
|
if (err != null && err.message) {
|
||||||
|
console.log('Data insertion error', err.message);
|
||||||
|
throw err.message;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
storage.reload(function loaded() {
|
|
||||||
fn(null, obj);
|
await storageReload();
|
||||||
});
|
return obj;
|
||||||
});
|
}, fn);
|
||||||
}
|
}
|
||||||
return doCreate;
|
return doCreate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function list (collection) {
|
function list (collection) {
|
||||||
function doList(opts, fn) {
|
function doList(opts, fn) {
|
||||||
// these functions, find, sort, and limit, are used to
|
// these functions, find, sort, and limit, are used to
|
||||||
// dynamically configure the request, based on the options we've
|
// dynamically configure the request, based on the options we've
|
||||||
// been given
|
// been given
|
||||||
@@ -60,18 +78,14 @@ function init (env, ctx) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle all the results
|
|
||||||
function toArray(err, entries) {
|
|
||||||
fn(err, entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Loading',opts);
|
console.log('Loading',opts);
|
||||||
|
|
||||||
// now just stitch them all together
|
return runWithCallback(function () {
|
||||||
limit.call(collection
|
return limit.call(collection
|
||||||
.find(query_for(opts))
|
.find(query_for(opts))
|
||||||
.sort(sort())
|
.sort(sort())
|
||||||
).toArray(toArray);
|
).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
return doList;
|
return doList;
|
||||||
@@ -79,27 +93,33 @@ function init (env, ctx) {
|
|||||||
|
|
||||||
function remove (collection) {
|
function remove (collection) {
|
||||||
function doRemove (_id, callback) {
|
function doRemove (_id, callback) {
|
||||||
collection.deleteOne({ '_id': new ObjectID(_id) }, function (err) {
|
return runWithCallback(async function () {
|
||||||
storage.reload(function loaded() {
|
await collection.deleteOne({ '_id': new ObjectID(_id) });
|
||||||
callback(err, null);
|
await storageReload();
|
||||||
});
|
return null;
|
||||||
});
|
}, callback);
|
||||||
}
|
}
|
||||||
return doRemove;
|
return doRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save (collection) {
|
function save (collection) {
|
||||||
function doSave (obj, callback) {
|
function doSave (obj, callback) {
|
||||||
obj._id = new ObjectID(obj._id);
|
var idResult = normalizeRequiredObjectId(obj && obj._id);
|
||||||
|
if (idResult.error) {
|
||||||
|
callback(idResult.error, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj._id = idResult.value;
|
||||||
if (!obj.created_at) {
|
if (!obj.created_at) {
|
||||||
obj.created_at = (new Date()).toISOString();
|
obj.created_at = (new Date()).toISOString();
|
||||||
}
|
}
|
||||||
collection.insertOne(obj, function (err) {
|
|
||||||
//id should be added for new docs
|
return runWithCallback(async function () {
|
||||||
storage.reload(function loaded() {
|
await collection.replaceOne({ _id: obj._id }, obj, { upsert: true });
|
||||||
callback(err, obj);
|
await storageReload();
|
||||||
});
|
return obj;
|
||||||
});
|
}, callback);
|
||||||
}
|
}
|
||||||
return doSave;
|
return doSave;
|
||||||
}
|
}
|
||||||
@@ -183,6 +203,20 @@ function init (env, ctx) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function storageReload () {
|
||||||
|
return runWithCallback(function () {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
storage.reload(function loaded(err) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
storage.findRole = function findRole (roleName) {
|
storage.findRole = function findRole (roleName) {
|
||||||
return _.find(storage.roles, {name: roleName});
|
return _.find(storage.roles, {name: roleName});
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-10
@@ -493,19 +493,27 @@ function loadDeviceStatus(ddata, env, ctx, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadDatabaseStats(ddata, ctx, callback) {
|
function loadDatabaseStats(ddata, ctx, callback) {
|
||||||
ctx.store.db.stats(function mongoDone (err, result) {
|
Promise.resolve()
|
||||||
|
.then(function () {
|
||||||
|
return ctx.store.db.stats();
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
if (result) {
|
||||||
|
ddata.dbstats = {
|
||||||
|
dataSize: result.dataSize,
|
||||||
|
indexSize: result.indexSize
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.log("Problem loading database stats");
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log("Problem loading database stats");
|
console.error(err);
|
||||||
}
|
|
||||||
if (!err && result) {
|
|
||||||
ddata.dbstats = {
|
|
||||||
dataSize: result.dataSize
|
|
||||||
, indexSize: result.indexSize
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = init;
|
module.exports = init;
|
||||||
|
|
||||||
|
|||||||
+36
-20
@@ -1,10 +1,19 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var find_options = require('./query');
|
var find_options = require('./query');
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
|
|
||||||
function storage (env, ctx) {
|
function storage (env, ctx) {
|
||||||
var ObjectID = require('mongodb-legacy').ObjectId;
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
|
|
||||||
|
function normalizeObjectId(id) {
|
||||||
|
try {
|
||||||
|
return new ObjectID(id);
|
||||||
|
} catch (err) {
|
||||||
|
return new ObjectID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function create (docs, fn) {
|
function create (docs, fn) {
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
@@ -26,10 +35,14 @@ function storage (env, ctx) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
|
return runWithCallback(async function () {
|
||||||
if (err) {
|
var bulkResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
bulkResult = await api().bulkWrite(bulkOps, { ordered: true });
|
||||||
|
} catch (err) {
|
||||||
console.error('Problem upserting activity batch', err);
|
console.error('Problem upserting activity batch', err);
|
||||||
return fn(err, []);
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign _ids from upserted results
|
// Assign _ids from upserted results
|
||||||
@@ -39,20 +52,26 @@ function storage (env, ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn(null, docs);
|
return docs;
|
||||||
|
}, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
fn(err, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(null, result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function save (obj, fn) {
|
function save (obj, fn) {
|
||||||
obj._id = new ObjectID(obj._id);
|
obj._id = normalizeObjectId(obj._id);
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) {
|
if (!Object.prototype.hasOwnProperty.call(obj, 'created_at')) {
|
||||||
obj.created_at = (new Date( )).toISOString( );
|
obj.created_at = (new Date( )).toISOString( );
|
||||||
}
|
}
|
||||||
api().insertOne(obj, function (err) {
|
return runWithCallback(async function () {
|
||||||
//id should be added for new docs
|
await api().replaceOne({ _id: obj._id }, obj, { upsert: true });
|
||||||
fn(err, obj);
|
return obj;
|
||||||
});
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function query_for (opts) {
|
function query_for (opts) {
|
||||||
@@ -77,21 +96,19 @@ function storage (env, ctx) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle all the results
|
return runWithCallback(function () {
|
||||||
function toArray (err, entries) {
|
return limit.call(api( )
|
||||||
fn(err, entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
// now just stitch them all together
|
|
||||||
limit.call(api( )
|
|
||||||
.find(query_for(opts))
|
.find(query_for(opts))
|
||||||
.sort(sort( ))
|
.sort(sort( ))
|
||||||
).toArray(toArray);
|
).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove (_id, fn) {
|
function remove (_id, fn) {
|
||||||
var objId = new ObjectID(_id);
|
var objId = new ObjectID(_id);
|
||||||
return api( ).deleteOne({ '_id': objId }, fn);
|
return runWithCallback(function () {
|
||||||
|
return api().deleteOne({ '_id': objId });
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function api ( ) {
|
function api ( ) {
|
||||||
@@ -112,4 +129,3 @@ storage.queryOpts = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = storage;
|
module.exports = storage;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
var find_options = require('./query');
|
var find_options = require('./query');
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
function create (conf, api) {
|
function create (conf, api) {
|
||||||
|
|
||||||
@@ -21,7 +22,9 @@ function create (conf, api) {
|
|||||||
var groupBy = [ {$match: query } ].concat(pipeline).concat(template( ));
|
var groupBy = [ {$match: query } ].concat(pipeline).concat(template( ));
|
||||||
console.log('$match query', query);
|
console.log('$match query', query);
|
||||||
console.log('AGGREGATE', groupBy);
|
console.log('AGGREGATE', groupBy);
|
||||||
api( ).aggregate(groupBy, done);
|
return runWithCallback(function () {
|
||||||
|
return api().aggregate(groupBy).toArray();
|
||||||
|
}, done);
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregate;
|
return aggregate;
|
||||||
@@ -29,4 +32,3 @@ function create (conf, api) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = create;
|
module.exports = create;
|
||||||
|
|
||||||
|
|||||||
+22
-26
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
var find_options = require('./query');
|
var find_options = require('./query');
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
function truncatePredictions (obj, maxSize) {
|
function truncatePredictions (obj, maxSize) {
|
||||||
if (!maxSize || maxSize <= 0) return obj;
|
if (!maxSize || maxSize <= 0) return obj;
|
||||||
@@ -52,12 +53,15 @@ function storage (env, ctx) {
|
|||||||
truncatePredictions(obj, predictionsMaxSize);
|
truncatePredictions(obj, predictionsMaxSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use insertMany for batch insert
|
return runWithCallback(async function () {
|
||||||
api().insertMany(statuses, { ordered: true }, function(err, insertResult) {
|
var insertResult;
|
||||||
if (err) {
|
|
||||||
|
try {
|
||||||
|
// Use insertMany for batch insert
|
||||||
|
insertResult = await api().insertMany(statuses, { ordered: true });
|
||||||
|
} catch (err) {
|
||||||
console.log('Error inserting device status objects', err.message);
|
console.log('Error inserting device status objects', err.message);
|
||||||
fn(err.message || err, null);
|
throw err.message || err;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign _ids from insertMany result
|
// Assign _ids from insertMany result
|
||||||
@@ -75,8 +79,8 @@ function storage (env, ctx) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
fn(null, statuses);
|
return statuses;
|
||||||
});
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function last (fn) {
|
function last (fn) {
|
||||||
@@ -111,24 +115,19 @@ function storage (env, ctx) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle all the results
|
return runWithCallback(function () {
|
||||||
function toArray (err, entries) {
|
return limit.call(api()
|
||||||
fn(err, entries);
|
.find(query_for(opts))
|
||||||
}
|
.sort(sort())
|
||||||
|
).toArray();
|
||||||
// now just stitch them all together
|
}, fn);
|
||||||
limit.call(api()
|
|
||||||
.find(query_for(opts))
|
|
||||||
.sort(sort())
|
|
||||||
).toArray(toArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove (opts, fn) {
|
function remove (opts, fn) {
|
||||||
|
|
||||||
function removed (err, stat) {
|
return runWithCallback(async function () {
|
||||||
|
var stat = await api().deleteMany(query_for(opts));
|
||||||
console.log('removed', err, stat);
|
console.log('removed', null, stat);
|
||||||
|
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
type: 'devicestatus'
|
type: 'devicestatus'
|
||||||
, op: 'remove'
|
, op: 'remove'
|
||||||
@@ -136,11 +135,8 @@ function storage (env, ctx) {
|
|||||||
, changes: opts.find._id
|
, changes: opts.find._id
|
||||||
});
|
});
|
||||||
|
|
||||||
fn(err, stat);
|
return stat;
|
||||||
}
|
}, fn);
|
||||||
|
|
||||||
return api().deleteMany(
|
|
||||||
query_for(opts), removed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function api () {
|
function api () {
|
||||||
|
|||||||
+30
-27
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
var es = require('event-stream');
|
var es = require('event-stream');
|
||||||
var find_options = require('./query');
|
var find_options = require('./query');
|
||||||
var ObjectId = require('mongodb-legacy').ObjectId;
|
var ObjectId = require('mongodb').ObjectId;
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
// REQ-SYNC-072: Pattern to match valid MongoDB ObjectId hex strings
|
// REQ-SYNC-072: Pattern to match valid MongoDB ObjectId hex strings
|
||||||
var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
|
var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
|
||||||
@@ -37,20 +38,17 @@ function storage (env, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle all the results
|
// handle all the results
|
||||||
function toArray (err, entries) {
|
return runWithCallback(function () {
|
||||||
fn(err, entries);
|
return limit.call(api()
|
||||||
}
|
.find(query_for(opts))
|
||||||
|
.sort(sort())
|
||||||
// now just stitch them all together
|
).toArray();
|
||||||
limit.call(api()
|
}, fn);
|
||||||
.find(query_for(opts))
|
|
||||||
.sort(sort())
|
|
||||||
).toArray(toArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove (opts, fn) {
|
function remove (opts, fn) {
|
||||||
api().deleteMany(query_for(opts), function(err, stat) {
|
return runWithCallback(async function () {
|
||||||
|
var stat = await api().deleteMany(query_for(opts));
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
type: 'entries'
|
type: 'entries'
|
||||||
, op: 'remove'
|
, op: 'remove'
|
||||||
@@ -60,8 +58,8 @@ function storage (env, ctx) {
|
|||||||
|
|
||||||
//TODO: this is triggering a read from Mongo, we can do better
|
//TODO: this is triggering a read from Mongo, we can do better
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
fn(err, stat);
|
return stat;
|
||||||
});
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return writable stream to lint each sgv record passing through it
|
// return writable stream to lint each sgv record passing through it
|
||||||
@@ -126,12 +124,15 @@ function storage (env, ctx) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use bulkWrite for batch upsert
|
return runWithCallback(async function () {
|
||||||
api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
|
var bulkResult;
|
||||||
if (err) {
|
|
||||||
|
try {
|
||||||
|
// Use bulkWrite for batch upsert
|
||||||
|
bulkResult = await api().bulkWrite(bulkOps, { ordered: true });
|
||||||
|
} catch (err) {
|
||||||
console.error('Problem upserting entries batch', err);
|
console.error('Problem upserting entries batch', err);
|
||||||
fn(err, docs);
|
throw err;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign _ids from upserted results
|
// Assign _ids from upserted results
|
||||||
@@ -148,18 +149,20 @@ function storage (env, ctx) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
fn(null, docs);
|
return docs;
|
||||||
|
}, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
fn(err, docs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(null, result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntry (id, fn) {
|
function getEntry (id, fn) {
|
||||||
api().findOne({ "_id": new ObjectId(id) }, function(err, entry) {
|
return runWithCallback(function () {
|
||||||
if (err) {
|
return api().findOne({ "_id": new ObjectId(id) });
|
||||||
fn(err);
|
}, fn);
|
||||||
} else {
|
|
||||||
fn(null, entry);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function query_for (opts) {
|
function query_for (opts) {
|
||||||
|
|||||||
+52
-21
@@ -1,7 +1,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function storage (env, ctx) {
|
function storage (env, ctx) {
|
||||||
var ObjectID = require('mongodb-legacy').ObjectId;
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
|
function normalizeObjectId(id) {
|
||||||
|
try {
|
||||||
|
return new ObjectID(id);
|
||||||
|
} catch (err) {
|
||||||
|
return new ObjectID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function create (docs, fn) {
|
function create (docs, fn) {
|
||||||
// Normalize to array for consistent handling (allows direct storage calls with single objects)
|
// Normalize to array for consistent handling (allows direct storage calls with single objects)
|
||||||
@@ -26,10 +35,14 @@ function storage (env, ctx) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
|
return runWithCallback(async function () {
|
||||||
if (err) {
|
var bulkResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
bulkResult = await api().bulkWrite(bulkOps, { ordered: true });
|
||||||
|
} catch (err) {
|
||||||
console.error('Problem upserting food batch', err);
|
console.error('Problem upserting food batch', err);
|
||||||
return fn(err, []);
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign _ids from upserted results
|
// Assign _ids from upserted results
|
||||||
@@ -39,7 +52,13 @@ function storage (env, ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn(null, docs);
|
return docs;
|
||||||
|
}, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
fn(err, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(null, result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,30 +74,28 @@ function storage (env, ctx) {
|
|||||||
|
|
||||||
// Build bulkWrite operations for batch upsert
|
// Build bulkWrite operations for batch upsert
|
||||||
var bulkOps = docs.map(function(doc) {
|
var bulkOps = docs.map(function(doc) {
|
||||||
try {
|
doc._id = normalizeObjectId(doc._id);
|
||||||
doc._id = new ObjectID(doc._id);
|
|
||||||
} catch (err){
|
|
||||||
console.error(err);
|
|
||||||
doc._id = new ObjectID();
|
|
||||||
}
|
|
||||||
if (!doc.created_at) {
|
if (!doc.created_at) {
|
||||||
doc.created_at = (new Date()).toISOString();
|
doc.created_at = (new Date()).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
var query = (doc.created_at && doc._id) ? { _id: doc._id, created_at: doc.created_at } : doc;
|
|
||||||
return {
|
return {
|
||||||
replaceOne: {
|
replaceOne: {
|
||||||
filter: query,
|
filter: { _id: doc._id },
|
||||||
replacement: doc,
|
replacement: doc,
|
||||||
upsert: true
|
upsert: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
|
return runWithCallback(async function () {
|
||||||
if (err) {
|
var bulkResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
bulkResult = await api().bulkWrite(bulkOps, { ordered: true });
|
||||||
|
} catch (err) {
|
||||||
console.error('Problem saving food batch', err);
|
console.error('Problem saving food batch', err);
|
||||||
return fn(err, []);
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign _ids from upserted results
|
// Assign _ids from upserted results
|
||||||
@@ -88,25 +105,39 @@ function storage (env, ctx) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn(null, docs);
|
return docs;
|
||||||
|
}, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
fn(err, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fn(null, result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function list (fn) {
|
function list (fn) {
|
||||||
return api( ).find({ }).toArray(fn);
|
return runWithCallback(function () {
|
||||||
|
return api().find({ }).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listquickpicks (fn) {
|
function listquickpicks (fn) {
|
||||||
return api( ).find({ $and: [ { 'type': 'quickpick'} , { 'hidden' : 'false' } ] }).sort({'position': 1}).toArray(fn);
|
return runWithCallback(function () {
|
||||||
|
return api().find({ $and: [ { 'type': 'quickpick'} , { 'hidden' : 'false' } ] }).sort({'position': 1}).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function listregular (fn) {
|
function listregular (fn) {
|
||||||
return api( ).find( { 'type': 'food'} ).toArray(fn);
|
return runWithCallback(function () {
|
||||||
|
return api().find( { 'type': 'food'} ).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove (_id, fn) {
|
function remove (_id, fn) {
|
||||||
var objId = new ObjectID(_id);
|
var objId = new ObjectID(_id);
|
||||||
return api( ).deleteOne({ '_id': objId }, fn);
|
return runWithCallback(function () {
|
||||||
|
return api().deleteOne({ '_id': objId });
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+40
-17
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
var find_options = require('./query');
|
var find_options = require('./query');
|
||||||
var consts = require('../constants');
|
var consts = require('../constants');
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
function storage (collection, ctx) {
|
function storage (collection, ctx) {
|
||||||
var ObjectID = require('mongodb-legacy').ObjectId;
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
|
|
||||||
function create (objOrArray, fn) {
|
function create (objOrArray, fn) {
|
||||||
// Normalize to array (supports both single object and array inputs)
|
// Normalize to array (supports both single object and array inputs)
|
||||||
@@ -13,7 +14,7 @@ function storage (collection, ctx) {
|
|||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
fn(null, []);
|
fn(null, []);
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
return;
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add created_at to each document
|
// Add created_at to each document
|
||||||
@@ -23,16 +24,27 @@ function storage (collection, ctx) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
api().insertMany(docs, function (err, result) {
|
const promise = runWithCallback(async function () {
|
||||||
|
const result = await api().insertMany(docs);
|
||||||
|
if (result && result.insertedIds) {
|
||||||
|
Object.keys(result.insertedIds).forEach(function (index) {
|
||||||
|
if (!docs[index]._id) {
|
||||||
|
docs[index]._id = result.insertedIds[index];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return docs;
|
||||||
|
}, function (err, result) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log("Error saving profile data", docs, err);
|
console.log('Error saving profile data', docs, err);
|
||||||
fn(err);
|
fn(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Return the inserted documents with _id (NightscoutKit expects array)
|
fn(null, result);
|
||||||
fn(null, docs);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save (obj, fn) {
|
function save (obj, fn) {
|
||||||
@@ -45,16 +57,20 @@ function storage (collection, ctx) {
|
|||||||
obj.created_at = (new Date( )).toISOString( );
|
obj.created_at = (new Date( )).toISOString( );
|
||||||
}
|
}
|
||||||
// Match existing profiles by _id only. The profile editor rewrites created_at on save.
|
// Match existing profiles by _id only. The profile editor rewrites created_at on save.
|
||||||
api().replaceOne({ _id: obj._id }, obj, { upsert: true }, function (err) {
|
const promise = runWithCallback(async function () {
|
||||||
//id should be added for new docs
|
await api().replaceOne({ _id: obj._id }, obj, { upsert: true });
|
||||||
fn(err, obj);
|
return obj;
|
||||||
});
|
}, fn);
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function list (fn, count) {
|
function list (fn, count) {
|
||||||
const limit = count !== null ? count : Number(consts.PROFILES_DEFAULT_COUNT);
|
const limit = count !== null ? count : Number(consts.PROFILES_DEFAULT_COUNT);
|
||||||
return api( ).find({ }).limit(limit).sort({startDate: -1}).toArray(fn);
|
return runWithCallback(function () {
|
||||||
|
return api().find({ }).limit(limit).sort({startDate: -1}).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function list_query (opts, fn) {
|
function list_query (opts, fn) {
|
||||||
@@ -71,10 +87,12 @@ function storage (collection, ctx) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
return limit.call(api()
|
return runWithCallback(function () {
|
||||||
.find(query_for(opts))
|
return limit.call(api()
|
||||||
.sort(opts && opts.sort && query_sort(opts) || { startDate: -1 }), opts)
|
.find(query_for(opts))
|
||||||
.toArray(fn);
|
.sort(opts && opts.sort && query_sort(opts) || { startDate: -1 }), opts)
|
||||||
|
.toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function query_for (opts) {
|
function query_for (opts) {
|
||||||
@@ -100,14 +118,19 @@ function storage (collection, ctx) {
|
|||||||
|
|
||||||
|
|
||||||
function last (fn) {
|
function last (fn) {
|
||||||
return api().find().sort({startDate: -1}).limit(1).toArray(fn);
|
return runWithCallback(function () {
|
||||||
|
return api().find().sort({startDate: -1}).limit(1).toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove (_id, fn) {
|
function remove (_id, fn) {
|
||||||
var objId = new ObjectID(_id);
|
var objId = new ObjectID(_id);
|
||||||
api( ).deleteOne({ '_id': objId }, fn);
|
const promise = runWithCallback(function () {
|
||||||
|
return api().deleteOne({ '_id': objId });
|
||||||
|
}, fn);
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function api () {
|
function api () {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const traverse = require('traverse');
|
const traverse = require('traverse');
|
||||||
const ObjectID = require('mongodb-legacy').ObjectId;
|
const ObjectID = require('mongodb').ObjectId;
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
|
const OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
|
||||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
|||||||
+111
-103
@@ -4,9 +4,10 @@ var _ = require('lodash');
|
|||||||
var async = require('async');
|
var async = require('async');
|
||||||
var moment = require('moment');
|
var moment = require('moment');
|
||||||
var find_options = require('./query');
|
var find_options = require('./query');
|
||||||
|
var runWithCallback = require('../storage/run-with-callback');
|
||||||
|
|
||||||
function storage (env, ctx) {
|
function storage (env, ctx) {
|
||||||
var ObjectID = require('mongodb-legacy').ObjectId;
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
|
var OBJECT_ID_HEX_RE = /^[0-9a-fA-F]{24}$/;
|
||||||
|
|
||||||
function create (objOrArray, fn) {
|
function create (objOrArray, fn) {
|
||||||
@@ -61,10 +62,14 @@ function storage (env, ctx) {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
api().bulkWrite(bulkOps, { ordered: true }, function(err, bulkResult) {
|
return runWithCallback(async function () {
|
||||||
if (err) {
|
var bulkResult;
|
||||||
|
|
||||||
|
try {
|
||||||
|
bulkResult = await api().bulkWrite(bulkOps, { ordered: true });
|
||||||
|
} catch (err) {
|
||||||
console.error('Problem upserting treatments batch', err);
|
console.error('Problem upserting treatments batch', err);
|
||||||
return done(err, []);
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign _ids from upserted results
|
// Assign _ids from upserted results
|
||||||
@@ -82,9 +87,10 @@ function storage (env, ctx) {
|
|||||||
|
|
||||||
if (docsNeedingId.length > 0) {
|
if (docsNeedingId.length > 0) {
|
||||||
var identifiers = docsNeedingId.map(function(obj) { return obj.identifier; });
|
var identifiers = docsNeedingId.map(function(obj) { return obj.identifier; });
|
||||||
|
|
||||||
api().find({ identifier: { $in: identifiers } }).toArray(function(findErr, existing) {
|
try {
|
||||||
if (!findErr && existing) {
|
var existing = await api().find({ identifier: { $in: identifiers } }).toArray();
|
||||||
|
if (existing) {
|
||||||
var idMap = {};
|
var idMap = {};
|
||||||
existing.forEach(function(doc) {
|
existing.forEach(function(doc) {
|
||||||
if (doc.identifier) idMap[doc.identifier] = doc._id;
|
if (doc.identifier) idMap[doc.identifier] = doc._id;
|
||||||
@@ -95,16 +101,9 @@ function storage (env, ctx) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (findErr) {
|
||||||
ctx.bus.emit('data-update', {
|
// Preserve existing behavior: still report success even if the id lookup fails.
|
||||||
type: 'treatments',
|
}
|
||||||
op: 'update',
|
|
||||||
changes: ctx.ddata.processRawDataForRuntime(objOrArray)
|
|
||||||
});
|
|
||||||
|
|
||||||
done(null, objOrArray);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
@@ -113,7 +112,13 @@ function storage (env, ctx) {
|
|||||||
changes: ctx.ddata.processRawDataForRuntime(objOrArray)
|
changes: ctx.ddata.processRawDataForRuntime(objOrArray)
|
||||||
});
|
});
|
||||||
|
|
||||||
done(null, objOrArray);
|
return objOrArray;
|
||||||
|
}, function (err, result) {
|
||||||
|
if (err) {
|
||||||
|
done(err, []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done(null, result);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
upsert(objOrArray, function upserted (err, docs) {
|
upsert(objOrArray, function upserted (err, docs) {
|
||||||
@@ -130,29 +135,34 @@ function storage (env, ctx) {
|
|||||||
var results = prepareData(obj);
|
var results = prepareData(obj);
|
||||||
var query = upsertQueryFor(obj, results);
|
var query = upsertQueryFor(obj, results);
|
||||||
|
|
||||||
api( ).replaceOne(query, obj, {upsert: true}, function complete (err, updateResults) {
|
(async function () {
|
||||||
|
try {
|
||||||
|
var updateResults = await api().replaceOne(query, obj, {upsert: true});
|
||||||
|
|
||||||
if (err) console.error('Problem upserting treatment', err);
|
if (updateResults) {
|
||||||
|
if (updateResults.upsertedCount == 1) {
|
||||||
if (updateResults) {
|
obj._id = updateResults.upsertedId;
|
||||||
if (updateResults.upsertedCount == 1) {
|
} else if (updateResults.matchedCount >= 1 && obj.identifier && !obj._id) {
|
||||||
obj._id = updateResults.upsertedId;
|
// REQ-SYNC-072: On update by identifier, fetch the existing _id
|
||||||
} else if (updateResults.matchedCount >= 1 && obj.identifier && !obj._id) {
|
try {
|
||||||
// REQ-SYNC-072: On update by identifier, fetch the existing _id
|
var existing = await api().findOne(query);
|
||||||
api().findOne(query, function(findErr, existing) {
|
if (existing) {
|
||||||
if (!findErr && existing) {
|
obj._id = existing._id;
|
||||||
obj._id = existing._id;
|
}
|
||||||
|
} catch (findErr) {
|
||||||
|
// Preserve existing behavior: update success does not fail if the lookup fails.
|
||||||
}
|
}
|
||||||
finishUpsert(err, obj, results);
|
}
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await finishUpsert(null, obj, results);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Problem upserting treatment', err);
|
||||||
|
await finishUpsert(err, obj, results);
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
finishUpsert(err, obj, results);
|
async function finishUpsert(err, obj, results) {
|
||||||
});
|
|
||||||
|
|
||||||
function finishUpsert(err, obj, results) {
|
|
||||||
// TODO document this feature
|
// TODO document this feature
|
||||||
if (!err && obj.preBolus) {
|
if (!err && obj.preBolus) {
|
||||||
//create a new object to insert copying only the needed fields
|
//create a new object to insert copying only the needed fields
|
||||||
@@ -170,24 +180,28 @@ function storage (env, ctx) {
|
|||||||
created_at: pbTreat.created_at,
|
created_at: pbTreat.created_at,
|
||||||
eventType: pbTreat.eventType
|
eventType: pbTreat.eventType
|
||||||
};
|
};
|
||||||
api( ).replaceOne(pbQuery, pbTreat, {upsert: true}, function pbComplete (err, updateResults) {
|
var updateResults;
|
||||||
|
try {
|
||||||
|
updateResults = await api().replaceOne(pbQuery, pbTreat, {upsert: true});
|
||||||
|
} catch (pbErr) {
|
||||||
|
err = pbErr;
|
||||||
|
}
|
||||||
|
|
||||||
if (updateResults) {
|
if (updateResults) {
|
||||||
if (updateResults.upsertedCount == 1) {
|
if (updateResults.upsertedCount == 1) {
|
||||||
pbTreat._id = updateResults.upsertedId
|
pbTreat._id = updateResults.upsertedId;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var treatments = _.compact([obj, pbTreat]);
|
var treatments = _.compact([obj, pbTreat]);
|
||||||
|
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
type: 'treatments',
|
type: 'treatments',
|
||||||
op: 'update',
|
op: 'update',
|
||||||
changes: ctx.ddata.processRawDataForRuntime(treatments)
|
changes: ctx.ddata.processRawDataForRuntime(treatments)
|
||||||
});
|
|
||||||
|
|
||||||
fn(err, treatments);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fn(err, treatments);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
@@ -210,10 +224,12 @@ function storage (env, ctx) {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
return limit.call(api()
|
return runWithCallback(function () {
|
||||||
.find(query_for(opts))
|
return limit.call(api()
|
||||||
.sort(opts && opts.sort || {created_at: -1}), opts)
|
.find(query_for(opts))
|
||||||
.toArray(fn);
|
.sort(opts && opts.sort || {created_at: -1}), opts)
|
||||||
|
.toArray();
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function query_for (opts) {
|
function query_for (opts) {
|
||||||
@@ -234,20 +250,21 @@ function storage (env, ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function remove (opts, fn) {
|
function remove (opts, fn) {
|
||||||
return api( ).deleteMany(query_for(opts), {}, function (err, stat) {
|
return runWithCallback(async function () {
|
||||||
//TODO: this is triggering a read from Mongo, we can do better
|
var stat = await api().deleteMany(query_for(opts), {});
|
||||||
//console.log('Treatment removed', opts); // , stat);
|
//TODO: this is triggering a read from Mongo, we can do better
|
||||||
|
//console.log('Treatment removed', opts); // , stat);
|
||||||
|
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
type: 'treatments',
|
type: 'treatments',
|
||||||
op: 'remove',
|
op: 'remove',
|
||||||
count: stat.deletedCount,
|
count: stat.deletedCount,
|
||||||
changes: opts.find._id
|
changes: opts.find._id
|
||||||
});
|
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
|
||||||
fn(err, stat);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ctx.bus.emit('data-received');
|
||||||
|
return stat;
|
||||||
|
}, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save (obj, fn) {
|
function save (obj, fn) {
|
||||||
@@ -256,52 +273,43 @@ function storage (env, ctx) {
|
|||||||
|
|
||||||
var query = upsertQueryFor(obj, { created_at: obj.created_at });
|
var query = upsertQueryFor(obj, { created_at: obj.created_at });
|
||||||
|
|
||||||
function saved (err, updateResults) {
|
const promise = runWithCallback(async function () {
|
||||||
if (!err) {
|
var updateResults = await api().replaceOne(query, obj, {upsert: true});
|
||||||
if (updateResults && updateResults.upsertedCount == 1) {
|
|
||||||
obj._id = updateResults.upsertedId;
|
if (updateResults && updateResults.upsertedCount == 1) {
|
||||||
} else if (updateResults && updateResults.matchedCount >= 1 && obj.identifier && !obj._id) {
|
obj._id = updateResults.upsertedId;
|
||||||
// REQ-SYNC-072: On update by identifier, fetch the existing _id
|
} else if (updateResults && updateResults.matchedCount >= 1 && obj.identifier && !obj._id) {
|
||||||
api().findOne(query, function(findErr, existing) {
|
// REQ-SYNC-072: On update by identifier, fetch the existing _id
|
||||||
if (!findErr && existing) {
|
try {
|
||||||
obj._id = existing._id;
|
var existing = await api().findOne(query);
|
||||||
}
|
if (existing) {
|
||||||
finishSave(err, obj);
|
obj._id = existing._id;
|
||||||
});
|
}
|
||||||
return;
|
} catch (findErr) {
|
||||||
|
// Preserve existing behavior: update success does not fail if the lookup fails.
|
||||||
}
|
}
|
||||||
// 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, obj);
|
ctx.ddata.processRawDataForRuntime(obj);
|
||||||
}
|
ctx.bus.emit('data-update', {
|
||||||
|
type: 'treatments',
|
||||||
|
op: 'update',
|
||||||
|
changes: ctx.ddata.processRawDataForRuntime([obj])
|
||||||
|
});
|
||||||
|
|
||||||
function finishSave(err, obj) {
|
return obj;
|
||||||
if (!err) {
|
}, function (err, result) {
|
||||||
ctx.ddata.processRawDataForRuntime(obj);
|
if (err) {
|
||||||
ctx.bus.emit('data-update', {
|
console.error('Problem saving treating', err);
|
||||||
type: 'treatments',
|
fn(err, obj);
|
||||||
op: 'update',
|
return;
|
||||||
changes: ctx.ddata.processRawDataForRuntime([obj])
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (err) console.error('Problem saving treating', err);
|
|
||||||
fn(err, obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
api().replaceOne(query, obj, {upsert: true}, saved);
|
fn(null, result);
|
||||||
|
});
|
||||||
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function api ( ) {
|
function api ( ) {
|
||||||
|
|||||||
+184
-195
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
var times = require('../times');
|
var times = require('../times');
|
||||||
var calcData = require('../data/calcdelta');
|
var calcData = require('../data/calcdelta');
|
||||||
var ObjectID = require('mongodb-legacy').ObjectId;
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
const forwarded = require('forwarded-for');
|
const forwarded = require('forwarded-for');
|
||||||
|
|
||||||
function getRemoteIP (req) {
|
function getRemoteIP (req) {
|
||||||
@@ -230,25 +230,23 @@ function init (env, ctx, server) {
|
|||||||
}
|
}
|
||||||
var id = safeObjectID(data._id);
|
var id = safeObjectID(data._id);
|
||||||
|
|
||||||
ctx.store.collection(collection).updateOne({ '_id': id }
|
(async function () {
|
||||||
, { $set: data.data }
|
try {
|
||||||
, function(err, results) {
|
var mongoCollection = ctx.store.collection(collection);
|
||||||
|
await mongoCollection.updateOne({ '_id': id }, { $set: data.data });
|
||||||
if (!err) {
|
var results = await mongoCollection.findOne({ '_id': id });
|
||||||
ctx.store.collection(collection).findOne({ '_id': id }
|
console.log('Got results', results);
|
||||||
, function(err, results) {
|
if (results !== null) {
|
||||||
console.log('Got results', results);
|
ctx.bus.emit('data-update', {
|
||||||
if (!err && results !== null) {
|
type: data.collection
|
||||||
ctx.bus.emit('data-update', {
|
, op: 'update'
|
||||||
type: data.collection
|
, changes: ctx.ddata.processRawDataForRuntime([results])
|
||||||
, op: 'update'
|
});
|
||||||
, changes: ctx.ddata.processRawDataForRuntime([results])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
);
|
})();
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({ result: 'success' });
|
callback({ result: 'success' });
|
||||||
@@ -278,23 +276,23 @@ function init (env, ctx, server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var objId = safeObjectID(data._id);
|
var objId = safeObjectID(data._id);
|
||||||
ctx.store.collection(collection).updateOne({ '_id': objId }, { $unset: data.data }
|
(async function () {
|
||||||
, function(err, results) {
|
try {
|
||||||
|
var mongoCollection = ctx.store.collection(collection);
|
||||||
if (!err) {
|
await mongoCollection.updateOne({ '_id': objId }, { $unset: data.data });
|
||||||
ctx.store.collection(collection).findOne({ '_id': objId }
|
var results = await mongoCollection.findOne({ '_id': objId });
|
||||||
, function(err, results) {
|
console.log('Got results', results);
|
||||||
console.log('Got results', results);
|
if (results !== null) {
|
||||||
if (!err && results !== null) {
|
ctx.bus.emit('data-update', {
|
||||||
ctx.bus.emit('data-update', {
|
type: data.collection
|
||||||
type: data.collection
|
, op: 'update'
|
||||||
, op: 'update'
|
, changes: ctx.ddata.processRawDataForRuntime([results])
|
||||||
, changes: ctx.ddata.processRawDataForRuntime([results])
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({ result: 'success' });
|
callback({ result: 'success' });
|
||||||
@@ -328,40 +326,50 @@ function init (env, ctx, server) {
|
|||||||
// Handle array input: process each item sequentially
|
// Handle array input: process each item sequentially
|
||||||
if (Array.isArray(data.data)) {
|
if (Array.isArray(data.data)) {
|
||||||
console.log(LOG_WS + 'dbAdd received array with ' + data.data.length + ' items');
|
console.log(LOG_WS + 'dbAdd received array with ' + data.data.length + ' items');
|
||||||
var results = [];
|
(async function () {
|
||||||
var processIndex = 0;
|
var results = [];
|
||||||
|
|
||||||
function processNextItem() {
|
for (var processIndex = 0; processIndex < data.data.length; processIndex += 1) {
|
||||||
if (processIndex >= data.data.length) {
|
var itemData = {
|
||||||
if (callback) {
|
collection: data.collection,
|
||||||
callback(results);
|
data: data.data[processIndex]
|
||||||
}
|
};
|
||||||
return;
|
var itemResult = await processSingleDbAdd(itemData, collection, maxtimediff);
|
||||||
}
|
|
||||||
|
|
||||||
var itemData = {
|
|
||||||
collection: data.collection,
|
|
||||||
data: data.data[processIndex]
|
|
||||||
};
|
|
||||||
|
|
||||||
processIndex++;
|
|
||||||
processSingleDbAdd(itemData, collection, maxtimediff, function(itemResult) {
|
|
||||||
if (itemResult && itemResult.length > 0) {
|
if (itemResult && itemResult.length > 0) {
|
||||||
results = results.concat(itemResult);
|
results = results.concat(itemResult);
|
||||||
}
|
}
|
||||||
processNextItem();
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
processNextItem();
|
if (callback) {
|
||||||
|
callback(results);
|
||||||
|
}
|
||||||
|
})().catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (callback) {
|
||||||
|
callback([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single object processing
|
// Single object processing
|
||||||
processSingleDbAdd(data, collection, maxtimediff, callback);
|
processSingleDbAdd(data, collection, maxtimediff)
|
||||||
|
.then(function (result) {
|
||||||
|
if (callback) {
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (callback) {
|
||||||
|
callback([]);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function processSingleDbAdd(data, collection, maxtimediff, callback) {
|
async function processSingleDbAdd(data, collection, maxtimediff) {
|
||||||
|
var mongoCollection = ctx.store.collection(collection);
|
||||||
|
|
||||||
if (data.collection === 'treatments' && !('eventType' in data.data)) {
|
if (data.collection === 'treatments' && !('eventType' in data.data)) {
|
||||||
data.data.eventType = '<none>';
|
data.data.eventType = '<none>';
|
||||||
}
|
}
|
||||||
@@ -382,97 +390,86 @@ function init (env, ctx, server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try to find exact match
|
// try to find exact match
|
||||||
ctx.store.collection(collection).find(query).toArray(function findResult (err, array) {
|
try {
|
||||||
if (err) {
|
var array = await mongoCollection.find(query).toArray();
|
||||||
console.error(err);
|
|
||||||
callback([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array.length > 0) {
|
if (array.length > 0) {
|
||||||
console.log(LOG_DEDUP + 'Exact match');
|
console.log(LOG_DEDUP + 'Exact match');
|
||||||
if (callback) {
|
return [array[0]];
|
||||||
callback([array[0]]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
var selected = false;
|
var selected = false;
|
||||||
var query_similiar = {
|
var query_similiar = {
|
||||||
created_at: { $gte: new Date(new Date(data.data.created_at).getTime() - maxtimediff).toISOString(), $lte: new Date(new Date(data.data.created_at).getTime() + maxtimediff).toISOString() }
|
created_at: { $gte: new Date(new Date(data.data.created_at).getTime() - maxtimediff).toISOString(), $lte: new Date(new Date(data.data.created_at).getTime() + maxtimediff).toISOString() }
|
||||||
};
|
};
|
||||||
if (data.data.insulin) {
|
if (data.data.insulin) {
|
||||||
query_similiar.insulin = data.data.insulin;
|
query_similiar.insulin = data.data.insulin;
|
||||||
selected = true;
|
selected = true;
|
||||||
|
}
|
||||||
|
if (data.data.carbs) {
|
||||||
|
query_similiar.carbs = data.data.carbs;
|
||||||
|
selected = true;
|
||||||
|
}
|
||||||
|
if (data.data.percent) {
|
||||||
|
query_similiar.percent = data.data.percent;
|
||||||
|
selected = true;
|
||||||
|
}
|
||||||
|
if (data.data.absolute) {
|
||||||
|
query_similiar.absolute = data.data.absolute;
|
||||||
|
selected = true;
|
||||||
|
}
|
||||||
|
if (data.data.duration) {
|
||||||
|
query_similiar.duration = data.data.duration;
|
||||||
|
selected = true;
|
||||||
|
}
|
||||||
|
if (data.data.NSCLIENT_ID) {
|
||||||
|
query_similiar.NSCLIENT_ID = data.data.NSCLIENT_ID;
|
||||||
|
selected = true;
|
||||||
|
}
|
||||||
|
// if none assigned add at least eventType
|
||||||
|
if (!selected) {
|
||||||
|
query_similiar.eventType = data.data.eventType;
|
||||||
|
}
|
||||||
|
// try to find similiar
|
||||||
|
try {
|
||||||
|
var similar = await mongoCollection.find(query_similiar).toArray();
|
||||||
|
// if found similiar just update date. next time it will match exactly
|
||||||
|
if (similar.length > 0) {
|
||||||
|
console.log(LOG_DEDUP + 'Found similiar', similar[0]);
|
||||||
|
similar[0].created_at = data.data.created_at;
|
||||||
|
var objId = safeObjectID(similar[0]._id);
|
||||||
|
await mongoCollection.updateOne({ '_id': objId }, { $set: { created_at: data.data.created_at } });
|
||||||
|
ctx.bus.emit('data-received');
|
||||||
|
return [similar[0]];
|
||||||
}
|
}
|
||||||
if (data.data.carbs) {
|
} catch (err) {
|
||||||
query_similiar.carbs = data.data.carbs;
|
console.error(err);
|
||||||
selected = true;
|
return [];
|
||||||
}
|
}
|
||||||
if (data.data.percent) {
|
|
||||||
query_similiar.percent = data.data.percent;
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
if (data.data.absolute) {
|
|
||||||
query_similiar.absolute = data.data.absolute;
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
if (data.data.duration) {
|
|
||||||
query_similiar.duration = data.data.duration;
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
if (data.data.NSCLIENT_ID) {
|
|
||||||
query_similiar.NSCLIENT_ID = data.data.NSCLIENT_ID;
|
|
||||||
selected = true;
|
|
||||||
}
|
|
||||||
// if none assigned add at least eventType
|
|
||||||
if (!selected) {
|
|
||||||
query_similiar.eventType = data.data.eventType;
|
|
||||||
}
|
|
||||||
// try to find similiar
|
|
||||||
ctx.store.collection(collection).find(query_similiar).toArray(function findSimiliarResult (err, array) {
|
|
||||||
// if found similiar just update date. next time it will match exactly
|
|
||||||
|
|
||||||
if (err) {
|
// if not found create new record
|
||||||
console.error(err);
|
console.log(LOG_DEDUP + 'Adding new record');
|
||||||
callback([]);
|
try {
|
||||||
return;
|
var insertResult = await mongoCollection.insertOne(data.data);
|
||||||
}
|
var doc = data.data;
|
||||||
|
doc._id = insertResult.insertedId;
|
||||||
if (array.length > 0) {
|
ctx.bus.emit('data-update', {
|
||||||
console.log(LOG_DEDUP + 'Found similiar', array[0]);
|
type: data.collection
|
||||||
array[0].created_at = data.data.created_at;
|
, op: 'update'
|
||||||
var objId = safeObjectID(array[0]._id);
|
, changes: ctx.ddata.processRawDataForRuntime([doc])
|
||||||
ctx.store.collection(collection).updateOne({ '_id': objId }, { $set: { created_at: data.data.created_at } });
|
|
||||||
if (callback) {
|
|
||||||
callback([array[0]]);
|
|
||||||
}
|
|
||||||
ctx.bus.emit('data-received');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// if not found create new record
|
|
||||||
console.log(LOG_DEDUP + 'Adding new record');
|
|
||||||
ctx.store.collection(collection).insertOne(data.data, function insertResult (err, ops) {
|
|
||||||
if (err != null && err.message) {
|
|
||||||
console.log('treatments data insertion error: ', err.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc = data.data;
|
|
||||||
doc._id = ops.insertedId;
|
|
||||||
ctx.bus.emit('data-update', {
|
|
||||||
type: data.collection
|
|
||||||
, op: 'update'
|
|
||||||
, changes: ctx.ddata.processRawDataForRuntime([doc])
|
|
||||||
});
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
callback([doc]);
|
|
||||||
}
|
|
||||||
ctx.bus.emit('data-received');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
ctx.bus.emit('data-received');
|
||||||
|
return [doc];
|
||||||
|
} catch (err) {
|
||||||
|
if (err != null && err.message) {
|
||||||
|
console.log('treatments data insertion error: ', err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
// devicestatus deduping
|
// devicestatus deduping
|
||||||
} else if (data.collection === 'devicestatus') {
|
} else if (data.collection === 'devicestatus') {
|
||||||
var queryDev;
|
var queryDev;
|
||||||
@@ -485,62 +482,54 @@ function init (env, ctx, server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// try to find exact match
|
// try to find exact match
|
||||||
ctx.store.collection(collection).find(queryDev).toArray(function findResult (err, array) {
|
try {
|
||||||
if (err) {
|
var existingStatus = await mongoCollection.find(queryDev).toArray();
|
||||||
console.error(err);
|
if (existingStatus.length > 0) {
|
||||||
callback([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array.length > 0) {
|
|
||||||
console.log(LOG_DEDUP + 'Devicestatus exact match');
|
console.log(LOG_DEDUP + 'Devicestatus exact match');
|
||||||
if (callback) {
|
return [existingStatus[0]];
|
||||||
callback([array[0]]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
});
|
try {
|
||||||
|
var devicestatusInsertResult = await mongoCollection.insertOne(data.data);
|
||||||
ctx.store.collection(collection).insertOne(data.data, function insertResult (err, ops) {
|
var devicestatusDoc = data.data;
|
||||||
if (err != null && err.message) {
|
devicestatusDoc._id = devicestatusInsertResult.insertedId;
|
||||||
console.log('devicestatus insertion error: ', err.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc = data.data;
|
|
||||||
doc._id = ops.insertedId;
|
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
type: 'devicestatus'
|
type: 'devicestatus'
|
||||||
, op: 'update'
|
, op: 'update'
|
||||||
, changes: ctx.ddata.processRawDataForRuntime([doc])
|
, changes: ctx.ddata.processRawDataForRuntime([devicestatusDoc])
|
||||||
});
|
});
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
callback([doc]);
|
|
||||||
}
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
});
|
return [devicestatusDoc];
|
||||||
} else {
|
} catch (err) {
|
||||||
ctx.store.collection(collection).insertOne(data.data, function insertResult (err, ops) {
|
|
||||||
if (err != null && err.message) {
|
if (err != null && err.message) {
|
||||||
console.log(data.collection + ' insertion error: ', err.message);
|
console.log('devicestatus insertion error: ', err.message);
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
var doc = data.data;
|
}
|
||||||
doc._id = ops.insertedId;
|
} else {
|
||||||
|
try {
|
||||||
|
var genericInsertResult = await mongoCollection.insertOne(data.data);
|
||||||
|
var genericDoc = data.data;
|
||||||
|
genericDoc._id = genericInsertResult.insertedId;
|
||||||
ctx.bus.emit('data-update', {
|
ctx.bus.emit('data-update', {
|
||||||
type: data.collection
|
type: data.collection
|
||||||
, op: 'update'
|
, op: 'update'
|
||||||
, changes: ctx.ddata.processRawDataForRuntime([doc])
|
, changes: ctx.ddata.processRawDataForRuntime([genericDoc])
|
||||||
});
|
});
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
callback([doc]);
|
|
||||||
}
|
|
||||||
ctx.bus.emit('data-received');
|
ctx.bus.emit('data-received');
|
||||||
});
|
return [genericDoc];
|
||||||
|
} catch (err) {
|
||||||
|
if (err != null && err.message) {
|
||||||
|
console.log(data.collection + ' insertion error: ', err.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,19 +551,19 @@ function init (env, ctx, server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var objId = safeObjectID(data._id);
|
var objId = safeObjectID(data._id);
|
||||||
ctx.store.collection(collection).deleteOne({ '_id': objId }
|
(async function () {
|
||||||
, function(err, stat) {
|
try {
|
||||||
|
var stat = await ctx.store.collection(collection).deleteOne({ '_id': objId });
|
||||||
if (!err) {
|
ctx.bus.emit('data-update', {
|
||||||
ctx.bus.emit('data-update', {
|
type: data.collection
|
||||||
type: data.collection
|
, op: 'remove'
|
||||||
, op: 'remove'
|
, count: stat.deletedCount
|
||||||
, count: stat.deletedCount
|
, changes: data._id
|
||||||
, changes: data._id
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
|
|
||||||
if (callback) {
|
if (callback) {
|
||||||
callback({ result: 'success' });
|
callback({ result: 'success' });
|
||||||
|
|||||||
+105
-40
@@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const MongoClient = require('mongodb-legacy').MongoClient;
|
const MongoClient = require('mongodb').MongoClient;
|
||||||
|
|
||||||
const mongo = {
|
const mongo = {
|
||||||
client: null,
|
client: null,
|
||||||
@@ -10,6 +10,44 @@ const mongo = {
|
|||||||
const DEFAULT_POOL_SIZE = 5;
|
const DEFAULT_POOL_SIZE = 5;
|
||||||
const LEGACY_POOL_SIZE = 100;
|
const LEGACY_POOL_SIZE = 100;
|
||||||
|
|
||||||
|
function getRetryDelay(attempt) {
|
||||||
|
return attempt > 15 ? 60000 : attempt * 3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait(ms) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeClient(client) {
|
||||||
|
if (!client || typeof client.close !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error closing failed MongoDB client: %j', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapConnectionError(err) {
|
||||||
|
if (err && err.name === 'MongoReadOnlyConnectionError') {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err && err.message && err.message.includes('AuthenticationFailed')) {
|
||||||
|
return new Error('MongoDB authentication failed! Double check the URL has the right username and password in MONGODB_URI.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Error('MONGODB_URI seems invalid: ' + err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRetryableConnectionError(err) {
|
||||||
|
return !!(err && err.name === 'MongoServerSelectionError' && !(err.message && err.message.includes('AuthenticationFailed')));
|
||||||
|
}
|
||||||
|
|
||||||
function getPoolOptions(env) {
|
function getPoolOptions(env) {
|
||||||
const poolSize = env.mongo_pool_size
|
const poolSize = env.mongo_pool_size
|
||||||
? parseInt(env.mongo_pool_size, 10)
|
? parseInt(env.mongo_pool_size, 10)
|
||||||
@@ -75,6 +113,8 @@ function init(env, cb, forceNewConnection) {
|
|||||||
if (cb && cb.call) {
|
if (cb && cb.call) {
|
||||||
cb(null, mongo);
|
cb(null, mongo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(mongo);
|
||||||
} else {
|
} else {
|
||||||
if (!env.storageURI) {
|
if (!env.storageURI) {
|
||||||
throw new Error('MongoDB connection string is missing. Please set MONGODB_URI environment variable');
|
throw new Error('MongoDB connection string is missing. Please set MONGODB_URI environment variable');
|
||||||
@@ -84,56 +124,82 @@ function init(env, cb, forceNewConnection) {
|
|||||||
console.log('Setting up new connection to MongoDB with pool options:', poolOptions);
|
console.log('Setting up new connection to MongoDB with pool options:', poolOptions);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
useNewUrlParser: true,
|
|
||||||
useUnifiedTopology: true,
|
|
||||||
...poolOptions,
|
...poolOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const connect_with_retry = async function (i) {
|
const connectWithRetry = async function () {
|
||||||
|
let attempt = 1;
|
||||||
|
|
||||||
mongo.client = new MongoClient(env.storageURI, options);
|
if (forceNewConnection) {
|
||||||
setupPoolMonitoring(mongo.client, env);
|
const previousClient = mongo.client;
|
||||||
|
mongo.client = null;
|
||||||
try {
|
mongo.db = null;
|
||||||
await mongo.client.connect();
|
await closeClient(previousClient);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Successfully established connection to MongoDB');
|
while (true) {
|
||||||
|
let client = null;
|
||||||
|
|
||||||
const dbName = mongo.client.s.options.dbName;
|
try {
|
||||||
mongo.db = mongo.client.db(dbName);
|
client = new MongoClient(env.storageURI, options);
|
||||||
|
mongo.client = client;
|
||||||
|
setupPoolMonitoring(client, env);
|
||||||
|
|
||||||
const result = await mongo.db.command({ connectionStatus: 1 });
|
await client.connect();
|
||||||
const roles = result.authInfo.authenticatedUserRoles;
|
|
||||||
if (roles && roles.length > 0 && roles[0].role == 'readAnyDatabase') {
|
|
||||||
console.error('Mongo user is read only');
|
|
||||||
cb(new Error('MongoDB connection is in read only mode! Go back to MongoDB configuration and check your database user has read and write access.'), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Mongo user role seems ok:', roles);
|
console.log('Successfully established connection to MongoDB');
|
||||||
|
|
||||||
// If there is a valid callback, then invoke the function to perform the callback
|
mongo.db = client.db();
|
||||||
if (cb && cb.call) {
|
|
||||||
cb(null, mongo);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err.message && err.message.includes('AuthenticationFailed')) {
|
|
||||||
console.log('Authentication to Mongo failed');
|
|
||||||
cb(new Error('MongoDB authentication failed! Double check the URL has the right username and password in MONGODB_URI.'), null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err.name && err.name === "MongoServerSelectionError") {
|
const result = await mongo.db.command({ connectionStatus: 1 });
|
||||||
const timeout = (i > 15) ? 60000 : i * 3000;
|
const roles = result.authInfo.authenticatedUserRoles;
|
||||||
|
if (roles && roles.length > 0 && roles[0].role == 'readAnyDatabase') {
|
||||||
|
console.error('Mongo user is read only');
|
||||||
|
const readOnlyError = new Error('MongoDB connection is in read only mode! Go back to MongoDB configuration and check your database user has read and write access.');
|
||||||
|
readOnlyError.name = 'MongoReadOnlyConnectionError';
|
||||||
|
throw readOnlyError;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Mongo user role seems ok:', roles);
|
||||||
|
return mongo;
|
||||||
|
} catch (err) {
|
||||||
|
const retryable = isRetryableConnectionError(err);
|
||||||
|
const wrappedError = wrapConnectionError(err);
|
||||||
|
|
||||||
|
if (err && err.message && err.message.includes('AuthenticationFailed')) {
|
||||||
|
console.log('Authentication to Mongo failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
mongo.db = null;
|
||||||
|
if (mongo.client === client) {
|
||||||
|
mongo.client = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await closeClient(client);
|
||||||
|
|
||||||
|
if (!retryable) {
|
||||||
|
throw wrappedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = getRetryDelay(attempt);
|
||||||
console.log('Error connecting to MongoDB: %j - retrying in ' + timeout / 1000 + ' sec', err);
|
console.log('Error connecting to MongoDB: %j - retrying in ' + timeout / 1000 + ' sec', err);
|
||||||
setTimeout(connect_with_retry, timeout, i + 1);
|
await wait(timeout);
|
||||||
if (i == 1) cb(new Error('MongoDB connection failed! Double check the MONGODB_URI setting in Heroku.'), null);
|
attempt += 1;
|
||||||
} else {
|
|
||||||
cb(new Error('MONGODB_URI seems invalid: ' + err.message));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return connect_with_retry(1);
|
const promise = connectWithRetry();
|
||||||
|
|
||||||
|
if (cb && cb.call) {
|
||||||
|
promise.then(function (store) {
|
||||||
|
cb(null, store);
|
||||||
|
}, function (err) {
|
||||||
|
cb(err, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,10 +212,8 @@ function init(env, cb, forceNewConnection) {
|
|||||||
fields.forEach(function (field) {
|
fields.forEach(function (field) {
|
||||||
const name = collection.collectionName + "." + field;
|
const name = collection.collectionName + "." + field;
|
||||||
console.info('ensuring index for: ' + name);
|
console.info('ensuring index for: ' + name);
|
||||||
collection.createIndex(field, { 'background': true }, function (err) {
|
collection.createIndex(field).catch(function (err) {
|
||||||
if (err) {
|
console.error('unable to ensureIndex for: ' + name + ' - ' + err);
|
||||||
console.error('unable to ensureIndex for: ' + name + ' - ' + err);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -161,3 +225,4 @@ module.exports = init;
|
|||||||
module.exports.DEFAULT_POOL_SIZE = DEFAULT_POOL_SIZE;
|
module.exports.DEFAULT_POOL_SIZE = DEFAULT_POOL_SIZE;
|
||||||
module.exports.LEGACY_POOL_SIZE = LEGACY_POOL_SIZE;
|
module.exports.LEGACY_POOL_SIZE = LEGACY_POOL_SIZE;
|
||||||
module.exports.getPoolOptions = getPoolOptions;
|
module.exports.getPoolOptions = getPoolOptions;
|
||||||
|
module.exports.getRetryDelay = getRetryDelay;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
function runWithCallback (work, callback) {
|
||||||
|
const promise = Promise.resolve().then(work);
|
||||||
|
|
||||||
|
if (callback && callback.call) {
|
||||||
|
promise.then(
|
||||||
|
function onSuccess(result) {
|
||||||
|
callback(null, result);
|
||||||
|
},
|
||||||
|
function onError(err) {
|
||||||
|
callback(err, null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = runWithCallback;
|
||||||
Generated
+1
-11
@@ -56,7 +56,7 @@
|
|||||||
"moment-timezone": "^0.5.31",
|
"moment-timezone": "^0.5.31",
|
||||||
"moment-timezone-data-webpack-plugin": "^1.5.0",
|
"moment-timezone-data-webpack-plugin": "^1.5.0",
|
||||||
"mongo-url-parser": "^1.0.2",
|
"mongo-url-parser": "^1.0.2",
|
||||||
"mongodb-legacy": "^5.0.0",
|
"mongodb": "^5.9.2",
|
||||||
"mongomock": "^0.1.2",
|
"mongomock": "^0.1.2",
|
||||||
"nightscout-connect": "^0.0.12",
|
"nightscout-connect": "^0.0.12",
|
||||||
"node-cache": "^4.2.1",
|
"node-cache": "^4.2.1",
|
||||||
@@ -6947,16 +6947,6 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mongodb-legacy": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"mongodb": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.20.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mongomock": {
|
"node_modules/mongomock": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+2
-2
@@ -90,9 +90,9 @@
|
|||||||
"npm": ">=10.x"
|
"npm": ">=10.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mongodb-js/saslprep": "^1.4.5",
|
|
||||||
"@babel/core": "^7.18.10",
|
"@babel/core": "^7.18.10",
|
||||||
"@babel/preset-env": "^7.18.10",
|
"@babel/preset-env": "^7.18.10",
|
||||||
|
"@mongodb-js/saslprep": "^1.4.5",
|
||||||
"@parse/node-apn": "^5.1.3",
|
"@parse/node-apn": "^5.1.3",
|
||||||
"acorn": "^8.0.5",
|
"acorn": "^8.0.5",
|
||||||
"acorn-jsx": "^5.3.1",
|
"acorn-jsx": "^5.3.1",
|
||||||
@@ -136,7 +136,7 @@
|
|||||||
"moment-timezone": "^0.5.31",
|
"moment-timezone": "^0.5.31",
|
||||||
"moment-timezone-data-webpack-plugin": "^1.5.0",
|
"moment-timezone-data-webpack-plugin": "^1.5.0",
|
||||||
"mongo-url-parser": "^1.0.2",
|
"mongo-url-parser": "^1.0.2",
|
||||||
"mongodb-legacy": "^5.0.0",
|
"mongodb": "^5.9.2",
|
||||||
"mongomock": "^0.1.2",
|
"mongomock": "^0.1.2",
|
||||||
"nightscout-connect": "^0.0.12",
|
"nightscout-connect": "^0.0.12",
|
||||||
"node-cache": "^4.2.1",
|
"node-cache": "^4.2.1",
|
||||||
|
|||||||
@@ -26,16 +26,12 @@ describe('Clean MONGO after tests', function ( ) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('wipe treatment data', function (done) {
|
it('wipe treatment data', async function () {
|
||||||
self.ctx.treatments().deleteMany({ }, function ( ) {
|
await self.ctx.treatments().deleteMany({ });
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('wipe entries data', function (done) {
|
it('wipe entries data', async function () {
|
||||||
self.ctx.entries().deleteMany({ }, function ( ) {
|
await self.ctx.entries().deleteMany({ });
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,12 +55,14 @@ describe('v1 API Deduplication Behavior', function() {
|
|||||||
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
|
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
|
||||||
}, function() {
|
}, function() {
|
||||||
// Use deleteMany for faster cleanup of entries
|
// Use deleteMany for faster cleanup of entries
|
||||||
self.ctx.entries().deleteMany({}, function() {
|
self.ctx.entries().deleteMany({})
|
||||||
// Also clear devicestatus to reduce database load
|
.then(function() {
|
||||||
self.ctx.devicestatus.remove({
|
// Also clear devicestatus to reduce database load
|
||||||
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
|
self.ctx.devicestatus.remove({
|
||||||
}, done);
|
find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } }
|
||||||
});
|
}, done);
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -62,12 +62,12 @@ describe('Entries REST api', function ( ) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.archive( ).deleteMany({ }, done);
|
await self.archive( ).deleteMany({ });
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function (done) {
|
after(async function () {
|
||||||
self.archive( ).deleteMany({ }, done);
|
await self.archive( ).deleteMany({ });
|
||||||
});
|
});
|
||||||
|
|
||||||
// keep this test pinned at or near the top in order to validate all
|
// keep this test pinned at or near the top in order to validate all
|
||||||
|
|||||||
@@ -43,12 +43,12 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function(done) {
|
afterEach(async function() {
|
||||||
self.archive().deleteMany({}, done);
|
await self.archive().deleteMany({});
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function(done) {
|
after(async function() {
|
||||||
self.archive().deleteMany({}, done);
|
await self.archive().deleteMany({});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,14 +98,14 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
|
|||||||
if (err2) return done(err2);
|
if (err2) return done(err2);
|
||||||
|
|
||||||
// Verify: only 1 entry exists with updated sgv 125
|
// Verify: only 1 entry exists with updated sgv 125
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err3) return done(err3);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(1);
|
||||||
docs.should.have.lengthOf(1);
|
docs[0].sgv.should.equal(125);
|
||||||
docs[0].sgv.should.equal(125);
|
docs[0].direction.should.equal('FortyFiveUp');
|
||||||
docs[0].direction.should.equal('FortyFiveUp');
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -155,14 +155,14 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
|
|||||||
if (err2) return done(err2);
|
if (err2) return done(err2);
|
||||||
|
|
||||||
// Verify: 2 entries exist (different types)
|
// Verify: 2 entries exist (different types)
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err3) return done(err3);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(2);
|
||||||
docs.should.have.lengthOf(2);
|
var types = docs.map(d => d.type).sort();
|
||||||
var types = docs.map(d => d.type).sort();
|
types.should.eql(['mbg', 'sgv']);
|
||||||
types.should.eql(['mbg', 'sgv']);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -213,12 +213,12 @@ describe('Entry sysTime+type dedup (Baseline)', function() {
|
|||||||
if (err2) return done(err2);
|
if (err2) return done(err2);
|
||||||
|
|
||||||
// Verify: 2 entries exist
|
// Verify: 2 entries exist
|
||||||
self.archive().find({ type: 'sgv', date: { $in: [timestamp1, timestamp2] } }).toArray(function(err3, docs) {
|
self.archive().find({ type: 'sgv', date: { $in: [timestamp1, timestamp2] } }).toArray()
|
||||||
if (err3) return done(err3);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(2);
|
||||||
docs.should.have.lengthOf(2);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -249,12 +249,12 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function(done) {
|
afterEach(async function() {
|
||||||
self.archive().deleteMany({}, done);
|
await self.archive().deleteMany({});
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function(done) {
|
after(async function() {
|
||||||
self.archive().deleteMany({}, done);
|
await self.archive().deleteMany({});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,14 +286,14 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
// Entry should be created (may have ObjectId _id, UUID in identifier)
|
// Entry should be created (may have ObjectId _id, UUID in identifier)
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err2, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err2) return done(err2);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(1);
|
||||||
docs.should.have.lengthOf(1);
|
docs[0].sgv.should.equal(120);
|
||||||
docs[0].sgv.should.equal(120);
|
// Note: After fix, expect docs[0].identifier === uuid
|
||||||
// Note: After fix, expect docs[0].identifier === uuid
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,13 +347,13 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
if (err2) return done(err2);
|
if (err2) return done(err2);
|
||||||
|
|
||||||
// Verify: single entry, updated value
|
// Verify: single entry, updated value
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err3) return done(err3);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(1);
|
||||||
docs.should.have.lengthOf(1);
|
docs[0].sgv.should.equal(125);
|
||||||
docs[0].sgv.should.equal(125);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -412,13 +412,13 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
if (err2) return done(err2);
|
if (err2) return done(err2);
|
||||||
|
|
||||||
// Verify: single entry (dedup by sysTime+type, not UUID)
|
// Verify: single entry (dedup by sysTime+type, not UUID)
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err3) return done(err3);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(1);
|
||||||
docs.should.have.lengthOf(1);
|
docs[0].sgv.should.equal(125);
|
||||||
docs[0].sgv.should.equal(125);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -469,14 +469,14 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
// Verify: all 3 entries created
|
// Verify: all 3 entries created
|
||||||
self.archive().find({ date: { $in: [timestamp1, timestamp2, timestamp3] } }).toArray(function(err2, docs) {
|
self.archive().find({ date: { $in: [timestamp1, timestamp2, timestamp3] } }).toArray()
|
||||||
if (err2) return done(err2);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(3);
|
||||||
docs.should.have.lengthOf(3);
|
var sgvValues = docs.map(d => d.sgv).sort();
|
||||||
var sgvValues = docs.map(d => d.sgv).sort();
|
sgvValues.should.eql([120, 125, 130]);
|
||||||
sgvValues.should.eql([120, 125, 130]);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -500,9 +500,7 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
dateString: sysTime,
|
dateString: sysTime,
|
||||||
sysTime: sysTime,
|
sysTime: sysTime,
|
||||||
device: 'Trio'
|
device: 'Trio'
|
||||||
}, function(err) {
|
}).then(function() {
|
||||||
if (err) return done(err);
|
|
||||||
|
|
||||||
// POST via API with same timestamp
|
// POST via API with same timestamp
|
||||||
var entry = {
|
var entry = {
|
||||||
_id: uuid,
|
_id: uuid,
|
||||||
@@ -513,7 +511,7 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
dateString: sysTime,
|
dateString: sysTime,
|
||||||
device: 'Trio'
|
device: 'Trio'
|
||||||
};
|
};
|
||||||
|
|
||||||
request(self.app)
|
request(self.app)
|
||||||
.post('/entries/')
|
.post('/entries/')
|
||||||
.set('api-secret', self.known)
|
.set('api-secret', self.known)
|
||||||
@@ -521,17 +519,17 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
.end(function(err2, res) {
|
.end(function(err2, res) {
|
||||||
if (err2) return done(err2);
|
if (err2) return done(err2);
|
||||||
|
|
||||||
// Verify: single entry, updated value
|
// Verify: single entry, updated value
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err3, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err3) return done(err3);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(1);
|
||||||
docs.should.have.lengthOf(1);
|
docs[0].sgv.should.equal(125);
|
||||||
docs[0].sgv.should.equal(125);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
}).catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -563,15 +561,15 @@ describe('Entry UUID _id handling (GAP-SYNC-045)', function() {
|
|||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
|
|
||||||
// Verify: entry has identifier field with UUID
|
// Verify: entry has identifier field with UUID
|
||||||
self.archive().find({ date: timestamp }).toArray(function(err2, docs) {
|
self.archive().find({ date: timestamp }).toArray()
|
||||||
if (err2) return done(err2);
|
.then(function(docs) {
|
||||||
|
docs.should.have.lengthOf(1);
|
||||||
docs.should.have.lengthOf(1);
|
docs[0].should.have.property('identifier', uuid);
|
||||||
docs[0].should.have.property('identifier', uuid);
|
// _id should be ObjectId, not UUID
|
||||||
// _id should be ObjectId, not UUID
|
docs[0]._id.should.not.equal(uuid);
|
||||||
docs[0]._id.should.not.equal(uuid);
|
done();
|
||||||
done();
|
})
|
||||||
});
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var should = require('should');
|
||||||
|
|
||||||
|
var objectIdValidation = require('../lib/api/shared/objectid-validation');
|
||||||
|
|
||||||
|
describe('API ObjectId validation helper', function () {
|
||||||
|
it('accepts missing ids for create-tolerant routes', function () {
|
||||||
|
should(objectIdValidation.isValidObjectId(undefined)).be.true();
|
||||||
|
should(objectIdValidation.isValidObjectId(null)).be.true();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts 24-character hex ids and rejects non-hex values', function () {
|
||||||
|
should(objectIdValidation.isValidObjectId('507f1f77bcf86cd799439011')).be.true();
|
||||||
|
should(objectIdValidation.isValidObjectId('550e8400-e29b-41d4-a716-446655440000')).be.false();
|
||||||
|
should(objectIdValidation.isValidObjectId(123)).be.false();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds the first invalid id in a batch', function () {
|
||||||
|
objectIdValidation.findInvalidId([
|
||||||
|
{ _id: '507f1f77bcf86cd799439011' },
|
||||||
|
{ _id: 'invalid-id' },
|
||||||
|
{ _id: '507f191e810c19729de860ea' }
|
||||||
|
]).should.eql({ index: 1, id: 'invalid-id' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when all ids are valid or omitted', function () {
|
||||||
|
should.not.exist(objectIdValidation.findInvalidId([
|
||||||
|
{ _id: '507f1f77bcf86cd799439011' },
|
||||||
|
{},
|
||||||
|
{ _id: null }
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,22 @@ describe('Security of REST API V1', function() {
|
|||||||
|
|
||||||
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1';
|
||||||
|
|
||||||
|
function rolesCollection() {
|
||||||
|
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'roles');
|
||||||
|
}
|
||||||
|
|
||||||
|
function subjectsCollection() {
|
||||||
|
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'subjects');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBearerToken(accessToken) {
|
||||||
|
const res = await request(self.app)
|
||||||
|
.get('/api/v2/authorization/request/' + accessToken)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
return res.body.token;
|
||||||
|
}
|
||||||
|
|
||||||
before(function(done) {
|
before(function(done) {
|
||||||
var api = require('../lib/api/');
|
var api = require('../lib/api/');
|
||||||
delete process.env.API_SECRET;
|
delete process.env.API_SECRET;
|
||||||
@@ -26,6 +42,7 @@ describe('Security of REST API V1', function() {
|
|||||||
self.app = require('express')();
|
self.app = require('express')();
|
||||||
self.app.enable('api');
|
self.app.enable('api');
|
||||||
require('../lib/server/bootevent')(self.env, language).boot(async function booted (ctx) {
|
require('../lib/server/bootevent')(self.env, language).boot(async function booted (ctx) {
|
||||||
|
self.ctx = ctx;
|
||||||
self.app.use('/api/v1', api(self.env, ctx));
|
self.app.use('/api/v1', api(self.env, ctx));
|
||||||
self.app.use('/api/v2/authorization', ctx.authorization.endpoints);
|
self.app.use('/api/v2/authorization', ctx.authorization.endpoints);
|
||||||
let authResult = await authSubject(ctx.authorization.storage);
|
let authResult = await authSubject(ctx.authorization.storage);
|
||||||
@@ -159,4 +176,140 @@ describe('Security of REST API V1', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Authorization admin save endpoints', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await rolesCollection().deleteMany({ name: /^api-security-role/ });
|
||||||
|
await subjectsCollection().deleteMany({ name: /^api-security-subject/ });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
await rolesCollection().deleteMany({ name: /^api-security-role/ });
|
||||||
|
await subjectsCollection().deleteMany({ name: /^api-security-subject/ });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/v2/authorization/subjects updates an existing subject by _id', async function () {
|
||||||
|
const insertResult = await subjectsCollection().insertOne({
|
||||||
|
name: 'api-security-subject-update',
|
||||||
|
roles: ['readable'],
|
||||||
|
notes: 'original',
|
||||||
|
created_at: '2024-10-26T20:32:49.173Z'
|
||||||
|
});
|
||||||
|
const token = await getBearerToken(self.token.adminAll);
|
||||||
|
|
||||||
|
await request(self.app)
|
||||||
|
.put('/api/v2/authorization/subjects')
|
||||||
|
.set('Authorization', 'Bearer ' + token)
|
||||||
|
.send({
|
||||||
|
_id: insertResult.insertedId.toString(),
|
||||||
|
name: 'api-security-subject-update',
|
||||||
|
roles: ['admin'],
|
||||||
|
notes: 'updated',
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z'
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const docs = await subjectsCollection().find({ name: 'api-security-subject-update' }).toArray();
|
||||||
|
docs.length.should.equal(1);
|
||||||
|
docs[0].roles.should.deepEqual(['admin']);
|
||||||
|
docs[0].notes.should.equal('updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/v2/authorization/roles updates an existing role by _id', async function () {
|
||||||
|
const insertResult = await rolesCollection().insertOne({
|
||||||
|
name: 'api-security-role-update',
|
||||||
|
permissions: ['api:entries:read'],
|
||||||
|
notes: 'original',
|
||||||
|
created_at: '2024-10-26T20:32:49.173Z'
|
||||||
|
});
|
||||||
|
const token = await getBearerToken(self.token.adminAll);
|
||||||
|
|
||||||
|
await request(self.app)
|
||||||
|
.put('/api/v2/authorization/roles')
|
||||||
|
.set('Authorization', 'Bearer ' + token)
|
||||||
|
.send({
|
||||||
|
_id: insertResult.insertedId.toString(),
|
||||||
|
name: 'api-security-role-update',
|
||||||
|
permissions: ['api:entries:update'],
|
||||||
|
notes: 'updated',
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z'
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const docs = await rolesCollection().find({ name: 'api-security-role-update' }).toArray();
|
||||||
|
docs.length.should.equal(1);
|
||||||
|
docs[0].permissions.should.deepEqual(['api:entries:update']);
|
||||||
|
docs[0].notes.should.equal('updated');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/v2/authorization/subjects fails when _id is missing', async function () {
|
||||||
|
const token = await getBearerToken(self.token.adminAll);
|
||||||
|
|
||||||
|
await request(self.app)
|
||||||
|
.put('/api/v2/authorization/subjects')
|
||||||
|
.set('Authorization', 'Bearer ' + token)
|
||||||
|
.send({
|
||||||
|
name: 'api-security-subject-missing-id',
|
||||||
|
roles: ['readable'],
|
||||||
|
notes: 'should fail'
|
||||||
|
})
|
||||||
|
.expect(500);
|
||||||
|
|
||||||
|
const docs = await subjectsCollection().find({ name: 'api-security-subject-missing-id' }).toArray();
|
||||||
|
docs.length.should.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/v2/authorization/subjects fails when _id is invalid', async function () {
|
||||||
|
const token = await getBearerToken(self.token.adminAll);
|
||||||
|
|
||||||
|
await request(self.app)
|
||||||
|
.put('/api/v2/authorization/subjects')
|
||||||
|
.set('Authorization', 'Bearer ' + token)
|
||||||
|
.send({
|
||||||
|
_id: 'not-a-valid-objectid',
|
||||||
|
name: 'api-security-subject-invalid-id',
|
||||||
|
roles: ['readable'],
|
||||||
|
notes: 'should fail'
|
||||||
|
})
|
||||||
|
.expect(500);
|
||||||
|
|
||||||
|
const docs = await subjectsCollection().find({ name: 'api-security-subject-invalid-id' }).toArray();
|
||||||
|
docs.length.should.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/v2/authorization/roles fails when _id is missing', async function () {
|
||||||
|
const token = await getBearerToken(self.token.adminAll);
|
||||||
|
|
||||||
|
await request(self.app)
|
||||||
|
.put('/api/v2/authorization/roles')
|
||||||
|
.set('Authorization', 'Bearer ' + token)
|
||||||
|
.send({
|
||||||
|
name: 'api-security-role-missing-id',
|
||||||
|
permissions: ['api:entries:read'],
|
||||||
|
notes: 'should fail'
|
||||||
|
})
|
||||||
|
.expect(500);
|
||||||
|
|
||||||
|
const docs = await rolesCollection().find({ name: 'api-security-role-missing-id' }).toArray();
|
||||||
|
docs.length.should.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/v2/authorization/roles fails when _id is invalid', async function () {
|
||||||
|
const token = await getBearerToken(self.token.adminAll);
|
||||||
|
|
||||||
|
await request(self.app)
|
||||||
|
.put('/api/v2/authorization/roles')
|
||||||
|
.set('Authorization', 'Bearer ' + token)
|
||||||
|
.send({
|
||||||
|
_id: 'not-a-valid-objectid',
|
||||||
|
name: 'api-security-role-invalid-id',
|
||||||
|
permissions: ['api:entries:read'],
|
||||||
|
notes: 'should fail'
|
||||||
|
})
|
||||||
|
.expect(500);
|
||||||
|
|
||||||
|
const docs = await rolesCollection().find({ name: 'api-security-role-invalid-id' }).toArray();
|
||||||
|
docs.length.should.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -352,16 +352,12 @@ describe('API Shape Handling - Single Object vs Array Input', function () {
|
|||||||
|
|
||||||
describe('Entries API - /api/entries/', function () {
|
describe('Entries API - /api/entries/', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
await self.ctx.entries().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
await self.ctx.entries().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST accepts single SGV entry object', function (done) {
|
it('POST accepts single SGV entry object', function (done) {
|
||||||
@@ -838,7 +834,95 @@ describe('API Shape Handling - Single Object vs Array Input', function () {
|
|||||||
p.should.have.property('_id');
|
p.should.have.property('_id');
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Legacy PUT update semantics', function () {
|
||||||
|
beforeEach(async function () {
|
||||||
|
await self.ctx.food().deleteMany({});
|
||||||
|
await self.ctx.activity().deleteMany({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
await self.ctx.food().deleteMany({});
|
||||||
|
await self.ctx.activity().deleteMany({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/food/ updates an existing record by _id when created_at changes', function (done) {
|
||||||
|
self.ctx.food.create({
|
||||||
|
name: 'API shape food',
|
||||||
|
category: 'Test',
|
||||||
|
carbs: 20,
|
||||||
|
protein: 10,
|
||||||
|
fat: 5
|
||||||
|
}, function (createErr, docs) {
|
||||||
|
if (createErr) return done(createErr);
|
||||||
|
|
||||||
|
var created = docs[0];
|
||||||
|
request(self.app)
|
||||||
|
.put('/api/food/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({
|
||||||
|
_id: created._id.toString(),
|
||||||
|
name: 'API shape food',
|
||||||
|
category: 'Test',
|
||||||
|
carbs: 25,
|
||||||
|
protein: 10,
|
||||||
|
fat: 5,
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z'
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.end(function (err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
self.ctx.food().find({ _id: created._id }).toArray()
|
||||||
|
.then(function (updatedDocs) {
|
||||||
|
updatedDocs.length.should.equal(1);
|
||||||
|
updatedDocs[0].carbs.should.equal(25);
|
||||||
|
updatedDocs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT /api/activity/ updates an existing record by _id', function (done) {
|
||||||
|
self.ctx.activity.create([{
|
||||||
|
created_at: '2024-10-26T20:32:49.173Z',
|
||||||
|
heartrate: 80,
|
||||||
|
steps: 100,
|
||||||
|
activitylevel: 'walking'
|
||||||
|
}], function (createErr, docs) {
|
||||||
|
if (createErr) return done(createErr);
|
||||||
|
|
||||||
|
var created = docs[0];
|
||||||
|
request(self.app)
|
||||||
|
.put('/api/activity/')
|
||||||
|
.set('api-secret', known)
|
||||||
|
.send({
|
||||||
|
_id: created._id.toString(),
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z',
|
||||||
|
heartrate: 95,
|
||||||
|
steps: 250,
|
||||||
|
activitylevel: 'running'
|
||||||
|
})
|
||||||
|
.expect(200)
|
||||||
|
.end(function (err) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
self.ctx.activity().find({ _id: created._id }).toArray()
|
||||||
|
.then(function (updatedDocs) {
|
||||||
|
updatedDocs.length.should.equal(1);
|
||||||
|
updatedDocs[0].heartrate.should.equal(95);
|
||||||
|
updatedDocs[0].steps.should.equal(250);
|
||||||
|
updatedDocs[0].activitylevel.should.equal('running');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ describe('authed REST api', function ( ) {
|
|||||||
this.archive.create(creating, done);
|
this.archive.create(creating, done);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
this.archive( ).deleteMany({ }, done);
|
await this.archive( ).deleteMany({ });
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function (done) {
|
after(async function () {
|
||||||
this.archive( ).deleteMany({ }, done);
|
await this.archive( ).deleteMany({ });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disallow unauthorized POST', function (done) {
|
it('disallow unauthorized POST', function (done) {
|
||||||
|
|||||||
@@ -3,13 +3,17 @@
|
|||||||
require('should');
|
require('should');
|
||||||
|
|
||||||
const find = require('../lib/api3/storage/mongoCollection/find');
|
const find = require('../lib/api3/storage/mongoCollection/find');
|
||||||
|
const { ObjectId } = require('mongodb');
|
||||||
|
|
||||||
describe('API3 mongoCollection findMany', function () {
|
describe('API3 mongoCollection find helpers', function () {
|
||||||
function createStubCollection (observed) {
|
function createStubCursor (observed, docs) {
|
||||||
return {
|
return {
|
||||||
find: function () {
|
find: function () {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
project: function () {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
sort: function () {
|
sort: function () {
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -21,11 +25,17 @@ describe('API3 mongoCollection findMany', function () {
|
|||||||
observed.skip = value;
|
observed.skip = value;
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
project: function () {
|
toArray: function () {
|
||||||
return this;
|
observed.toArrayCalls = (observed.toArrayCalls || 0) + 1;
|
||||||
},
|
return Promise.resolve(docs || []);
|
||||||
toArray: function (callback) {
|
}
|
||||||
callback(null, []);
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStubCollection (observed, docs) {
|
||||||
|
return {
|
||||||
|
find: function () {
|
||||||
|
return createStubCursor(observed, docs);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -44,6 +54,7 @@ describe('API3 mongoCollection findMany', function () {
|
|||||||
|
|
||||||
observed.limit.should.equal(5);
|
observed.limit.should.equal(5);
|
||||||
observed.skip.should.equal(2);
|
observed.skip.should.equal(2);
|
||||||
|
observed.toArrayCalls.should.equal(1);
|
||||||
result.should.eql([]);
|
result.should.eql([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,4 +74,30 @@ describe('API3 mongoCollection findMany', function () {
|
|||||||
observed.limit.should.equal(5);
|
observed.limit.should.equal(5);
|
||||||
observed.skip.should.equal(2);
|
observed.skip.should.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes findOne results when using promise-based cursors', async function () {
|
||||||
|
const observed = {};
|
||||||
|
const docId = new ObjectId();
|
||||||
|
const col = createStubCollection(observed, [{ _id: docId, type: 'sgv' }]);
|
||||||
|
|
||||||
|
const result = await find.findOne(col, docId.toString(), {});
|
||||||
|
|
||||||
|
observed.toArrayCalls.should.equal(1);
|
||||||
|
result.should.have.length(1);
|
||||||
|
result[0].should.have.property('identifier', docId.toString());
|
||||||
|
result[0].should.not.have.property('_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports promise-based findOneFilter without normalization when requested', async function () {
|
||||||
|
const observed = {};
|
||||||
|
const docId = new ObjectId();
|
||||||
|
const col = createStubCollection(observed, [{ _id: docId, type: 'mbg' }]);
|
||||||
|
|
||||||
|
const result = await find.findOneFilter(col, { type: 'mbg' }, {}, { normalize: false });
|
||||||
|
|
||||||
|
observed.toArrayCalls.should.equal(1);
|
||||||
|
result.should.have.length(1);
|
||||||
|
result[0].should.have.property('_id');
|
||||||
|
result[0]._id.toString().should.equal(docId.toString());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('should');
|
||||||
|
|
||||||
|
const { ObjectId } = require('mongodb');
|
||||||
|
const modify = require('../lib/api3/storage/mongoCollection/modify');
|
||||||
|
const MongoCollection = require('../lib/api3/storage/mongoCollection');
|
||||||
|
|
||||||
|
describe('API3 mongoCollection promise-based helpers', function () {
|
||||||
|
describe('modify helpers', function () {
|
||||||
|
it('insertOne uses promise-based collection methods and keeps existing behavior', async function () {
|
||||||
|
const doc = { _id: new ObjectId(), type: 'sgv' };
|
||||||
|
const insertedId = doc._id.toString();
|
||||||
|
const col = {
|
||||||
|
insertOne: function (receivedDoc) {
|
||||||
|
arguments.length.should.equal(1);
|
||||||
|
receivedDoc.should.equal(doc);
|
||||||
|
return Promise.resolve({ insertedId: receivedDoc._id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const identifier = await modify.insertOne(col, doc);
|
||||||
|
|
||||||
|
identifier.should.equal(insertedId);
|
||||||
|
doc.should.not.have.property('_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaceOne uses promise-based upsert without a callback', async function () {
|
||||||
|
const doc = { value: 42 };
|
||||||
|
const col = {
|
||||||
|
replaceOne: function (filter, receivedDoc, options) {
|
||||||
|
arguments.length.should.equal(3);
|
||||||
|
filter.should.eql({ $or: [{ identifier: 'record-1' }] });
|
||||||
|
receivedDoc.should.equal(doc);
|
||||||
|
options.should.eql({ upsert: true });
|
||||||
|
return Promise.resolve({ matchedCount: 1 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchedCount = await modify.replaceOne(col, 'record-1', doc);
|
||||||
|
|
||||||
|
matchedCount.should.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update and delete helpers use promise-based collection methods', async function () {
|
||||||
|
const col = {
|
||||||
|
updateOne: function (filter, update) {
|
||||||
|
arguments.length.should.equal(2);
|
||||||
|
filter.should.eql({ $or: [{ identifier: 'record-2' }] });
|
||||||
|
update.should.eql({ $set: { value: 84 } });
|
||||||
|
return Promise.resolve({ modifiedCount: 1 });
|
||||||
|
},
|
||||||
|
deleteOne: function (filter) {
|
||||||
|
arguments.length.should.equal(1);
|
||||||
|
filter.should.eql({ $or: [{ identifier: 'record-2' }] });
|
||||||
|
return Promise.resolve({ deletedCount: 1 });
|
||||||
|
},
|
||||||
|
deleteMany: function (filter) {
|
||||||
|
arguments.length.should.equal(1);
|
||||||
|
filter.should.eql({ $or: [{ kind: 'sgv' }, { kind: 'mbg' }] });
|
||||||
|
return Promise.resolve({ deletedCount: 2 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updated = await modify.updateOne(col, 'record-2', { value: 84 });
|
||||||
|
const deletedOne = await modify.deleteOne(col, 'record-2');
|
||||||
|
const deletedMany = await modify.deleteManyOr(col, [
|
||||||
|
{ field: 'kind', operator: 'eq', value: 'sgv' },
|
||||||
|
{ field: 'kind', operator: 'eq', value: 'mbg' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
updated.should.eql({ updated: 1 });
|
||||||
|
deletedOne.should.eql({ deleted: 1 });
|
||||||
|
deletedMany.should.eql({ deleted: 2 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collection metadata helpers', function () {
|
||||||
|
function createCursor (observed, docs) {
|
||||||
|
return {
|
||||||
|
sort: function (spec) {
|
||||||
|
observed.sort = spec;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
limit: function (value) {
|
||||||
|
observed.limit = value;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
project: function (projection) {
|
||||||
|
observed.projection = projection;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
toArray: function () {
|
||||||
|
observed.toArrayCalls = (observed.toArrayCalls || 0) + 1;
|
||||||
|
return Promise.resolve(docs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStorage (observed, docs) {
|
||||||
|
const col = {
|
||||||
|
find: function () {
|
||||||
|
arguments.length.should.equal(0);
|
||||||
|
observed.findCalls = (observed.findCalls || 0) + 1;
|
||||||
|
return createCursor(observed, docs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
store: {
|
||||||
|
collection: function (name) {
|
||||||
|
observed.collectionName = name;
|
||||||
|
return col;
|
||||||
|
},
|
||||||
|
ensureIndexes: function (receivedCol, fields) {
|
||||||
|
receivedCol.should.equal(col);
|
||||||
|
observed.indexFields = fields;
|
||||||
|
},
|
||||||
|
db: {
|
||||||
|
admin: function () {
|
||||||
|
return {
|
||||||
|
buildInfo: function () {
|
||||||
|
arguments.length.should.equal(0);
|
||||||
|
observed.buildInfoCalls = (observed.buildInfoCalls || 0) + 1;
|
||||||
|
return Promise.resolve({ version: '5.9.2' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return new MongoCollection(ctx, {}, 'entries');
|
||||||
|
}
|
||||||
|
|
||||||
|
it('version uses promise-based buildInfo', async function () {
|
||||||
|
const observed = {};
|
||||||
|
const storage = createStorage(observed, []);
|
||||||
|
|
||||||
|
const version = await storage.version();
|
||||||
|
|
||||||
|
observed.collectionName.should.equal('entries');
|
||||||
|
observed.buildInfoCalls.should.equal(1);
|
||||||
|
version.should.eql({ storage: 'mongodb', version: '5.9.2' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getLastModified uses promise-based cursor methods', async function () {
|
||||||
|
const observed = {};
|
||||||
|
const storage = createStorage(observed, [{ srvModified: 1234 }]);
|
||||||
|
|
||||||
|
const lastModified = await storage.getLastModified('srvModified');
|
||||||
|
|
||||||
|
observed.findCalls.should.equal(1);
|
||||||
|
observed.sort.should.eql({ srvModified: -1 });
|
||||||
|
observed.limit.should.equal(1);
|
||||||
|
observed.projection.should.eql({ srvModified: 1 });
|
||||||
|
observed.toArrayCalls.should.equal(1);
|
||||||
|
lastModified.should.eql({ srvModified: 1234 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
* On mongodb driver 3.x (v15.0.6), Object.keys(new ObjectID()) returned
|
* On mongodb driver 3.x (v15.0.6), Object.keys(new ObjectID()) returned
|
||||||
* ['_bsontype','id'], making _.isEmpty return false (correct).
|
* ['_bsontype','id'], making _.isEmpty return false (correct).
|
||||||
*
|
*
|
||||||
* On mongodb driver 5.x / mongodb-legacy (dev), Object.keys(new ObjectId())
|
* On mongodb driver 5.x, Object.keys(new ObjectId())
|
||||||
* returns [], making _.isEmpty return true (incorrect – treats valid
|
* returns [], making _.isEmpty return true (incorrect – treats valid
|
||||||
* ObjectId _id as empty).
|
* ObjectId _id as empty).
|
||||||
*
|
*
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { ObjectId } = require('mongodb-legacy');
|
const { ObjectId } = require('mongodb');
|
||||||
const should = require('should');
|
const should = require('should');
|
||||||
|
|
||||||
describe('Cache ObjectId compatibility', function () {
|
describe('Cache ObjectId compatibility', function () {
|
||||||
|
|||||||
@@ -306,16 +306,12 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
|
|||||||
|
|
||||||
describe('Simultaneous POST requests to entries', function () {
|
describe('Simultaneous POST requests to entries', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
await self.ctx.entries().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
await self.ctx.entries().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles 5 simultaneous single entry POSTs', function (done) {
|
it('handles 5 simultaneous single entry POSTs', function (done) {
|
||||||
@@ -422,9 +418,11 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
|
|||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
||||||
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
self.ctx.entries().deleteMany({})
|
||||||
done();
|
.then(function () {
|
||||||
});
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -432,9 +430,11 @@ describe('Concurrent Write Tests - MongoDB 5.x Compatibility', function () {
|
|||||||
afterEach(function (done) {
|
afterEach(function (done) {
|
||||||
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
self.ctx.treatments.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
||||||
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
self.ctx.devicestatus.remove({ find: { created_at: { '$gte': '1999-01-01T00:00:00.000Z' } } }, function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
self.ctx.entries().deleteMany({})
|
||||||
done();
|
.then(function () {
|
||||||
});
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('should');
|
||||||
|
|
||||||
|
const dataloaderInit = require('../lib/data/dataloader');
|
||||||
|
const createDData = require('../lib/data/ddata');
|
||||||
|
|
||||||
|
describe('dataloader', function () {
|
||||||
|
it('completes update when db.stats is promise-based', function (done) {
|
||||||
|
const ddata = createDData();
|
||||||
|
ddata.processTreatments = function () {};
|
||||||
|
const ctx = {
|
||||||
|
settings: {},
|
||||||
|
language: {
|
||||||
|
translate: function (value) { return value; }
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
isEmpty: function () { return true; },
|
||||||
|
insertData: function (key, results) { return results; }
|
||||||
|
},
|
||||||
|
ddata: ddata,
|
||||||
|
entries: {
|
||||||
|
list: function (query, callback) { callback(null, []); }
|
||||||
|
},
|
||||||
|
treatments: {
|
||||||
|
list: function (query, callback) { callback(null, []); }
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
last: function (callback) { callback(null, []); }
|
||||||
|
},
|
||||||
|
food: {
|
||||||
|
list: function (callback) { callback(null, []); }
|
||||||
|
},
|
||||||
|
devicestatus: {
|
||||||
|
list: function (query, callback) { callback(null, []); }
|
||||||
|
},
|
||||||
|
activity: {
|
||||||
|
list: function (query, callback) { callback(null, []); }
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
db: {
|
||||||
|
stats: function () {
|
||||||
|
return Promise.resolve({ dataSize: 123, indexSize: 456 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const env = {
|
||||||
|
settings: {
|
||||||
|
isEnabled: function () { return false; },
|
||||||
|
units: 'mg/dl'
|
||||||
|
},
|
||||||
|
extendedSettings: {}
|
||||||
|
};
|
||||||
|
const loader = dataloaderInit(env, ctx);
|
||||||
|
|
||||||
|
loader.update(ddata, function (err) {
|
||||||
|
should.not.exist(err);
|
||||||
|
ddata.dbstats.should.eql({
|
||||||
|
dataSize: 123,
|
||||||
|
indexSize: 456
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Vendored
+16
-2
@@ -75,7 +75,14 @@ function guardedDeleteMany(collection, filter, callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Safe to proceed
|
// Safe to proceed
|
||||||
return collection.deleteMany(filter, callback);
|
const promise = collection.deleteMany(filter);
|
||||||
|
if (callback) {
|
||||||
|
promise.then(
|
||||||
|
function onSuccess(result) { callback(null, result); },
|
||||||
|
function onError(err) { callback(err); }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +102,14 @@ function guardedDrop(collection, callback) {
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
return collection.drop(callback);
|
const promise = collection.drop();
|
||||||
|
if (callback) {
|
||||||
|
promise.then(
|
||||||
|
function onSuccess(result) { callback(null, result); },
|
||||||
|
function onError(err) { callback(err); }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -63,10 +63,8 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
|
|||||||
return self.ctx.store.collection(self.env.treatments_collection);
|
return self.ctx.store.collection(self.env.treatments_collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
rawCollection().deleteMany({}, function () {
|
await rawCollection().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,16 +73,19 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
|
|||||||
*/
|
*/
|
||||||
function insertLegacyDoc (callback) {
|
function insertLegacyDoc (callback) {
|
||||||
var doc = Object.assign({}, LEGACY_OVERRIDE);
|
var doc = Object.assign({}, LEGACY_OVERRIDE);
|
||||||
rawCollection().insertOne(doc, function (err) {
|
rawCollection().insertOne(doc)
|
||||||
should.not.exist(err);
|
.then(function () {
|
||||||
rawCollection().findOne({ _id: LEGACY_UUID }, function (err, stored) {
|
return rawCollection().findOne({ _id: LEGACY_UUID });
|
||||||
should.not.exist(err);
|
})
|
||||||
|
.then(function (stored) {
|
||||||
should.exist(stored, 'Legacy doc should exist after direct insert');
|
should.exist(stored, 'Legacy doc should exist after direct insert');
|
||||||
stored._id.should.equal(LEGACY_UUID);
|
stored._id.should.equal(LEGACY_UUID);
|
||||||
should.not.exist(stored.identifier, 'Legacy doc must NOT have identifier field');
|
should.not.exist(stored.identifier, 'Legacy doc must NOT have identifier field');
|
||||||
callback(stored);
|
callback(stored);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
should.not.exist(err);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('DELETE legacy UUID override via API', function () {
|
describe('DELETE legacy UUID override via API', function () {
|
||||||
@@ -130,10 +131,8 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
|
|||||||
|
|
||||||
// Check the database after the server has had time to process the upsert
|
// Check the database after the server has had time to process the upsert
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
rawCollection().find({ eventType: 'Temporary Override' }).toArray(function (err, docs) {
|
rawCollection().find({ eventType: 'Temporary Override' }).toArray()
|
||||||
try {
|
.then(function (docs) {
|
||||||
should.not.exist(err);
|
|
||||||
|
|
||||||
docs.length.should.equal(1,
|
docs.length.should.equal(1,
|
||||||
'PUT should update the existing legacy override, not create a duplicate. '
|
'PUT should update the existing legacy override, not create a duplicate. '
|
||||||
+ 'Found ' + docs.length + ' documents. '
|
+ 'Found ' + docs.length + ' documents. '
|
||||||
@@ -143,10 +142,8 @@ describe('Issue #6923: Legacy UUID override edit/delete', function () {
|
|||||||
);
|
);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
} catch (e) {
|
})
|
||||||
done(e);
|
.catch(done);
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var should = require('should');
|
||||||
|
var mongodb = require('mongodb');
|
||||||
|
|
||||||
|
describe('mongo storage retry lifecycle', function () {
|
||||||
|
var originalMongoClient = mongodb.MongoClient;
|
||||||
|
var originalSetTimeout = global.setTimeout;
|
||||||
|
|
||||||
|
function setMongoClient(fakeMongoClient) {
|
||||||
|
Object.defineProperty(mongodb, 'MongoClient', {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
value: fakeMongoClient,
|
||||||
|
writable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
setMongoClient(originalMongoClient);
|
||||||
|
global.setTimeout = originalSetTimeout;
|
||||||
|
delete require.cache[require.resolve('../lib/storage/mongo-storage')];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes failed retry clients and calls back only after a successful retry', function (done) {
|
||||||
|
var createdClients = [];
|
||||||
|
var connectAttempts = 0;
|
||||||
|
var retryDelays = [];
|
||||||
|
|
||||||
|
function FakeMongoClient() {
|
||||||
|
this.closed = 0;
|
||||||
|
createdClients.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeMongoClient.prototype.on = function () {};
|
||||||
|
FakeMongoClient.prototype.connect = function () {
|
||||||
|
connectAttempts += 1;
|
||||||
|
|
||||||
|
if (connectAttempts === 1) {
|
||||||
|
var err = new Error('server selection failed');
|
||||||
|
err.name = 'MongoServerSelectionError';
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
FakeMongoClient.prototype.db = function () {
|
||||||
|
return {
|
||||||
|
databaseName: 'testdb',
|
||||||
|
command: function () {
|
||||||
|
return Promise.resolve({ authInfo: { authenticatedUserRoles: [] } });
|
||||||
|
},
|
||||||
|
collection: function (name) {
|
||||||
|
return { collectionName: name };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
FakeMongoClient.prototype.close = function () {
|
||||||
|
this.closed += 1;
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
setMongoClient(FakeMongoClient);
|
||||||
|
global.setTimeout = function (fn, ms) {
|
||||||
|
retryDelays.push(ms);
|
||||||
|
Promise.resolve().then(fn);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
delete require.cache[require.resolve('../lib/storage/mongo-storage')];
|
||||||
|
var store = require('../lib/storage/mongo-storage');
|
||||||
|
var callbackCount = 0;
|
||||||
|
|
||||||
|
store({ storageURI: 'mongodb://example/testdb' }, function (err, db) {
|
||||||
|
callbackCount += 1;
|
||||||
|
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(db);
|
||||||
|
callbackCount.should.equal(1);
|
||||||
|
connectAttempts.should.equal(2);
|
||||||
|
createdClients.length.should.equal(2);
|
||||||
|
createdClients[0].closed.should.equal(1);
|
||||||
|
createdClients[1].closed.should.equal(0);
|
||||||
|
retryDelays.should.eql([3000]);
|
||||||
|
db.db.databaseName.should.equal('testdb');
|
||||||
|
|
||||||
|
done();
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the client and reports authentication failure once', function (done) {
|
||||||
|
var createdClients = [];
|
||||||
|
var retryDelays = [];
|
||||||
|
|
||||||
|
function FakeMongoClient() {
|
||||||
|
this.closed = 0;
|
||||||
|
createdClients.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeMongoClient.prototype.on = function () {};
|
||||||
|
FakeMongoClient.prototype.connect = function () {
|
||||||
|
return Promise.reject(new Error('AuthenticationFailed: bad auth'));
|
||||||
|
};
|
||||||
|
FakeMongoClient.prototype.close = function () {
|
||||||
|
this.closed += 1;
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
setMongoClient(FakeMongoClient);
|
||||||
|
global.setTimeout = function (fn, ms) {
|
||||||
|
retryDelays.push(ms);
|
||||||
|
Promise.resolve().then(fn);
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
delete require.cache[require.resolve('../lib/storage/mongo-storage')];
|
||||||
|
var store = require('../lib/storage/mongo-storage');
|
||||||
|
var callbackCount = 0;
|
||||||
|
|
||||||
|
store({ storageURI: 'mongodb://example/testdb' }, function (err, db) {
|
||||||
|
callbackCount += 1;
|
||||||
|
|
||||||
|
should.exist(err);
|
||||||
|
should.not.exist(db);
|
||||||
|
err.message.should.equal('MongoDB authentication failed! Double check the URL has the right username and password in MONGODB_URI.');
|
||||||
|
callbackCount.should.equal(1);
|
||||||
|
createdClients.length.should.equal(1);
|
||||||
|
createdClients[0].closed.should.equal(1);
|
||||||
|
retryDelays.should.eql([]);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -30,6 +30,42 @@ describe('mongo storage', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Uses the default database from the connection string via the public client API', function (done) {
|
||||||
|
var store = require('../lib/storage/mongo-storage');
|
||||||
|
store(env, function (err, db) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(db.db);
|
||||||
|
should.exist(db.client);
|
||||||
|
|
||||||
|
db.client.db().databaseName.should.equal(db.db.databaseName);
|
||||||
|
db.db.databaseName.should.equal('testdb');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ensureIndexes uses createIndex without the legacy background option', function (done) {
|
||||||
|
var store = require('../lib/storage/mongo-storage');
|
||||||
|
var calls = [];
|
||||||
|
|
||||||
|
store(env, function (err, db) {
|
||||||
|
should.not.exist(err);
|
||||||
|
|
||||||
|
db.ensureIndexes({
|
||||||
|
collectionName: 'entries',
|
||||||
|
createIndex: function (field, options) {
|
||||||
|
calls.push({ field: field, options: options });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}, ['date']);
|
||||||
|
|
||||||
|
calls.length.should.equal(1);
|
||||||
|
calls[0].field.should.equal('date');
|
||||||
|
should.not.exist(calls[0].options);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('When no connection-string is given the storage-class should throw an error.', function (done) {
|
it('When no connection-string is given the storage-class should throw an error.', function (done) {
|
||||||
delete env.storageURI;
|
delete env.storageURI;
|
||||||
should.not.exist(env.storageURI);
|
should.not.exist(env.storageURI);
|
||||||
@@ -58,4 +94,3 @@ describe('mongo storage', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ describe('Loop SGV Entry Upload Tests', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(function(done) {
|
beforeEach(async function() {
|
||||||
self.ctx.entries().deleteMany({}, done);
|
await self.ctx.entries().deleteMany({});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('TEST-SGV-001: Single SGV entry', function() {
|
describe('TEST-SGV-001: Single SGV entry', function() {
|
||||||
|
|||||||
@@ -173,16 +173,12 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
|
|||||||
|
|
||||||
describe('Entries Storage - lib/server/entries.js', function () {
|
describe('Entries Storage - lib/server/entries.js', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
await self.ctx.entries().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.ctx.entries().deleteMany({}, function () {
|
await self.ctx.entries().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create() accepts single entry in array', function (done) {
|
it('create() accepts single entry in array', function (done) {
|
||||||
@@ -218,16 +214,12 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
|
|||||||
|
|
||||||
describe('Profile Storage - lib/server/profile.js', function () {
|
describe('Profile Storage - lib/server/profile.js', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
self.ctx.profile().deleteMany({}, function () {
|
await self.ctx.profile().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.ctx.profile().deleteMany({}, function () {
|
await self.ctx.profile().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create() accepts single profile object', function (done) {
|
it('create() accepts single profile object', function (done) {
|
||||||
@@ -347,13 +339,14 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
|
|||||||
self.ctx.profile.save(updated, function (saveErr) {
|
self.ctx.profile.save(updated, function (saveErr) {
|
||||||
should.not.exist(saveErr);
|
should.not.exist(saveErr);
|
||||||
|
|
||||||
self.ctx.profile().find({ _id: savedId }).toArray(function (findErr, docs) {
|
self.ctx.profile().find({ _id: savedId }).toArray()
|
||||||
should.not.exist(findErr);
|
.then(function (docs) {
|
||||||
docs.length.should.equal(1);
|
docs.length.should.equal(1);
|
||||||
docs[0].store.Default.dia.should.equal(4);
|
docs[0].store.Default.dia.should.equal(4);
|
||||||
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
||||||
done();
|
done();
|
||||||
});
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -438,28 +431,25 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
|
|||||||
should.not.exist(err);
|
should.not.exist(err);
|
||||||
saved.created_at.should.equal('2020-01-01T00:00:00.000Z');
|
saved.created_at.should.equal('2020-01-01T00:00:00.000Z');
|
||||||
|
|
||||||
self.ctx.profile().find({ _id: saved._id }).toArray(function (findErr, docs) {
|
self.ctx.profile().find({ _id: saved._id }).toArray()
|
||||||
should.not.exist(findErr);
|
.then(function (docs) {
|
||||||
docs.length.should.equal(1);
|
docs.length.should.equal(1);
|
||||||
docs[0].created_at.should.equal('2020-01-01T00:00:00.000Z');
|
docs[0].created_at.should.equal('2020-01-01T00:00:00.000Z');
|
||||||
done();
|
done();
|
||||||
});
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Food Storage - lib/server/food.js', function () {
|
describe('Food Storage - lib/server/food.js', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
self.ctx.food().deleteMany({}, function () {
|
await self.ctx.food().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.ctx.food().deleteMany({}, function () {
|
await self.ctx.food().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create() accepts single food object', function (done) {
|
it('create() accepts single food object', function (done) {
|
||||||
@@ -480,20 +470,59 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('save() updates an existing food by _id when created_at changes', function (done) {
|
||||||
|
self.ctx.food.create({
|
||||||
|
name: 'Test Food',
|
||||||
|
category: 'Test',
|
||||||
|
carbs: 20,
|
||||||
|
protein: 10,
|
||||||
|
fat: 5
|
||||||
|
}, function (err, createdDocs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(createdDocs);
|
||||||
|
createdDocs.should.be.an.Array();
|
||||||
|
createdDocs.length.should.equal(1);
|
||||||
|
|
||||||
|
var savedId = createdDocs[0]._id;
|
||||||
|
var updated = {
|
||||||
|
_id: savedId.toString(),
|
||||||
|
name: 'Updated Food',
|
||||||
|
category: 'Test',
|
||||||
|
carbs: 25,
|
||||||
|
protein: 10,
|
||||||
|
fat: 5,
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
self.ctx.food.save(updated, function (saveErr, savedDocs) {
|
||||||
|
should.not.exist(saveErr);
|
||||||
|
should.exist(savedDocs);
|
||||||
|
savedDocs.should.be.an.Array();
|
||||||
|
savedDocs.length.should.equal(1);
|
||||||
|
|
||||||
|
self.ctx.food().find({ _id: savedId }).toArray()
|
||||||
|
.then(function (docs) {
|
||||||
|
docs.length.should.equal(1);
|
||||||
|
docs[0].name.should.equal('Updated Food');
|
||||||
|
docs[0].carbs.should.equal(25);
|
||||||
|
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Activity Storage - lib/server/activity.js', function () {
|
describe('Activity Storage - lib/server/activity.js', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(async function () {
|
||||||
self.ctx.activity().deleteMany({}, function () {
|
await self.ctx.activity().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (done) {
|
afterEach(async function () {
|
||||||
self.ctx.activity().deleteMany({}, function () {
|
await self.ctx.activity().deleteMany({});
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('create() accepts array of activity objects (single object not supported)', function (done) {
|
it('create() accepts array of activity objects (single object not supported)', function (done) {
|
||||||
@@ -511,6 +540,124 @@ describe('Storage Layer Shape Handling - Direct Storage Tests', function () {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('save() updates an existing activity by _id', function (done) {
|
||||||
|
self.ctx.activity.create([{
|
||||||
|
created_at: '2024-10-26T20:32:49.173Z',
|
||||||
|
heartrate: 80,
|
||||||
|
steps: 100,
|
||||||
|
activitylevel: 'walking'
|
||||||
|
}], function (err, createdDocs) {
|
||||||
|
should.not.exist(err);
|
||||||
|
should.exist(createdDocs);
|
||||||
|
createdDocs.length.should.equal(1);
|
||||||
|
should.exist(createdDocs[0]._id);
|
||||||
|
|
||||||
|
var savedId = createdDocs[0]._id;
|
||||||
|
var updated = {
|
||||||
|
_id: savedId.toString(),
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z',
|
||||||
|
heartrate: 95,
|
||||||
|
steps: 250,
|
||||||
|
activitylevel: 'running'
|
||||||
|
};
|
||||||
|
|
||||||
|
self.ctx.activity.save(updated, function (saveErr, savedDoc) {
|
||||||
|
should.not.exist(saveErr);
|
||||||
|
should.exist(savedDoc);
|
||||||
|
savedDoc._id.toString().should.equal(savedId.toString());
|
||||||
|
|
||||||
|
self.ctx.activity().find({ _id: savedId }).toArray()
|
||||||
|
.then(function (docs) {
|
||||||
|
docs.length.should.equal(1);
|
||||||
|
docs[0].heartrate.should.equal(95);
|
||||||
|
docs[0].steps.should.equal(250);
|
||||||
|
docs[0].activitylevel.should.equal('running');
|
||||||
|
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Authorization Storage - lib/authorization/storage.js', function () {
|
||||||
|
function rolesCollection() {
|
||||||
|
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'roles');
|
||||||
|
}
|
||||||
|
|
||||||
|
function subjectsCollection() {
|
||||||
|
return self.ctx.store.collection(self.env.authentication_collections_prefix + 'subjects');
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
await rolesCollection().deleteMany({ name: 'mongo-save-role' });
|
||||||
|
await subjectsCollection().deleteMany({ name: 'mongo-save-subject' });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
await rolesCollection().deleteMany({ name: 'mongo-save-role' });
|
||||||
|
await subjectsCollection().deleteMany({ name: 'mongo-save-subject' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveRole() updates an existing role without duplicating it', function (done) {
|
||||||
|
rolesCollection().insertOne({
|
||||||
|
name: 'mongo-save-role',
|
||||||
|
permissions: ['api:entries:read'],
|
||||||
|
notes: 'original',
|
||||||
|
created_at: '2024-10-26T20:32:49.173Z'
|
||||||
|
}).then(function (result) {
|
||||||
|
self.ctx.authorization.storage.saveRole({
|
||||||
|
_id: result.insertedId.toString(),
|
||||||
|
name: 'mongo-save-role',
|
||||||
|
permissions: ['api:entries:update'],
|
||||||
|
notes: 'updated',
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z'
|
||||||
|
}, function (saveErr) {
|
||||||
|
should.not.exist(saveErr);
|
||||||
|
|
||||||
|
rolesCollection().find({ name: 'mongo-save-role' }).toArray()
|
||||||
|
.then(function (docs) {
|
||||||
|
docs.length.should.equal(1);
|
||||||
|
docs[0].permissions.should.deepEqual(['api:entries:update']);
|
||||||
|
docs[0].notes.should.equal('updated');
|
||||||
|
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveSubject() updates an existing subject without duplicating it', function (done) {
|
||||||
|
subjectsCollection().insertOne({
|
||||||
|
name: 'mongo-save-subject',
|
||||||
|
roles: ['readable'],
|
||||||
|
notes: 'original',
|
||||||
|
created_at: '2024-10-26T20:32:49.173Z'
|
||||||
|
}).then(function (result) {
|
||||||
|
self.ctx.authorization.storage.saveSubject({
|
||||||
|
_id: result.insertedId.toString(),
|
||||||
|
name: 'mongo-save-subject',
|
||||||
|
roles: ['admin'],
|
||||||
|
notes: 'updated',
|
||||||
|
created_at: '2024-10-26T21:32:49.173Z'
|
||||||
|
}, function (saveErr) {
|
||||||
|
should.not.exist(saveErr);
|
||||||
|
|
||||||
|
subjectsCollection().find({ name: 'mongo-save-subject' }).toArray()
|
||||||
|
.then(function (docs) {
|
||||||
|
docs.length.should.equal(1);
|
||||||
|
docs[0].roles.should.deepEqual(['admin']);
|
||||||
|
docs[0].notes.should.equal('updated');
|
||||||
|
docs[0].created_at.should.equal('2024-10-26T21:32:49.173Z');
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
|
});
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -533,71 +680,61 @@ describe('MongoDB insertOne vs insertMany Behavior', function () {
|
|||||||
|
|
||||||
describe('Direct MongoDB operations - testing insertOne with array data', function () {
|
describe('Direct MongoDB operations - testing insertOne with array data', function () {
|
||||||
|
|
||||||
it('insertOne with object inserts correctly', function (done) {
|
it('insertOne with object inserts correctly', async function () {
|
||||||
var testCollection = self.ctx.store.collection('test_shape_handling');
|
var testCollection = self.ctx.store.collection('test_shape_handling');
|
||||||
|
|
||||||
testCollection.deleteMany({}, function () {
|
await testCollection.deleteMany({});
|
||||||
testCollection.insertOne({ type: 'test', value: 42 }, function (err, result) {
|
var result = await testCollection.insertOne({ type: 'test', value: 42 });
|
||||||
should.not.exist(err);
|
should.exist(result);
|
||||||
should.exist(result);
|
result.insertedId.should.be.ok();
|
||||||
result.insertedId.should.be.ok();
|
|
||||||
|
var docs = await testCollection.find({}).toArray();
|
||||||
testCollection.find({}).toArray(function (err, docs) {
|
docs.length.should.equal(1);
|
||||||
docs.length.should.equal(1);
|
docs[0].value.should.equal(42);
|
||||||
docs[0].value.should.equal(42);
|
await testCollection.deleteMany({});
|
||||||
testCollection.deleteMany({}, done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('insertOne with array creates single document containing array (NOT multiple docs)', function (done) {
|
it('insertOne with array creates single document containing array (NOT multiple docs)', async function () {
|
||||||
var testCollection = self.ctx.store.collection('test_shape_handling');
|
var testCollection = self.ctx.store.collection('test_shape_handling');
|
||||||
|
|
||||||
testCollection.deleteMany({}, function () {
|
await testCollection.deleteMany({});
|
||||||
var arrayData = [
|
var arrayData = [
|
||||||
{ type: 'test', value: 1 },
|
{ type: 'test', value: 1 },
|
||||||
{ type: 'test', value: 2 },
|
{ type: 'test', value: 2 },
|
||||||
{ type: 'test', value: 3 }
|
{ type: 'test', value: 3 }
|
||||||
];
|
];
|
||||||
|
|
||||||
testCollection.insertOne(arrayData, function (err, result) {
|
try {
|
||||||
if (err) {
|
await testCollection.insertOne(arrayData);
|
||||||
console.log('insertOne with array error:', err.message);
|
} catch (err) {
|
||||||
done();
|
console.log('insertOne with array error:', err.message);
|
||||||
} else {
|
await testCollection.deleteMany({});
|
||||||
testCollection.find({}).toArray(function (err, docs) {
|
return;
|
||||||
console.log('Documents after insertOne with array:', JSON.stringify(docs, null, 2));
|
}
|
||||||
console.log('Number of documents:', docs.length);
|
|
||||||
|
var docs = await testCollection.find({}).toArray();
|
||||||
testCollection.deleteMany({}, done);
|
console.log('Documents after insertOne with array:', JSON.stringify(docs, null, 2));
|
||||||
});
|
console.log('Number of documents:', docs.length);
|
||||||
}
|
await testCollection.deleteMany({});
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('insertMany with array creates multiple documents', function (done) {
|
it('insertMany with array creates multiple documents', async function () {
|
||||||
var testCollection = self.ctx.store.collection('test_shape_handling');
|
var testCollection = self.ctx.store.collection('test_shape_handling');
|
||||||
|
|
||||||
testCollection.deleteMany({}, function () {
|
await testCollection.deleteMany({});
|
||||||
var arrayData = [
|
var arrayData = [
|
||||||
{ type: 'test', value: 1 },
|
{ type: 'test', value: 1 },
|
||||||
{ type: 'test', value: 2 },
|
{ type: 'test', value: 2 },
|
||||||
{ type: 'test', value: 3 }
|
{ type: 'test', value: 3 }
|
||||||
];
|
];
|
||||||
|
|
||||||
testCollection.insertMany(arrayData, function (err, result) {
|
var result = await testCollection.insertMany(arrayData);
|
||||||
should.not.exist(err);
|
should.exist(result);
|
||||||
should.exist(result);
|
result.insertedCount.should.equal(3);
|
||||||
result.insertedCount.should.equal(3);
|
|
||||||
|
var docs = await testCollection.find({}).toArray();
|
||||||
testCollection.find({}).toArray(function (err, docs) {
|
docs.length.should.equal(3);
|
||||||
docs.length.should.equal(3);
|
await testCollection.deleteMany({});
|
||||||
testCollection.deleteMany({}, done);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
var request = require('supertest');
|
var request = require('supertest');
|
||||||
var should = require('should');
|
var should = require('should');
|
||||||
|
var ObjectID = require('mongodb').ObjectId;
|
||||||
var language = require('../lib/language')();
|
var language = require('../lib/language')();
|
||||||
var api = require('../lib/api/');
|
var api = require('../lib/api/');
|
||||||
|
|
||||||
@@ -243,7 +244,6 @@ describe('UUID_HANDLING=true', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('UUID-ON-003: ObjectId still works normally', function(done) {
|
it('UUID-ON-003: ObjectId still works normally', function(done) {
|
||||||
var ObjectID = require('mongodb').ObjectId;
|
|
||||||
var testId = new ObjectID();
|
var testId = new ObjectID();
|
||||||
|
|
||||||
// Insert treatment with ObjectId
|
// Insert treatment with ObjectId
|
||||||
@@ -491,7 +491,6 @@ describe('UUID Edge Cases', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('UUID-EDGE-007: Valid ObjectId still works normally', function(done) {
|
it('UUID-EDGE-007: Valid ObjectId still works normally', function(done) {
|
||||||
var ObjectID = require('mongodb').ObjectId;
|
|
||||||
var testId = new ObjectID();
|
var testId = new ObjectID();
|
||||||
|
|
||||||
self.ctx.treatments.create([{
|
self.ctx.treatments.create([{
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
return self.ctx.store.collection(self.env.treatments_collection);
|
return self.ctx.store.collection(self.env.treatments_collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function foodCollection() {
|
||||||
|
return self.ctx.food();
|
||||||
|
}
|
||||||
|
|
||||||
describe('dbAdd with treatments collection', function () {
|
describe('dbAdd with treatments collection', function () {
|
||||||
|
|
||||||
beforeEach(function (done) {
|
beforeEach(function (done) {
|
||||||
@@ -278,8 +282,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
eventType: 'Note',
|
eventType: 'Note',
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
notes: 'legacy original'
|
notes: 'legacy original'
|
||||||
}, function (insertErr) {
|
}).then(function () {
|
||||||
if (insertErr) return done(insertErr);
|
|
||||||
|
|
||||||
socket.emit('dbUpdate', {
|
socket.emit('dbUpdate', {
|
||||||
collection: 'treatments',
|
collection: 'treatments',
|
||||||
@@ -293,7 +296,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
|
|
||||||
waitForConditionWithWarning({
|
waitForConditionWithWarning({
|
||||||
condition: function (cb) {
|
condition: function (cb) {
|
||||||
treatmentsCollection().findOne({ _id: legacyId }, cb);
|
treatmentsCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
},
|
},
|
||||||
assertion: function (doc) {
|
assertion: function (doc) {
|
||||||
should.exist(doc);
|
should.exist(doc);
|
||||||
@@ -303,7 +308,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
operationName: 'verify websocket dbUpdate with custom string _id'
|
operationName: 'verify websocket dbUpdate with custom string _id'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}).catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -328,8 +333,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
eventType: 'Note',
|
eventType: 'Note',
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
notes: 'remove me'
|
notes: 'remove me'
|
||||||
}, function (insertErr) {
|
}).then(function () {
|
||||||
if (insertErr) return done(insertErr);
|
|
||||||
|
|
||||||
socket.emit('dbUpdateUnset', {
|
socket.emit('dbUpdateUnset', {
|
||||||
collection: 'treatments',
|
collection: 'treatments',
|
||||||
@@ -343,7 +347,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
|
|
||||||
waitForConditionWithWarning({
|
waitForConditionWithWarning({
|
||||||
condition: function (cb) {
|
condition: function (cb) {
|
||||||
treatmentsCollection().findOne({ _id: legacyId }, cb);
|
treatmentsCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
},
|
},
|
||||||
assertion: function (doc) {
|
assertion: function (doc) {
|
||||||
should.exist(doc);
|
should.exist(doc);
|
||||||
@@ -353,7 +359,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
operationName: 'verify websocket dbUpdateUnset with custom string _id'
|
operationName: 'verify websocket dbUpdateUnset with custom string _id'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}).catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -408,8 +414,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
eventType: 'Note',
|
eventType: 'Note',
|
||||||
created_at: createdAt,
|
created_at: createdAt,
|
||||||
notes: 'delete me'
|
notes: 'delete me'
|
||||||
}, function (insertErr) {
|
}).then(function () {
|
||||||
if (insertErr) return done(insertErr);
|
|
||||||
|
|
||||||
socket.emit('dbRemove', {
|
socket.emit('dbRemove', {
|
||||||
collection: 'treatments',
|
collection: 'treatments',
|
||||||
@@ -420,7 +425,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
|
|
||||||
waitForConditionWithWarning({
|
waitForConditionWithWarning({
|
||||||
condition: function (cb) {
|
condition: function (cb) {
|
||||||
treatmentsCollection().findOne({ _id: legacyId }, cb);
|
treatmentsCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
},
|
},
|
||||||
assertion: function (doc) {
|
assertion: function (doc) {
|
||||||
should.not.exist(doc);
|
should.not.exist(doc);
|
||||||
@@ -429,7 +436,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
operationName: 'verify websocket dbRemove with custom string _id'
|
operationName: 'verify websocket dbRemove with custom string _id'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}).catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -455,8 +462,7 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
eventType: 'Note',
|
eventType: 'Note',
|
||||||
created_at: originalCreatedAt,
|
created_at: originalCreatedAt,
|
||||||
notes: 'existing legacy note'
|
notes: 'existing legacy note'
|
||||||
}, function (insertErr) {
|
}).then(function () {
|
||||||
if (insertErr) return done(insertErr);
|
|
||||||
|
|
||||||
socket.emit('dbAdd', {
|
socket.emit('dbAdd', {
|
||||||
collection: 'treatments',
|
collection: 'treatments',
|
||||||
@@ -473,7 +479,9 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
|
|
||||||
waitForConditionWithWarning({
|
waitForConditionWithWarning({
|
||||||
condition: function (cb) {
|
condition: function (cb) {
|
||||||
treatmentsCollection().findOne({ _id: legacyId }, cb);
|
treatmentsCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
},
|
},
|
||||||
assertion: function (doc) {
|
assertion: function (doc) {
|
||||||
should.exist(doc);
|
should.exist(doc);
|
||||||
@@ -483,8 +491,132 @@ describe('WebSocket Shape Handling - dbAdd Single vs Array Input', function () {
|
|||||||
operationName: 'verify websocket dbAdd dedupe with custom string _id'
|
operationName: 'verify websocket dbAdd dedupe with custom string _id'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generic collection raw write watchpoints', function () {
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
await foodCollection().deleteMany({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dbAdd preserves custom string _id values for generic collections', function (done) {
|
||||||
|
connectAndAuthorize(function (err, socket, authResult) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
authResult.write.should.equal(true);
|
||||||
|
|
||||||
|
var legacyId = 'legacy-food-id-add';
|
||||||
|
socket.emit('dbAdd', {
|
||||||
|
collection: 'food',
|
||||||
|
data: {
|
||||||
|
_id: legacyId,
|
||||||
|
name: 'ws food',
|
||||||
|
carbs: 15
|
||||||
|
}
|
||||||
|
}, function (result) {
|
||||||
|
should.exist(result);
|
||||||
|
result.should.be.instanceof(Array);
|
||||||
|
result.length.should.equal(1);
|
||||||
|
result[0]._id.should.equal(legacyId);
|
||||||
|
|
||||||
|
waitForConditionWithWarning({
|
||||||
|
condition: function (cb) {
|
||||||
|
foodCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
|
},
|
||||||
|
assertion: function (doc) {
|
||||||
|
should.exist(doc);
|
||||||
|
doc.name.should.equal('ws food');
|
||||||
|
doc.carbs.should.equal(15);
|
||||||
|
},
|
||||||
|
done: done,
|
||||||
|
operationName: 'verify websocket dbAdd generic collection custom string _id'
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('dbUpdate supports custom string _id values for generic collections', function (done) {
|
||||||
|
connectAndAuthorize(function (err, socket, authResult) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
authResult.write.should.equal(true);
|
||||||
|
|
||||||
|
var legacyId = 'legacy-food-id-update';
|
||||||
|
foodCollection().insertOne({
|
||||||
|
_id: legacyId,
|
||||||
|
name: 'original food',
|
||||||
|
carbs: 10,
|
||||||
|
protein: 2
|
||||||
|
}).then(function () {
|
||||||
|
socket.emit('dbUpdate', {
|
||||||
|
collection: 'food',
|
||||||
|
_id: legacyId,
|
||||||
|
data: {
|
||||||
|
carbs: 18,
|
||||||
|
protein: 4
|
||||||
|
}
|
||||||
|
}, function (updateResult) {
|
||||||
|
should.exist(updateResult);
|
||||||
|
updateResult.result.should.equal('success');
|
||||||
|
|
||||||
|
waitForConditionWithWarning({
|
||||||
|
condition: function (cb) {
|
||||||
|
foodCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
|
},
|
||||||
|
assertion: function (doc) {
|
||||||
|
should.exist(doc);
|
||||||
|
doc.carbs.should.equal(18);
|
||||||
|
doc.protein.should.equal(4);
|
||||||
|
},
|
||||||
|
done: done,
|
||||||
|
operationName: 'verify websocket dbUpdate generic collection custom string _id'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dbRemove supports custom string _id values for generic collections', function (done) {
|
||||||
|
connectAndAuthorize(function (err, socket, authResult) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
authResult.write.should.equal(true);
|
||||||
|
|
||||||
|
var legacyId = 'legacy-food-id-remove';
|
||||||
|
foodCollection().insertOne({
|
||||||
|
_id: legacyId,
|
||||||
|
name: 'remove food',
|
||||||
|
carbs: 9
|
||||||
|
}).then(function () {
|
||||||
|
socket.emit('dbRemove', {
|
||||||
|
collection: 'food',
|
||||||
|
_id: legacyId
|
||||||
|
}, function (removeResult) {
|
||||||
|
should.exist(removeResult);
|
||||||
|
removeResult.result.should.equal('success');
|
||||||
|
|
||||||
|
waitForConditionWithWarning({
|
||||||
|
condition: function (cb) {
|
||||||
|
foodCollection().findOne({ _id: legacyId })
|
||||||
|
.then(function (doc) { cb(null, doc); })
|
||||||
|
.catch(cb);
|
||||||
|
},
|
||||||
|
assertion: function (doc) {
|
||||||
|
should.not.exist(doc);
|
||||||
|
},
|
||||||
|
done: done,
|
||||||
|
operationName: 'verify websocket dbRemove generic collection custom string _id'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user