Initial release, parse-server, 2.0.0

This commit is contained in:
Fosco Marotto
2016-01-28 10:58:12 -08:00
commit 7f5d744ce2
53 changed files with 14974 additions and 0 deletions

555
RestQuery.js Normal file
View File

@@ -0,0 +1,555 @@
// An object that encapsulates everything we need to run a 'find'
// operation, encoded in the REST API format.
var Parse = require('parse/node').Parse;
// restOptions can include:
// skip
// limit
// order
// count
// include
// keys
// redirectClassNameForKey
function RestQuery(config, auth, className, restWhere, restOptions) {
restOptions = 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;
}
}
updateParseFiles(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;
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 (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;
}
// Find file references in REST-format object and adds the url key
// with the current mount point and app id
// Object may be a single object or list of REST-format objects
function updateParseFiles(config, object) {
if (object instanceof Array) {
object.map((obj) => updateParseFiles(config, obj));
return;
}
if (typeof object !== 'object') {
return;
}
for (var key in object) {
if (object[key] && object[key]['__type'] &&
object[key]['__type'] == 'File') {
var filename = object[key]['name'];
var encoded = encodeURIComponent(filename);
encoded = encoded.replace('%40', '@');
if (filename.indexOf('tfss-') === 0) {
object[key]['url'] = 'http://files.parsetfss.com/' +
config.fileKey + '/' + encoded;
} else {
object[key]['url'] = config.mount + '/files/' +
config.applicationId + '/' +
encoded;
}
}
}
}
// 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;