Skip to content

Commit 9c522be

Browse files
authored
Support for nested .select() calls (#2737)
* Reproduction for #1567 * Recursive handling of nested pointer keys in select * Better support for multi-level nested keys * Adds support for selecting columns natively (mongo) * Support for postgres column selections * Filter-out empty keys for pg
1 parent 4974dbe commit 9c522be

File tree

7 files changed

+133
-29
lines changed

7 files changed

+133
-29
lines changed

spec/ParseQuery.spec.js

+70
Original file line numberDiff line numberDiff line change
@@ -2567,6 +2567,76 @@ describe('Parse.Query testing', () => {
25672567
})
25682568
});
25692569

2570+
it('select nested keys (issue #1567)', function(done) {
2571+
var Foobar = new Parse.Object('Foobar');
2572+
var BarBaz = new Parse.Object('Barbaz');
2573+
BarBaz.set('key', 'value');
2574+
BarBaz.set('otherKey', 'value');
2575+
BarBaz.save().then(() => {
2576+
Foobar.set('foo', 'bar');
2577+
Foobar.set('fizz', 'buzz');
2578+
Foobar.set('barBaz', BarBaz);
2579+
return Foobar.save();
2580+
}).then(function(savedFoobar){
2581+
var foobarQuery = new Parse.Query('Foobar');
2582+
foobarQuery.include('barBaz');
2583+
foobarQuery.select(['fizz', 'barBaz.key']);
2584+
foobarQuery.get(savedFoobar.id,{
2585+
success: function(foobarObj){
2586+
equal(foobarObj.get('fizz'), 'buzz');
2587+
equal(foobarObj.get('foo'), undefined);
2588+
if (foobarObj.has('barBaz')) {
2589+
equal(foobarObj.get('barBaz').get('key'), 'value');
2590+
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
2591+
} else {
2592+
fail('barBaz should be set');
2593+
}
2594+
done();
2595+
}
2596+
});
2597+
});
2598+
});
2599+
2600+
it('select nested keys 2 level (issue #1567)', function(done) {
2601+
var Foobar = new Parse.Object('Foobar');
2602+
var BarBaz = new Parse.Object('Barbaz');
2603+
var Bazoo = new Parse.Object('Bazoo');
2604+
2605+
Bazoo.set('some', 'thing');
2606+
Bazoo.set('otherSome', 'value');
2607+
Bazoo.save().then(() => {
2608+
BarBaz.set('key', 'value');
2609+
BarBaz.set('otherKey', 'value');
2610+
BarBaz.set('bazoo', Bazoo);
2611+
return BarBaz.save();
2612+
}).then(() => {
2613+
Foobar.set('foo', 'bar');
2614+
Foobar.set('fizz', 'buzz');
2615+
Foobar.set('barBaz', BarBaz);
2616+
return Foobar.save();
2617+
}).then(function(savedFoobar){
2618+
var foobarQuery = new Parse.Query('Foobar');
2619+
foobarQuery.include('barBaz');
2620+
foobarQuery.include('barBaz.bazoo');
2621+
foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']);
2622+
foobarQuery.get(savedFoobar.id,{
2623+
success: function(foobarObj){
2624+
equal(foobarObj.get('fizz'), 'buzz');
2625+
equal(foobarObj.get('foo'), undefined);
2626+
if (foobarObj.has('barBaz')) {
2627+
equal(foobarObj.get('barBaz').get('key'), 'value');
2628+
equal(foobarObj.get('barBaz').get('otherKey'), undefined);
2629+
equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing');
2630+
equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined);
2631+
} else {
2632+
fail('barBaz should be set');
2633+
}
2634+
done();
2635+
}
2636+
});
2637+
});
2638+
});
2639+
25702640
it('properly handles nested ors', function(done) {
25712641
var objects = [];
25722642
while(objects.length != 4) {

src/Adapters/Storage/Mongo/MongoCollection.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export default class MongoCollection {
1313
// none, then build the geoindex.
1414
// This could be improved a lot but it's not clear if that's a good
1515
// idea. Or even if this behavior is a good idea.
16-
find(query, { skip, limit, sort } = {}) {
17-
return this._rawFind(query, { skip, limit, sort })
16+
find(query, { skip, limit, sort, keys } = {}) {
17+
return this._rawFind(query, { skip, limit, sort, keys })
1818
.catch(error => {
1919
// Check for "no geoindex" error
2020
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
@@ -30,14 +30,18 @@ export default class MongoCollection {
3030
index[key] = '2d';
3131
return this._mongoCollection.createIndex(index)
3232
// Retry, but just once.
33-
.then(() => this._rawFind(query, { skip, limit, sort }));
33+
.then(() => this._rawFind(query, { skip, limit, sort, keys }));
3434
});
3535
}
3636

37-
_rawFind(query, { skip, limit, sort } = {}) {
38-
return this._mongoCollection
37+
_rawFind(query, { skip, limit, sort, keys } = {}) {
38+
let findOperation = this._mongoCollection
3939
.find(query, { skip, limit, sort })
40-
.toArray();
40+
41+
if (keys) {
42+
findOperation = findOperation.project(keys);
43+
}
44+
return findOperation.toArray();
4145
}
4246

4347
count(query, { skip, limit, sort } = {}) {

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -320,12 +320,16 @@ export class MongoStorageAdapter {
320320
}
321321

322322
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
323-
find(className, schema, query, { skip, limit, sort }) {
323+
find(className, schema, query, { skip, limit, sort, keys }) {
324324
schema = convertParseSchemaToMongoSchema(schema);
325325
let mongoWhere = transformWhere(className, query, schema);
326326
let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
327+
let mongoKeys = _.reduce(keys, (memo, key) => {
328+
memo[transformKey(className, key, schema)] = 1;
329+
return memo;
330+
}, {});
327331
return this._adaptiveCollection(className)
328-
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort }))
332+
.then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort, keys: mongoKeys }))
329333
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
330334
}
331335

src/Adapters/Storage/Mongo/MongoTransform.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ const transformKey = (className, fieldName, schema) => {
1111
case 'updatedAt': return '_updated_at';
1212
case 'sessionToken': return '_session_token';
1313
}
14-
14+
1515
if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') {
1616
fieldName = '_p_' + fieldName;
17+
} else if (schema.fields[fieldName] && schema.fields[fieldName].type == 'Pointer') {
18+
fieldName = '_p_' + fieldName;
1719
}
1820

1921
return fieldName;

src/Adapters/Storage/Postgres/PostgresStorageAdapter.js

+15-3
Original file line numberDiff line numberDiff line change
@@ -921,8 +921,8 @@ export class PostgresStorageAdapter {
921921
});
922922
}
923923

