diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 586ea958..54b440fb 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -351,28 +351,46 @@ Memory.prototype.fromDb = function(model, data) { data = deserialize(data); var props = this._models[model].properties; for (var key in data) { - var val = data[key]; - if (val === undefined || val === null) { - continue; - } - if (props[key]) { - switch (props[key].type.name) { - case 'Date': - val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); - break; - case 'Boolean': - val = Boolean(val); - break; - case 'Number': - val = Number(val); - break; - } - } - data[key] = val; + data[key] = this._castPropertyValue(key, data[key], props); } return data; }; +Memory.prototype._castPropertyValue = function(prop, val, props) { + var self = this; + if (val === undefined || val === null || !props[prop]) { + return val; + } + + if (Array.isArray(val)) { + return val.map(function(val) { + return self._castPropertyValue(prop, val, props); + }); + } + + var isArray = Array.isArray(props[prop].type); + var propType = isArray ? props[prop].type[0] : props[prop].type; + + switch (propType.name) { + case 'Date': + val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); + break; + case 'Boolean': + val = Boolean(val); + break; + case 'Number': + val = Number(val); + break; + case 'ModelConstructor': + for (var subProp in val) { + val[subProp] = this._castPropertyValue(subProp, val[subProp], propType.definition.properties); + } + break; + } + + return val; +}; + function getValue(obj, path) { if (obj == null) { return undefined; diff --git a/lib/dao.js b/lib/dao.js index 719dadfe..02297b53 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -1565,12 +1565,14 @@ function coerceArray(val) { * @returns {Object} The coerced where clause * @private */ -DataAccessObject._coerce = function(where) { +DataAccessObject._coerce = function(where, props) { var self = this; if (!where) { return where; } + props = props || self.definition.properties; + var err; if (typeof where !== 'object' || Array.isArray(where)) { err = new Error(g.f('The where clause %j is not an {{object}}', where)); @@ -1578,7 +1580,6 @@ DataAccessObject._coerce = function(where) { throw err; } - var props = self.definition.properties; for (var p in where) { // Handle logical operators if (p === 'and' || p === 'or' || p === 'nor') { @@ -1597,6 +1598,22 @@ DataAccessObject._coerce = function(where) { continue; } + + if (p.match(/\./)) { + var model = p.split('.')[0]; + var prop = p.split('.').slice(1); + + if (props[model]) { + var clause = {}; + clause[prop] = where[p]; + where[p] = Array.isArray(props[model].type) ? + self._coerce(clause, props[model].type[0].definition.properties)[prop] : + self._coerce(clause, props[model].type.definition.properties)[prop]; + + continue; + } + } + var DataType = props[p] && props[p].type; if (!DataType) { continue; diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index ccabc951..1ab8a439 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -1374,6 +1374,11 @@ describe('DataAccessObject', function() { age: Number, vip: Boolean, date: Date, + sub: { + date: Date, + bool: Boolean, + number: Number, + }, location: 'GeoPoint', scores: [Number], }); @@ -1433,6 +1438,27 @@ describe('DataAccessObject', function() { assert.deepEqual(where, {date: d}); }); + it('coerces where clause for date types in nested properties', function() { + var d = new Date(); + where = model._coerce({'sub.date': d}); + assert.deepEqual(where, {'sub.date': d}); + + where = model._coerce({'sub.date': d.toISOString()}); + assert.deepEqual(where, {'sub.date': d}); + }); + + it('coerces where clause for Boolean types in nested properties', function() { + var bool = 'true'; + where = model._coerce({'sub.bool': bool}); + assert.strictEqual(where['sub.bool'], true); + }); + + it('coerces where clause for Number type in nested properties', function() { + var number = '123'; + where = model._coerce({'sub.number': number}); + assert.strictEqual(where['sub.number'], 123); + }); + it('coerces where clause for boolean types', function() { where = model._coerce({vip: 'true'}); assert.deepEqual(where, {vip: true}); @@ -1518,8 +1544,7 @@ describe('DataAccessObject', function() { INVALID_CLAUSES.forEach(function(where) { var whereStr = JSON.stringify(where); - it('throws an error on malformed array-like object ' + whereStr, - function() { + it('throws an error on malformed array-like object ' + whereStr, function() { assert.throws( function() { model._coerce(where); }, /property has invalid clause/); @@ -1694,31 +1719,28 @@ describe('DataAccessObject', function() { assert.deepEqual(where, {date: undefined}); }); - it('does not coerce to a number for a simple value that produces NaN', - function() { - where = model._coerce({age: 'xyz'}); - assert.deepEqual(where, {age: 'xyz'}); - }); + it('does not coerce to a number for a simple value that produces NaN', function() { + where = model._coerce({age: 'xyz'}); + assert.deepEqual(where, {age: 'xyz'}); + }); - it('does not coerce to a number for a simple value in an array that produces NaN', - function() { - where = model._coerce({age: {inq: ['xyz', '12']}}); - assert.deepEqual(where, {age: {inq: ['xyz', 12]}}); - }); + it('does not coerce to a number for a simple value in an array that produces NaN', function() { + where = model._coerce({age: {inq: ['xyz', '12']}}); + assert.deepEqual(where, {age: {inq: ['xyz', 12]}}); + }); // settings - it('gets settings in priority', - function() { - ds.settings.test = 'test'; - assert.equal(model._getSetting('test'), ds.settings.test, 'Should get datasource setting'); - ds.settings.test = undefined; + it('gets settings in priority', function() { + ds.settings.test = 'test'; + assert.equal(model._getSetting('test'), ds.settings.test, 'Should get datasource setting'); + ds.settings.test = undefined; - model.settings.test = 'test'; - assert.equal(model._getSetting('test'), model.settings.test, 'Should get model settings'); + model.settings.test = 'test'; + assert.equal(model._getSetting('test'), model.settings.test, 'Should get model settings'); - ds.settings.test = 'willNotGet'; - assert.notEqual(model._getSetting('test'), ds.settings.test, 'Should not get datasource setting'); - }); + ds.settings.test = 'willNotGet'; + assert.notEqual(model._getSetting('test'), ds.settings.test, 'Should not get datasource setting'); + }); }); describe('ModelBuilder processing json files', function() { diff --git a/test/memory.test.js b/test/memory.test.js index 8650e535..68c75ff3 100644 --- a/test/memory.test.js +++ b/test/memory.test.js @@ -157,6 +157,7 @@ describe('Memory connector', function() { city: String, state: String, zipCode: String, + since: Date, tags: [ { tag: String, @@ -166,6 +167,7 @@ describe('Memory connector', function() { friends: [ { name: String, + since: Date, }, ], }); @@ -294,8 +296,7 @@ describe('Memory connector', function() { }); }); - it('should successfully extract 2 users using implied and & and', - function(done) { + it('should successfully extract 2 users using implied and & and', function(done) { User.find({ where: { name: 'John Lennon', @@ -435,15 +436,14 @@ describe('Memory connector', function() { }); }); - it('should work when a regex is provided without the regexp operator', - function(done) { - User.find({where: {name: /John.*/i}}, function(err, users) { - should.not.exist(err); - users.length.should.equal(1); - users[0].name.should.equal('John Lennon'); - done(); - }); - }); + it('should work when a regex is provided without the regexp operator', function(done) { + User.find({where: {name: /John.*/i}}, function(err, users) { + should.not.exist(err); + users.length.should.equal(1); + users[0].name.should.equal('John Lennon'); + done(); + }); + }); it('should support the regexp operator with regex strings', function(done) { User.find({where: {name: {regexp: '^J'}}}, function(err, users) { @@ -516,6 +516,28 @@ describe('Memory connector', function() { }); }); + it('should support date as nested property in query', function(done) { + var d = new Date('2017-01-01'); + User.find({where: {'address.since': d}}, + function(err, users) { + should.not.exist(err); + users.length.should.be.equal(1); + users[0].address.since.should.be.eql(d); + done(); + }); + }); + + it('should support date as array property in query', function(done) { + var d = new Date('1960-01-01'); + User.find({where: {'friends.since': d}}, + function(err, users) { + should.not.exist(err); + users.length.should.be.equal(2); + users[0].friends[0].since.should.be.eql(d); + done(); + }); + }); + it('should deserialize values after saving in upsert', function(done) { User.findOne({where: {seq: 1}}, function(err, paul) { User.updateOrCreate({id: paul.id, name: 'Sir Paul McCartney'}, @@ -553,15 +575,16 @@ describe('Memory connector', function() { city: 'San Jose', state: 'CA', zipCode: '95131', + since: new Date('2017-01-01'), tags: [ {tag: 'business'}, {tag: 'rent'}, ], }, friends: [ - {name: 'Paul McCartney'}, - {name: 'George Harrison'}, - {name: 'Ringo Starr'}, + {name: 'Paul McCartney', since: new Date('1960-01-01')}, + {name: 'George Harrison', since: new Date('1960-01-01')}, + {name: 'Ringo Starr', since: new Date('1960-01-01')}, ], children: ['Sean', 'Julian'], }, @@ -578,11 +601,12 @@ describe('Memory connector', function() { city: 'San Mateo', state: 'CA', zipCode: '94065', + since: new Date('2017-02-01'), }, friends: [ - {name: 'John Lennon'}, - {name: 'George Harrison'}, - {name: 'Ringo Starr'}, + {name: 'John Lennon', since: new Date('1960-01-01')}, + {name: 'George Harrison', since: new Date('1960-01-01')}, + {name: 'Ringo Starr', since: new Date('1960-01-01')}, ], children: ['Stella', 'Mary', 'Heather', 'Beatrice', 'James'], },