diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index c2f7a1fc2b..030deed921 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2655,6 +2655,46 @@ describe('Parse.Query testing', () => { }); }); + it('select nested keys 2 level without include (issue #3185)', function(done) { + var Foobar = new Parse.Object('Foobar'); + var BarBaz = new Parse.Object('Barbaz'); + var Bazoo = new Parse.Object('Bazoo'); + + Bazoo.set('some', 'thing'); + Bazoo.set('otherSome', 'value'); + Bazoo.save().then(() => { + BarBaz.set('key', 'value'); + BarBaz.set('otherKey', 'value'); + BarBaz.set('bazoo', Bazoo); + return BarBaz.save(); + }).then(() => { + Foobar.set('foo', 'bar'); + Foobar.set('fizz', 'buzz'); + Foobar.set('barBaz', BarBaz); + return Foobar.save(); + }).then(function(savedFoobar){ + var foobarQuery = new Parse.Query('Foobar'); + foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']); + return foobarQuery.get(savedFoobar.id); + }).then((foobarObj) => { + equal(foobarObj.get('fizz'), 'buzz'); + equal(foobarObj.get('foo'), undefined); + if (foobarObj.has('barBaz')) { + equal(foobarObj.get('barBaz').get('key'), 'value'); + equal(foobarObj.get('barBaz').get('otherKey'), undefined); + if (foobarObj.get('barBaz').has('bazoo')) { + equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing'); + equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined); + } else { + fail('bazoo should be set'); + } + } else { + fail('barBaz should be set'); + } + done(); + }) + }); + it('properly handles nested ors', function(done) { var objects = []; while(objects.length != 4) { diff --git a/src/RestQuery.js b/src/RestQuery.js index 2cdfc2aaed..07a309fbf5 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -5,6 +5,7 @@ var SchemaController = require('./Controllers/SchemaController'); var Parse = require('parse/node').Parse; const triggers = require('./triggers'); +const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt']; // restOptions can include: // skip // limit @@ -52,15 +53,36 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl // this.include = [['foo'], ['foo', 'baz'], ['foo', 'bar']] this.include = []; + // If we have keys, we probably want to force some includes (n-1 level) + // See issue: https://github.com/ParsePlatform/parse-server/issues/3185 + if (restOptions.hasOwnProperty('keys')) { + const keysForInclude = restOptions.keys.split(',').filter((key) => { + // At least 2 components + return key.split(".").length > 1; + }).map((key) => { + // Slice the last component (a.b.c -> a.b) + // Otherwise we'll include one level too much. + return key.slice(0, key.lastIndexOf(".")); + }).join(','); + + // Concat the possibly present include string with the one from the keys + // Dedup / sorting is handle in 'include' case. + if (keysForInclude.length > 0) { + if (!restOptions.include || restOptions.include.length == 0) { + restOptions.include = keysForInclude; + } else { + restOptions.include += "," + keysForInclude; + } + } + } + for (var option in restOptions) { switch(option) { - case 'keys': - this.keys = new Set(restOptions.keys.split(',')); - // Add the default - this.keys.add('objectId'); - this.keys.add('createdAt'); - this.keys.add('updatedAt'); + case 'keys': { + const keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys); + this.keys = Array.from(new Set(keys)); break; + } case 'count': this.doCount = true; break; @@ -80,22 +102,26 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl } this.findOptions.sort = sortMap; break; - case 'include': - var paths = restOptions.include.split(','); - var pathSet = {}; - for (var path of paths) { - // Add all prefixes with a .-split to pathSet - var parts = path.split('.'); - for (var len = 1; len <= parts.length; len++) { - pathSet[parts.slice(0, len).join('.')] = true; - } - } - this.include = Object.keys(pathSet).sort((a, b) => { - return a.length - b.length; - }).map((s) => { + case 'include': { + const paths = restOptions.include.split(','); + // Load the existing includes (from keys) + const pathSet = paths.reduce((memo, path) => { + // Split each paths on . (a.b.c -> [a,b,c]) + // reduce to create all paths + // ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true}) + return path.split('.').reduce((memo, path, index, parts) => { + memo[parts.slice(0, index+1).join('.')] = true; + return memo; + }, memo); + }, {}); + + this.include = Object.keys(pathSet).map((s) => { return s.split('.'); + }).sort((a, b) => { + return a.length - b.length; // Sort by number of components }); break; + } case 'redirectClassNameForKey': this.redirectKey = restOptions.redirectClassNameForKey; this.redirectClassName = null; @@ -421,7 +447,7 @@ RestQuery.prototype.runFind = function(options = {}) { } let findOptions = Object.assign({}, this.findOptions); if (this.keys) { - findOptions.keys = Array.from(this.keys).map((key) => { + findOptions.keys = this.keys.map((key) => { return key.split('.')[0]; }); }