924-
find(className, schema, query, { skip, limit, sort }) {
925-
debug('find', className, query, {skip, limit, sort});
924+
find(className, schema, query, { skip, limit, sort, keys }) {
925+
debug('find', className, query, {skip, limit, sort, keys });
926926
const hasLimit = limit !== undefined;
927927
const hasSkip = skip !== undefined;
928928
let values = [className];
@@ -954,7 +954,19 @@ export class PostgresStorageAdapter {
954954
sortPattern = `ORDER BY ${where.sorts.join(',')}`;
955955
}
956956

957-
const qs = `SELECT * FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
957+
let columns = '*';
958+
if (keys) {
959+
// Exclude empty keys
960+
keys = keys.filter((key) => {
961+
return key.length > 0;
962+
});
963+
columns = keys.map((key, index) => {
964+
return `$${index+values.length+1}:name`;
965+
}).join(',');
966+
values = values.concat(keys);
967+
}
968+
969+
const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
958970
debug(qs, values);
959971
return this._client.any(qs, values)
960972
.catch((err) => {

src/Controllers/DatabaseController.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ DatabaseController.prototype.find = function(className, query, {
711711
acl,
712712
sort = {},
713713
count,
714+
keys
714715
} = {}) {
715716
let isMaster = acl === undefined;
716717
let aclGroup = acl || [];
@@ -779,7 +780,7 @@ DatabaseController.prototype.find = function(className, query, {
779780
if (!classExists) {
780781
return [];
781782
} else {
782-
return this.adapter.find(className, schema, query, { skip, limit, sort })
783+
return this.adapter.find(className, schema, query, { skip, limit, sort, keys })
783784
.then(objects => objects.map(object => {
784785
object = untransformObjectACL(object);
785786
return filterSensitiveData(isMaster, aclGroup, className, object)

src/RestQuery.js

+27-16
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
2020
this.auth = auth;
2121
this.className = className;
2222
this.restWhere = restWhere;
23+
this.restOptions = restOptions;
2324
this.clientSDK = clientSDK;
2425
this.response = null;
2526
this.findOptions = {};
@@ -56,6 +57,7 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
5657
switch(option) {
5758
case 'keys':
5859
this.keys = new Set(restOptions.keys.split(','));
60+
// Add the default
5961
this.keys.add('objectId');
6062
this.keys.add('createdAt');
6163
this.keys.add('updatedAt');
@@ -390,6 +392,11 @@ RestQuery.prototype.runFind = function() {
390392
this.response = {results: []};
391393
return Promise.resolve();
392394
}
395+
if (this.keys) {
396+
this.findOptions.keys = Array.from(this.keys).map((key) => {
397+
return key.split('.')[0];
398+
});
399+
}
393400
return this.config.database.find(
394401
this.className, this.restWhere, this.findOptions).then((results) => {
395402
if (this.className === '_User') {
@@ -411,19 +418,6 @@ RestQuery.prototype.runFind = function() {
411418

412419
this.config.filesController.expandFilesInObject(this.config, results);
413420

414-
if (this.keys) {
415-
var keySet = this.keys;
416-
results = results.map((object) => {
417-
var newObject = {};
418-
for (var key in object) {
419-
if (keySet.has(key)) {
420-
newObject[key] = object[key];
421-
}
422-
}
423-
return newObject;
424-
});
425-
}
426-
427421
if (this.redirectClassName) {
428422
for (var r of results) {
429423
r.className = this.redirectClassName;
@@ -455,7 +449,7 @@ RestQuery.prototype.handleInclude = function() {
455449
}
456450

457451
var pathResponse = includePath(this.config, this.auth,
458-
this.response, this.include[0]);
452+
this.response, this.include[0], this.restOptions);
459453
if (pathResponse.then) {
460454
return pathResponse.then((newResponse) => {
461455
this.response = newResponse;
@@ -473,7 +467,7 @@ RestQuery.prototype.handleInclude = function() {
473467
// Adds included values to the response.
474468
// Path is a list of field names.
475469
// Returns a promise for an augmented response.
476-
function includePath(config, auth, response, path) {
470+
function includePath(config, auth, response, path, restOptions = {}) {
477471
var pointers = findPointers(response.results, path);
478472
if (pointers.length == 0) {
479473
return response;
@@ -492,9 +486,26 @@ function includePath(config, auth, response, path) {
492486
}
493487
}
494488

489+
let includeRestOptions = {};
490+
if (restOptions.keys) {
491+
let keys = new Set(restOptions.keys.split(','));
492+
let keySet = Array.from(keys).reduce((set, key) => {
493+
let keyPath = key.split('.');
494+
let i=0;
495+
for (i; i<path.length; i++) {
496+
if (path[i] != keyPath[i]) {
497+
return set;
498+
}
499+
}
500+
set.add(keyPath[i]);
501+
return set;
502+
}, new Set());
503+
includeRestOptions.keys = Array.from(keySet).join(',');
504+
}
505+
495506
let queryPromises = Object.keys(pointersHash).map((className) => {
496507
var where = {'objectId': {'$in': pointersHash[className]}};
497-
var query = new RestQuery(config, auth, className, where);
508+
var query = new RestQuery(config, auth, className, where, includeRestOptions);
498509
return query.execute().then((results) => {
499510
results.className = className;
500511
return Promise.resolve(results);

0 commit comments

Comments
 (0)