Skip to content

Commit fb36dfa

Browse files
authored
Fix #3185 (#3186)
* Adds tests that reproduce the issue * Use values from keys to force include when needed
1 parent 998b271 commit fb36dfa

File tree

2 files changed

+86
-20
lines changed

2 files changed

+86
-20
lines changed

spec/ParseQuery.spec.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,46 @@ describe('Parse.Query testing', () => {
26552655
});
26562656
});
26572657

2658+
it('select nested keys 2 level without include (issue #3185)', function(done) {
2659+
var Foobar = new Parse.Object('Foobar');
2660+
var BarBaz = new Parse.Object('Barbaz');
2661+
var Bazoo = new Parse.Object('Bazoo');
2662+
2663+
Bazoo.set('some', 'thing');
2664+
Bazoo.set('otherSome', 'value');
2665+
Bazoo.save().then(() => {
2666+
BarBaz.set('key', 'value');
2667+
BarBaz.set('otherKey', 'value');
2668+
BarBaz.set('bazoo', Bazoo);
2669+
return BarBaz.save();
2670+
}).then(() => {
2671+
Foobar.set('foo', 'bar');
2672+
Foobar.set('fizz', 'buzz');
2673+
Foobar.set('barBaz', BarBaz);
2674+
return Foobar.save();
2675+
}).then(function(savedFoobar){
2676+
var foobarQuery = new Parse.Query('Foobar');
2677+
foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']);
2678+
return foobarQuery.get(savedFoobar.id);
2679+
}).then((foobarObj) => {
2680+
equal(foobarObj.get('fizz'), 'buzz');
2681+
equal(foobarObj.get('foo'), undefined);
2682+
if (foobarObj.has('barBaz')) {
2683+
equal(foobarObj.get('barBaz').get('key'), 'value');
2684+
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
2685+
if (foobarObj.get('barBaz').has('bazoo')) {
2686+
equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing');
2687+
equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined);
2688+
} else {
2689+
fail('bazoo should be set');
2690+
}
2691+
} else {
2692+
fail('barBaz should be set');
2693+
}
2694+
done();
2695+
})
2696+
});
2697+
26582698
it('properly handles nested ors', function(done) {
26592699
var objects = [];
26602700
while(objects.length != 4) {

src/RestQuery.js

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var SchemaController = require('./Controllers/SchemaController');
55
var Parse = require('parse/node').Parse;
66
const triggers = require('./triggers');
77

8+
const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt'];
89
// restOptions can include:
910
// skip
1011
// limit
@@ -52,15 +53,36 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
5253
// this.include = [['foo'], ['foo', 'baz'], ['foo', 'bar']]
5354
this.include = [];
5455

56+
// If we have keys, we probably want to force some includes (n-1 level)
57+
// See issue: https://github.com/ParsePlatform/parse-server/issues/3185
58+
if (restOptions.hasOwnProperty('keys')) {
59+
const keysForInclude = restOptions.keys.split(',').filter((key) => {
60+
// At least 2 components
61+
return key.split(".").length > 1;
62+
}).map((key) => {
63+
// Slice the last component (a.b.c -> a.b)
64+
// Otherwise we'll include one level too much.
65+
return key.slice(0, key.lastIndexOf("."));
66+
}).join(',');
67+
68+
// Concat the possibly present include string with the one from the keys
69+
// Dedup / sorting is handle in 'include' case.
70+
if (keysForInclude.length > 0) {
71+
if (!restOptions.include || restOptions.include.length == 0) {
72+
restOptions.include = keysForInclude;
73+
} else {
74+
restOptions.include += "," + keysForInclude;
75+
}
76+
}
77+
}
78+
5579
for (var option in restOptions) {
5680
switch(option) {
57-
case 'keys':
58-
this.keys = new Set(restOptions.keys.split(','));
59-
// Add the default
60-
this.keys.add('objectId');
61-
this.keys.add('createdAt');
62-
this.keys.add('updatedAt');
81+
case 'keys': {
82+
const keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys);
83+
this.keys = Array.from(new Set(keys));
6384
break;
85+
}
6486
case 'count':
6587
this.doCount = true;
6688
break;
@@ -80,22 +102,26 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
80102
}
81103
this.findOptions.sort = sortMap;
82104
break;
83-
case 'include':
84-
var paths = restOptions.include.split(',');
85-
var pathSet = {};
86-
for (var path of paths) {
87-
// Add all prefixes with a .-split to pathSet
88-
var parts = path.split('.');
89-
for (var len = 1; len <= parts.length; len++) {
90-
pathSet[parts.slice(0, len).join('.')] = true;
91-
}
92-
}
93-
this.include = Object.keys(pathSet).sort((a, b) => {
94-
return a.length - b.length;
95-
}).map((s) => {
105+
case 'include': {
106+
const paths = restOptions.include.split(',');
107+
// Load the existing includes (from keys)
108+
const pathSet = paths.reduce((memo, path) => {
109+
// Split each paths on . (a.b.c -> [a,b,c])
110+
// reduce to create all paths
111+
// ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true})
112+
return path.split('.').reduce((memo, path, index, parts) => {
113+
memo[parts.slice(0, index+1).join('.')] = true;
114+
return memo;
115+
}, memo);
116+
}, {});
117+
118+
this.include = Object.keys(pathSet).map((s) => {
96119
return s.split('.');
120+
}).sort((a, b) => {
121+
return a.length - b.length; // Sort by number of components
97122
});
98123
break;
124+
}
99125
case 'redirectClassNameForKey':
100126
this.redirectKey = restOptions.redirectClassNameForKey;
101127
this.redirectClassName = null;
@@ -421,7 +447,7 @@ RestQuery.prototype.runFind = function(options = {}) {
421447
}
422448
let findOptions = Object.assign({}, this.findOptions);
423449
if (this.keys) {
424-
findOptions.keys = Array.from(this.keys).map((key) => {
450+
findOptions.keys = this.keys.map((key) => {
425451
return key.split('.')[0];
426452
});
427453
}

0 commit comments

Comments
 (0)