Files
kami-parse-server/src/RestQuery.js
Francis Lessard 90a4ac70ac Fix session token issue
In _User collection a field _session_token is present and if you fetch
the user data form server, this field override the sessionToken saved
in your browser.

If you don't fetch the user, all request to server contain the right
sessionToken and if you fetch the user data from the server, all next
requests will contain the wrong sessionToken come form the
_session_token in user data fetched.
2016-02-11 20:32:31 -05:00

533 lines
15 KiB
JavaScript

// An object that encapsulates everything we need to run a 'find'
// operation, encoded in the REST API format.
var Parse = require('parse/node').Parse;
import { default as FilesController } from './Controllers/FilesController';
// restOptions can include:
// skip
// limit
// order
// count
// include
// keys
// redirectClassNameForKey
function RestQuery(config, auth, className, restWhere = {}, restOptions = {}) {
this.config = config;
this.auth = auth;
this.className = className;
this.restWhere = restWhere;
this.response = null;
this.findOptions = {};
if (!this.auth.isMaster) {
this.findOptions.acl = this.auth.user ? [this.auth.user.id] : null;
if (this.className == '_Session') {
if (!this.findOptions.acl) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'This session token is invalid.');
}
this.restWhere = {
'$and': [this.restWhere, {
'user': {
__type: 'Pointer',
className: '_User',
objectId: this.auth.user.id
}
}]
};
}
}
this.doCount = false;
// The format for this.include is not the same as the format for the
// include option - it's the paths we should include, in order,
// stored as arrays, taking into account that we need to include foo
// before including foo.bar. Also it should dedupe.
// For example, passing an arg of include=foo.bar,foo.baz could lead to
// this.include = [['foo'], ['foo', 'baz'], ['foo', 'bar']]
this.include = [];
for (var option in restOptions) {
switch(option) {
case 'keys':
this.keys = new Set(restOptions.keys.split(','));
this.keys.add('objectId');
this.keys.add('createdAt');
this.keys.add('updatedAt');
break;
case 'count':
this.doCount = true;
break;
case 'skip':
case 'limit':
this.findOptions[option] = restOptions[option];
break;
case 'order':
var fields = restOptions.order.split(',');
var sortMap = {};
for (var field of fields) {
if (field[0] == '-') {
sortMap[field.slice(1)] = -1;
} else {
sortMap[field] = 1;
}
}
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) => {
return s.split('.');
});
break;
case 'redirectClassNameForKey':
this.redirectKey = restOptions.redirectClassNameForKey;
this.redirectClassName = null;
break;
default:
throw new Parse.Error(Parse.Error.INVALID_JSON,
'bad option: ' + option);
}
}
}
// A convenient method to perform all the steps of processing a query
// in order.
// Returns a promise for the response - an object with optional keys
// 'results' and 'count'.
// TODO: consolidate the replaceX functions
RestQuery.prototype.execute = function() {
return Promise.resolve().then(() => {
return this.getUserAndRoleACL();
}).then(() => {
return this.redirectClassNameForKey();
}).then(() => {
return this.replaceSelect();
}).then(() => {
return this.replaceDontSelect();
}).then(() => {
return this.replaceInQuery();
}).then(() => {
return this.replaceNotInQuery();
}).then(() => {
return this.runFind();
}).then(() => {
return this.runCount();
}).then(() => {
return this.handleInclude();
}).then(() => {
return this.response;
});
};
// Uses the Auth object to get the list of roles, adds the user id
RestQuery.prototype.getUserAndRoleACL = function() {
if (this.auth.isMaster || !this.auth.user) {
return Promise.resolve();
}
return this.auth.getUserRoles().then((roles) => {
roles.push(this.auth.user.id);
this.findOptions.acl = roles;
return Promise.resolve();
});
};
// Changes the className if redirectClassNameForKey is set.
// Returns a promise.
RestQuery.prototype.redirectClassNameForKey = function() {
if (!this.redirectKey) {
return Promise.resolve();
}
// We need to change the class name based on the schema
return this.config.database.redirectClassNameForKey(
this.className, this.redirectKey).then((newClassName) => {
this.className = newClassName;
this.redirectClassName = newClassName;
});
};
// Replaces a $inQuery clause by running the subquery, if there is an
// $inQuery clause.
// The $inQuery clause turns into an $in with values that are just
// pointers to the objects returned in the subquery.
RestQuery.prototype.replaceInQuery = function() {
var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery');
if (!inQueryObject) {
return;
}
// The inQuery value must have precisely two keys - where and className
var inQueryValue = inQueryObject['$inQuery'];
if (!inQueryValue.where || !inQueryValue.className) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'improper usage of $inQuery');
}
var subquery = new RestQuery(
this.config, this.auth, inQueryValue.className,
inQueryValue.where);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push({
__type: 'Pointer',
className: inQueryValue.className,
objectId: result.objectId
});
}
delete inQueryObject['$inQuery'];
inQueryObject['$in'] = values;
// Recurse to repeat
return this.replaceInQuery();
});
};
// Replaces a $notInQuery clause by running the subquery, if there is an
// $notInQuery clause.
// The $notInQuery clause turns into a $nin with values that are just
// pointers to the objects returned in the subquery.
RestQuery.prototype.replaceNotInQuery = function() {
var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery');
if (!notInQueryObject) {
return;
}
// The notInQuery value must have precisely two keys - where and className
var notInQueryValue = notInQueryObject['$notInQuery'];
if (!notInQueryValue.where || !notInQueryValue.className) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'improper usage of $notInQuery');
}
var subquery = new RestQuery(
this.config, this.auth, notInQueryValue.className,
notInQueryValue.where);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push({
__type: 'Pointer',
className: notInQueryValue.className,
objectId: result.objectId
});
}
delete notInQueryObject['$notInQuery'];
notInQueryObject['$nin'] = values;
// Recurse to repeat
return this.replaceNotInQuery();
});
};
// Replaces a $select clause by running the subquery, if there is a
// $select clause.
// The $select clause turns into an $in with values selected out of
// the subquery.
// Returns a possible-promise.
RestQuery.prototype.replaceSelect = function() {
var selectObject = findObjectWithKey(this.restWhere, '$select');
if (!selectObject) {
return;
}
// The select value must have precisely two keys - query and key
var selectValue = selectObject['$select'];
if (!selectValue.query ||
!selectValue.key ||
typeof selectValue.query !== 'object' ||
!selectValue.query.className ||
!selectValue.query.where ||
Object.keys(selectValue).length !== 2) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'improper usage of $select');
}
var subquery = new RestQuery(
this.config, this.auth, selectValue.query.className,
selectValue.query.where);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push(result[selectValue.key]);
}
delete selectObject['$select'];
selectObject['$in'] = values;
// Keep replacing $select clauses
return this.replaceSelect();
})
};
// Replaces a $dontSelect clause by running the subquery, if there is a
// $dontSelect clause.
// The $dontSelect clause turns into an $nin with values selected out of
// the subquery.
// Returns a possible-promise.
RestQuery.prototype.replaceDontSelect = function() {
var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect');
if (!dontSelectObject) {
return;
}
// The dontSelect value must have precisely two keys - query and key
var dontSelectValue = dontSelectObject['$dontSelect'];
if (!dontSelectValue.query ||
!dontSelectValue.key ||
typeof dontSelectValue.query !== 'object' ||
!dontSelectValue.query.className ||
!dontSelectValue.query.where ||
Object.keys(dontSelectValue).length !== 2) {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'improper usage of $dontSelect');
}
var subquery = new RestQuery(
this.config, this.auth, dontSelectValue.query.className,
dontSelectValue.query.where);
return subquery.execute().then((response) => {
var values = [];
for (var result of response.results) {
values.push(result[dontSelectValue.key]);
}
delete dontSelectObject['$dontSelect'];
dontSelectObject['$nin'] = values;
// Keep replacing $dontSelect clauses
return this.replaceDontSelect();
})
};
// Returns a promise for whether it was successful.
// Populates this.response with an object that only has 'results'.
RestQuery.prototype.runFind = function() {
return this.config.database.find(
this.className, this.restWhere, this.findOptions).then((results) => {
if (this.className == '_User') {
for (var result of results) {
delete result.password;
}
}
this.config.filesController.expandFilesInObject(this.config, results);
if (this.keys) {
var keySet = this.keys;
results = results.map((object) => {
var newObject = {};
for (var key in object) {
if (keySet.has(key)) {
newObject[key] = object[key];
}
}
return newObject;
});
}
if (this.redirectClassName) {
for (var r of results) {
r.className = this.redirectClassName;
}
}
this.response = {results: results};
});
};
// Returns a promise for whether it was successful.
// Populates this.response.count with the count
RestQuery.prototype.runCount = function() {
if (!this.doCount) {
return;
}
this.findOptions.count = true;
delete this.findOptions.skip;
return this.config.database.find(
this.className, this.restWhere, this.findOptions).then((c) => {
this.response.count = c;
});
};
// Augments this.response with data at the paths provided in this.include.
RestQuery.prototype.handleInclude = function() {
if (this.include.length == 0) {
return;
}
var pathResponse = includePath(this.config, this.auth,
this.response, this.include[0]);
if (pathResponse.then) {
return pathResponse.then((newResponse) => {
this.response = newResponse;
this.include = this.include.slice(1);
return this.handleInclude();
});
}
return pathResponse;
};
// Adds included values to the response.
// Path is a list of field names.
// Returns a promise for an augmented response.
function includePath(config, auth, response, path) {
var pointers = findPointers(response.results, path);
if (pointers.length == 0) {
return response;
}
var className = null;
var objectIds = {};
for (var pointer of pointers) {
if (className === null) {
className = pointer.className;
} else {
if (className != pointer.className) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'inconsistent type data for include');
}
}
objectIds[pointer.objectId] = true;
}
if (!className) {
throw new Parse.Error(Parse.Error.INVALID_JSON,
'bad pointers');
}
// Get the objects for all these object ids
var where = {'objectId': {'$in': Object.keys(objectIds)}};
var query = new RestQuery(config, auth, className, where);
return query.execute().then((includeResponse) => {
var replace = {};
for (var obj of includeResponse.results) {
obj.__type = 'Object';
obj.className = className;
if(className == "_User"){
delete obj.sessionToken;
}
replace[obj.objectId] = obj;
}
var resp = {
results: replacePointers(response.results, path, replace)
};
if (response.count) {
resp.count = response.count;
}
return resp;
});
}
// Object may be a list of REST-format object to find pointers in, or
// it may be a single object.
// If the path yields things that aren't pointers, this throws an error.
// Path is a list of fields to search into.
// Returns a list of pointers in REST format.
function findPointers(object, path) {
if (object instanceof Array) {
var answer = [];
for (var x of object) {
answer = answer.concat(findPointers(x, path));
}
return answer;
}
if (typeof object !== 'object') {
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'can only include pointer fields');
}
if (path.length == 0) {
if (object.__type == 'Pointer') {
return [object];
}
throw new Parse.Error(Parse.Error.INVALID_QUERY,
'can only include pointer fields');
}
var subobject = object[path[0]];
if (!subobject) {
return [];
}
return findPointers(subobject, path.slice(1));
}
// Object may be a list of REST-format objects to replace pointers
// in, or it may be a single object.
// Path is a list of fields to search into.
// replace is a map from object id -> object.
// Returns something analogous to object, but with the appropriate
// pointers inflated.
function replacePointers(object, path, replace) {
if (object instanceof Array) {
return object.map((obj) => replacePointers(obj, path, replace));
}
if (typeof object !== 'object') {
return object;
}
if (path.length == 0) {
if (object.__type == 'Pointer' && replace[object.objectId]) {
return replace[object.objectId];
}
return object;
}
var subobject = object[path[0]];
if (!subobject) {
return object;
}
var newsub = replacePointers(subobject, path.slice(1), replace);
var answer = {};
for (var key in object) {
if (key == path[0]) {
answer[key] = newsub;
} else {
answer[key] = object[key];
}
}
return answer;
}
// Finds a subobject that has the given key, if there is one.
// Returns undefined otherwise.
function findObjectWithKey(root, key) {
if (typeof root !== 'object') {
return;
}
if (root instanceof Array) {
for (var item of root) {
var answer = findObjectWithKey(item, key);
if (answer) {
return answer;
}
}
}
if (root && root[key]) {
return root;
}
for (var subkey in root) {
var answer = findObjectWithKey(root[subkey], key);
if (answer) {
return answer;
}
}
}
module.exports = RestQuery;