diff --git a/README.md b/README.md index b1cc942e..2e43e651 100644 --- a/README.md +++ b/README.md @@ -369,6 +369,37 @@ See [LoopBack types](http://loopback.io/doc/en/lb3/LoopBack-types.html) for de For details, see the corresponding [driver issue](https://github.com/brianc/node-pg-types/issues/28). +## Querying JSON fields + +**Note** The fields you are querying should be setup to use the JSON postgresql data type - see Defining models + +Assuming a model such as this: + +```json +{ + "name": "Customer", + "properties": { + "address": { + "type": "object", + "postgresql": { + "dataType": "json" + } + } + } +} +``` + +You can query the nested fields with dot notation: + +```javascript +Customer.find({ + where: { + 'address.state': 'California' + }, + order: 'address.city' +}) +``` + ## Discovery and auto-migration ### Model discovery diff --git a/lib/postgresql.js b/lib/postgresql.js index cbca864b..8d9761d9 100644 --- a/lib/postgresql.js +++ b/lib/postgresql.js @@ -321,6 +321,37 @@ function escapeLiteral(str) { return escaped; } +/* + * Check if a value is attempting to use nested json keys + * @param {String} property The property being queried from where clause + * @returns {Boolean} True of the property contains dots for nested json + */ +function isNested(property) { + return property.split('.').length > 1; +} + +/* + * Overwrite the loopback-connector column escape + * to allow querying nested json keys + * @param {String} model The model name + * @param {String} property The property name + * @returns {String} The escaped column name, or column with nested keys for deep json columns + */ +PostgreSQL.prototype.columnEscaped = function(model, property) { + if (isNested(property)) { + // Convert column to PostgreSQL json style query: "model"->>'val' + var self = this; + return property + .split('.') + .map(function(val, idx) { return (idx === 0 ? self.columnEscaped(model, val) : escapeLiteral(val)); }) + .reduce(function(prev, next, idx, arr) { + return idx == 0 ? next : idx < arr.length - 1 ? prev + '->' + next : prev + '->>' + next; + }); + } else { + return this.escapeName(this.column(model, property)); + } +}; + /*! * Escape the name for PostgreSQL DB * @param {String} name The name @@ -499,6 +530,12 @@ PostgreSQL.prototype._buildWhere = function(model, where) { // The value is not an array, fall back to regular fields } var p = props[key]; + + if (p == null && isNested(key)) { + // See if we are querying nested json + p = props[key.split('.')[0]]; + } + if (p == null) { // Unknown property, ignore it debug('Unknown property %s is skipped for model %s', key, model); diff --git a/test/postgresql.test.js b/test/postgresql.test.js index 88169cde..c27021cb 100644 --- a/test/postgresql.test.js +++ b/test/postgresql.test.js @@ -658,6 +658,86 @@ describe('postgresql connector', function() { }); }); }); + + context('json data type', function() { + var Customer; + + before(function(done) { + db = getDataSource(); + + Customer = db.define('Customer', { + address: { + type: 'object', + postgresql: { + dataType: 'json', + }, + }, + }); + + db.automigrate(function(err) { + if (err) return done(err); + Customer.create([{ + address: { + city: 'Springfield', + street: { + number: 42, + }, + }, + }, { + address: { + city: 'Hill Valley', + street: { + number: 56, + }, + }, + }], function(err, customers) { + return done(err); + }); + }); + }); + + it('allows querying for nested json properties', function(done) { + Customer.find({ + where: { + 'address.city': 'Hill Valley', + }, + }, function(err, results) { + if (err) return done(err); + results.length.should.eql(1); + results[0].address.city.should.eql('Hill Valley'); + done(); + }); + }); + + it('queries multiple levels of nesting', function(done) { + Customer.find({ + where: { + 'address.street.number': 56, + }, + }, function(err, results) { + if (err) return done(err); + results.length.should.eql(1); + results[0].address.city.should.eql('Hill Valley'); + done(); + }); + }); + + it('allows ordering by nested json properties', function(done) { + Customer.find({ + order: ['address.city DESC'], + }, function(err, results1) { + if (err) return done(err); + results1[0].address.city.should.eql('Springfield'); + Customer.find({ + order: ['address.city ASC'], + }, function(err, results2) { + if (err) return done(err); + results2[0].address.city.should.eql('Hill Valley'); + done(); + }); + }); + }); + }); }); describe('Serial properties', function() {