diff --git a/.gitignore b/.gitignore index 525a160d9..6b2bce8b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ node_modules coverage .idea - +.vscode/* diff --git a/README.md b/README.md index 4fbb6abcc..4accb2e6e 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ You can set the `url` property to a connection URL in `datasources.json` to over Additionally, you can override the global `url` property in environment-specific data source configuration files, for example for production in `datasources.production.json`, and use the individual connection parameters `host`, `user`, `password`, and `port`. To do this, you _must_ set `url` to `false`, null, or “” (empty string). If you set `url` to `undefined` or remove the `url` property altogether, the override will not work. -For example, for production, use `datasources.production.json` as follows (for example) to overide the `url` setting in `datasources.json: +For example, for production, use `datasources.production.json` as follows (for example) to override the `url` setting in `datasources.json: ```javascript "mydb": { @@ -280,6 +280,12 @@ myModelName.find( ) ``` +## Advanced features + +### decimal128 type + +You can check [document](https://github.com/strongloop/loopback-connector-mongodb/blob/master/docs/decimal128.md) for details. + ## Release notes * 1.1.7 - Do not return MongoDB-specific _id to client API, except if specifically specified in the model definition diff --git a/docs/decimal128.md b/docs/decimal128.md index 566b946bc..374f90a69 100644 --- a/docs/decimal128.md +++ b/docs/decimal128.md @@ -1,28 +1,66 @@ ## Decimal128 -You can define a Decimal128 type property as +You can define Decimal128 type properties as ```js // in model.json file -"aproperty": { +// 1st level property +"count": { "type": "String", "mongodb": {"dataType": "Decimal128"} -} +}, +// nested array +"lines": [ + { + "unitPrice": { + "type": "string", + "title": "The unitPrice Schema ", + "mongodb": { + "dataType": "Decimal128" + } + } + } +], +// nested object +"summary": { + "totalValue": { + "type": "string", + "title": "The totalValue Schema ", + "mongodb": { + "dataType": "Decimal128" + } + } +}, ``` -Suppose model `Order` has a decimal property called `count`. Read the sections below for the usage of CRUD operations. +Suppose model `Order` has the model definition above, read the sections below for the usage of CRUD operations. ### Create ```js -Order.create({count: '0.0005'}, function(err, order) { - // order: {_id: '5bc8cc7f71cade4a8b5af886', count: '0.0005'} +const sample = { + count: '0.0005', + summary: {totalValue: '100.0005'}, + lines: [{unitPrice: '0.0005'}], +}; +Order.create(sample, function(err, order) { + // order: { + // _id: '5bc8cc7f71cade4a8b5af886', + // count: '0.0005', + // summary: {totalValue: '100.0005'}, + // lines: [{unitPrice: '0.0005'}], + // } }); ``` The created record in the mongodb database will be: ```js -{ "_id" : ObjectId("5bc8cc7f71cade4a8b5af886"), "count" : NumberDecimal("0.0005") } +{ + "_id" : ObjectId("5bc8cc7f71cade4a8b5af886"), + "count" : NumberDecimal("0.0005"), + "lines" : [ { "unitPrice" : NumberDecimal("0.0006") ], + "summary": { "totalValue": NumberDecimal("100.0005")} +} ``` The returned model instance is generated by the `create` method in `loopback-datasource-juggler/lib/dao.js`, connector only @@ -34,12 +72,13 @@ var Decimal128 = require('mongodb').Decimal128; Order.create({count: '0.0005'}, function(err, order) { // convert the string to decimal order.count = Decimal128.fromString(order.count); + // ... same conversion for nested properties }); ``` ### Find -You can filter a decimal property like +You can filter a first level decimal property like ```js OrderDecimal.find({where: {count: '0.0005'}}, function(err, orders) { @@ -49,6 +88,17 @@ OrderDecimal.find({where: {count: '0.0005'}}, function(err, orders) { The connector automatically converts the condition from string to decimal. +When query nested properties, you still need to do the conversion yourself, as follows + +```js +// query decimal property in a nested array +const arrCond = {where: {lines: {elemMatch: {unitPrice: Decimal128.fromString('0.0005')}}}}; +OrderDecimal.find(arrCond, function(err, orders) {}); + +// query decimal property in a nested object +const objCond = {where: {'summary.totalValue': Decimal128.fromString('100.0005')}}; +OrderDecimal.find(objCond, function(err, orders) {}); +``` ### DestroyAll @@ -56,4 +106,10 @@ You can destroy data with a filter contains decimal property. For example: ```js OrderDecimal.destroyAll({count: '0.0005'}, cb); -``` \ No newline at end of file +// or +const arrCond = {where: {lines: {elemMatch: {unitPrice: Decimal128.fromString('0.0005')}}}}; +OrderDecimal.destroyAll(arrCond, cb); +// or +const objCond = {where: {'summary.totalValue': Decimal128.fromString('100.0005')}}; +OrderDecimal.destroyAll(objCond, cb); +``` diff --git a/lib/mongodb.js b/lib/mongodb.js index a0d562d65..ba185c986 100644 --- a/lib/mongodb.js +++ b/lib/mongodb.js @@ -410,21 +410,16 @@ MongoDB.prototype.fromDatabase = function(model, data) { MongoDB.prototype.toDatabase = function(model, data) { var props = this._models[model].properties; - if (this.settings.enableGeoIndexing !== true) { - convertDecimalProps(data, props); - // Override custom column names - data = this.fromPropertyToDatabaseNames(model, data); - return data; - } - - for (var p in props) { - var prop = props[p]; - const isGeoPoint = data[p] && prop && prop.type && prop.type.name === 'GeoPoint'; - if (isGeoPoint) { - data[p] = { - coordinates: [data[p].lng, data[p].lat], - type: 'Point', - }; + if (this.settings.enableGeoIndexing === true) { + for (var p in props) { + var prop = props[p]; + const isGeoPoint = data[p] && prop && prop.type && prop.type.name === 'GeoPoint'; + if (isGeoPoint) { + data[p] = { + coordinates: [data[p].lng, data[p].lat], + type: 'Point', + }; + } } } @@ -956,6 +951,7 @@ MongoDB.prototype.buildWhere = function(model, where, options) { cond = cond[spec]; } if (spec) { + if (spec.charAt(0) === '$') spec = spec.substr(1); if (spec === 'between') { query[k] = {$gte: cond[0], $lte: cond[1]}; } else if (spec === 'inq') { @@ -1996,17 +1992,50 @@ function optimizedFindOrCreate(model, filter, data, options, callback) { * @param {Object} data The data that might contain a decimal property * @param {Object} props The model property definitions */ -function convertDecimalProps(data, props) { - if (debug.enabled) debug('convertDecimalProps props: ', util.inspect(props)); - for (const p in props) { - const prop = props[p]; - const isDecimal = data[p] && prop && prop.mongodb && - prop.mongodb.dataType && - prop.mongodb.dataType.toLowerCase() === 'decimal128'; +function convertDecimalProps(data, propDef) { + if (propDef == null) return data; + + if (Array.isArray(data)) { + const arrType = getArrayItemDef(propDef); + if (arrType) { + data.forEach(function(elem, i) { + data[i] = convertDecimalProps(elem, arrType); + }); + if (debug.enabled) debug('convertDecimalProps converted array: ', util.inspect(data)); + }; + } else if (!!data && typeof data === 'object') { + // !!data: skips executing the code when data is `null` + const ownData = Object.getOwnPropertyNames(data); + ownData.forEach(function(k) { + data[k] = convertDecimalProps(data[k], getObjectDef(propDef, k)); + }); + if (debug.enabled) debug('convertDecimalProps converted object: ', util.inspect(data)); + } else { + const isDecimal = propDef && propDef.mongodb && + propDef.mongodb.dataType && + propDef.mongodb.dataType.toLowerCase() === 'decimal128'; if (isDecimal) { - data[p] = Decimal128.fromString(data[p]); - debug('convertDecimalProps decimal value: ', data[p]); + data = Decimal128.fromString(data); + if (debug.enabled) debug('convertDecimalProps decimal value: ', data); } } + return data; } + +function getArrayItemDef(propDef) { + if (debug.enabled) debug('getArrayItemDef property definition: ', util.inspect(propDef)); + if (isLBArr(propDef)) return propDef[0]; + return null; +} + +function getObjectDef(propDef, name) { + if (debug.enabled) debug('getObjectDef property definition: %o, name: %s', + util.inspect(propDef), name); + if (typeof propDef === 'object') return propDef[name]; + return null; +} + +function isLBArr(propDef) { + return typeof propDef === 'object' && Array.isArray(propDef.type); +} diff --git a/test/decimal.test.js b/test/decimal.test.js index e47236414..44bd0da4e 100644 --- a/test/decimal.test.js +++ b/test/decimal.test.js @@ -8,59 +8,195 @@ require('./init.js'); const Decimal128 = require('mongodb').Decimal128; -var db, OrderDecimal; - -describe('model with decimal property', function() { - before(function(done) { - db = global.getDataSource(); - var propertyDef = { - count: { - type: String, - mongodb: { - dataType: 'Decimal128', +var db, OrderDecimal, OrderDecimalArr, OrderDecimalObj; + +describe('decimal128', function() { + describe('model with decimal property - first level', function() { + before(function(done) { + db = global.getDataSource(); + var propertyDef = { + count: { + type: String, + mongodb: { + dataType: 'Decimal128', + }, }, - }, - }; + }; - OrderDecimal = db.createModel('OrderDecimal', propertyDef); - OrderDecimal.destroyAll(done); - }); + OrderDecimal = db.createModel('OrderDecimal', propertyDef); + OrderDecimal.destroyAll(done); + }); + + it('create - coerces strings to decimal 128', function(done) { + OrderDecimal.create({count: '0.0005'}, function(err, order) { + if (err) return done(err); + // FIXME Ideally the returned data should have a decimal `count` + // not a string. While juggler generates the returned data, connector + // doesn't have any control. + order.count.should.equal('0.0005'); + done(); + }); + }); + + it('find - filters decimal property', function(done) { + OrderDecimal.find({where: {count: '0.0005'}}, function(err, orders) { + if (err) return done(err); + orders.length.should.be.above(0); + const o = orders[0]; + o.count.should.deepEqual(Decimal128.fromString('0.0005')); + o.count.should.be.an.instanceOf(Decimal128); + done(); + }); + }); - it('create - coerces strings to decimal 128', function(done) { - OrderDecimal.create({count: '0.0005'}, function(err, order) { - if (err) return done(err); - // FIXME Ideally the returned data should have a decimal `count` - // not a string. While juggler generates the returned data, connector - // doesn't have any control. - order.count.should.equal('0.0005'); - done(); + it('destroyAll - deletes all with where', function(done) { + OrderDecimal.create({count: '0.0006'}) + .then(function() { + return OrderDecimal.destroyAll({count: '0.0005'}); + }) + .then(function() { + return OrderDecimal.find(); + }) + .then(function(r) { + r.length.should.equal(1); + r[0].count.should.deepEqual(Decimal128.fromString('0.0006')); + done(); + }) + .catch(done); }); }); - it('find - filters decimal property', function(done) { - OrderDecimal.find({where: {count: '0.0005'}}, function(err, orders) { - if (err) return done(err); - orders.length.should.be.above(0); - const o = orders[0]; - o.count.should.deepEqual(Decimal128.fromString('0.0005')); - o.count.should.be.an.instanceOf(Decimal128); - done(); + describe('model with decimal property - nested array', function() { + before(function(done) { + db = global.getDataSource(); + var propertyDef = { + // nested property in an array + lines: [ + { + unitPrice: { + type: 'string', + title: 'The unitPrice Schema ', + mongodb: { + dataType: 'Decimal128', + }, + }, + }, + ], + }; + + OrderDecimalArr = db.createModel('OrderDecimalNested', propertyDef); + OrderDecimalArr.destroyAll(done); + }); + + it('create - coerces strings to decimal 128', function(done) { + const sample = { + lines: [{unitPrice: '0.0005'}], + }; + OrderDecimalArr.create(sample, function(err, order) { + if (err) return done(err); + // FIXME Ideally the returned data should have a decimal `count` + // not a string. While juggler generates the returned data, connector + // doesn't have any control. + JSON.parse(JSON.stringify(order.lines)).should.deepEqual(sample.lines); + done(); + }); + }); + + it('find - filters nested array decimal property', function(done) { + const cond = {where: {lines: {elemMatch: {unitPrice: Decimal128.fromString('0.0005')}}}}; + OrderDecimalArr.find(cond, function(err, orders) { + if (err) return done(err); + orders.length.should.be.above(0); + const o = orders[0]; + o.lines[0].unitPrice.should.equal('0.0005'); + done(); + }); + }); + + it('destroyAll - deletes all with where', function(done) { + const anotherSample = { + lines: [{unitPrice: '0.0006'}], + }; + const cond = {lines: {elemMatch: {unitPrice: Decimal128.fromString('0.0005')}}}; + OrderDecimalArr.create(anotherSample) + .then(function() { + return OrderDecimalArr.destroyAll(cond); + }) + .then(function() { + return OrderDecimalArr.find(); + }) + .then(function(r) { + r.length.should.equal(1); + r[0].lines[0].unitPrice.should.equal('0.0006'); + done(); + }) + .catch(done); }); }); - it('destroyAll - deletes all with where', function(done) { - OrderDecimal.create({count: '0.0006'}) - .then(function() { - return OrderDecimal.destroyAll({count: '0.0005'}); - }) - .then(function() { - return OrderDecimal.find(); - }) - .then(function(r) { - r.length.should.equal(1); - r[0].count.should.deepEqual(Decimal128.fromString('0.0006')); + describe('model with decimal property - nested object', function() { + before(function(done) { + db = global.getDataSource(); + var propertyDef = { + // nested property in an object + summary: { + totalValue: { + type: 'string', + title: 'The totalValue Schema ', + mongodb: { + dataType: 'Decimal128', + }, + }, + }, + }; + + OrderDecimalObj = db.createModel('OrderDecimalNested', propertyDef); + OrderDecimalObj.destroyAll(done); + }); + + it('create - coerces strings to decimal 128', function(done) { + const sample = { + summary: {totalValue: '100.0005'}, + }; + OrderDecimalObj.create(sample, function(err, order) { + if (err) return done(err); + // FIXME Ideally the returned data should have a decimal `count` + // not a string. While juggler generates the returned data, connector + // doesn't have any control. + JSON.parse(JSON.stringify(order.summary)).should.deepEqual(sample.summary); + done(); + }); + }); + + it('find - filters nested object decimal property', function(done) { + const cond = {where: {'summary.totalValue': Decimal128.fromString('100.0005')}}; + OrderDecimalObj.find(cond, function(err, orders) { + if (err) return done(err); + orders.length.should.be.above(0); + const o = orders[0]; + o.summary.totalValue.should.equal('100.0005'); done(); - }) - .catch(done); + }); + }); + + it('destroyAll - deletes all with where', function(done) { + const anotherSample = { + summary: {totalValue: '100.0006'}, + }; + const cond = {'summary.totalValue': Decimal128.fromString('100.0005')}; + OrderDecimalObj.create(anotherSample) + .then(function() { + return OrderDecimalObj.destroyAll(cond); + }) + .then(function() { + return OrderDecimalObj.find(); + }) + .then(function(r) { + r.length.should.equal(1); + r[0].summary.totalValue.should.equal('100.0006'); + done(); + }) + .catch(done); + }); }); }); diff --git a/test/mongodb.test.js b/test/mongodb.test.js index 426f1f585..87663ef2f 100644 --- a/test/mongodb.test.js +++ b/test/mongodb.test.js @@ -778,12 +778,14 @@ describe('mongodb connector', function() { }); it('does not execute a nested `$where`', function(done) { + const filter = {where: {content: {$where: 'function() {return this.content.contains("content")}'}}}; + Post.create({title: 'Post1', content: 'Post1 content'}, (err, p1) => { Post.create({title: 'Post2', content: 'Post2 content'}, (err2, p2) => { Post.create({title: 'Post3', content: 'Post3 data'}, (err3, p3) => { - Post.find({where: {content: {$where: 'function() {return this.content.contains("content")}'}}}, (err, p) => { - should.not.exist(err); - p.length.should.be.equal(0); + Post.find(filter, {allowExtendedOperators: true}, (err, p) => { + should.exist(err); + err.message.should.match(/^\$where/); done(); }); });