Initial release, parse-server, 2.0.0
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Emacs
|
||||||
|
*~
|
||||||
170
Auth.js
Normal file
170
Auth.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
var deepcopy = require('deepcopy');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
var RestQuery = require('./RestQuery');
|
||||||
|
|
||||||
|
var cache = require('./cache');
|
||||||
|
|
||||||
|
// An Auth object tells you who is requesting something and whether
|
||||||
|
// the master key was used.
|
||||||
|
// userObject is a Parse.User and can be null if there's no user.
|
||||||
|
function Auth(config, isMaster, userObject) {
|
||||||
|
this.config = config;
|
||||||
|
this.isMaster = isMaster;
|
||||||
|
this.user = userObject;
|
||||||
|
|
||||||
|
// Assuming a users roles won't change during a single request, we'll
|
||||||
|
// only load them once.
|
||||||
|
this.userRoles = [];
|
||||||
|
this.fetchedRoles = false;
|
||||||
|
this.rolePromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether this auth could possibly modify the given user id.
|
||||||
|
// It still could be forbidden via ACLs even if this returns true.
|
||||||
|
Auth.prototype.couldUpdateUserId = function(userId) {
|
||||||
|
if (this.isMaster) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.user && this.user.id === userId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A helper to get a master-level Auth object
|
||||||
|
function master(config) {
|
||||||
|
return new Auth(config, true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper to get a nobody-level Auth object
|
||||||
|
function nobody(config) {
|
||||||
|
return new Auth(config, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves to an Auth object
|
||||||
|
var getAuthForSessionToken = function(config, sessionToken) {
|
||||||
|
var cachedUser = cache.getUser(sessionToken);
|
||||||
|
if (cachedUser) {
|
||||||
|
return Promise.resolve(new Auth(config, false, cachedUser));
|
||||||
|
}
|
||||||
|
var restOptions = {
|
||||||
|
limit: 1,
|
||||||
|
include: 'user'
|
||||||
|
};
|
||||||
|
var restWhere = {
|
||||||
|
_session_token: sessionToken
|
||||||
|
};
|
||||||
|
var query = new RestQuery(config, master(config), '_Session',
|
||||||
|
restWhere, restOptions);
|
||||||
|
return query.execute().then((response) => {
|
||||||
|
var results = response.results;
|
||||||
|
if (results.length !== 1 || !results[0]['user']) {
|
||||||
|
return nobody(config);
|
||||||
|
}
|
||||||
|
var obj = results[0]['user'];
|
||||||
|
delete obj.password;
|
||||||
|
obj['className'] = '_User';
|
||||||
|
var userObject = Parse.Object.fromJSON(obj);
|
||||||
|
cache.setUser(sessionToken, userObject);
|
||||||
|
return new Auth(config, false, userObject);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise that resolves to an array of role names
|
||||||
|
Auth.prototype.getUserRoles = function() {
|
||||||
|
if (this.isMaster || !this.user) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
if (this.fetchedRoles) {
|
||||||
|
return Promise.resolve(this.userRoles);
|
||||||
|
}
|
||||||
|
if (this.rolePromise) {
|
||||||
|
return rolePromise;
|
||||||
|
}
|
||||||
|
this.rolePromise = this._loadRoles();
|
||||||
|
return this.rolePromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iterates through the role tree and compiles a users roles
|
||||||
|
Auth.prototype._loadRoles = function() {
|
||||||
|
var restWhere = {
|
||||||
|
'users': {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_User',
|
||||||
|
objectId: this.user.id
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// First get the role ids this user is directly a member of
|
||||||
|
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||||
|
restWhere, {});
|
||||||
|
return query.execute().then((response) => {
|
||||||
|
var results = response.results;
|
||||||
|
if (!results.length) {
|
||||||
|
this.userRoles = [];
|
||||||
|
this.fetchedRoles = true;
|
||||||
|
this.rolePromise = null;
|
||||||
|
return Promise.resolve(this.userRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
var roleIDs = results.map(r => r.objectId);
|
||||||
|
var promises = [Promise.resolve(roleIDs)];
|
||||||
|
for (var role of roleIDs) {
|
||||||
|
promises.push(this._getAllRoleNamesForId(role));
|
||||||
|
}
|
||||||
|
return Promise.all(promises).then((results) => {
|
||||||
|
var allIDs = [];
|
||||||
|
for (var x of results) {
|
||||||
|
Array.prototype.push.apply(allIDs, x);
|
||||||
|
}
|
||||||
|
var restWhere = {
|
||||||
|
objectId: {
|
||||||
|
'$in': allIDs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var query = new RestQuery(this.config, master(this.config),
|
||||||
|
'_Role', restWhere, {});
|
||||||
|
return query.execute();
|
||||||
|
}).then((response) => {
|
||||||
|
var results = response.results;
|
||||||
|
this.userRoles = results.map((r) => {
|
||||||
|
return 'role:' + r.name;
|
||||||
|
});
|
||||||
|
this.fetchedRoles = true;
|
||||||
|
this.rolePromise = null;
|
||||||
|
return Promise.resolve(this.userRoles);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Given a role object id, get any other roles it is part of
|
||||||
|
// TODO: Make recursive to support role nesting beyond 1 level deep
|
||||||
|
Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||||
|
var rolePointer = {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_Role',
|
||||||
|
objectId: roleID
|
||||||
|
};
|
||||||
|
var restWhere = {
|
||||||
|
'$relatedTo': {
|
||||||
|
key: 'roles',
|
||||||
|
object: rolePointer
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var query = new RestQuery(this.config, master(this.config), '_Role',
|
||||||
|
restWhere, {});
|
||||||
|
return query.execute().then((response) => {
|
||||||
|
var results = response.results;
|
||||||
|
if (!results.length) {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
var roleIDs = results.map(r => r.objectId);
|
||||||
|
return Promise.resolve(roleIDs);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Auth: Auth,
|
||||||
|
master: master,
|
||||||
|
nobody: nobody,
|
||||||
|
getAuthForSessionToken: getAuthForSessionToken
|
||||||
|
};
|
||||||
27
Config.js
Normal file
27
Config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// A Config object provides information about how a specific app is
|
||||||
|
// configured.
|
||||||
|
// mount is the URL for the root of the API; includes http, domain, etc.
|
||||||
|
function Config(applicationId, mount) {
|
||||||
|
var cache = require('./cache');
|
||||||
|
var DatabaseAdapter = require('./DatabaseAdapter');
|
||||||
|
|
||||||
|
var cacheInfo = cache.apps[applicationId];
|
||||||
|
this.valid = !!cacheInfo;
|
||||||
|
if (!this.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applicationId = applicationId;
|
||||||
|
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
||||||
|
this.database = DatabaseAdapter.getDatabaseConnection(applicationId);
|
||||||
|
this.masterKey = cacheInfo.masterKey;
|
||||||
|
this.clientKey = cacheInfo.clientKey;
|
||||||
|
this.javascriptKey = cacheInfo.javascriptKey;
|
||||||
|
this.dotNetKey = cacheInfo.dotNetKey;
|
||||||
|
this.restAPIKey = cacheInfo.restAPIKey;
|
||||||
|
this.fileKey = cacheInfo.fileKey;
|
||||||
|
this.mount = mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = Config;
|
||||||
48
DatabaseAdapter.js
Normal file
48
DatabaseAdapter.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// Database Adapter
|
||||||
|
//
|
||||||
|
// Allows you to change the underlying database.
|
||||||
|
//
|
||||||
|
// Adapter classes must implement the following methods:
|
||||||
|
// * a constructor with signature (connectionString, optionsObject)
|
||||||
|
// * connect()
|
||||||
|
// * loadSchema()
|
||||||
|
// * create(className, object)
|
||||||
|
// * find(className, query, options)
|
||||||
|
// * update(className, query, update, options)
|
||||||
|
// * destroy(className, query, options)
|
||||||
|
// * This list is incomplete and the database process is not fully modularized.
|
||||||
|
//
|
||||||
|
// Default is ExportAdapter, which uses mongo.
|
||||||
|
|
||||||
|
var ExportAdapter = require('./ExportAdapter');
|
||||||
|
|
||||||
|
var adapter = ExportAdapter;
|
||||||
|
var cache = require('./cache');
|
||||||
|
var dbConnections = {};
|
||||||
|
var databaseURI = 'mongodb://localhost:27017/parse';
|
||||||
|
|
||||||
|
function setAdapter(databaseAdapter) {
|
||||||
|
adapter = databaseAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDatabaseURI(uri) {
|
||||||
|
databaseURI = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDatabaseConnection(appId) {
|
||||||
|
if (dbConnections[appId]) {
|
||||||
|
return dbConnections[appId];
|
||||||
|
}
|
||||||
|
dbConnections[appId] = new adapter(databaseURI, {
|
||||||
|
collectionPrefix: cache.apps[appId]['collectionPrefix']
|
||||||
|
});
|
||||||
|
dbConnections[appId].connect();
|
||||||
|
return dbConnections[appId];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
dbConnections: dbConnections,
|
||||||
|
getDatabaseConnection: getDatabaseConnection,
|
||||||
|
setAdapter: setAdapter,
|
||||||
|
setDatabaseURI: setDatabaseURI
|
||||||
|
};
|
||||||
576
ExportAdapter.js
Normal file
576
ExportAdapter.js
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
// A database adapter that works with data exported from the hosted
|
||||||
|
// Parse database.
|
||||||
|
|
||||||
|
var mongodb = require('mongodb');
|
||||||
|
var MongoClient = mongodb.MongoClient;
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
var Schema = require('./Schema');
|
||||||
|
var transform = require('./transform');
|
||||||
|
|
||||||
|
// options can contain:
|
||||||
|
// collectionPrefix: the string to put in front of every collection name.
|
||||||
|
function ExportAdapter(mongoURI, options) {
|
||||||
|
this.mongoURI = mongoURI;
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
this.collectionPrefix = options.collectionPrefix;
|
||||||
|
|
||||||
|
// We don't want a mutable this.schema, because then you could have
|
||||||
|
// one request that uses different schemas for different parts of
|
||||||
|
// it. Instead, use loadSchema to get a schema.
|
||||||
|
this.schemaPromise = null;
|
||||||
|
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connects to the database. Returns a promise that resolves when the
|
||||||
|
// connection is successful.
|
||||||
|
// this.db will be populated with a Mongo "Db" object when the
|
||||||
|
// promise resolves successfully.
|
||||||
|
ExportAdapter.prototype.connect = function() {
|
||||||
|
if (this.connectionPromise) {
|
||||||
|
// There's already a connection in progress.
|
||||||
|
return this.connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connectionPromise = Promise.resolve().then(() => {
|
||||||
|
return MongoClient.connect(this.mongoURI);
|
||||||
|
}).then((db) => {
|
||||||
|
this.db = db;
|
||||||
|
});
|
||||||
|
return this.connectionPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise for a Mongo collection.
|
||||||
|
// Generally just for internal use.
|
||||||
|
ExportAdapter.prototype.collection = function(className) {
|
||||||
|
if (className !== '_User' &&
|
||||||
|
className !== '_Installation' &&
|
||||||
|
className !== '_Session' &&
|
||||||
|
className !== '_SCHEMA' &&
|
||||||
|
className !== '_Role' &&
|
||||||
|
!className.match(/^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/) &&
|
||||||
|
!className.match(/^[A-Za-z][A-Za-z0-9_]*$/)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
||||||
|
'invalid className: ' + className);
|
||||||
|
}
|
||||||
|
return this.connect().then(() => {
|
||||||
|
return this.db.collection(this.collectionPrefix + className);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function returnsTrue() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a schema object.
|
||||||
|
// If we are provided a acceptor, then we run it on the schema.
|
||||||
|
// If the schema isn't accepted, we reload it at most once.
|
||||||
|
ExportAdapter.prototype.loadSchema = function(acceptor) {
|
||||||
|
acceptor = acceptor || returnsTrue;
|
||||||
|
|
||||||
|
if (!this.schemaPromise) {
|
||||||
|
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
|
||||||
|
delete this.schemaPromise;
|
||||||
|
return Schema.load(coll);
|
||||||
|
});
|
||||||
|
return this.schemaPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.schemaPromise.then((schema) => {
|
||||||
|
if (acceptor(schema)) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
|
||||||
|
delete this.schemaPromise;
|
||||||
|
return Schema.load(coll);
|
||||||
|
});
|
||||||
|
return this.schemaPromise;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise for the classname that is related to the given
|
||||||
|
// classname through the key.
|
||||||
|
// TODO: make this not in the ExportAdapter interface
|
||||||
|
ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
|
||||||
|
return this.loadSchema().then((schema) => {
|
||||||
|
var t = schema.getExpectedType(className, key);
|
||||||
|
var match = t.match(/^relation<(.*)>$/);
|
||||||
|
if (match) {
|
||||||
|
return match[1];
|
||||||
|
} else {
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Uses the schema to validate the object (REST API format).
|
||||||
|
// Returns a promise that resolves to the new schema.
|
||||||
|
// This does not update this.schema, because in a situation like a
|
||||||
|
// batch request, that could confuse other users of the schema.
|
||||||
|
ExportAdapter.prototype.validateObject = function(className, object) {
|
||||||
|
return this.loadSchema().then((schema) => {
|
||||||
|
return schema.validateObject(className, object);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Like transform.untransformObject but you need to provide a className.
|
||||||
|
// Filters out any data that shouldn't be on this REST-formatted object.
|
||||||
|
ExportAdapter.prototype.untransformObject = function(
|
||||||
|
schema, isMaster, aclGroup, className, mongoObject) {
|
||||||
|
var object = transform.untransformObject(schema, className, mongoObject);
|
||||||
|
|
||||||
|
if (className !== '_User') {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMaster || (aclGroup.indexOf(object.objectId) > -1)) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete object.authData;
|
||||||
|
delete object.sessionToken;
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs an update on the database.
|
||||||
|
// Returns a promise for an object with the new values for field
|
||||||
|
// modifications that don't know their results ahead of time, like
|
||||||
|
// 'increment'.
|
||||||
|
// Options:
|
||||||
|
// acl: a list of strings. If the object to be updated has an ACL,
|
||||||
|
// one of the provided strings must provide the caller with
|
||||||
|
// write permissions.
|
||||||
|
ExportAdapter.prototype.update = function(className, query, update, options) {
|
||||||
|
var acceptor = function(schema) {
|
||||||
|
return schema.hasKeys(className, Object.keys(query));
|
||||||
|
};
|
||||||
|
var isMaster = !('acl' in options);
|
||||||
|
var aclGroup = options.acl || [];
|
||||||
|
var mongoUpdate, schema;
|
||||||
|
return this.loadSchema(acceptor).then((s) => {
|
||||||
|
schema = s;
|
||||||
|
if (!isMaster) {
|
||||||
|
return schema.validatePermission(className, aclGroup, 'update');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
return this.handleRelationUpdates(className, query.objectId, update);
|
||||||
|
}).then(() => {
|
||||||
|
return this.collection(className);
|
||||||
|
}).then((coll) => {
|
||||||
|
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||||
|
if (options.acl) {
|
||||||
|
var writePerms = [
|
||||||
|
{_wperm: {'$exists': false}}
|
||||||
|
];
|
||||||
|
for (var entry of options.acl) {
|
||||||
|
writePerms.push({_wperm: {'$in': [entry]}});
|
||||||
|
}
|
||||||
|
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||||
|
}
|
||||||
|
|
||||||
|
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||||
|
|
||||||
|
return coll.findAndModify(mongoWhere, {}, mongoUpdate, {});
|
||||||
|
}).then((result) => {
|
||||||
|
if (!result.value) {
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.'));
|
||||||
|
}
|
||||||
|
if (result.lastErrorObject.n != 1) {
|
||||||
|
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = {};
|
||||||
|
var inc = mongoUpdate['$inc'];
|
||||||
|
if (inc) {
|
||||||
|
for (var key in inc) {
|
||||||
|
response[key] = (result.value[key] || 0) + inc[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Processes relation-updating operations from a REST-format update.
|
||||||
|
// Returns a promise that resolves successfully when these are
|
||||||
|
// processed.
|
||||||
|
// This mutates update.
|
||||||
|
ExportAdapter.prototype.handleRelationUpdates = function(className,
|
||||||
|
objectId,
|
||||||
|
update) {
|
||||||
|
var pending = [];
|
||||||
|
var deleteMe = [];
|
||||||
|
objectId = update.objectId || objectId;
|
||||||
|
|
||||||
|
var process = (op, key) => {
|
||||||
|
if (!op) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (op.__op == 'AddRelation') {
|
||||||
|
for (var object of op.objects) {
|
||||||
|
pending.push(this.addRelation(key, className,
|
||||||
|
objectId,
|
||||||
|
object.objectId));
|
||||||
|
}
|
||||||
|
deleteMe.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.__op == 'RemoveRelation') {
|
||||||
|
for (var object of op.objects) {
|
||||||
|
pending.push(this.removeRelation(key, className,
|
||||||
|
objectId,
|
||||||
|
object.objectId));
|
||||||
|
}
|
||||||
|
deleteMe.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.__op == 'Batch') {
|
||||||
|
for (x of op.ops) {
|
||||||
|
process(x, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var key in update) {
|
||||||
|
process(update[key], key);
|
||||||
|
}
|
||||||
|
for (var key of deleteMe) {
|
||||||
|
delete update[key];
|
||||||
|
}
|
||||||
|
return Promise.all(pending);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adds a relation.
|
||||||
|
// Returns a promise that resolves successfully iff the add was successful.
|
||||||
|
ExportAdapter.prototype.addRelation = function(key, fromClassName,
|
||||||
|
fromId, toId) {
|
||||||
|
var doc = {
|
||||||
|
relatedId: toId,
|
||||||
|
owningId: fromId
|
||||||
|
};
|
||||||
|
var className = '_Join:' + key + ':' + fromClassName;
|
||||||
|
return this.collection(className).then((coll) => {
|
||||||
|
return coll.update(doc, doc, {upsert: true});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Removes a relation.
|
||||||
|
// Returns a promise that resolves successfully iff the remove was
|
||||||
|
// successful.
|
||||||
|
ExportAdapter.prototype.removeRelation = function(key, fromClassName,
|
||||||
|
fromId, toId) {
|
||||||
|
var doc = {
|
||||||
|
relatedId: toId,
|
||||||
|
owningId: fromId
|
||||||
|
};
|
||||||
|
var className = '_Join:' + key + ':' + fromClassName;
|
||||||
|
return this.collection(className).then((coll) => {
|
||||||
|
return coll.remove(doc);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Removes objects matches this query from the database.
|
||||||
|
// Returns a promise that resolves successfully iff the object was
|
||||||
|
// deleted.
|
||||||
|
// Options:
|
||||||
|
// acl: a list of strings. If the object to be updated has an ACL,
|
||||||
|
// one of the provided strings must provide the caller with
|
||||||
|
// write permissions.
|
||||||
|
ExportAdapter.prototype.destroy = function(className, query, options) {
|
||||||
|
options = options || {};
|
||||||
|
var isMaster = !('acl' in options);
|
||||||
|
var aclGroup = options.acl || [];
|
||||||
|
|
||||||
|
var schema;
|
||||||
|
return this.loadSchema().then((s) => {
|
||||||
|
schema = s;
|
||||||
|
if (!isMaster) {
|
||||||
|
return schema.validatePermission(className, aclGroup, 'delete');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
return this.collection(className);
|
||||||
|
}).then((coll) => {
|
||||||
|
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||||
|
|
||||||
|
if (options.acl) {
|
||||||
|
var writePerms = [
|
||||||
|
{_wperm: {'$exists': false}}
|
||||||
|
];
|
||||||
|
for (var entry of options.acl) {
|
||||||
|
writePerms.push({_wperm: {'$in': [entry]}});
|
||||||
|
}
|
||||||
|
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||||
|
}
|
||||||
|
|
||||||
|
return coll.remove(mongoWhere);
|
||||||
|
}).then((resp) => {
|
||||||
|
if (resp.result.n === 0) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.'));
|
||||||
|
|
||||||
|
}
|
||||||
|
}, (error) => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inserts an object into the database.
|
||||||
|
// Returns a promise that resolves successfully iff the object saved.
|
||||||
|
ExportAdapter.prototype.create = function(className, object, options) {
|
||||||
|
var schema;
|
||||||
|
var isMaster = !('acl' in options);
|
||||||
|
var aclGroup = options.acl || [];
|
||||||
|
|
||||||
|
return this.loadSchema().then((s) => {
|
||||||
|
schema = s;
|
||||||
|
if (!isMaster) {
|
||||||
|
return schema.validatePermission(className, aclGroup, 'create');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
|
||||||
|
return this.handleRelationUpdates(className, null, object);
|
||||||
|
}).then(() => {
|
||||||
|
return this.collection(className);
|
||||||
|
}).then((coll) => {
|
||||||
|
var mongoObject = transform.transformCreate(schema, className, object);
|
||||||
|
return coll.insert([mongoObject]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs a mongo query on the database.
|
||||||
|
// This should only be used for testing - use 'find' for normal code
|
||||||
|
// to avoid Mongo-format dependencies.
|
||||||
|
// Returns a promise that resolves to a list of items.
|
||||||
|
ExportAdapter.prototype.mongoFind = function(className, query, options) {
|
||||||
|
options = options || {};
|
||||||
|
return this.collection(className).then((coll) => {
|
||||||
|
return coll.find(query, options).toArray();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deletes everything in the database matching the current collectionPrefix
|
||||||
|
// Won't delete collections in the system namespace
|
||||||
|
// Returns a promise.
|
||||||
|
ExportAdapter.prototype.deleteEverything = function() {
|
||||||
|
this.schemaPromise = null;
|
||||||
|
|
||||||
|
return this.connect().then(() => {
|
||||||
|
return this.db.collections();
|
||||||
|
}).then((colls) => {
|
||||||
|
var promises = [];
|
||||||
|
for (var coll of colls) {
|
||||||
|
if (!coll.namespace.match(/\.system\./) &&
|
||||||
|
coll.collectionName.indexOf(this.collectionPrefix) === 0) {
|
||||||
|
promises.push(coll.drop());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Finds the keys in a query. Returns a Set. REST format only
|
||||||
|
function keysForQuery(query) {
|
||||||
|
var sublist = query['$and'] || query['$or'];
|
||||||
|
if (sublist) {
|
||||||
|
var answer = new Set();
|
||||||
|
for (var subquery of sublist) {
|
||||||
|
for (var key of keysForQuery(subquery)) {
|
||||||
|
answer.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(Object.keys(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a list of related ids given an owning id.
|
||||||
|
// className here is the owning className.
|
||||||
|
ExportAdapter.prototype.relatedIds = function(className, key, owningId) {
|
||||||
|
var joinTable = '_Join:' + key + ':' + className;
|
||||||
|
return this.collection(joinTable).then((coll) => {
|
||||||
|
return coll.find({owningId: owningId}).toArray();
|
||||||
|
}).then((results) => {
|
||||||
|
return results.map(r => r.relatedId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise for a list of owning ids given some related ids.
|
||||||
|
// className here is the owning className.
|
||||||
|
ExportAdapter.prototype.owningIds = function(className, key, relatedIds) {
|
||||||
|
var joinTable = '_Join:' + key + ':' + className;
|
||||||
|
return this.collection(joinTable).then((coll) => {
|
||||||
|
return coll.find({relatedId: {'$in': relatedIds}}).toArray();
|
||||||
|
}).then((results) => {
|
||||||
|
return results.map(r => r.owningId);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modifies query so that it no longer has $in on relation fields, or
|
||||||
|
// equal-to-pointer constraints on relation fields.
|
||||||
|
// Returns a promise that resolves when query is mutated
|
||||||
|
// TODO: this only handles one of these at a time - make it handle more
|
||||||
|
ExportAdapter.prototype.reduceInRelation = function(className, query, schema) {
|
||||||
|
// Search for an in-relation or equal-to-relation
|
||||||
|
for (var key in query) {
|
||||||
|
if (query[key] &&
|
||||||
|
(query[key]['$in'] || query[key].__type == 'Pointer')) {
|
||||||
|
var t = schema.getExpectedType(className, key);
|
||||||
|
var match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var relatedClassName = match[1];
|
||||||
|
var relatedIds;
|
||||||
|
if (query[key]['$in']) {
|
||||||
|
relatedIds = query[key]['$in'].map(r => r.objectId);
|
||||||
|
} else {
|
||||||
|
relatedIds = [query[key].objectId];
|
||||||
|
}
|
||||||
|
return this.owningIds(className, key, relatedIds).then((ids) => {
|
||||||
|
delete query[key];
|
||||||
|
query.objectId = {'$in': ids};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modifies query so that it no longer has $relatedTo
|
||||||
|
// Returns a promise that resolves when query is mutated
|
||||||
|
ExportAdapter.prototype.reduceRelationKeys = function(className, query) {
|
||||||
|
var relatedTo = query['$relatedTo'];
|
||||||
|
if (relatedTo) {
|
||||||
|
return this.relatedIds(
|
||||||
|
relatedTo.object.className,
|
||||||
|
relatedTo.key,
|
||||||
|
relatedTo.object.objectId).then((ids) => {
|
||||||
|
delete query['$relatedTo'];
|
||||||
|
query['objectId'] = {'$in': ids};
|
||||||
|
return this.reduceRelationKeys(className, query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Does a find with "smart indexing".
|
||||||
|
// Currently this just means, if it needs a geoindex and there is
|
||||||
|
// none, then build the geoindex.
|
||||||
|
// This could be improved a lot but it's not clear if that's a good
|
||||||
|
// idea. Or even if this behavior is a good idea.
|
||||||
|
ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||||
|
return coll.find(where, options).toArray()
|
||||||
|
.then((result) => {
|
||||||
|
return result;
|
||||||
|
}, (error) => {
|
||||||
|
// Check for "no geoindex" error
|
||||||
|
if (!error.message.match(/unable to find index for .geoNear/) ||
|
||||||
|
error.code != 17007) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out what key needs an index
|
||||||
|
var key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||||
|
if (!key) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = {};
|
||||||
|
index[key] = '2d';
|
||||||
|
return coll.createIndex(index).then(() => {
|
||||||
|
// Retry, but just once.
|
||||||
|
return coll.find(where, options).toArray();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs a query on the database.
|
||||||
|
// Returns a promise that resolves to a list of items.
|
||||||
|
// Options:
|
||||||
|
// skip number of results to skip.
|
||||||
|
// limit limit to this number of results.
|
||||||
|
// sort an object where keys are the fields to sort by.
|
||||||
|
// the value is +1 for ascending, -1 for descending.
|
||||||
|
// count run a count instead of returning results.
|
||||||
|
// acl restrict this operation with an ACL for the provided array
|
||||||
|
// of user objectIds and roles. acl: null means no user.
|
||||||
|
// when this field is not present, don't do anything regarding ACLs.
|
||||||
|
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||||
|
// anything about users, ideally. Then, improve the format of the ACL
|
||||||
|
// arg to work like the others.
|
||||||
|
ExportAdapter.prototype.find = function(className, query, options) {
|
||||||
|
options = options || {};
|
||||||
|
var mongoOptions = {};
|
||||||
|
if (options.skip) {
|
||||||
|
mongoOptions.skip = options.skip;
|
||||||
|
}
|
||||||
|
if (options.limit) {
|
||||||
|
mongoOptions.limit = options.limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isMaster = !('acl' in options);
|
||||||
|
var aclGroup = options.acl || [];
|
||||||
|
var acceptor = function(schema) {
|
||||||
|
return schema.hasKeys(className, keysForQuery(query));
|
||||||
|
};
|
||||||
|
var schema;
|
||||||
|
return this.loadSchema(acceptor).then((s) => {
|
||||||
|
schema = s;
|
||||||
|
if (options.sort) {
|
||||||
|
mongoOptions.sort = {};
|
||||||
|
for (var key in options.sort) {
|
||||||
|
var mongoKey = transform.transformKey(schema, className, key);
|
||||||
|
mongoOptions.sort[mongoKey] = options.sort[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMaster) {
|
||||||
|
var op = 'find';
|
||||||
|
var k = Object.keys(query);
|
||||||
|
if (k.length == 1 && typeof query.objectId == 'string') {
|
||||||
|
op = 'get';
|
||||||
|
}
|
||||||
|
return schema.validatePermission(className, aclGroup, op);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}).then(() => {
|
||||||
|
return this.reduceRelationKeys(className, query);
|
||||||
|
}).then(() => {
|
||||||
|
return this.reduceInRelation(className, query, schema);
|
||||||
|
}).then(() => {
|
||||||
|
return this.collection(className);
|
||||||
|
}).then((coll) => {
|
||||||
|
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||||
|
if (!isMaster) {
|
||||||
|
var orParts = [
|
||||||
|
{"_rperm" : { "$exists": false }},
|
||||||
|
{"_rperm" : { "$in" : ["*"]}}
|
||||||
|
];
|
||||||
|
for (var acl of aclGroup) {
|
||||||
|
orParts.push({"_rperm" : { "$in" : [acl]}});
|
||||||
|
}
|
||||||
|
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
|
||||||
|
}
|
||||||
|
if (options.count) {
|
||||||
|
return coll.count(mongoWhere, mongoOptions);
|
||||||
|
} else {
|
||||||
|
return this.smartFind(coll, mongoWhere, mongoOptions)
|
||||||
|
.then((mongoResults) => {
|
||||||
|
return mongoResults.map((r) => {
|
||||||
|
return this.untransformObject(
|
||||||
|
schema, isMaster, aclGroup, className, r);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = ExportAdapter;
|
||||||
28
FilesAdapter.js
Normal file
28
FilesAdapter.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Files Adapter
|
||||||
|
//
|
||||||
|
// Allows you to change the file storage mechanism.
|
||||||
|
//
|
||||||
|
// Adapter classes must implement the following functions:
|
||||||
|
// * create(config, filename, data)
|
||||||
|
// * get(config, filename)
|
||||||
|
//
|
||||||
|
// Default is GridStoreAdapter, which requires mongo
|
||||||
|
// and for the API server to be using the ExportAdapter
|
||||||
|
// database adapter.
|
||||||
|
|
||||||
|
var GridStoreAdapter = require('./GridStoreAdapter');
|
||||||
|
|
||||||
|
var adapter = GridStoreAdapter;
|
||||||
|
|
||||||
|
function setAdapter(filesAdapter) {
|
||||||
|
adapter = filesAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdapter() {
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAdapter: getAdapter,
|
||||||
|
setAdapter: setAdapter
|
||||||
|
};
|
||||||
38
GridStoreAdapter.js
Normal file
38
GridStoreAdapter.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// GridStoreAdapter
|
||||||
|
//
|
||||||
|
// Stores files in Mongo using GridStore
|
||||||
|
// Requires the database adapter to be based on mongoclient
|
||||||
|
|
||||||
|
var GridStore = require('mongodb').GridStore;
|
||||||
|
|
||||||
|
// For a given config object, filename, and data, store a file
|
||||||
|
// Returns a promise
|
||||||
|
function create(config, filename, data) {
|
||||||
|
return config.database.connect().then(() => {
|
||||||
|
var gridStore = new GridStore(config.database.db, filename, 'w');
|
||||||
|
return gridStore.open();
|
||||||
|
}).then((gridStore) => {
|
||||||
|
return gridStore.write(data);
|
||||||
|
}).then((gridStore) => {
|
||||||
|
return gridStore.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for and return a file if found by filename
|
||||||
|
// Resolves a promise that succeeds with the buffer result
|
||||||
|
// from GridStore
|
||||||
|
function get(config, filename) {
|
||||||
|
return config.database.connect().then(() => {
|
||||||
|
return GridStore.exist(config.database.db, filename);
|
||||||
|
}).then(() => {
|
||||||
|
var gridStore = new GridStore(config.database.db, filename, 'r');
|
||||||
|
return gridStore.open();
|
||||||
|
}).then((gridStore) => {
|
||||||
|
return gridStore.read();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
create: create,
|
||||||
|
get: get
|
||||||
|
};
|
||||||
30
LICENSE
Normal file
30
LICENSE
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
BSD License
|
||||||
|
|
||||||
|
For Parse Server software
|
||||||
|
|
||||||
|
Copyright (c) 2015-present, Parse, LLC. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name Parse nor the names of its contributors may be used to
|
||||||
|
endorse or promote products derived from this software without specific
|
||||||
|
prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
|
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
33
PATENTS
Normal file
33
PATENTS
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
Additional Grant of Patent Rights Version 2
|
||||||
|
|
||||||
|
"Software" means the Parse Server software distributed by Parse, LLC.
|
||||||
|
|
||||||
|
Parse, LLC. ("Parse") hereby grants to each recipient of the Software
|
||||||
|
("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
|
||||||
|
(subject to the termination provision below) license under any Necessary
|
||||||
|
Claims, to make, have made, use, sell, offer to sell, import, and otherwise
|
||||||
|
transfer the Software. For avoidance of doubt, no license is granted under
|
||||||
|
Parse’s rights in any patent claims that are infringed by (i) modifications
|
||||||
|
to the Software made by you or any third party or (ii) the Software in
|
||||||
|
combination with any software or other technology.
|
||||||
|
|
||||||
|
The license granted hereunder will terminate, automatically and without notice,
|
||||||
|
if you (or any of your subsidiaries, corporate affiliates or agents) initiate
|
||||||
|
directly or indirectly, or take a direct financial interest in, any Patent
|
||||||
|
Assertion: (i) against Parse or any of its subsidiaries or corporate
|
||||||
|
affiliates, (ii) against any party if such Patent Assertion arises in whole or
|
||||||
|
in part from any software, technology, product or service of Parse or any of
|
||||||
|
its subsidiaries or corporate affiliates, or (iii) against any party relating
|
||||||
|
to the Software. Notwithstanding the foregoing, if Parse or any of its
|
||||||
|
subsidiaries or corporate affiliates files a lawsuit alleging patent
|
||||||
|
infringement against you in the first instance, and you respond by filing a
|
||||||
|
patent infringement counterclaim in that lawsuit against that party that is
|
||||||
|
unrelated to the Software, the license granted hereunder will not terminate
|
||||||
|
under section (i) of this paragraph due to such counterclaim.
|
||||||
|
|
||||||
|
A "Necessary Claim" is a claim of a patent owned by Parse that is
|
||||||
|
necessarily infringed by the Software standing alone.
|
||||||
|
|
||||||
|
A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
|
||||||
|
or contributory infringement or inducement to infringe any patent, including a
|
||||||
|
cross-claim or counterclaim.
|
||||||
148
PromiseRouter.js
Normal file
148
PromiseRouter.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// A router that is based on promises rather than req/res/next.
|
||||||
|
// This is intended to replace the use of express.Router to handle
|
||||||
|
// subsections of the API surface.
|
||||||
|
// This will make it easier to have methods like 'batch' that
|
||||||
|
// themselves use our routing information, without disturbing express
|
||||||
|
// components that external developers may be modifying.
|
||||||
|
|
||||||
|
function PromiseRouter() {
|
||||||
|
// Each entry should be an object with:
|
||||||
|
// path: the path to route, in express format
|
||||||
|
// method: the HTTP method that this route handles.
|
||||||
|
// Must be one of: POST, GET, PUT, DELETE
|
||||||
|
// handler: a function that takes request, and returns a promise.
|
||||||
|
// Successful handlers should resolve to an object with fields:
|
||||||
|
// status: optional. the http status code. defaults to 200
|
||||||
|
// response: a json object with the content of the response
|
||||||
|
// location: optional. a location header
|
||||||
|
this.routes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global flag. Set this to true to log every request and response.
|
||||||
|
PromiseRouter.verbose = process.env.VERBOSE || false;
|
||||||
|
|
||||||
|
// Merge the routes into this one
|
||||||
|
PromiseRouter.prototype.merge = function(router) {
|
||||||
|
for (var route of router.routes) {
|
||||||
|
this.routes.push(route);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PromiseRouter.prototype.route = function(method, path, handler) {
|
||||||
|
switch(method) {
|
||||||
|
case 'POST':
|
||||||
|
case 'GET':
|
||||||
|
case 'PUT':
|
||||||
|
case 'DELETE':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw 'cannot route method: ' + method;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.routes.push({
|
||||||
|
path: path,
|
||||||
|
method: method,
|
||||||
|
handler: handler
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns an object with:
|
||||||
|
// handler: the handler that should deal with this request
|
||||||
|
// params: any :-params that got parsed from the path
|
||||||
|
// Returns undefined if there is no match.
|
||||||
|
PromiseRouter.prototype.match = function(method, path) {
|
||||||
|
for (var route of this.routes) {
|
||||||
|
if (route.method != method) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: we can only route the specific wildcards :className and
|
||||||
|
// :objectId, and in that order.
|
||||||
|
// This is pretty hacky but I don't want to rebuild the entire
|
||||||
|
// express route matcher. Maybe there's a way to reuse its logic.
|
||||||
|
var pattern = '^' + route.path + '$';
|
||||||
|
|
||||||
|
pattern = pattern.replace(':className',
|
||||||
|
'(_?[A-Za-z][A-Za-z_0-9]*)');
|
||||||
|
pattern = pattern.replace(':objectId',
|
||||||
|
'([A-Za-z0-9]+)');
|
||||||
|
var re = new RegExp(pattern);
|
||||||
|
var m = path.match(re);
|
||||||
|
if (!m) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var params = {};
|
||||||
|
if (m[1]) {
|
||||||
|
params.className = m[1];
|
||||||
|
}
|
||||||
|
if (m[2]) {
|
||||||
|
params.objectId = m[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {params: params, handler: route.handler};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// A helper function to make an express handler out of a a promise
|
||||||
|
// handler.
|
||||||
|
// Express handlers should never throw; if a promise handler throws we
|
||||||
|
// just treat it like it resolved to an error.
|
||||||
|
function makeExpressHandler(promiseHandler) {
|
||||||
|
return function(req, res, next) {
|
||||||
|
try {
|
||||||
|
if (PromiseRouter.verbose) {
|
||||||
|
console.log(req.method, req.originalUrl, req.headers,
|
||||||
|
JSON.stringify(req.body, null, 2));
|
||||||
|
}
|
||||||
|
promiseHandler(req).then((result) => {
|
||||||
|
if (!result.response) {
|
||||||
|
console.log('BUG: the handler did not include a "response" field');
|
||||||
|
throw 'control should not get here';
|
||||||
|
}
|
||||||
|
if (PromiseRouter.verbose) {
|
||||||
|
console.log('response:', JSON.stringify(result.response, null, 2));
|
||||||
|
}
|
||||||
|
var status = result.status || 200;
|
||||||
|
res.status(status);
|
||||||
|
if (result.location) {
|
||||||
|
res.set('Location', result.location);
|
||||||
|
}
|
||||||
|
res.json(result.response);
|
||||||
|
}, (e) => {
|
||||||
|
if (PromiseRouter.verbose) {
|
||||||
|
console.log('error:', e);
|
||||||
|
}
|
||||||
|
next(e);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (PromiseRouter.verbose) {
|
||||||
|
console.log('error:', e);
|
||||||
|
}
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mount the routes on this router onto an express app (or express router)
|
||||||
|
PromiseRouter.prototype.mountOnto = function(expressApp) {
|
||||||
|
for (var route of this.routes) {
|
||||||
|
switch(route.method) {
|
||||||
|
case 'POST':
|
||||||
|
expressApp.post(route.path, makeExpressHandler(route.handler));
|
||||||
|
break;
|
||||||
|
case 'GET':
|
||||||
|
expressApp.get(route.path, makeExpressHandler(route.handler));
|
||||||
|
break;
|
||||||
|
case 'PUT':
|
||||||
|
expressApp.put(route.path, makeExpressHandler(route.handler));
|
||||||
|
break;
|
||||||
|
case 'DELETE':
|
||||||
|
expressApp.delete(route.path, makeExpressHandler(route.handler));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw 'unexpected code branch';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = PromiseRouter;
|
||||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
## parse-server
|
||||||
|
|
||||||
|
A Parse.com API compatible router package for Express
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Basic options:
|
||||||
|
|
||||||
|
* databaseURI (required) - The connection string for your database, i.e. `mongodb://user:pass@host.com/dbname`
|
||||||
|
* appId (required) - The application id to host with this server instance
|
||||||
|
* masterKey (required) - The master key to use for overriding ACL security
|
||||||
|
* cloud - The absolute path to your cloud code main.js file
|
||||||
|
* fileKey - For migrated apps, this is necessary to provide access to files already hosted on Parse.
|
||||||
|
|
||||||
|
#### Client key options:
|
||||||
|
|
||||||
|
The client keys used with Parse are no longer necessary with parse-server. If you wish to still require them, perhaps to be able to refuse access to older clients, you can set the keys at intialization time. Setting any of these keys will require all requests to provide one of the configured keys.
|
||||||
|
|
||||||
|
* clientKey
|
||||||
|
* javascriptKey
|
||||||
|
* restAPIKey
|
||||||
|
* dotNetKey
|
||||||
|
|
||||||
|
#### Advanced options:
|
||||||
|
|
||||||
|
* filesAdapter - The default behavior (GridStore) can be changed by creating an adapter class (see `FilesAdapter.js`)
|
||||||
|
* databaseAdapter (unfinished) - The backing store can be changed by creating an adapter class (see `DatabaseAdapter.js`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
You can create an instance of ParseServer, and mount it on a new or existing Express website:
|
||||||
|
|
||||||
|
```
|
||||||
|
var express = require('express');
|
||||||
|
var ParseServer = require('parse-server').ParseServer;
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
// Specify the connection string for your mongodb database
|
||||||
|
// and the location to your Parse cloud code
|
||||||
|
var api = new ParseServer({
|
||||||
|
databaseURI: 'mongodb://localhost:27017/dev',
|
||||||
|
cloud: '/home/myApp/cloud/main.js', // Provide an absolute path
|
||||||
|
appId: 'myAppId',
|
||||||
|
masterKey: 'mySecretMasterKey',
|
||||||
|
fileKey: 'optionalFileKey'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Serve the Parse API on the /parse URL prefix
|
||||||
|
app.use('/parse', api);
|
||||||
|
|
||||||
|
// Hello world
|
||||||
|
app.get('/', function(req, res) {
|
||||||
|
res.status(200).send('Express is running here.');
|
||||||
|
});
|
||||||
|
|
||||||
|
var port = process.env.PORT || 1337;
|
||||||
|
app.listen(port, function() {
|
||||||
|
console.log('parse-server-example running on port ' + port + '.');
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported
|
||||||
|
|
||||||
|
* CRUD operations
|
||||||
|
* Schema validation
|
||||||
|
* Pointers
|
||||||
|
* Users, including Facebook login and anonymous users
|
||||||
|
* Files
|
||||||
|
* Installations
|
||||||
|
* Sessions
|
||||||
|
* Geopoints
|
||||||
|
* Roles
|
||||||
|
* Class-level Permissions (see below)
|
||||||
|
|
||||||
|
Parse server does not include a web-based dashboard, which is where class-level permissions have always been configured. If you migrate an app from Parse, you'll see the format for CLPs in the SCHEMA collection. There is also a `setPermissions` method on the `Schema` class, which you can see used in the unit-tests in `Schema.spec.js`
|
||||||
|
You can also set up an app on Parse, providing the connection string for your mongo database, and continue to use the dashboard on Parse.com.
|
||||||
|
|
||||||
|
### Not supported
|
||||||
|
|
||||||
|
* Push - We did not rebuild a new push delivery system for parse-server, but we are open to working on one together with the community.
|
||||||
555
RestQuery.js
Normal file
555
RestQuery.js
Normal 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;
|
||||||
718
RestWrite.js
Normal file
718
RestWrite.js
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
// A RestWrite encapsulates everything we need to run an operation
|
||||||
|
// that writes to the database.
|
||||||
|
// This could be either a "create" or an "update".
|
||||||
|
|
||||||
|
var deepcopy = require('deepcopy');
|
||||||
|
var rack = require('hat').rack();
|
||||||
|
|
||||||
|
var Auth = require('./Auth');
|
||||||
|
var cache = require('./cache');
|
||||||
|
var Config = require('./Config');
|
||||||
|
var crypto = require('./crypto');
|
||||||
|
var facebook = require('./facebook');
|
||||||
|
var Parse = require('parse/node');
|
||||||
|
var triggers = require('./triggers');
|
||||||
|
|
||||||
|
// query and data are both provided in REST API format. So data
|
||||||
|
// types are encoded by plain old objects.
|
||||||
|
// If query is null, this is a "create" and the data in data should be
|
||||||
|
// created.
|
||||||
|
// Otherwise this is an "update" - the object matching the query
|
||||||
|
// should get updated with data.
|
||||||
|
// RestWrite will handle objectId, createdAt, and updatedAt for
|
||||||
|
// everything. It also knows to use triggers and special modifications
|
||||||
|
// for the _User class.
|
||||||
|
function RestWrite(config, auth, className, query, data, originalData) {
|
||||||
|
this.config = config;
|
||||||
|
this.auth = auth;
|
||||||
|
this.className = className;
|
||||||
|
this.storage = {};
|
||||||
|
|
||||||
|
if (!query && data.objectId) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' +
|
||||||
|
'is an invalid field name.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the operation is complete, this.response may have several
|
||||||
|
// fields.
|
||||||
|
// response: the actual data to be returned
|
||||||
|
// status: the http status code. if not present, treated like a 200
|
||||||
|
// location: the location header. if not present, no location header
|
||||||
|
this.response = null;
|
||||||
|
|
||||||
|
// Processing this operation may mutate our data, so we operate on a
|
||||||
|
// copy
|
||||||
|
this.query = deepcopy(query);
|
||||||
|
this.data = deepcopy(data);
|
||||||
|
// We never change originalData, so we do not need a deep copy
|
||||||
|
this.originalData = originalData;
|
||||||
|
|
||||||
|
// The timestamp we'll use for this whole operation
|
||||||
|
this.updatedAt = Parse._encode(new Date()).iso;
|
||||||
|
|
||||||
|
if (this.data) {
|
||||||
|
// Add default fields
|
||||||
|
this.data.updatedAt = this.updatedAt;
|
||||||
|
if (!this.query) {
|
||||||
|
this.data.createdAt = this.updatedAt;
|
||||||
|
this.data.objectId = newObjectId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A convenient method to perform all the steps of processing the
|
||||||
|
// write, in order.
|
||||||
|
// Returns a promise for a {response, status, location} object.
|
||||||
|
// status and location are optional.
|
||||||
|
RestWrite.prototype.execute = function() {
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
return this.validateSchema();
|
||||||
|
}).then(() => {
|
||||||
|
return this.handleInstallation();
|
||||||
|
}).then(() => {
|
||||||
|
return this.handleSession();
|
||||||
|
}).then(() => {
|
||||||
|
return this.runBeforeTrigger();
|
||||||
|
}).then(() => {
|
||||||
|
return this.validateAuthData();
|
||||||
|
}).then(() => {
|
||||||
|
return this.transformUser();
|
||||||
|
}).then(() => {
|
||||||
|
return this.runDatabaseOperation();
|
||||||
|
}).then(() => {
|
||||||
|
return this.handleFollowup();
|
||||||
|
}).then(() => {
|
||||||
|
return this.runAfterTrigger();
|
||||||
|
}).then(() => {
|
||||||
|
return this.response;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validates this operation against the schema.
|
||||||
|
RestWrite.prototype.validateSchema = function() {
|
||||||
|
return this.config.database.validateObject(this.className, this.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs any beforeSave triggers against this operation.
|
||||||
|
// Any change leads to our data being mutated.
|
||||||
|
RestWrite.prototype.runBeforeTrigger = function() {
|
||||||
|
// Cloud code gets a bit of extra data for its objects
|
||||||
|
var extraData = {className: this.className};
|
||||||
|
if (this.query && this.query.objectId) {
|
||||||
|
extraData.objectId = this.query.objectId;
|
||||||
|
}
|
||||||
|
// Build the inflated object, for a create write, originalData is empty
|
||||||
|
var inflatedObject = triggers.inflate(extraData, this.originalData);;
|
||||||
|
inflatedObject._finishFetch(this.data);
|
||||||
|
// Build the original object, we only do this for a update write
|
||||||
|
var originalObject;
|
||||||
|
if (this.query && this.query.objectId) {
|
||||||
|
originalObject = triggers.inflate(extraData, this.originalData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
return triggers.maybeRunTrigger(
|
||||||
|
'beforeSave', this.auth, inflatedObject, originalObject);
|
||||||
|
}).then((response) => {
|
||||||
|
if (response && response.object) {
|
||||||
|
this.data = response.object;
|
||||||
|
// We should delete the objectId for an update write
|
||||||
|
if (this.query && this.query.objectId) {
|
||||||
|
delete this.data.objectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Transforms auth data for a user object.
|
||||||
|
// Does nothing if this isn't a user object.
|
||||||
|
// Returns a promise for when we're done if it can't finish this tick.
|
||||||
|
RestWrite.prototype.validateAuthData = function() {
|
||||||
|
if (this.className !== '_User') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.query && !this.data.authData) {
|
||||||
|
if (typeof this.data.username !== 'string') {
|
||||||
|
throw new Parse.Error(Parse.Error.USERNAME_MISSING,
|
||||||
|
'bad or missing username');
|
||||||
|
}
|
||||||
|
if (typeof this.data.password !== 'string') {
|
||||||
|
throw new Parse.Error(Parse.Error.PASSWORD_MISSING,
|
||||||
|
'password is required');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.data.authData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var facebookData = this.data.authData.facebook;
|
||||||
|
var anonData = this.data.authData.anonymous;
|
||||||
|
|
||||||
|
if (anonData === null ||
|
||||||
|
(anonData && anonData.id)) {
|
||||||
|
return this.handleAnonymousAuthData();
|
||||||
|
} else if (facebookData === null ||
|
||||||
|
(facebookData && facebookData.id && facebookData.access_token)) {
|
||||||
|
return this.handleFacebookAuthData();
|
||||||
|
} else {
|
||||||
|
throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE,
|
||||||
|
'This authentication method is unsupported.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RestWrite.prototype.handleAnonymousAuthData = function() {
|
||||||
|
var anonData = this.data.authData.anonymous;
|
||||||
|
if (anonData === null && this.query) {
|
||||||
|
// We are unlinking the user from the anonymous provider
|
||||||
|
this.data._auth_data_anonymous = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this user already exists
|
||||||
|
return this.config.database.find(
|
||||||
|
this.className,
|
||||||
|
{'authData.anonymous.id': anonData.id}, {})
|
||||||
|
.then((results) => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
if (!this.query) {
|
||||||
|
// We're signing up, but this user already exists. Short-circuit
|
||||||
|
delete results[0].password;
|
||||||
|
this.response = {
|
||||||
|
response: results[0],
|
||||||
|
location: this.location()
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a PUT for the same user, allow the linking
|
||||||
|
if (results[0].objectId === this.query.objectId) {
|
||||||
|
// Delete the rest format key before saving
|
||||||
|
delete this.data.authData;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're trying to create a duplicate account. Forbid it
|
||||||
|
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
|
||||||
|
'this auth is already used');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This anonymous user does not already exist, so transform it
|
||||||
|
// to a saveable format
|
||||||
|
this.data._auth_data_anonymous = anonData;
|
||||||
|
|
||||||
|
// Delete the rest format key before saving
|
||||||
|
delete this.data.authData;
|
||||||
|
})
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
RestWrite.prototype.handleFacebookAuthData = function() {
|
||||||
|
var facebookData = this.data.authData.facebook;
|
||||||
|
if (facebookData === null && this.query) {
|
||||||
|
// We are unlinking from Facebook.
|
||||||
|
this.data._auth_data_facebook = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return facebook.validateUserId(facebookData.id,
|
||||||
|
facebookData.access_token)
|
||||||
|
.then(() => {
|
||||||
|
return facebook.validateAppId(process.env.FACEBOOK_APP_ID,
|
||||||
|
facebookData.access_token);
|
||||||
|
}).then(() => {
|
||||||
|
// Check if this user already exists
|
||||||
|
// TODO: does this handle re-linking correctly?
|
||||||
|
return this.config.database.find(
|
||||||
|
this.className,
|
||||||
|
{'authData.facebook.id': facebookData.id}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
if (!this.query) {
|
||||||
|
// We're signing up, but this user already exists. Short-circuit
|
||||||
|
delete results[0].password;
|
||||||
|
this.response = {
|
||||||
|
response: results[0],
|
||||||
|
location: this.location()
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a PUT for the same user, allow the linking
|
||||||
|
if (results[0].objectId === this.query.objectId) {
|
||||||
|
// Delete the rest format key before saving
|
||||||
|
delete this.data.authData;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We're trying to create a duplicate FB auth. Forbid it
|
||||||
|
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
|
||||||
|
'this auth is already used');
|
||||||
|
}
|
||||||
|
|
||||||
|
// This FB auth does not already exist, so transform it to a
|
||||||
|
// saveable format
|
||||||
|
this.data._auth_data_facebook = facebookData;
|
||||||
|
|
||||||
|
// Delete the rest format key before saving
|
||||||
|
delete this.data.authData;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// The non-third-party parts of User transformation
|
||||||
|
RestWrite.prototype.transformUser = function() {
|
||||||
|
if (this.response || this.className !== '_User') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var promise = Promise.resolve();
|
||||||
|
|
||||||
|
if (!this.query) {
|
||||||
|
var token = 'r:' + rack();
|
||||||
|
this.storage['token'] = token;
|
||||||
|
promise = promise.then(() => {
|
||||||
|
// TODO: Proper createdWith options, pass installationId
|
||||||
|
var sessionData = {
|
||||||
|
sessionToken: token,
|
||||||
|
user: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_User',
|
||||||
|
objectId: this.objectId()
|
||||||
|
},
|
||||||
|
createdWith: {
|
||||||
|
'action': 'login',
|
||||||
|
'authProvider': 'password'
|
||||||
|
},
|
||||||
|
restricted: false,
|
||||||
|
expiresAt: 0
|
||||||
|
};
|
||||||
|
var create = new RestWrite(this.config, Auth.master(this.config),
|
||||||
|
'_Session', null, sessionData);
|
||||||
|
return create.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(() => {
|
||||||
|
// Transform the password
|
||||||
|
if (!this.data.password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.query) {
|
||||||
|
this.storage['clearSessions'] = true;
|
||||||
|
}
|
||||||
|
return crypto.hash(this.data.password).then((hashedPassword) => {
|
||||||
|
this.data._hashed_password = hashedPassword;
|
||||||
|
delete this.data.password;
|
||||||
|
});
|
||||||
|
|
||||||
|
}).then(() => {
|
||||||
|
// Check for username uniqueness
|
||||||
|
if (!this.data.username) {
|
||||||
|
if (!this.query) {
|
||||||
|
// TODO: what's correct behavior here
|
||||||
|
this.data.username = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.config.database.find(
|
||||||
|
this.className, {
|
||||||
|
username: this.data.username,
|
||||||
|
objectId: {'$ne': this.objectId()}
|
||||||
|
}, {limit: 1}).then((results) => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.USERNAME_TAKEN,
|
||||||
|
'Account already exists for this username');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
if (!this.data.email) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Validate basic email address format
|
||||||
|
if (!this.data.email.match(/^.+@.+$/)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS,
|
||||||
|
'Email address format is invalid.');
|
||||||
|
}
|
||||||
|
// Check for email uniqueness
|
||||||
|
return this.config.database.find(
|
||||||
|
this.className, {
|
||||||
|
email: this.data.email,
|
||||||
|
objectId: {'$ne': this.objectId()}
|
||||||
|
}, {limit: 1}).then((results) => {
|
||||||
|
if (results.length > 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.EMAIL_TAKEN,
|
||||||
|
'Account already exists for this email ' +
|
||||||
|
'address');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles any followup logic
|
||||||
|
RestWrite.prototype.handleFollowup = function() {
|
||||||
|
if (this.storage && this.storage['clearSessions']) {
|
||||||
|
var sessionQuery = {
|
||||||
|
user: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_User',
|
||||||
|
objectId: this.objectId()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
delete this.storage['clearSessions'];
|
||||||
|
return this.config.database.destroy('_Session', sessionQuery)
|
||||||
|
.then(this.handleFollowup);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the _Role class specialness.
|
||||||
|
// Does nothing if this isn't a role object.
|
||||||
|
RestWrite.prototype.handleRole = function() {
|
||||||
|
if (this.response || this.className !== '_Role') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.auth.user && !this.auth.isMaster) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||||
|
'Session token required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.data.name) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_ROLE_NAME,
|
||||||
|
'Invalid role name.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the _Session class specialness.
|
||||||
|
// Does nothing if this isn't an installation object.
|
||||||
|
RestWrite.prototype.handleSession = function() {
|
||||||
|
if (this.response || this.className !== '_Session') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.auth.user && !this.auth.isMaster) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||||
|
'Session token required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Verify proper error to throw
|
||||||
|
if (this.data.ACL) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Cannot set ' +
|
||||||
|
'ACL on a Session.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.query && !this.auth.isMaster) {
|
||||||
|
var token = 'r:' + rack();
|
||||||
|
var sessionData = {
|
||||||
|
sessionToken: token,
|
||||||
|
user: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_User',
|
||||||
|
objectId: this.auth.user.id
|
||||||
|
},
|
||||||
|
createdWith: {
|
||||||
|
'action': 'create'
|
||||||
|
},
|
||||||
|
restricted: true,
|
||||||
|
expiresAt: 0
|
||||||
|
};
|
||||||
|
for (var key in this.data) {
|
||||||
|
if (key == 'objectId') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sessionData[key] = this.data[key];
|
||||||
|
}
|
||||||
|
var create = new RestWrite(this.config, Auth.master(this.config),
|
||||||
|
'_Session', null, sessionData);
|
||||||
|
return create.execute().then((results) => {
|
||||||
|
if (!results.response) {
|
||||||
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
|
||||||
|
'Error creating session.');
|
||||||
|
}
|
||||||
|
sessionData['objectId'] = results.response['objectId'];
|
||||||
|
this.response = {
|
||||||
|
status: 201,
|
||||||
|
location: results.location,
|
||||||
|
response: sessionData
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handles the _Installation class specialness.
|
||||||
|
// Does nothing if this isn't an installation object.
|
||||||
|
// If an installation is found, this can mutate this.query and turn a create
|
||||||
|
// into an update.
|
||||||
|
// Returns a promise for when we're done if it can't finish this tick.
|
||||||
|
RestWrite.prototype.handleInstallation = function() {
|
||||||
|
if (this.response || this.className !== '_Installation') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.query && !this.data.deviceToken && !this.data.installationId) {
|
||||||
|
throw new Parse.Error(135,
|
||||||
|
'at least one ID field (deviceToken, installationId) ' +
|
||||||
|
'must be specified in this operation');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.query && !this.data.deviceType) {
|
||||||
|
throw new Parse.Error(135,
|
||||||
|
'deviceType must be specified in this operation');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the device token is 64 characters long, we assume it is for iOS
|
||||||
|
// and lowercase it.
|
||||||
|
if (this.data.deviceToken && this.data.deviceToken.length == 64) {
|
||||||
|
this.data.deviceToken = this.data.deviceToken.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We may need installationId from headers, plumb through Auth?
|
||||||
|
// per installation_handler.go
|
||||||
|
|
||||||
|
// We lowercase the installationId if present
|
||||||
|
if (this.data.installationId) {
|
||||||
|
this.data.installationId = this.data.installationId.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data.deviceToken && this.data.deviceType == 'android') {
|
||||||
|
throw new Parse.Error(114,
|
||||||
|
'deviceToken may not be set for deviceType android');
|
||||||
|
}
|
||||||
|
|
||||||
|
var promise = Promise.resolve();
|
||||||
|
|
||||||
|
if (this.query && this.query.objectId) {
|
||||||
|
promise = promise.then(() => {
|
||||||
|
return this.config.database.find('_Installation', {
|
||||||
|
objectId: this.query.objectId
|
||||||
|
}, {}).then((results) => {
|
||||||
|
if (!results.length) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found for update.');
|
||||||
|
}
|
||||||
|
var existing = results[0];
|
||||||
|
if (this.data.installationId && existing.installationId &&
|
||||||
|
this.data.installationId !== existing.installationId) {
|
||||||
|
throw new Parse.Error(136,
|
||||||
|
'installationId may not be changed in this ' +
|
||||||
|
'operation');
|
||||||
|
}
|
||||||
|
if (this.data.deviceToken && existing.deviceToken &&
|
||||||
|
this.data.deviceToken !== existing.deviceToken &&
|
||||||
|
!this.data.installationId && !existing.installationId) {
|
||||||
|
throw new Parse.Error(136,
|
||||||
|
'deviceToken may not be changed in this ' +
|
||||||
|
'operation');
|
||||||
|
}
|
||||||
|
if (this.data.deviceType && this.data.deviceType &&
|
||||||
|
this.data.deviceType !== existing.deviceType) {
|
||||||
|
throw new Parse.Error(136,
|
||||||
|
'deviceType may not be changed in this ' +
|
||||||
|
'operation');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have installations for the installationId/deviceToken
|
||||||
|
var installationMatch;
|
||||||
|
var deviceTokenMatches = [];
|
||||||
|
promise = promise.then(() => {
|
||||||
|
if (this.data.installationId) {
|
||||||
|
return this.config.database.find('_Installation', {
|
||||||
|
'installationId': this.data.installationId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}).then((results) => {
|
||||||
|
if (results && results.length) {
|
||||||
|
// We only take the first match by installationId
|
||||||
|
installationMatch = results[0];
|
||||||
|
}
|
||||||
|
if (this.data.deviceToken) {
|
||||||
|
return this.config.database.find(
|
||||||
|
'_Installation',
|
||||||
|
{'deviceToken': this.data.deviceToken});
|
||||||
|
}
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}).then((results) => {
|
||||||
|
if (results) {
|
||||||
|
deviceTokenMatches = results;
|
||||||
|
}
|
||||||
|
if (!installationMatch) {
|
||||||
|
if (!deviceTokenMatches.length) {
|
||||||
|
return;
|
||||||
|
} else if (deviceTokenMatches.length == 1 &&
|
||||||
|
(!deviceTokenMatches[0]['installationId'] || !this.data.installationId)
|
||||||
|
) {
|
||||||
|
// Single match on device token but none on installationId, and either
|
||||||
|
// the passed object or the match is missing an installationId, so we
|
||||||
|
// can just return the match.
|
||||||
|
return deviceTokenMatches[0]['objectId'];
|
||||||
|
} else if (!this.data.installationId) {
|
||||||
|
throw new Parse.Error(132,
|
||||||
|
'Must specify installationId when deviceToken ' +
|
||||||
|
'matches multiple Installation objects');
|
||||||
|
} else {
|
||||||
|
// Multiple device token matches and we specified an installation ID,
|
||||||
|
// or a single match where both the passed and matching objects have
|
||||||
|
// an installation ID. Try cleaning out old installations that match
|
||||||
|
// the deviceToken, and return nil to signal that a new object should
|
||||||
|
// be created.
|
||||||
|
var delQuery = {
|
||||||
|
'deviceToken': this.data.deviceToken,
|
||||||
|
'installationId': {
|
||||||
|
'$ne': this.data.installationId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.data.appIdentifier) {
|
||||||
|
delQuery['appIdentifier'] = this.data.appIdentifier;
|
||||||
|
}
|
||||||
|
this.config.database.destroy('_Installation', delQuery);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (deviceTokenMatches.length == 1 &&
|
||||||
|
!deviceTokenMatches[0]['installationId']) {
|
||||||
|
// Exactly one device token match and it doesn't have an installation
|
||||||
|
// ID. This is the one case where we want to merge with the existing
|
||||||
|
// object.
|
||||||
|
var delQuery = {objectId: installationMatch.objectId};
|
||||||
|
return this.config.database.destroy('_Installation', delQuery)
|
||||||
|
.then(() => {
|
||||||
|
return deviceTokenMatches[0]['objectId'];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (this.data.deviceToken &&
|
||||||
|
installationMatch.deviceToken != this.data.deviceToken) {
|
||||||
|
// We're setting the device token on an existing installation, so
|
||||||
|
// we should try cleaning out old installations that match this
|
||||||
|
// device token.
|
||||||
|
var delQuery = {
|
||||||
|
'deviceToken': this.data.deviceToken,
|
||||||
|
'installationId': {
|
||||||
|
'$ne': this.data.installationId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (this.data.appIdentifier) {
|
||||||
|
delQuery['appIdentifier'] = this.data.appIdentifier;
|
||||||
|
}
|
||||||
|
this.config.database.destroy('_Installation', delQuery);
|
||||||
|
}
|
||||||
|
// In non-merge scenarios, just return the installation match id
|
||||||
|
return installationMatch.objectId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((objId) => {
|
||||||
|
if (objId) {
|
||||||
|
this.query = {objectId: objId};
|
||||||
|
delete this.data.objectId;
|
||||||
|
delete this.data.createdAt;
|
||||||
|
}
|
||||||
|
// TODO: Validate ops (add/remove on channels, $inc on badge, etc.)
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
RestWrite.prototype.runDatabaseOperation = function() {
|
||||||
|
if (this.response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.className === '_User' &&
|
||||||
|
this.query &&
|
||||||
|
!this.auth.couldUpdateUserId(this.query.objectId)) {
|
||||||
|
throw new Parse.Error(Parse.Error.SESSION_MISSING,
|
||||||
|
'cannot modify user ' + this.objectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add better detection for ACL, ensuring a user can't be locked from
|
||||||
|
// their own user record.
|
||||||
|
if (this.data.ACL && this.data.ACL['*unresolved']) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_ACL, 'Invalid ACL.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = {};
|
||||||
|
if (!this.auth.isMaster) {
|
||||||
|
options.acl = ['*'];
|
||||||
|
if (this.auth.user) {
|
||||||
|
options.acl.push(this.auth.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.query) {
|
||||||
|
// Run an update
|
||||||
|
return this.config.database.update(
|
||||||
|
this.className, this.query, this.data, options).then((resp) => {
|
||||||
|
this.response = resp;
|
||||||
|
this.response.updatedAt = this.updatedAt;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Run a create
|
||||||
|
return this.config.database.create(this.className, this.data, options)
|
||||||
|
.then(() => {
|
||||||
|
var resp = {
|
||||||
|
objectId: this.data.objectId,
|
||||||
|
createdAt: this.data.createdAt
|
||||||
|
};
|
||||||
|
if (this.storage['token']) {
|
||||||
|
resp.sessionToken = this.storage['token'];
|
||||||
|
}
|
||||||
|
this.response = {
|
||||||
|
status: 201,
|
||||||
|
response: resp,
|
||||||
|
location: this.location()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns nothing - doesn't wait for the trigger.
|
||||||
|
RestWrite.prototype.runAfterTrigger = function() {
|
||||||
|
var extraData = {className: this.className};
|
||||||
|
if (this.query && this.query.objectId) {
|
||||||
|
extraData.objectId = this.query.objectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the inflated object, different from beforeSave, originalData is not empty
|
||||||
|
// since developers can change data in the beforeSave.
|
||||||
|
var inflatedObject = triggers.inflate(extraData, this.originalData);
|
||||||
|
inflatedObject._finishFetch(this.data);
|
||||||
|
// Build the original object, we only do this for a update write.
|
||||||
|
var originalObject;
|
||||||
|
if (this.query && this.query.objectId) {
|
||||||
|
originalObject = triggers.inflate(extraData, this.originalData);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggers.maybeRunTrigger('afterSave', this.auth, inflatedObject, originalObject);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A helper to figure out what location this operation happens at.
|
||||||
|
RestWrite.prototype.location = function() {
|
||||||
|
var middle = (this.className === '_User' ? '/users/' :
|
||||||
|
'/classes/' + this.className + '/');
|
||||||
|
return this.config.mount + middle + this.data.objectId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A helper to get the object id for this operation.
|
||||||
|
// Because it could be either on the query or on the data
|
||||||
|
RestWrite.prototype.objectId = function() {
|
||||||
|
return this.data.objectId || this.query.objectId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a string that's usable as an object id.
|
||||||
|
// Probably unique. Good enough? Probably!
|
||||||
|
function newObjectId() {
|
||||||
|
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
|
||||||
|
'abcdefghijklmnopqrstuvwxyz' +
|
||||||
|
'0123456789');
|
||||||
|
var objectId = '';
|
||||||
|
for (var i = 0; i < 10; ++i) {
|
||||||
|
objectId += chars[Math.floor(Math.random() * chars.length)];
|
||||||
|
}
|
||||||
|
return objectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RestWrite;
|
||||||
347
Schema.js
Normal file
347
Schema.js
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
// This class handles schema validation, persistence, and modification.
|
||||||
|
//
|
||||||
|
// Each individual Schema object should be immutable. The helpers to
|
||||||
|
// do things with the Schema just return a new schema when the schema
|
||||||
|
// is changed.
|
||||||
|
//
|
||||||
|
// The canonical place to store this Schema is in the database itself,
|
||||||
|
// in a _SCHEMA collection. This is not the right way to do it for an
|
||||||
|
// open source framework, but it's backward compatible, so we're
|
||||||
|
// keeping it this way for now.
|
||||||
|
//
|
||||||
|
// In API-handling code, you should only use the Schema class via the
|
||||||
|
// ExportAdapter. This will let us replace the schema logic for
|
||||||
|
// different databases.
|
||||||
|
// TODO: hide all schema logic inside the database adapter.
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
var transform = require('./transform');
|
||||||
|
|
||||||
|
|
||||||
|
// Create a schema from a Mongo collection and the exported schema format.
|
||||||
|
// mongoSchema should be a list of objects, each with:
|
||||||
|
// '_id' indicates the className
|
||||||
|
// '_metadata' is ignored for now
|
||||||
|
// Everything else is expected to be a userspace field.
|
||||||
|
function Schema(collection, mongoSchema) {
|
||||||
|
this.collection = collection;
|
||||||
|
|
||||||
|
// this.data[className][fieldName] tells you the type of that field
|
||||||
|
this.data = {};
|
||||||
|
// this.perms[className][operation] tells you the acl-style permissions
|
||||||
|
this.perms = {};
|
||||||
|
|
||||||
|
for (var obj of mongoSchema) {
|
||||||
|
var className = null;
|
||||||
|
var classData = {};
|
||||||
|
var permsData = null;
|
||||||
|
for (var key in obj) {
|
||||||
|
var value = obj[key];
|
||||||
|
switch(key) {
|
||||||
|
case '_id':
|
||||||
|
className = value;
|
||||||
|
break;
|
||||||
|
case '_metadata':
|
||||||
|
if (value && value['class_permissions']) {
|
||||||
|
permsData = value['class_permissions'];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
classData[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (className) {
|
||||||
|
this.data[className] = classData;
|
||||||
|
if (permsData) {
|
||||||
|
this.perms[className] = permsData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a new Schema.
|
||||||
|
function load(collection) {
|
||||||
|
return collection.find({}, {}).toArray().then((mongoSchema) => {
|
||||||
|
return new Schema(collection, mongoSchema);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a new, reloaded schema.
|
||||||
|
Schema.prototype.reload = function() {
|
||||||
|
return load(this.collection);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise that resolves successfully to the new schema
|
||||||
|
// object.
|
||||||
|
// If 'freeze' is true, refuse to update the schema.
|
||||||
|
Schema.prototype.validateClassName = function(className, freeze) {
|
||||||
|
if (this.data[className]) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
if (freeze) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema is frozen, cannot add: ' + className);
|
||||||
|
}
|
||||||
|
// We don't have this class. Update the schema
|
||||||
|
return this.collection.insert([{_id: className}]).then(() => {
|
||||||
|
// The schema update succeeded. Reload the schema
|
||||||
|
return this.reload();
|
||||||
|
}, () => {
|
||||||
|
// The schema update failed. This can be okay - it might
|
||||||
|
// have failed because there's a race condition and a different
|
||||||
|
// client is making the exact same schema update that we want.
|
||||||
|
// So just reload the schema.
|
||||||
|
return this.reload();
|
||||||
|
}).then((schema) => {
|
||||||
|
// Ensure that the schema now validates
|
||||||
|
return schema.validateClassName(className, true);
|
||||||
|
}, (error) => {
|
||||||
|
// The schema still doesn't validate. Give up
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema class name does not revalidate');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns whether the schema knows the type of all these keys.
|
||||||
|
Schema.prototype.hasKeys = function(className, keys) {
|
||||||
|
for (var key of keys) {
|
||||||
|
if (!this.data[className] || !this.data[className][key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sets the Class-level permissions for a given className, which must
|
||||||
|
// exist.
|
||||||
|
Schema.prototype.setPermissions = function(className, perms) {
|
||||||
|
var query = {_id: className};
|
||||||
|
var update = {
|
||||||
|
_metadata: {
|
||||||
|
class_permissions: perms
|
||||||
|
}
|
||||||
|
};
|
||||||
|
update = {'$set': update};
|
||||||
|
return this.collection.findAndModify(query, {}, update, {}).then(() => {
|
||||||
|
// The update succeeded. Reload the schema
|
||||||
|
return this.reload();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a promise that resolves successfully to the new schema
|
||||||
|
// object if the provided className-key-type tuple is valid.
|
||||||
|
// The className must already be validated.
|
||||||
|
// If 'freeze' is true, refuse to update the schema for this field.
|
||||||
|
Schema.prototype.validateField = function(className, key, type, freeze) {
|
||||||
|
// Just to check that the key is valid
|
||||||
|
transform.transformKey(this, className, key);
|
||||||
|
|
||||||
|
var expected = this.data[className][key];
|
||||||
|
if (expected) {
|
||||||
|
if (expected === type) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
} else {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
'schema mismatch for ' + className + '.' + key +
|
||||||
|
'; expected ' + expected + ' but got ' + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (freeze) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema is frozen, cannot add ' + key + ' field');
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have this field, but if the value is null or undefined,
|
||||||
|
// we won't update the schema until we get a value with a type.
|
||||||
|
if (!type) {
|
||||||
|
return Promise.resolve(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'geopoint') {
|
||||||
|
// Make sure there are not other geopoint fields
|
||||||
|
for (var otherKey in this.data[className]) {
|
||||||
|
if (this.data[className][otherKey] === 'geopoint') {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
'there can only be one geopoint field in a class');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't have this field. Update the schema.
|
||||||
|
// Note that we use the $exists guard and $set to avoid race
|
||||||
|
// conditions in the database. This is important!
|
||||||
|
var query = {_id: className};
|
||||||
|
query[key] = {'$exists': false};
|
||||||
|
var update = {};
|
||||||
|
update[key] = type;
|
||||||
|
update = {'$set': update};
|
||||||
|
return this.collection.findAndModify(query, {}, update, {}).then(() => {
|
||||||
|
// The update succeeded. Reload the schema
|
||||||
|
return this.reload();
|
||||||
|
}, () => {
|
||||||
|
// The update failed. This can be okay - it might have been a race
|
||||||
|
// condition where another client updated the schema in the same
|
||||||
|
// way that we wanted to. So, just reload the schema
|
||||||
|
return this.reload();
|
||||||
|
}).then((schema) => {
|
||||||
|
// Ensure that the schema now validates
|
||||||
|
return schema.validateField(className, key, type, true);
|
||||||
|
}, (error) => {
|
||||||
|
// The schema still doesn't validate. Give up
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'schema key will not revalidate');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Given a schema promise, construct another schema promise that
|
||||||
|
// validates this field once the schema loads.
|
||||||
|
function thenValidateField(schemaPromise, className, key, type) {
|
||||||
|
return schemaPromise.then((schema) => {
|
||||||
|
return schema.validateField(className, key, type);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates an object provided in REST format.
|
||||||
|
// Returns a promise that resolves to the new schema if this object is
|
||||||
|
// valid.
|
||||||
|
Schema.prototype.validateObject = function(className, object) {
|
||||||
|
var geocount = 0;
|
||||||
|
var promise = this.validateClassName(className);
|
||||||
|
for (var key in object) {
|
||||||
|
var expected = getType(object[key]);
|
||||||
|
if (expected === 'geopoint') {
|
||||||
|
geocount++;
|
||||||
|
}
|
||||||
|
if (geocount > 1) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INCORRECT_TYPE,
|
||||||
|
'there can only be one geopoint field in a class');
|
||||||
|
}
|
||||||
|
if (!expected) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
promise = thenValidateField(promise, className, key, expected);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validates an operation passes class-level-permissions set in the schema
|
||||||
|
Schema.prototype.validatePermission = function(className, aclGroup, operation) {
|
||||||
|
if (!this.perms[className] || !this.perms[className][operation]) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
var perms = this.perms[className][operation];
|
||||||
|
// Handle the public scenario quickly
|
||||||
|
if (perms['*']) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
// Check permissions against the aclGroup provided (array of userId/roles)
|
||||||
|
var found = false;
|
||||||
|
for (var i = 0; i < aclGroup.length && !found; i++) {
|
||||||
|
if (perms[aclGroup[i]]) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
// TODO: Verify correct error code
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Permission denied for this action.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns the expected type for a className+key combination
|
||||||
|
// or undefined if the schema is not set
|
||||||
|
Schema.prototype.getExpectedType = function(className, key) {
|
||||||
|
if (this.data && this.data[className]) {
|
||||||
|
return this.data[className][key];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to check if a field is a pointer, returns true or false.
|
||||||
|
Schema.prototype.isPointer = function(className, key) {
|
||||||
|
var expected = this.getExpectedType(className, key);
|
||||||
|
if (expected && expected.charAt(0) == '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gets the type from a REST API formatted object, where 'type' is
|
||||||
|
// extended past javascript types to include the rest of the Parse
|
||||||
|
// type system.
|
||||||
|
// The output should be a valid schema value.
|
||||||
|
// TODO: ensure that this is compatible with the format used in Open DB
|
||||||
|
function getType(obj) {
|
||||||
|
var type = typeof obj;
|
||||||
|
switch(type) {
|
||||||
|
case 'boolean':
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
return type;
|
||||||
|
case 'object':
|
||||||
|
if (!obj) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getObjectType(obj);
|
||||||
|
case 'function':
|
||||||
|
case 'symbol':
|
||||||
|
case 'undefined':
|
||||||
|
default:
|
||||||
|
throw 'bad obj: ' + obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This gets the type for non-JSON types like pointers and files, but
|
||||||
|
// also gets the appropriate type for $ operators.
|
||||||
|
// Returns null if the type is unknown.
|
||||||
|
function getObjectType(obj) {
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return 'array';
|
||||||
|
}
|
||||||
|
if (obj.__type === 'Pointer' && obj.className) {
|
||||||
|
return '*' + obj.className;
|
||||||
|
}
|
||||||
|
if (obj.__type === 'File' && obj.url && obj.name) {
|
||||||
|
return 'file';
|
||||||
|
}
|
||||||
|
if (obj.__type === 'Date' && obj.iso) {
|
||||||
|
return 'date';
|
||||||
|
}
|
||||||
|
if (obj.__type == 'GeoPoint' &&
|
||||||
|
obj.latitude != null &&
|
||||||
|
obj.longitude != null) {
|
||||||
|
return 'geopoint';
|
||||||
|
}
|
||||||
|
if (obj['$ne']) {
|
||||||
|
return getObjectType(obj['$ne']);
|
||||||
|
}
|
||||||
|
if (obj.__op) {
|
||||||
|
switch(obj.__op) {
|
||||||
|
case 'Increment':
|
||||||
|
return 'number';
|
||||||
|
case 'Delete':
|
||||||
|
return null;
|
||||||
|
case 'Add':
|
||||||
|
case 'AddUnique':
|
||||||
|
case 'Remove':
|
||||||
|
return 'array';
|
||||||
|
case 'AddRelation':
|
||||||
|
case 'RemoveRelation':
|
||||||
|
return 'relation<' + obj.objects[0].className + '>';
|
||||||
|
case 'Batch':
|
||||||
|
return getObjectType(obj.ops[0]);
|
||||||
|
default:
|
||||||
|
throw 'unexpected op: ' + obj.__op;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
load: load
|
||||||
|
};
|
||||||
20
analytics.js
Normal file
20
analytics.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// analytics.js
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
|
||||||
|
// Returns a promise that resolves to an empty object response
|
||||||
|
function ignoreAndSucceed(req) {
|
||||||
|
return Promise.resolve({
|
||||||
|
response: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST','/events/AppOpened', ignoreAndSucceed);
|
||||||
|
router.route('POST','/events/:eventName', ignoreAndSucceed);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
72
batch.js
Normal file
72
batch.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// These methods handle batch requests.
|
||||||
|
var batchPath = '/batch';
|
||||||
|
|
||||||
|
// Mounts a batch-handler onto a PromiseRouter.
|
||||||
|
function mountOnto(router) {
|
||||||
|
router.route('POST', batchPath, (req) => {
|
||||||
|
return handleBatch(router, req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
// TODO: pass along auth correctly
|
||||||
|
function handleBatch(router, req) {
|
||||||
|
if (!req.body.requests instanceof Array) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'requests must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The batch paths are all from the root of our domain.
|
||||||
|
// That means they include the API prefix, that the API is mounted
|
||||||
|
// to. However, our promise router does not route the api prefix. So
|
||||||
|
// we need to figure out the API prefix, so that we can strip it
|
||||||
|
// from all the subrequests.
|
||||||
|
if (!req.originalUrl.endsWith(batchPath)) {
|
||||||
|
throw 'internal routing problem - expected url to end with batch';
|
||||||
|
}
|
||||||
|
var apiPrefixLength = req.originalUrl.length - batchPath.length;
|
||||||
|
var apiPrefix = req.originalUrl.slice(0, apiPrefixLength);
|
||||||
|
|
||||||
|
var promises = [];
|
||||||
|
for (var restRequest of req.body.requests) {
|
||||||
|
// The routablePath is the path minus the api prefix
|
||||||
|
if (restRequest.path.slice(0, apiPrefixLength) != apiPrefix) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'cannot route batch path ' + restRequest.path);
|
||||||
|
}
|
||||||
|
var routablePath = restRequest.path.slice(apiPrefixLength);
|
||||||
|
|
||||||
|
// Use the router to figure out what handler to use
|
||||||
|
var match = router.match(restRequest.method, routablePath);
|
||||||
|
if (!match) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'cannot route ' + restRequest.method + ' ' + routablePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a request that we can send to a handler
|
||||||
|
var request = {
|
||||||
|
body: restRequest.body,
|
||||||
|
params: match.params,
|
||||||
|
config: req.config,
|
||||||
|
auth: req.auth
|
||||||
|
};
|
||||||
|
|
||||||
|
promises.push(match.handler(request).then((response) => {
|
||||||
|
return {success: response.response};
|
||||||
|
}, (error) => {
|
||||||
|
return {error: {code: error.code, error: error.message}};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises).then((results) => {
|
||||||
|
return {response: results};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mountOnto: mountOnto
|
||||||
|
};
|
||||||
37
cache.js
Normal file
37
cache.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
var apps = {};
|
||||||
|
var stats = {};
|
||||||
|
var isLoaded = false;
|
||||||
|
var users = {};
|
||||||
|
|
||||||
|
function getApp(app, callback) {
|
||||||
|
if (apps[app]) return callback(true, apps[app]);
|
||||||
|
return callback(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStat(key, value) {
|
||||||
|
stats[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUser(sessionToken) {
|
||||||
|
if (users[sessionToken]) return users[sessionToken];
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUser(sessionToken, userObject) {
|
||||||
|
users[sessionToken] = userObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearUser(sessionToken) {
|
||||||
|
delete users[sessionToken];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apps: apps,
|
||||||
|
stats: stats,
|
||||||
|
isLoaded: isLoaded,
|
||||||
|
getApp: getApp,
|
||||||
|
updateStat: updateStat,
|
||||||
|
clearUser: clearUser,
|
||||||
|
getUser: getUser,
|
||||||
|
setUser: setUser
|
||||||
|
};
|
||||||
88
classes.js
Normal file
88
classes.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// These methods handle the 'classes' routes.
|
||||||
|
// Methods of the form 'handleX' return promises and are intended to
|
||||||
|
// be used with the PromiseRouter.
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
// Returns a promise that resolves to a {response} object.
|
||||||
|
function handleFind(req) {
|
||||||
|
var options = {};
|
||||||
|
if (req.body.skip) {
|
||||||
|
options.skip = Number(req.body.skip);
|
||||||
|
}
|
||||||
|
if (req.body.limit) {
|
||||||
|
options.limit = Number(req.body.limit);
|
||||||
|
}
|
||||||
|
if (req.body.order) {
|
||||||
|
options.order = String(req.body.order);
|
||||||
|
}
|
||||||
|
if (req.body.count) {
|
||||||
|
options.count = true;
|
||||||
|
}
|
||||||
|
if (typeof req.body.keys == 'string') {
|
||||||
|
options.keys = req.body.keys;
|
||||||
|
}
|
||||||
|
if (req.body.include) {
|
||||||
|
options.include = String(req.body.include);
|
||||||
|
}
|
||||||
|
if (req.body.redirectClassNameForKey) {
|
||||||
|
options.redirectClassNameForKey = String(req.body.redirectClassNameForKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest.find(req.config, req.auth,
|
||||||
|
req.params.className, req.body.where, options)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {status, response, location} object.
|
||||||
|
function handleCreate(req) {
|
||||||
|
return rest.create(req.config, req.auth,
|
||||||
|
req.params.className, req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleGet(req) {
|
||||||
|
return rest.find(req.config, req.auth,
|
||||||
|
req.params.className, {objectId: req.params.objectId})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
} else {
|
||||||
|
return {response: response.results[0]};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleDelete(req) {
|
||||||
|
return rest.del(req.config, req.auth,
|
||||||
|
req.params.className, req.params.objectId)
|
||||||
|
.then(() => {
|
||||||
|
return {response: {}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleUpdate(req) {
|
||||||
|
return rest.update(req.config, req.auth,
|
||||||
|
req.params.className, req.params.objectId, req.body)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('GET', '/classes/:className', handleFind);
|
||||||
|
router.route('POST', '/classes/:className', handleCreate);
|
||||||
|
router.route('GET', '/classes/:className/:objectId', handleGet);
|
||||||
|
router.route('DELETE', '/classes/:className/:objectId', handleDelete);
|
||||||
|
router.route('PUT', '/classes/:className/:objectId', handleUpdate);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
80
cloud/main.js
Normal file
80
cloud/main.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
Parse.Cloud.define('hello', function(req, res) {
|
||||||
|
res.success('Hello world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforeSave('BeforeSaveFailure', function(req, res) {
|
||||||
|
res.error('You shall not pass!');
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) {
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) {
|
||||||
|
req.object.set('foo', 'baz');
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.afterSave('AfterSaveTest', function(req) {
|
||||||
|
var obj = new Parse.Object('AfterSaveProof');
|
||||||
|
obj.set('proof', req.object.id);
|
||||||
|
obj.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) {
|
||||||
|
res.error('Nope');
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) {
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.afterDelete('AfterDeleteTest', function(req) {
|
||||||
|
var obj = new Parse.Object('AfterDeleteProof');
|
||||||
|
obj.set('proof', req.object.id);
|
||||||
|
obj.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) {
|
||||||
|
if (req.user && req.user.id) {
|
||||||
|
res.success();
|
||||||
|
} else {
|
||||||
|
res.error('No user present on request object for beforeSave.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.afterSave('SaveTriggerUser', function(req) {
|
||||||
|
if (!req.user || !req.user.id) {
|
||||||
|
console.log('No user present on request object for afterSave.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.define('foo', function(req, res) {
|
||||||
|
res.success({
|
||||||
|
object: {
|
||||||
|
__type: 'Object',
|
||||||
|
className: 'Foo',
|
||||||
|
objectId: '123',
|
||||||
|
x: 2,
|
||||||
|
relation: {
|
||||||
|
__type: 'Object',
|
||||||
|
className: 'Bar',
|
||||||
|
objectId: '234',
|
||||||
|
x: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
array: [{
|
||||||
|
__type: 'Object',
|
||||||
|
className: 'Bar',
|
||||||
|
objectId: '345',
|
||||||
|
x: 2
|
||||||
|
}],
|
||||||
|
a: 2
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Parse.Cloud.define('bar', function(req, res) {
|
||||||
|
res.error('baz');
|
||||||
|
});
|
||||||
35
crypto.js
Normal file
35
crypto.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Tools for encrypting and decrypting passwords.
|
||||||
|
// Basically promise-friendly wrappers for bcrypt.
|
||||||
|
var bcrypt = require('bcrypt');
|
||||||
|
|
||||||
|
// Returns a promise for a hashed password string.
|
||||||
|
function hash(password) {
|
||||||
|
return new Promise(function(fulfill, reject) {
|
||||||
|
bcrypt.hash(password, 8, function(err, hashedPassword) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
fulfill(hashedPassword);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for whether this password compares to equal this
|
||||||
|
// hashed password.
|
||||||
|
function compare(password, hashedPassword) {
|
||||||
|
return new Promise(function(fulfill, reject) {
|
||||||
|
bcrypt.compare(password, hashedPassword, function(err, success) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
fulfill(success);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
hash: hash,
|
||||||
|
compare: compare
|
||||||
|
};
|
||||||
52
facebook.js
Normal file
52
facebook.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Helper functions for accessing the Facebook Graph API.
|
||||||
|
var https = require('https');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this user id is valid.
|
||||||
|
function validateUserId(userId, access_token) {
|
||||||
|
return graphRequest('me?fields=id&access_token=' + access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.id == userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Facebook auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that fulfills iff this app id is valid.
|
||||||
|
function validateAppId(appId, access_token) {
|
||||||
|
return graphRequest('app?access_token=' + access_token)
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.id == appId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Facebook auth is invalid for this user.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// A promisey wrapper for FB graph requests.
|
||||||
|
function graphRequest(path) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
https.get('https://graph.facebook.com/v2.5/' + path, function(res) {
|
||||||
|
var data = '';
|
||||||
|
res.on('data', function(chunk) {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', function() {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
}).on('error', function(e) {
|
||||||
|
reject('Failed to validate this access token with Facebook.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
validateAppId: validateAppId,
|
||||||
|
validateUserId: validateUserId
|
||||||
|
};
|
||||||
89
files.js
Normal file
89
files.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// files.js
|
||||||
|
|
||||||
|
var bodyParser = require('body-parser'),
|
||||||
|
Config = require('./Config'),
|
||||||
|
express = require('express'),
|
||||||
|
FilesAdapter = require('./FilesAdapter'),
|
||||||
|
middlewares = require('./middlewares.js'),
|
||||||
|
mime = require('mime'),
|
||||||
|
Parse = require('parse/node').Parse,
|
||||||
|
path = require('path'),
|
||||||
|
rack = require('hat').rack();
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
var processCreate = function(req, res, next) {
|
||||||
|
if (!req.body || !req.body.length) {
|
||||||
|
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||||
|
'Invalid file upload.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.params.filename.length > 128) {
|
||||||
|
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||||
|
'Filename too long.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||||
|
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||||
|
'Filename contains invalid characters.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a content-type is included, we'll add an extension so we can
|
||||||
|
// return the same content-type.
|
||||||
|
var extension = '';
|
||||||
|
var hasExtension = req.params.filename.indexOf('.') > 0;
|
||||||
|
var contentType = req.get('Content-type');
|
||||||
|
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||||
|
extension = '.' + mime.extension(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = rack() + '_' + req.params.filename + extension;
|
||||||
|
FilesAdapter.getAdapter().create(req.config, filename, req.body)
|
||||||
|
.then(() => {
|
||||||
|
res.status(201);
|
||||||
|
var location = (req.protocol + '://' + req.get('host') +
|
||||||
|
path.dirname(req.originalUrl) + '/' +
|
||||||
|
req.config.applicationId + '/' +
|
||||||
|
encodeURIComponent(filename));
|
||||||
|
res.set('Location', location);
|
||||||
|
res.json({ url: location, name: filename });
|
||||||
|
}).catch((error) => {
|
||||||
|
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||||
|
'Could not store file.'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var processGet = function(req, res) {
|
||||||
|
var config = new Config(req.params.appId);
|
||||||
|
FilesAdapter.getAdapter().get(config, req.params.filename)
|
||||||
|
.then((data) => {
|
||||||
|
res.status(200);
|
||||||
|
var contentType = mime.lookup(req.params.filename);
|
||||||
|
res.set('Content-type', contentType);
|
||||||
|
res.end(data);
|
||||||
|
}).catch((error) => {
|
||||||
|
res.status(404);
|
||||||
|
res.set('Content-type', 'text/plain');
|
||||||
|
res.end('File not found.');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
router.get('/files/:appId/:filename', processGet);
|
||||||
|
|
||||||
|
router.post('/files', function(req, res, next) {
|
||||||
|
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||||
|
'Filename not provided.'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: do we need to allow crossdomain and method override?
|
||||||
|
router.post('/files/:filename',
|
||||||
|
bodyParser.raw({type: '*/*'}),
|
||||||
|
middlewares.handleParseHeaders,
|
||||||
|
processCreate);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
router: router
|
||||||
|
};
|
||||||
43
functions.js
Normal file
43
functions.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// functions.js
|
||||||
|
|
||||||
|
var express = require('express'),
|
||||||
|
Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
function handleCloudFunction(req) {
|
||||||
|
// TODO: set user from req.auth
|
||||||
|
if (Parse.Cloud.Functions[req.params.functionName]) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var response = createResponseObject(resolve, reject);
|
||||||
|
var request = {
|
||||||
|
params: req.body || {}
|
||||||
|
};
|
||||||
|
Parse.Cloud.Functions[req.params.functionName](request, response);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResponseObject(resolve, reject) {
|
||||||
|
return {
|
||||||
|
success: function(result) {
|
||||||
|
resolve({
|
||||||
|
response: {
|
||||||
|
result: result
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: function(error) {
|
||||||
|
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST', '/functions/:functionName', handleCloudFunction);
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
173
index.js
Normal file
173
index.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// ParseServer - open-source compatible API Server for Parse apps
|
||||||
|
|
||||||
|
var batch = require('./batch'),
|
||||||
|
bodyParser = require('body-parser'),
|
||||||
|
cache = require('./cache'),
|
||||||
|
DatabaseAdapter = require('./DatabaseAdapter'),
|
||||||
|
express = require('express'),
|
||||||
|
FilesAdapter = require('./FilesAdapter'),
|
||||||
|
middlewares = require('./middlewares'),
|
||||||
|
multer = require('multer'),
|
||||||
|
Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
request = require('request');
|
||||||
|
|
||||||
|
// Mutate the Parse object to add the Cloud Code handlers
|
||||||
|
addParseCloud();
|
||||||
|
|
||||||
|
// ParseServer works like a constructor of an express app.
|
||||||
|
// The args that we understand are:
|
||||||
|
// "databaseAdapter": a class like ExportAdapter providing create, find,
|
||||||
|
// update, and delete
|
||||||
|
// "filesAdapter": a class like GridStoreAdapter providing create, get,
|
||||||
|
// and delete
|
||||||
|
// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us
|
||||||
|
// what database this Parse API connects to.
|
||||||
|
// "cloud": relative location to cloud code to require
|
||||||
|
// "appId": the application id to host
|
||||||
|
// "masterKey": the master key for requests to this app
|
||||||
|
// "collectionPrefix": optional prefix for database collection names
|
||||||
|
// "fileKey": optional key from Parse dashboard for supporting older files
|
||||||
|
// hosted by Parse
|
||||||
|
// "clientKey": optional key from Parse dashboard
|
||||||
|
// "dotNetKey": optional key from Parse dashboard
|
||||||
|
// "restAPIKey": optional key from Parse dashboard
|
||||||
|
// "javascriptKey": optional key from Parse dashboard
|
||||||
|
function ParseServer(args) {
|
||||||
|
if (!args.appId || !args.masterKey) {
|
||||||
|
throw 'You must provide an appId and masterKey!';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.databaseAdapter) {
|
||||||
|
DatabaseAdapter.setAdapter(args.databaseAdapter);
|
||||||
|
}
|
||||||
|
if (args.filesAdapter) {
|
||||||
|
FilesAdapter.setAdapter(args.filesAdapter);
|
||||||
|
}
|
||||||
|
if (args.databaseURI) {
|
||||||
|
DatabaseAdapter.setDatabaseURI(args.databaseURI);
|
||||||
|
}
|
||||||
|
if (args.cloud) {
|
||||||
|
addParseCloud();
|
||||||
|
require(args.cloud);
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.apps[args.appId] = {
|
||||||
|
masterKey: args.masterKey,
|
||||||
|
collectionPrefix: args.collectionPrefix || '',
|
||||||
|
clientKey: args.clientKey || '',
|
||||||
|
javascriptKey: args.javascriptKey || '',
|
||||||
|
dotNetKey: args.dotNetKey || '',
|
||||||
|
restAPIKey: args.restAPIKey || '',
|
||||||
|
fileKey: args.fileKey || 'invalid-file-key'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize the node client SDK automatically
|
||||||
|
Parse.initialize(args.appId, args.javascriptKey || '', args.masterKey);
|
||||||
|
|
||||||
|
// This app serves the Parse API directly.
|
||||||
|
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
|
||||||
|
var api = express();
|
||||||
|
|
||||||
|
// File handling needs to be before default middlewares are applied
|
||||||
|
api.use('/', require('./files').router);
|
||||||
|
|
||||||
|
// TODO: separate this from the regular ParseServer object
|
||||||
|
if (process.env.TESTING == 1) {
|
||||||
|
console.log('enabling integration testing-routes');
|
||||||
|
api.use('/', require('./testing-routes').router);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.use(bodyParser.json({ 'type': '*/*' }));
|
||||||
|
api.use(middlewares.allowCrossDomain);
|
||||||
|
api.use(middlewares.allowMethodOverride);
|
||||||
|
api.use(middlewares.handleParseHeaders);
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
router.merge(require('./classes'));
|
||||||
|
router.merge(require('./users'));
|
||||||
|
router.merge(require('./sessions'));
|
||||||
|
router.merge(require('./roles'));
|
||||||
|
router.merge(require('./analytics'));
|
||||||
|
router.merge(require('./push'));
|
||||||
|
router.merge(require('./installations'));
|
||||||
|
router.merge(require('./functions'));
|
||||||
|
|
||||||
|
batch.mountOnto(router);
|
||||||
|
|
||||||
|
router.mountOnto(api);
|
||||||
|
|
||||||
|
api.use(middlewares.handleParseErrors);
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addParseCloud() {
|
||||||
|
Parse.Cloud.Functions = {};
|
||||||
|
Parse.Cloud.Triggers = {
|
||||||
|
beforeSave: {},
|
||||||
|
beforeDelete: {},
|
||||||
|
afterSave: {},
|
||||||
|
afterDelete: {}
|
||||||
|
};
|
||||||
|
Parse.Cloud.define = function(functionName, handler) {
|
||||||
|
Parse.Cloud.Functions[functionName] = handler;
|
||||||
|
};
|
||||||
|
Parse.Cloud.beforeSave = function(parseClass, handler) {
|
||||||
|
var className = getClassName(parseClass);
|
||||||
|
Parse.Cloud.Triggers.beforeSave[className] = handler;
|
||||||
|
};
|
||||||
|
Parse.Cloud.beforeDelete = function(parseClass, handler) {
|
||||||
|
var className = getClassName(parseClass);
|
||||||
|
Parse.Cloud.Triggers.beforeDelete[className] = handler;
|
||||||
|
};
|
||||||
|
Parse.Cloud.afterSave = function(parseClass, handler) {
|
||||||
|
var className = getClassName(parseClass);
|
||||||
|
Parse.Cloud.Triggers.afterSave[className] = handler;
|
||||||
|
};
|
||||||
|
Parse.Cloud.afterDelete = function(parseClass, handler) {
|
||||||
|
var className = getClassName(parseClass);
|
||||||
|
Parse.Cloud.Triggers.afterDelete[className] = handler;
|
||||||
|
};
|
||||||
|
Parse.Cloud.httpRequest = function(options) {
|
||||||
|
var promise = new Parse.Promise();
|
||||||
|
var callbacks = {
|
||||||
|
success: options.success,
|
||||||
|
error: options.error
|
||||||
|
};
|
||||||
|
delete options.success;
|
||||||
|
delete options.error;
|
||||||
|
if (options.uri && !options.url) {
|
||||||
|
options.uri = options.url;
|
||||||
|
delete options.url;
|
||||||
|
}
|
||||||
|
request(options, (error, response, body) => {
|
||||||
|
if (error) {
|
||||||
|
if (callbacks.error) {
|
||||||
|
return callbacks.error(error);
|
||||||
|
}
|
||||||
|
return promise.reject(error);
|
||||||
|
} else {
|
||||||
|
if (callbacks.success) {
|
||||||
|
return callbacks.success(body);
|
||||||
|
}
|
||||||
|
return promise.resolve(body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
global.Parse = Parse;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClassName(parseClass) {
|
||||||
|
if (parseClass && parseClass.className) {
|
||||||
|
return parseClass.className;
|
||||||
|
}
|
||||||
|
return parseClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ParseServer: ParseServer
|
||||||
|
};
|
||||||
|
|
||||||
80
installations.js
Normal file
80
installations.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// installations.js
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
var PromiseRouter = require('./PromiseRouter');
|
||||||
|
var rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
|
||||||
|
// Returns a promise for a {status, response, location} object.
|
||||||
|
function handleCreate(req) {
|
||||||
|
return rest.create(req.config,
|
||||||
|
req.auth, '_Installation', req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves to a {response} object.
|
||||||
|
function handleFind(req) {
|
||||||
|
var options = {};
|
||||||
|
if (req.body.skip) {
|
||||||
|
options.skip = Number(req.body.skip);
|
||||||
|
}
|
||||||
|
if (req.body.limit) {
|
||||||
|
options.limit = Number(req.body.limit);
|
||||||
|
}
|
||||||
|
if (req.body.order) {
|
||||||
|
options.order = String(req.body.order);
|
||||||
|
}
|
||||||
|
if (req.body.count) {
|
||||||
|
options.count = true;
|
||||||
|
}
|
||||||
|
if (req.body.include) {
|
||||||
|
options.include = String(req.body.include);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest.find(req.config, req.auth,
|
||||||
|
'_Installation', req.body.where, options)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleGet(req) {
|
||||||
|
return rest.find(req.config, req.auth, '_Installation',
|
||||||
|
{objectId: req.params.objectId})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
} else {
|
||||||
|
return {response: response.results[0]};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleUpdate(req) {
|
||||||
|
return rest.update(req.config, req.auth,
|
||||||
|
'_Installation', req.params.objectId, req.body)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleDelete(req) {
|
||||||
|
return rest.del(req.config, req.auth,
|
||||||
|
'_Installation', req.params.objectId)
|
||||||
|
.then(() => {
|
||||||
|
return {response: {}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST','/installations', handleCreate);
|
||||||
|
router.route('GET','/installations', handleFind);
|
||||||
|
router.route('GET','/installations/:objectId', handleGet);
|
||||||
|
router.route('PUT','/installations/:objectId', handleUpdate);
|
||||||
|
router.route('DELETE','/installations/:objectId', handleDelete);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
6
jsconfig.json
Normal file
6
jsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "commonjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
192
middlewares.js
Normal file
192
middlewares.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
var auth = require('./Auth');
|
||||||
|
var cache = require('./cache');
|
||||||
|
var Config = require('./Config');
|
||||||
|
|
||||||
|
// Checks that the request is authorized for this app and checks user
|
||||||
|
// auth too.
|
||||||
|
// The bodyparser should run before this middleware.
|
||||||
|
// Adds info to the request:
|
||||||
|
// req.config - the Config for this app
|
||||||
|
// req.auth - the Auth for this request
|
||||||
|
function handleParseHeaders(req, res, next) {
|
||||||
|
var mountPathLength = req.originalUrl.length - req.url.length;
|
||||||
|
var mountPath = req.originalUrl.slice(0, mountPathLength);
|
||||||
|
var mount = req.protocol + '://' + req.get('host') + mountPath;
|
||||||
|
|
||||||
|
var info = {
|
||||||
|
appId: req.get('X-Parse-Application-Id'),
|
||||||
|
sessionToken: req.get('X-Parse-Session-Token'),
|
||||||
|
masterKey: req.get('X-Parse-Master-Key'),
|
||||||
|
installationId: req.get('X-Parse-Installation-Id'),
|
||||||
|
clientKey: req.get('X-Parse-Client-Key'),
|
||||||
|
javascriptKey: req.get('X-Parse-Javascript-Key'),
|
||||||
|
dotNetKey: req.get('X-Parse-Windows-Key'),
|
||||||
|
restAPIKey: req.get('X-Parse-REST-API-Key')
|
||||||
|
};
|
||||||
|
|
||||||
|
var fileViaJSON = false;
|
||||||
|
|
||||||
|
if (!info.appId || !cache.apps[info.appId]) {
|
||||||
|
// See if we can find the app id on the body.
|
||||||
|
if (req.body instanceof Buffer) {
|
||||||
|
// The only chance to find the app id is if this is a file
|
||||||
|
// upload that actually is a JSON body. So try to parse it.
|
||||||
|
req.body = JSON.parse(req.body);
|
||||||
|
fileViaJSON = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.body && req.body._ApplicationId
|
||||||
|
&& cache.apps[req.body._ApplicationId]
|
||||||
|
&& (
|
||||||
|
!info.masterKey
|
||||||
|
||
|
||||||
|
cache.apps[req.body._ApplicationId]['masterKey'] === info.masterKey)
|
||||||
|
) {
|
||||||
|
info.appId = req.body._ApplicationId;
|
||||||
|
info.javascriptKey = req.body._JavaScriptKey || '';
|
||||||
|
delete req.body._ApplicationId;
|
||||||
|
delete req.body._JavaScriptKey;
|
||||||
|
// TODO: test that the REST API formats generated by the other
|
||||||
|
// SDKs are handled ok
|
||||||
|
if (req.body._ClientVersion) {
|
||||||
|
info.clientVersion = req.body._ClientVersion;
|
||||||
|
delete req.body._ClientVersion;
|
||||||
|
}
|
||||||
|
if (req.body._InstallationId) {
|
||||||
|
info.installationId = req.body._InstallationId;
|
||||||
|
delete req.body._InstallationId;
|
||||||
|
}
|
||||||
|
if (req.body._SessionToken) {
|
||||||
|
info.sessionToken = req.body._SessionToken;
|
||||||
|
delete req.body._SessionToken;
|
||||||
|
}
|
||||||
|
if (req.body._MasterKey) {
|
||||||
|
info.masterKey = req.body._MasterKey;
|
||||||
|
delete req.body._MasterKey;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return invalidRequest(req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileViaJSON) {
|
||||||
|
// We need to repopulate req.body with a buffer
|
||||||
|
var base64 = req.body.base64;
|
||||||
|
req.body = new Buffer(base64, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
info.app = cache.apps[info.appId];
|
||||||
|
req.config = new Config(info.appId, mount);
|
||||||
|
req.database = req.config.database;
|
||||||
|
req.info = info;
|
||||||
|
|
||||||
|
var isMaster = (info.masterKey === req.config.masterKey);
|
||||||
|
|
||||||
|
if (isMaster) {
|
||||||
|
req.auth = new auth.Auth(req.config, true);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client keys are not required in parse-server, but if any have been configured in the server, validate them
|
||||||
|
// to preserve original behavior.
|
||||||
|
var keyRequired = (req.config.clientKey
|
||||||
|
|| req.config.javascriptKey
|
||||||
|
|| req.config.dotNetKey
|
||||||
|
|| req.config.restAPIKey);
|
||||||
|
var keyHandled = false;
|
||||||
|
if (keyRequired
|
||||||
|
&& ((info.clientKey && req.config.clientKey && info.clientKey === req.config.clientKey)
|
||||||
|
|| (info.javascriptKey && req.config.javascriptKey && info.javascriptKey === req.config.javascriptKey)
|
||||||
|
|| (info.dotNetKey && req.config.dotNetKey && info.dotNetKey === req.config.dotNetKey)
|
||||||
|
|| (info.restAPIKey && req.config.restAPIKey && info.restAPIKey === req.config.restAPIKey)
|
||||||
|
)) {
|
||||||
|
keyHandled = true;
|
||||||
|
}
|
||||||
|
if (keyRequired && !keyHandled) {
|
||||||
|
return invalidRequest(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.sessionToken) {
|
||||||
|
req.auth = new auth.Auth(req.config, false);
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth.getAuthForSessionToken(
|
||||||
|
req.config, info.sessionToken).then((auth) => {
|
||||||
|
if (auth) {
|
||||||
|
req.auth = auth;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
// TODO: Determine the correct error scenario.
|
||||||
|
console.log(error);
|
||||||
|
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowCrossDomain = function(req, res, next) {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*');
|
||||||
|
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
|
||||||
|
res.header('Access-Control-Allow-Headers', '*');
|
||||||
|
|
||||||
|
// intercept OPTIONS method
|
||||||
|
if ('OPTIONS' == req.method) {
|
||||||
|
res.send(200);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var allowMethodOverride = function(req, res, next) {
|
||||||
|
if (req.method === 'POST' && req.body._method) {
|
||||||
|
req.originalMethod = req.method;
|
||||||
|
req.method = req.body._method;
|
||||||
|
delete req.body._method;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
var handleParseErrors = function(err, req, res, next) {
|
||||||
|
if (err instanceof Parse.Error) {
|
||||||
|
var httpStatus;
|
||||||
|
|
||||||
|
// TODO: fill out this mapping
|
||||||
|
switch (err.code) {
|
||||||
|
case Parse.Error.INTERNAL_SERVER_ERROR:
|
||||||
|
httpStatus = 500;
|
||||||
|
break;
|
||||||
|
case Parse.Error.OBJECT_NOT_FOUND:
|
||||||
|
httpStatus = 404;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
httpStatus = 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(httpStatus);
|
||||||
|
res.json({code: err.code, error: err.message});
|
||||||
|
} else {
|
||||||
|
console.log('Uncaught internal server error.', err, err.stack);
|
||||||
|
res.status(500);
|
||||||
|
res.json({code: Parse.Error.INTERNAL_SERVER_ERROR,
|
||||||
|
message: 'Internal server error.'});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function invalidRequest(req, res) {
|
||||||
|
res.status(403);
|
||||||
|
res.end('{"error":"unauthorized"}');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
allowCrossDomain: allowCrossDomain,
|
||||||
|
allowMethodOverride: allowMethodOverride,
|
||||||
|
handleParseErrors: handleParseErrors,
|
||||||
|
handleParseHeaders: handleParseHeaders
|
||||||
|
};
|
||||||
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "parse-server",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "An express module providing a Parse-compatible API server",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/ParsePlatform/parse-server"
|
||||||
|
},
|
||||||
|
"license": "BSD",
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt": "~0.8",
|
||||||
|
"body-parser": "~1.12.4",
|
||||||
|
"deepcopy": "^0.5.0",
|
||||||
|
"express": "~4.2.x",
|
||||||
|
"hat": "~0.0.3",
|
||||||
|
"mime": "^1.3.4",
|
||||||
|
"mongodb": "~2.0.33",
|
||||||
|
"multer": "~0.1.8",
|
||||||
|
"parse": "~1.6.12",
|
||||||
|
"request": "^2.65.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jasmine": "^2.3.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "TESTING=1 jasmine"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
push.js
Normal file
18
push.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// push.js
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function notImplementedYet(req) {
|
||||||
|
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
|
||||||
|
'This path is not implemented yet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST','/push', notImplementedYet);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
129
rest.js
Normal file
129
rest.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// This file contains helpers for running operations in REST format.
|
||||||
|
// The goal is that handlers that explicitly handle an express route
|
||||||
|
// should just be shallow wrappers around things in this file, but
|
||||||
|
// these functions should not explicitly depend on the request
|
||||||
|
// object.
|
||||||
|
// This means that one of these handlers can support multiple
|
||||||
|
// routes. That's useful for the routes that do really similar
|
||||||
|
// things.
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
var cache = require('./cache');
|
||||||
|
var RestQuery = require('./RestQuery');
|
||||||
|
var RestWrite = require('./RestWrite');
|
||||||
|
var triggers = require('./triggers');
|
||||||
|
|
||||||
|
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||||
|
function find(config, auth, className, restWhere, restOptions) {
|
||||||
|
enforceRoleSecurity('find', className, auth);
|
||||||
|
var query = new RestQuery(config, auth, className,
|
||||||
|
restWhere, restOptions);
|
||||||
|
return query.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that doesn't resolve to any useful value.
|
||||||
|
function del(config, auth, className, objectId) {
|
||||||
|
if (typeof objectId !== 'string') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'bad objectId');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (className === '_User' && !auth.couldUpdateUserId(objectId)) {
|
||||||
|
throw new Parse.Error(Parse.Error.SESSION_MISSING,
|
||||||
|
'insufficient auth to delete user');
|
||||||
|
}
|
||||||
|
|
||||||
|
enforceRoleSecurity('delete', className, auth);
|
||||||
|
|
||||||
|
var inflatedObject;
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
if (triggers.getTrigger(className, 'beforeDelete') ||
|
||||||
|
triggers.getTrigger(className, 'afterDelete') ||
|
||||||
|
className == '_Session') {
|
||||||
|
return find(config, auth, className, {objectId: objectId})
|
||||||
|
.then((response) => {
|
||||||
|
if (response && response.results && response.results.length) {
|
||||||
|
response.results[0].className = className;
|
||||||
|
cache.clearUser(response.results[0].sessionToken);
|
||||||
|
inflatedObject = Parse.Object.fromJSON(response.results[0]);
|
||||||
|
return triggers.maybeRunTrigger('beforeDelete',
|
||||||
|
auth, inflatedObject);
|
||||||
|
}
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found for delete.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({});
|
||||||
|
}).then(() => {
|
||||||
|
var options = {};
|
||||||
|
if (!auth.isMaster) {
|
||||||
|
options.acl = ['*'];
|
||||||
|
if (auth.user) {
|
||||||
|
options.acl.push(auth.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.database.destroy(className, {
|
||||||
|
objectId: objectId
|
||||||
|
}, options);
|
||||||
|
}).then(() => {
|
||||||
|
triggers.maybeRunTrigger('afterDelete', auth, inflatedObject);
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response, status, location} object.
|
||||||
|
function create(config, auth, className, restObject) {
|
||||||
|
enforceRoleSecurity('create', className, auth);
|
||||||
|
|
||||||
|
var write = new RestWrite(config, auth, className, null, restObject);
|
||||||
|
return write.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that contains the fields of the update that the
|
||||||
|
// REST API is supposed to return.
|
||||||
|
// Usually, this is just updatedAt.
|
||||||
|
function update(config, auth, className, objectId, restObject) {
|
||||||
|
enforceRoleSecurity('update', className, auth);
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
if (triggers.getTrigger(className, 'beforeSave') ||
|
||||||
|
triggers.getTrigger(className, 'afterSave')) {
|
||||||
|
return find(config, auth, className, {objectId: objectId});
|
||||||
|
}
|
||||||
|
return Promise.resolve({});
|
||||||
|
}).then((response) => {
|
||||||
|
var originalRestObject;
|
||||||
|
if (response && response.results && response.results.length) {
|
||||||
|
originalRestObject = response.results[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var write = new RestWrite(config, auth, className,
|
||||||
|
{objectId: objectId}, restObject, originalRestObject);
|
||||||
|
return write.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallowing access to the _Role collection except by master key
|
||||||
|
function enforceRoleSecurity(method, className, auth) {
|
||||||
|
if (className === '_Role' && !auth.isMaster) {
|
||||||
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'Clients aren\'t allowed to perform the ' +
|
||||||
|
method + ' operation on the role collection.');
|
||||||
|
}
|
||||||
|
if (method === 'delete' && className === '_Installation' && !auth.isMaster) {
|
||||||
|
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'Clients aren\'t allowed to perform the ' +
|
||||||
|
'delete operation on the installation collection.');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
create: create,
|
||||||
|
del: del,
|
||||||
|
find: find,
|
||||||
|
update: update
|
||||||
|
};
|
||||||
48
roles.js
Normal file
48
roles.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// roles.js
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
function handleCreate(req) {
|
||||||
|
return rest.create(req.config, req.auth,
|
||||||
|
'_Role', req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(req) {
|
||||||
|
return rest.update(req.config, req.auth, '_Role',
|
||||||
|
req.params.objectId, req.body)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(req) {
|
||||||
|
return rest.del(req.config, req.auth,
|
||||||
|
'_Role', req.params.objectId)
|
||||||
|
.then(() => {
|
||||||
|
return {response: {}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGet(req) {
|
||||||
|
return rest.find(req.config, req.auth, '_Role',
|
||||||
|
{objectId: req.params.objectId})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
} else {
|
||||||
|
return {response: response.results[0]};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST','/roles', handleCreate);
|
||||||
|
router.route('GET','/roles/:objectId', handleGet);
|
||||||
|
router.route('PUT','/roles/:objectId', handleUpdate);
|
||||||
|
router.route('DELETE','/roles/:objectId', handleDelete);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
122
sessions.js
Normal file
122
sessions.js
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// sessions.js
|
||||||
|
|
||||||
|
var Auth = require('./Auth'),
|
||||||
|
Parse = require('parse/node').Parse,
|
||||||
|
PromiseRouter = require('./PromiseRouter'),
|
||||||
|
rest = require('./rest');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
function handleCreate(req) {
|
||||||
|
return rest.create(req.config, req.auth,
|
||||||
|
'_Session', req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(req) {
|
||||||
|
return rest.update(req.config, req.auth, '_Session',
|
||||||
|
req.params.objectId, req.body)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(req) {
|
||||||
|
return rest.del(req.config, req.auth,
|
||||||
|
'_Session', req.params.objectId)
|
||||||
|
.then(() => {
|
||||||
|
return {response: {}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGet(req) {
|
||||||
|
return rest.find(req.config, req.auth, '_Session',
|
||||||
|
{objectId: req.params.objectId})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
} else {
|
||||||
|
return {response: response.results[0]};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout(req) {
|
||||||
|
// TODO: Verify correct behavior for logout without token
|
||||||
|
if (!req.info || !req.info.sessionToken) {
|
||||||
|
throw new Parse.Error(Parse.Error.SESSION_MISSING,
|
||||||
|
'Session token required for logout.');
|
||||||
|
}
|
||||||
|
return rest.find(req.config, Auth.master(req.config), '_Session',
|
||||||
|
{ _session_token: req.info.sessionToken})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||||
|
'Session token not found.');
|
||||||
|
}
|
||||||
|
return rest.del(req.config, Auth.master(req.config), '_Session',
|
||||||
|
response.results[0].objectId);
|
||||||
|
}).then(() => {
|
||||||
|
return {
|
||||||
|
status: 200,
|
||||||
|
response: {}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFind(req) {
|
||||||
|
var options = {};
|
||||||
|
if (req.body.skip) {
|
||||||
|
options.skip = Number(req.body.skip);
|
||||||
|
}
|
||||||
|
if (req.body.limit) {
|
||||||
|
options.limit = Number(req.body.limit);
|
||||||
|
}
|
||||||
|
if (req.body.order) {
|
||||||
|
options.order = String(req.body.order);
|
||||||
|
}
|
||||||
|
if (req.body.count) {
|
||||||
|
options.count = true;
|
||||||
|
}
|
||||||
|
if (typeof req.body.keys == 'string') {
|
||||||
|
options.keys = req.body.keys;
|
||||||
|
}
|
||||||
|
if (req.body.include) {
|
||||||
|
options.include = String(req.body.include);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest.find(req.config, req.auth,
|
||||||
|
'_Session', req.body.where, options)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMe(req) {
|
||||||
|
// TODO: Verify correct behavior
|
||||||
|
if (!req.info || !req.info.sessionToken) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||||
|
'Session token required.');
|
||||||
|
}
|
||||||
|
return rest.find(req.config, Auth.master(req.config), '_Session',
|
||||||
|
{ _session_token: req.info.sessionToken})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||||
|
'Session token not found.');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
response: response.results[0]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST', '/logout', handleLogout);
|
||||||
|
router.route('POST','/sessions', handleCreate);
|
||||||
|
router.route('GET','/sessions/me', handleMe);
|
||||||
|
router.route('GET','/sessions/:objectId', handleGet);
|
||||||
|
router.route('PUT','/sessions/:objectId', handleUpdate);
|
||||||
|
router.route('GET','/sessions', handleFind);
|
||||||
|
router.route('DELETE','/sessions/:objectId', handleDelete);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
15
spec/ExportAdapter.spec.js
Normal file
15
spec/ExportAdapter.spec.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
var ExportAdapter = require('../ExportAdapter');
|
||||||
|
|
||||||
|
describe('ExportAdapter', () => {
|
||||||
|
it('can be constructed', (done) => {
|
||||||
|
var database = new ExportAdapter('mongodb://localhost:27017/test',
|
||||||
|
{
|
||||||
|
collectionPrefix: 'test_'
|
||||||
|
});
|
||||||
|
database.connect().then(done, (error) => {
|
||||||
|
console.log('error', error.stack);
|
||||||
|
fail();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
1141
spec/ParseACL.spec.js
Normal file
1141
spec/ParseACL.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
615
spec/ParseAPI.spec.js
Normal file
615
spec/ParseAPI.spec.js
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
// A bunch of different tests are in here - it isn't very thematic.
|
||||||
|
// It would probably be better to refactor them into different files.
|
||||||
|
|
||||||
|
var DatabaseAdapter = require('../DatabaseAdapter');
|
||||||
|
var request = require('request');
|
||||||
|
|
||||||
|
describe('miscellaneous', function() {
|
||||||
|
it('create a GameScore object', function(done) {
|
||||||
|
var obj = new Parse.Object('GameScore');
|
||||||
|
obj.set('score', 1337);
|
||||||
|
obj.save().then(function(obj) {
|
||||||
|
expect(typeof obj.id).toBe('string');
|
||||||
|
expect(typeof obj.createdAt.toGMTString()).toBe('string');
|
||||||
|
done();
|
||||||
|
}, function(err) { console.log(err); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('get a TestObject', function(done) {
|
||||||
|
create({ 'bloop' : 'blarg' }, function(obj) {
|
||||||
|
var t2 = new TestObject({ objectId: obj.id });
|
||||||
|
t2.fetch({
|
||||||
|
success: function(obj2) {
|
||||||
|
expect(obj2.get('bloop')).toEqual('blarg');
|
||||||
|
expect(obj2.id).toBeTruthy();
|
||||||
|
expect(obj2.id).toEqual(obj.id);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
error: fail
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create a valid parse user', function(done) {
|
||||||
|
createTestUser(function(data) {
|
||||||
|
expect(data.id).not.toBeUndefined();
|
||||||
|
expect(data.getSessionToken()).not.toBeUndefined();
|
||||||
|
expect(data.get('password')).toBeUndefined();
|
||||||
|
done();
|
||||||
|
}, function(err) {
|
||||||
|
console.log(err);
|
||||||
|
fail(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fail to create a duplicate username', function(done) {
|
||||||
|
createTestUser(function(data) {
|
||||||
|
createTestUser(function(data) {
|
||||||
|
fail('Should not have been able to save duplicate username.');
|
||||||
|
}, function(error) {
|
||||||
|
expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('succeed in logging in', function(done) {
|
||||||
|
createTestUser(function(u) {
|
||||||
|
expect(typeof u.id).toEqual('string');
|
||||||
|
|
||||||
|
Parse.User.logIn('test', 'moon-y', {
|
||||||
|
success: function(user) {
|
||||||
|
expect(typeof user.id).toEqual('string');
|
||||||
|
expect(user.get('password')).toBeUndefined();
|
||||||
|
expect(user.getSessionToken()).not.toBeUndefined();
|
||||||
|
Parse.User.logOut();
|
||||||
|
done();
|
||||||
|
}, error: function(error) {
|
||||||
|
fail(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increment with a user object', function(done) {
|
||||||
|
createTestUser().then((user) => {
|
||||||
|
user.increment('foo');
|
||||||
|
return user.save();
|
||||||
|
}).then(() => {
|
||||||
|
return Parse.User.logIn('test', 'moon-y');
|
||||||
|
}).then((user) => {
|
||||||
|
expect(user.get('foo')).toEqual(1);
|
||||||
|
user.increment('foo');
|
||||||
|
return user.save();
|
||||||
|
}).then(() => {
|
||||||
|
Parse.User.logOut();
|
||||||
|
return Parse.User.logIn('test', 'moon-y');
|
||||||
|
}).then((user) => {
|
||||||
|
expect(user.get('foo')).toEqual(2);
|
||||||
|
Parse.User.logOut();
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('save various data types', function(done) {
|
||||||
|
var obj = new TestObject();
|
||||||
|
obj.set('date', new Date());
|
||||||
|
obj.set('array', [1, 2, 3]);
|
||||||
|
obj.set('object', {one: 1, two: 2});
|
||||||
|
obj.save().then(() => {
|
||||||
|
var obj2 = new TestObject({objectId: obj.id});
|
||||||
|
return obj2.fetch();
|
||||||
|
}).then((obj2) => {
|
||||||
|
expect(obj2.get('date') instanceof Date).toBe(true);
|
||||||
|
expect(obj2.get('array') instanceof Array).toBe(true);
|
||||||
|
expect(obj2.get('object') instanceof Array).toBe(false);
|
||||||
|
expect(obj2.get('object') instanceof Object).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query with limit', function(done) {
|
||||||
|
var baz = new TestObject({ foo: 'baz' });
|
||||||
|
var qux = new TestObject({ foo: 'qux' });
|
||||||
|
baz.save().then(() => {
|
||||||
|
return qux.save();
|
||||||
|
}).then(() => {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.limit(1);
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('basic saveAll', function(done) {
|
||||||
|
var alpha = new TestObject({ letter: 'alpha' });
|
||||||
|
var beta = new TestObject({ letter: 'beta' });
|
||||||
|
Parse.Object.saveAll([alpha, beta]).then(() => {
|
||||||
|
expect(alpha.id).toBeTruthy();
|
||||||
|
expect(beta.id).toBeTruthy();
|
||||||
|
return new Parse.Query(TestObject).find();
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(2);
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test cloud function', function(done) {
|
||||||
|
Parse.Cloud.run('hello', {}, function(result) {
|
||||||
|
expect(result).toEqual('Hello world!');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('basic beforeSave rejection', function(done) {
|
||||||
|
var obj = new Parse.Object('BeforeSaveFailure');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.save().then(function() {
|
||||||
|
fail('Should not have been able to save BeforeSaveFailure class.');
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeSave unchanged success', function(done) {
|
||||||
|
var obj = new Parse.Object('BeforeSaveUnchanged');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.save().then(function() {
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeSave changed object success', function(done) {
|
||||||
|
var obj = new Parse.Object('BeforeSaveChanged');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.save().then(function() {
|
||||||
|
var query = new Parse.Query('BeforeSaveChanged');
|
||||||
|
query.get(obj.id).then(function(objAgain) {
|
||||||
|
expect(objAgain.get('foo')).toEqual('baz');
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test afterSave ran and created an object', function(done) {
|
||||||
|
var obj = new Parse.Object('AfterSaveTest');
|
||||||
|
obj.save();
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
var query = new Parse.Query('AfterSaveProof');
|
||||||
|
query.equalTo('proof', obj.id);
|
||||||
|
query.find().then(function(results) {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeSave happens on update', function(done) {
|
||||||
|
var obj = new Parse.Object('BeforeSaveChanged');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.save().then(function() {
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
return obj.save();
|
||||||
|
}).then(function() {
|
||||||
|
var query = new Parse.Query('BeforeSaveChanged');
|
||||||
|
return query.get(obj.id).then(function(objAgain) {
|
||||||
|
expect(objAgain.get('foo')).toEqual('baz');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeDelete failure', function(done) {
|
||||||
|
var obj = new Parse.Object('BeforeDeleteFail');
|
||||||
|
var id;
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.save().then(() => {
|
||||||
|
id = obj.id;
|
||||||
|
return obj.destroy();
|
||||||
|
}).then(() => {
|
||||||
|
fail('obj.destroy() should have failed, but it succeeded');
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED);
|
||||||
|
expect(error.message).toEqual('Nope');
|
||||||
|
|
||||||
|
var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id});
|
||||||
|
return objAgain.fetch();
|
||||||
|
}).then((objAgain) => {
|
||||||
|
expect(objAgain.get('foo')).toEqual('bar');
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
// We should have been able to fetch the object again
|
||||||
|
fail(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeDelete success', function(done) {
|
||||||
|
var obj = new Parse.Object('BeforeDeleteTest');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.save().then(function() {
|
||||||
|
return obj.destroy();
|
||||||
|
}).then(function() {
|
||||||
|
var objAgain = new Parse.Object('BeforeDeleteTest', obj.id);
|
||||||
|
return objAgain.fetch().then(fail, done);
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test afterDelete ran and created an object', function(done) {
|
||||||
|
var obj = new Parse.Object('AfterDeleteTest');
|
||||||
|
obj.save().then(function() {
|
||||||
|
obj.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
var query = new Parse.Query('AfterDeleteProof');
|
||||||
|
query.equalTo('proof', obj.id);
|
||||||
|
query.find().then(function(results) {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test save triggers get user', function(done) {
|
||||||
|
var user = new Parse.User();
|
||||||
|
user.set("password", "asdf");
|
||||||
|
user.set("email", "asdf@example.com");
|
||||||
|
user.set("username", "zxcv");
|
||||||
|
user.signUp(null, {
|
||||||
|
success: function() {
|
||||||
|
var obj = new Parse.Object('SaveTriggerUser');
|
||||||
|
obj.save().then(function() {
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test cloud function return types', function(done) {
|
||||||
|
Parse.Cloud.run('foo').then((result) => {
|
||||||
|
expect(result.object instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(result.object.className).toEqual('Foo');
|
||||||
|
expect(result.object.get('x')).toEqual(2);
|
||||||
|
var bar = result.object.get('relation');
|
||||||
|
expect(bar instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(bar.className).toEqual('Bar');
|
||||||
|
expect(bar.get('x')).toEqual(3);
|
||||||
|
expect(Array.isArray(result.array)).toEqual(true);
|
||||||
|
expect(result.array[0] instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(result.array[0].get('x')).toEqual(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test rest_create_app', function(done) {
|
||||||
|
var appId;
|
||||||
|
Parse._request('POST', 'rest_create_app').then((res) => {
|
||||||
|
expect(typeof res.application_id).toEqual('string');
|
||||||
|
expect(res.master_key).toEqual('master');
|
||||||
|
appId = res.application_id;
|
||||||
|
Parse.initialize(appId, 'unused');
|
||||||
|
var obj = new Parse.Object('TestObject');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
return obj.save();
|
||||||
|
}).then(() => {
|
||||||
|
var db = DatabaseAdapter.getDatabaseConnection(appId);
|
||||||
|
return db.mongoFind('TestObject', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0]['foo']).toEqual('bar');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeSave get full object on create and update', function(done) {
|
||||||
|
var triggerTime = 0;
|
||||||
|
// Register a mock beforeSave hook
|
||||||
|
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||||
|
var object = req.object;
|
||||||
|
expect(object instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||||
|
expect(object.id).not.toBeUndefined();
|
||||||
|
expect(object.createdAt).not.toBeUndefined();
|
||||||
|
expect(object.updatedAt).not.toBeUndefined();
|
||||||
|
if (triggerTime == 0) {
|
||||||
|
// Create
|
||||||
|
expect(object.get('foo')).toEqual('bar');
|
||||||
|
} else if (triggerTime == 1) {
|
||||||
|
// Update
|
||||||
|
expect(object.get('foo')).toEqual('baz');
|
||||||
|
} else {
|
||||||
|
res.error();
|
||||||
|
}
|
||||||
|
triggerTime++;
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
var obj = new Parse.Object('GameScore');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.set('fooAgain', 'barAgain');
|
||||||
|
obj.save().then(function() {
|
||||||
|
// We only update foo
|
||||||
|
obj.set('foo', 'baz');
|
||||||
|
return obj.save();
|
||||||
|
}).then(function() {
|
||||||
|
// Make sure the checking has been triggered
|
||||||
|
expect(triggerTime).toBe(2);
|
||||||
|
// Clear mock beforeSave
|
||||||
|
delete Parse.Cloud.Triggers.beforeSave.GameScore;
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test afterSave get full object on create and update', function(done) {
|
||||||
|
var triggerTime = 0;
|
||||||
|
// Register a mock beforeSave hook
|
||||||
|
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||||
|
var object = req.object;
|
||||||
|
expect(object instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||||
|
expect(object.id).not.toBeUndefined();
|
||||||
|
expect(object.createdAt).not.toBeUndefined();
|
||||||
|
expect(object.updatedAt).not.toBeUndefined();
|
||||||
|
if (triggerTime == 0) {
|
||||||
|
// Create
|
||||||
|
expect(object.get('foo')).toEqual('bar');
|
||||||
|
} else if (triggerTime == 1) {
|
||||||
|
// Update
|
||||||
|
expect(object.get('foo')).toEqual('baz');
|
||||||
|
} else {
|
||||||
|
res.error();
|
||||||
|
}
|
||||||
|
triggerTime++;
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
var obj = new Parse.Object('GameScore');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.set('fooAgain', 'barAgain');
|
||||||
|
obj.save().then(function() {
|
||||||
|
// We only update foo
|
||||||
|
obj.set('foo', 'baz');
|
||||||
|
return obj.save();
|
||||||
|
}).then(function() {
|
||||||
|
// Make sure the checking has been triggered
|
||||||
|
expect(triggerTime).toBe(2);
|
||||||
|
// Clear mock afterSave
|
||||||
|
delete Parse.Cloud.Triggers.afterSave.GameScore;
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test beforeSave get original object on update', function(done) {
|
||||||
|
var triggerTime = 0;
|
||||||
|
// Register a mock beforeSave hook
|
||||||
|
Parse.Cloud.beforeSave('GameScore', function(req, res) {
|
||||||
|
var object = req.object;
|
||||||
|
expect(object instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||||
|
expect(object.id).not.toBeUndefined();
|
||||||
|
expect(object.createdAt).not.toBeUndefined();
|
||||||
|
expect(object.updatedAt).not.toBeUndefined();
|
||||||
|
var originalObject = req.original;
|
||||||
|
if (triggerTime == 0) {
|
||||||
|
// Create
|
||||||
|
expect(object.get('foo')).toEqual('bar');
|
||||||
|
// Check the originalObject is undefined
|
||||||
|
expect(originalObject).toBeUndefined();
|
||||||
|
} else if (triggerTime == 1) {
|
||||||
|
// Update
|
||||||
|
expect(object.get('foo')).toEqual('baz');
|
||||||
|
// Check the originalObject
|
||||||
|
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(originalObject.get('fooAgain')).toEqual('barAgain');
|
||||||
|
expect(originalObject.id).not.toBeUndefined();
|
||||||
|
expect(originalObject.createdAt).not.toBeUndefined();
|
||||||
|
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||||
|
expect(originalObject.get('foo')).toEqual('bar');
|
||||||
|
} else {
|
||||||
|
res.error();
|
||||||
|
}
|
||||||
|
triggerTime++;
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
var obj = new Parse.Object('GameScore');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.set('fooAgain', 'barAgain');
|
||||||
|
obj.save().then(function() {
|
||||||
|
// We only update foo
|
||||||
|
obj.set('foo', 'baz');
|
||||||
|
return obj.save();
|
||||||
|
}).then(function() {
|
||||||
|
// Make sure the checking has been triggered
|
||||||
|
expect(triggerTime).toBe(2);
|
||||||
|
// Clear mock beforeSave
|
||||||
|
delete Parse.Cloud.Triggers.beforeSave.GameScore;
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test afterSave get original object on update', function(done) {
|
||||||
|
var triggerTime = 0;
|
||||||
|
// Register a mock beforeSave hook
|
||||||
|
Parse.Cloud.afterSave('GameScore', function(req, res) {
|
||||||
|
var object = req.object;
|
||||||
|
expect(object instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(object.get('fooAgain')).toEqual('barAgain');
|
||||||
|
expect(object.id).not.toBeUndefined();
|
||||||
|
expect(object.createdAt).not.toBeUndefined();
|
||||||
|
expect(object.updatedAt).not.toBeUndefined();
|
||||||
|
var originalObject = req.original;
|
||||||
|
if (triggerTime == 0) {
|
||||||
|
// Create
|
||||||
|
expect(object.get('foo')).toEqual('bar');
|
||||||
|
// Check the originalObject is undefined
|
||||||
|
expect(originalObject).toBeUndefined();
|
||||||
|
} else if (triggerTime == 1) {
|
||||||
|
// Update
|
||||||
|
expect(object.get('foo')).toEqual('baz');
|
||||||
|
// Check the originalObject
|
||||||
|
expect(originalObject instanceof Parse.Object).toBeTruthy();
|
||||||
|
expect(originalObject.get('fooAgain')).toEqual('barAgain');
|
||||||
|
expect(originalObject.id).not.toBeUndefined();
|
||||||
|
expect(originalObject.createdAt).not.toBeUndefined();
|
||||||
|
expect(originalObject.updatedAt).not.toBeUndefined();
|
||||||
|
expect(originalObject.get('foo')).toEqual('bar');
|
||||||
|
} else {
|
||||||
|
res.error();
|
||||||
|
}
|
||||||
|
triggerTime++;
|
||||||
|
res.success();
|
||||||
|
});
|
||||||
|
|
||||||
|
var obj = new Parse.Object('GameScore');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
obj.set('fooAgain', 'barAgain');
|
||||||
|
obj.save().then(function() {
|
||||||
|
// We only update foo
|
||||||
|
obj.set('foo', 'baz');
|
||||||
|
return obj.save();
|
||||||
|
}).then(function() {
|
||||||
|
// Make sure the checking has been triggered
|
||||||
|
expect(triggerTime).toBe(2);
|
||||||
|
// Clear mock afterSave
|
||||||
|
delete Parse.Cloud.Triggers.afterSave.GameScore;
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test cloud function error handling', (done) => {
|
||||||
|
// Register a function which will fail
|
||||||
|
Parse.Cloud.define('willFail', (req, res) => {
|
||||||
|
res.error('noway');
|
||||||
|
});
|
||||||
|
Parse.Cloud.run('willFail').then((s) => {
|
||||||
|
fail('Should not have succeeded.');
|
||||||
|
delete Parse.Cloud.Functions['willFail'];
|
||||||
|
done();
|
||||||
|
}, (e) => {
|
||||||
|
expect(e.code).toEqual(141);
|
||||||
|
expect(e.message).toEqual('noway');
|
||||||
|
delete Parse.Cloud.Functions['willFail'];
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails on invalid client key', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-Client-Key': 'notclient'
|
||||||
|
};
|
||||||
|
request.get({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/classes/TestObject'
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.error).toEqual('unauthorized');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails on invalid windows key', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-Windows-Key': 'notwindows'
|
||||||
|
};
|
||||||
|
request.get({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/classes/TestObject'
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.error).toEqual('unauthorized');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails on invalid javascript key', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-Javascript-Key': 'notjavascript'
|
||||||
|
};
|
||||||
|
request.get({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/classes/TestObject'
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.error).toEqual('unauthorized');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails on invalid rest api key', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'notrest'
|
||||||
|
};
|
||||||
|
request.get({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/classes/TestObject'
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.error).toEqual('unauthorized');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
375
spec/ParseFile.spec.js
Normal file
375
spec/ParseFile.spec.js
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
// This is a port of the test suite:
|
||||||
|
// hungry/js/test/parse_file_test.js
|
||||||
|
|
||||||
|
var request = require('request');
|
||||||
|
|
||||||
|
var str = "Hello World!";
|
||||||
|
var data = [];
|
||||||
|
for (var i = 0; i < str.length; i++) {
|
||||||
|
data.push(str.charCodeAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Parse.File testing', () => {
|
||||||
|
it('works with REST API', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/files/file.txt',
|
||||||
|
body: 'argle bargle',
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.name).toMatch(/_file.txt$/);
|
||||||
|
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/);
|
||||||
|
request.get(b.url, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
expect(body).toEqual('argle bargle');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles other filetypes', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/files/file.jpg',
|
||||||
|
body: 'argle bargle',
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.name).toMatch(/_file.jpg$/);
|
||||||
|
expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/);
|
||||||
|
request.get(b.url, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
expect(body).toEqual('argle bargle');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("save file", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
file.save(expectSuccess({
|
||||||
|
success: function(result) {
|
||||||
|
strictEqual(result, file);
|
||||||
|
ok(file.name());
|
||||||
|
ok(file.url());
|
||||||
|
notEqual(file.name(), "hello.txt");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("save file in object", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
file.save(expectSuccess({
|
||||||
|
success: function(result) {
|
||||||
|
strictEqual(result, file);
|
||||||
|
ok(file.name());
|
||||||
|
ok(file.url());
|
||||||
|
notEqual(file.name(), "hello.txt");
|
||||||
|
|
||||||
|
var object = new Parse.Object("TestObject");
|
||||||
|
object.save({
|
||||||
|
file: file
|
||||||
|
}, expectSuccess({
|
||||||
|
success: function(object) {
|
||||||
|
(new Parse.Query("TestObject")).get(object.id, expectSuccess({
|
||||||
|
success: function(objectAgain) {
|
||||||
|
ok(objectAgain.get("file") instanceof Parse.File);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("save file in object with escaped characters in filename", done => {
|
||||||
|
var file = new Parse.File("hello . txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
file.save(expectSuccess({
|
||||||
|
success: function(result) {
|
||||||
|
strictEqual(result, file);
|
||||||
|
ok(file.name());
|
||||||
|
ok(file.url());
|
||||||
|
notEqual(file.name(), "hello . txt");
|
||||||
|
|
||||||
|
var object = new Parse.Object("TestObject");
|
||||||
|
object.save({
|
||||||
|
file: file
|
||||||
|
}, expectSuccess({
|
||||||
|
success: function(object) {
|
||||||
|
(new Parse.Query("TestObject")).get(object.id, expectSuccess({
|
||||||
|
success: function(objectAgain) {
|
||||||
|
ok(objectAgain.get("file") instanceof Parse.File);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("autosave file in object", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
var object = new Parse.Object("TestObject");
|
||||||
|
object.save({
|
||||||
|
file: file
|
||||||
|
}, expectSuccess({
|
||||||
|
success: function(object) {
|
||||||
|
(new Parse.Query("TestObject")).get(object.id, expectSuccess({
|
||||||
|
success: function(objectAgain) {
|
||||||
|
file = objectAgain.get("file");
|
||||||
|
ok(file instanceof Parse.File);
|
||||||
|
ok(file.name());
|
||||||
|
ok(file.url());
|
||||||
|
notEqual(file.name(), "hello.txt");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("autosave file in object in object", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
|
||||||
|
var child = new Parse.Object("Child");
|
||||||
|
child.set("file", file);
|
||||||
|
|
||||||
|
var parent = new Parse.Object("Parent");
|
||||||
|
parent.set("child", child);
|
||||||
|
|
||||||
|
parent.save(expectSuccess({
|
||||||
|
success: function(parent) {
|
||||||
|
var query = new Parse.Query("Parent");
|
||||||
|
query.include("child");
|
||||||
|
query.get(parent.id, expectSuccess({
|
||||||
|
success: function(parentAgain) {
|
||||||
|
var childAgain = parentAgain.get("child");
|
||||||
|
file = childAgain.get("file");
|
||||||
|
ok(file instanceof Parse.File);
|
||||||
|
ok(file.name());
|
||||||
|
ok(file.url());
|
||||||
|
notEqual(file.name(), "hello.txt");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("saving an already saved file", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
file.save(expectSuccess({
|
||||||
|
success: function(result) {
|
||||||
|
strictEqual(result, file);
|
||||||
|
ok(file.name());
|
||||||
|
ok(file.url());
|
||||||
|
notEqual(file.name(), "hello.txt");
|
||||||
|
var previousName = file.name();
|
||||||
|
|
||||||
|
file.save(expectSuccess({
|
||||||
|
success: function() {
|
||||||
|
equal(file.name(), previousName);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("two saves at the same time", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
|
||||||
|
var firstName;
|
||||||
|
var secondName;
|
||||||
|
|
||||||
|
var firstSave = file.save().then(function() { firstName = file.name(); });
|
||||||
|
var secondSave = file.save().then(function() { secondName = file.name(); });
|
||||||
|
|
||||||
|
Parse.Promise.when(firstSave, secondSave).then(function() {
|
||||||
|
equal(firstName, secondName);
|
||||||
|
done();
|
||||||
|
}, function(error) {
|
||||||
|
ok(false, error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("file toJSON testing", done => {
|
||||||
|
var file = new Parse.File("hello.txt", data, "text/plain");
|
||||||
|
ok(!file.url());
|
||||||
|
var object = new Parse.Object("TestObject");
|
||||||
|
object.save({
|
||||||
|
file: file
|
||||||
|
}, expectSuccess({
|
||||||
|
success: function(obj) {
|
||||||
|
ok(object.toJSON().file.url);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("content-type used with no extension", done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/files/file',
|
||||||
|
body: 'fee fi fo',
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.name).toMatch(/\.html$/);
|
||||||
|
request.get(b.url, (error, response, body) => {
|
||||||
|
expect(response.headers['content-type']).toMatch(/^text\/html/);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filename is url encoded", done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'text/html',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/files/hello world.txt',
|
||||||
|
body: 'oh emm gee',
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(error).toBe(null);
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.url).toMatch(/hello%20world/);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports array of files', done => {
|
||||||
|
var file = {
|
||||||
|
__type: 'File',
|
||||||
|
url: 'http://meep.meep',
|
||||||
|
name: 'meep'
|
||||||
|
};
|
||||||
|
var files = [file, file];
|
||||||
|
var obj = new Parse.Object('FilesArrayTest');
|
||||||
|
obj.set('files', files);
|
||||||
|
obj.save().then(() => {
|
||||||
|
var query = new Parse.Query('FilesArrayTest');
|
||||||
|
return query.first();
|
||||||
|
}).then((result) => {
|
||||||
|
var filesAgain = result.get('files');
|
||||||
|
expect(filesAgain.length).toEqual(2);
|
||||||
|
expect(filesAgain[0].name()).toEqual('meep');
|
||||||
|
expect(filesAgain[0].url()).toEqual('http://meep.meep');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates filename characters', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/files/di$avowed.txt',
|
||||||
|
body: 'will fail',
|
||||||
|
}, (error, response, body) => {
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.code).toEqual(122);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates filename length', done => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
var fileName = 'Onceuponamidnightdrearywhileiponderedweak' +
|
||||||
|
'andwearyOveramanyquaintandcuriousvolumeof' +
|
||||||
|
'forgottenloreWhileinoddednearlynappingsud' +
|
||||||
|
'denlytherecameatapping';
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/files/' + fileName,
|
||||||
|
body: 'will fail',
|
||||||
|
}, (error, response, body) => {
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.code).toEqual(122);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports a dictionary with file', done => {
|
||||||
|
var file = {
|
||||||
|
__type: 'File',
|
||||||
|
url: 'http://meep.meep',
|
||||||
|
name: 'meep'
|
||||||
|
};
|
||||||
|
var dict = {
|
||||||
|
file: file
|
||||||
|
};
|
||||||
|
var obj = new Parse.Object('FileObjTest');
|
||||||
|
obj.set('obj', dict);
|
||||||
|
obj.save().then(() => {
|
||||||
|
var query = new Parse.Query('FileObjTest');
|
||||||
|
return query.first();
|
||||||
|
}).then((result) => {
|
||||||
|
var dictAgain = result.get('obj');
|
||||||
|
expect(typeof dictAgain).toEqual('object');
|
||||||
|
var fileAgain = dictAgain['file'];
|
||||||
|
expect(fileAgain.name()).toEqual('meep');
|
||||||
|
expect(fileAgain.url()).toEqual('http://meep.meep');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates correct url for old files hosted on parse', done => {
|
||||||
|
var file = {
|
||||||
|
__type: 'File',
|
||||||
|
url: 'http://irrelevant.elephant/',
|
||||||
|
name: 'tfss-123.txt'
|
||||||
|
};
|
||||||
|
var obj = new Parse.Object('OldFileTest');
|
||||||
|
obj.set('oldfile', file);
|
||||||
|
obj.save().then(() => {
|
||||||
|
var query = new Parse.Query('OldFileTest');
|
||||||
|
return query.first();
|
||||||
|
}).then((result) => {
|
||||||
|
var fileAgain = result.get('oldfile');
|
||||||
|
expect(fileAgain.url()).toEqual(
|
||||||
|
'http://files.parsetfss.com/test/tfss-123.txt'
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
290
spec/ParseGeoPoint.spec.js
Normal file
290
spec/ParseGeoPoint.spec.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
// This is a port of the test suite:
|
||||||
|
// hungry/js/test/parse_geo_point_test.js
|
||||||
|
|
||||||
|
var TestObject = Parse.Object.extend('TestObject');
|
||||||
|
|
||||||
|
describe('Parse.GeoPoint testing', () => {
|
||||||
|
it('geo point roundtrip', (done) => {
|
||||||
|
var point = new Parse.GeoPoint(44.0, -11.0);
|
||||||
|
var obj = new TestObject();
|
||||||
|
obj.set('location', point);
|
||||||
|
obj.set('name', 'Ferndale');
|
||||||
|
obj.save(null, {
|
||||||
|
success: function() {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 1);
|
||||||
|
var pointAgain = results[0].get('location');
|
||||||
|
ok(pointAgain);
|
||||||
|
equal(pointAgain.latitude, 44.0);
|
||||||
|
equal(pointAgain.longitude, -11.0);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo point exception two fields', (done) => {
|
||||||
|
var point = new Parse.GeoPoint(20, 20);
|
||||||
|
var obj = new TestObject();
|
||||||
|
obj.set('locationOne', point);
|
||||||
|
obj.set('locationTwo', point);
|
||||||
|
obj.save().then(() => {
|
||||||
|
fail('expected error');
|
||||||
|
}, (err) => {
|
||||||
|
equal(err.code, Parse.Error.INCORRECT_TYPE);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo line', (done) => {
|
||||||
|
var line = [];
|
||||||
|
for (var i = 0; i < 10; ++i) {
|
||||||
|
var obj = new TestObject();
|
||||||
|
var point = new Parse.GeoPoint(i * 4.0 - 12.0, i * 3.2 - 11.0);
|
||||||
|
obj.set('location', point);
|
||||||
|
obj.set('construct', 'line');
|
||||||
|
obj.set('seq', i);
|
||||||
|
line.push(obj);
|
||||||
|
}
|
||||||
|
Parse.Object.saveAll(line, {
|
||||||
|
success: function() {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
var point = new Parse.GeoPoint(24, 19);
|
||||||
|
query.equalTo('construct', 'line');
|
||||||
|
query.withinMiles('location', point, 10000);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 10);
|
||||||
|
equal(results[0].get('seq'), 9);
|
||||||
|
equal(results[3].get('seq'), 6);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance large', (done) => {
|
||||||
|
var objects = [];
|
||||||
|
[0, 1, 2].map(function(i) {
|
||||||
|
var obj = new TestObject();
|
||||||
|
var point = new Parse.GeoPoint(0.0, i * 45.0);
|
||||||
|
obj.set('location', point);
|
||||||
|
obj.set('index', i);
|
||||||
|
objects.push(obj);
|
||||||
|
});
|
||||||
|
Parse.Object.saveAll(objects).then((list) => {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
var point = new Parse.GeoPoint(1.0, -1.0);
|
||||||
|
query.withinRadians('location', point, 3.14);
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
equal(results.length, 3);
|
||||||
|
done();
|
||||||
|
}, (err) => {
|
||||||
|
console.log(err);
|
||||||
|
fail();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance medium', (done) => {
|
||||||
|
var objects = [];
|
||||||
|
[0, 1, 2].map(function(i) {
|
||||||
|
var obj = new TestObject();
|
||||||
|
var point = new Parse.GeoPoint(0.0, i * 45.0);
|
||||||
|
obj.set('location', point);
|
||||||
|
obj.set('index', i);
|
||||||
|
objects.push(obj);
|
||||||
|
});
|
||||||
|
Parse.Object.saveAll(objects, function(list) {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
var point = new Parse.GeoPoint(1.0, -1.0);
|
||||||
|
query.withinRadians('location', point, 3.14 * 0.5);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 2);
|
||||||
|
equal(results[0].get('index'), 0);
|
||||||
|
equal(results[1].get('index'), 1);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance small', (done) => {
|
||||||
|
var objects = [];
|
||||||
|
[0, 1, 2].map(function(i) {
|
||||||
|
var obj = new TestObject();
|
||||||
|
var point = new Parse.GeoPoint(0.0, i * 45.0);
|
||||||
|
obj.set('location', point);
|
||||||
|
obj.set('index', i);
|
||||||
|
objects.push(obj);
|
||||||
|
});
|
||||||
|
Parse.Object.saveAll(objects, function(list) {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
var point = new Parse.GeoPoint(1.0, -1.0);
|
||||||
|
query.withinRadians('location', point, 3.14 * 0.25);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 1);
|
||||||
|
equal(results[0].get('index'), 0);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var makeSomeGeoPoints = function(callback) {
|
||||||
|
var sacramento = new TestObject();
|
||||||
|
sacramento.set('location', new Parse.GeoPoint(38.52, -121.50));
|
||||||
|
sacramento.set('name', 'Sacramento');
|
||||||
|
|
||||||
|
var honolulu = new TestObject();
|
||||||
|
honolulu.set('location', new Parse.GeoPoint(21.35, -157.93));
|
||||||
|
honolulu.set('name', 'Honolulu');
|
||||||
|
|
||||||
|
var sf = new TestObject();
|
||||||
|
sf.set('location', new Parse.GeoPoint(37.75, -122.68));
|
||||||
|
sf.set('name', 'San Francisco');
|
||||||
|
|
||||||
|
Parse.Object.saveAll([sacramento, sf, honolulu], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
it('geo max distance in km everywhere', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinKilometers('location', sfo, 4000.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 3);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in km california', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinKilometers('location', sfo, 3700.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 2);
|
||||||
|
equal(results[0].get('name'), 'San Francisco');
|
||||||
|
equal(results[1].get('name'), 'Sacramento');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in km bay area', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinKilometers('location', sfo, 100.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 1);
|
||||||
|
equal(results[0].get('name'), 'San Francisco');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in km mid peninsula', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinKilometers('location', sfo, 10.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 0);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in miles everywhere', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinMiles('location', sfo, 2500.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 3);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in miles california', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinMiles('location', sfo, 2200.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 2);
|
||||||
|
equal(results[0].get('name'), 'San Francisco');
|
||||||
|
equal(results[1].get('name'), 'Sacramento');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in miles bay area', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinMiles('location', sfo, 75.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 1);
|
||||||
|
equal(results[0].get('name'), 'San Francisco');
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('geo max distance in miles mid peninsula', (done) => {
|
||||||
|
makeSomeGeoPoints(function(list) {
|
||||||
|
var sfo = new Parse.GeoPoint(37.6189722, -122.3748889);
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinMiles('location', sfo, 10.0);
|
||||||
|
query.find({
|
||||||
|
success: function(results) {
|
||||||
|
equal(results.length, 0);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with geobox queries', (done) => {
|
||||||
|
var inSF = new Parse.GeoPoint(37.75, -122.4);
|
||||||
|
var southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398);
|
||||||
|
var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962);
|
||||||
|
|
||||||
|
var object = new TestObject();
|
||||||
|
object.set('point', inSF);
|
||||||
|
object.save().then(() => {
|
||||||
|
var query = new Parse.Query(TestObject);
|
||||||
|
query.withinGeoBox('point', southwestOfSF, northeastOfSF);
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
equal(results.length, 1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
777
spec/ParseInstallation.spec.js
Normal file
777
spec/ParseInstallation.spec.js
Normal file
@@ -0,0 +1,777 @@
|
|||||||
|
// These tests check the Installations functionality of the REST API.
|
||||||
|
// Ported from installation_collection_test.go
|
||||||
|
|
||||||
|
var auth = require('../Auth');
|
||||||
|
var cache = require('../cache');
|
||||||
|
var Config = require('../Config');
|
||||||
|
var DatabaseAdapter = require('../DatabaseAdapter');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
var rest = require('../rest');
|
||||||
|
|
||||||
|
var config = new Config('test');
|
||||||
|
var database = DatabaseAdapter.getDatabaseConnection('test');
|
||||||
|
|
||||||
|
describe('Installations', () => {
|
||||||
|
|
||||||
|
it('creates an android installation with ids', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var device = 'android';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': device
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(obj.installationId).toEqual(installId);
|
||||||
|
expect(obj.deviceType).toEqual(device);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an ios installation with ids', (done) => {
|
||||||
|
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var device = 'ios';
|
||||||
|
var input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': device
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(obj.deviceToken).toEqual(t);
|
||||||
|
expect(obj.deviceType).toEqual(device);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an embedded installation with ids', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var device = 'embedded';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': device
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(obj.installationId).toEqual(installId);
|
||||||
|
expect(obj.deviceType).toEqual(device);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an android installation with all fields', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var device = 'android';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': device,
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(obj.installationId).toEqual(installId);
|
||||||
|
expect(obj.deviceType).toEqual(device);
|
||||||
|
expect(typeof obj.channels).toEqual('object');
|
||||||
|
expect(obj.channels.length).toEqual(2);
|
||||||
|
expect(obj.channels[0]).toEqual('foo');
|
||||||
|
expect(obj.channels[1]).toEqual('bar');
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an ios installation with all fields', (done) => {
|
||||||
|
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var device = 'ios';
|
||||||
|
var input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': device,
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(obj.deviceToken).toEqual(t);
|
||||||
|
expect(obj.deviceType).toEqual(device);
|
||||||
|
expect(typeof obj.channels).toEqual('object');
|
||||||
|
expect(obj.channels.length).toEqual(2);
|
||||||
|
expect(obj.channels[0]).toEqual('foo');
|
||||||
|
expect(obj.channels[1]).toEqual('bar');
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with missing ids', (done) => {
|
||||||
|
var input = {
|
||||||
|
'deviceType': 'android',
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
fail('Should not have been able to create an Installation.');
|
||||||
|
done();
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.code).toEqual(135);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails for android with device token', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var device = 'android';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': device,
|
||||||
|
'deviceToken': t,
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
fail('Should not have been able to create an Installation.');
|
||||||
|
done();
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.code).toEqual(114);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails for android with missing type', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
fail('Should not have been able to create an Installation.');
|
||||||
|
done();
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.code).toEqual(135);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates an object with custom fields', (done) => {
|
||||||
|
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'channels': ['foo', 'bar'],
|
||||||
|
'custom': 'allowed'
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(obj.custom).toEqual('allowed');
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: did not port test 'TestObjectIDForIdentifiers'
|
||||||
|
|
||||||
|
it('merging when installationId already exists', (done) => {
|
||||||
|
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var installId2 = '12345678-abcd-abcd-abcd-123456789abd';
|
||||||
|
var input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'installationId': installId1,
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
var firstObject;
|
||||||
|
var secondObject;
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
firstObject = results[0];
|
||||||
|
delete input.deviceToken;
|
||||||
|
delete input.channels;
|
||||||
|
input['foo'] = 'bar';
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
secondObject = results[0];
|
||||||
|
expect(firstObject._id).toEqual(secondObject._id);
|
||||||
|
expect(secondObject.channels.length).toEqual(2);
|
||||||
|
expect(secondObject.foo).toEqual('bar');
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merging when two objects both only have one id', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input1 = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
var input2 = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
var input3 = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
var firstObject;
|
||||||
|
var secondObject;
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input1)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
firstObject = results[0];
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input2);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(2);
|
||||||
|
if (results[0]['_id'] == firstObject._id) {
|
||||||
|
secondObject = results[1];
|
||||||
|
} else {
|
||||||
|
secondObject = results[0];
|
||||||
|
}
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input3);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0]['_id']).toEqual(secondObject._id);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
notWorking('creating multiple devices with same device token works', (done) => {
|
||||||
|
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||||
|
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||||
|
var installId3 = '33333333-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId1,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'deviceToken': t
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
input.installationId = installId2;
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
input.installationId = installId3;
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId1}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId2}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId3}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updating with new channels', (done) => {
|
||||||
|
var input = {
|
||||||
|
'installationId': '12345678-abcd-abcd-abcd-123456789abc',
|
||||||
|
'deviceType': 'android',
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var id = results[0]['_id'];
|
||||||
|
var update = {
|
||||||
|
'channels': ['baz']
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config),
|
||||||
|
'_Installation', id, update);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].channels.length).toEqual(1);
|
||||||
|
expect(results[0].channels[0]).toEqual('baz');
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update android fails with new installation id', (done) => {
|
||||||
|
var installId1 = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var installId2 = '87654321-abcd-abcd-abcd-123456789abc';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId1,
|
||||||
|
'deviceType': 'android',
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'installationId': installId2
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
fail('Updating the installation should have failed.');
|
||||||
|
done();
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.code).toEqual(136);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update ios fails with new deviceToken and no installationId', (done) => {
|
||||||
|
var a = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var b = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var input = {
|
||||||
|
'deviceToken': a,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'deviceToken': b
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
fail('Updating the installation should have failed.');
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.code).toEqual(136);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update ios updates device token', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var u = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'deviceToken': t,
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceToken': u,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].deviceToken).toEqual(u);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update fails to change deviceType', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'android',
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
fail('Should not have been able to update Installation.');
|
||||||
|
done();
|
||||||
|
}).catch((error) => {
|
||||||
|
expect(error.code).toEqual(136);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update android with custom field', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'android',
|
||||||
|
'channels': ['foo', 'bar']
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'custom': 'allowed'
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0]['custom']).toEqual('allowed');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update ios device token with duplicate device token', (done) => {
|
||||||
|
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||||
|
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId1,
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
var firstObject;
|
||||||
|
var secondObject;
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
input = {
|
||||||
|
'installationId': installId2,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId1}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
firstObject = results[0];
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{installationId: installId2}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
secondObject = results[0];
|
||||||
|
// Update second installation to conflict with first installation id
|
||||||
|
input = {
|
||||||
|
'installationId': installId2,
|
||||||
|
'deviceToken': t
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
secondObject._id, input);
|
||||||
|
}).then(() => {
|
||||||
|
// The first object should have been deleted
|
||||||
|
return database.mongoFind('_Installation', {_id: firstObject._id}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(0);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
notWorking('update ios device token with duplicate token different app', (done) => {
|
||||||
|
var installId1 = '11111111-abcd-abcd-abcd-123456789abc';
|
||||||
|
var installId2 = '22222222-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId1,
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'appIdentifier': 'foo'
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
input.installationId = installId2;
|
||||||
|
input.appIdentifier = 'bar';
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
// The first object should have been deleted during merge
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].installationId).toEqual(installId2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update ios token and channels', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'channels': []
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].installationId).toEqual(installId);
|
||||||
|
expect(results[0].deviceToken).toEqual(t);
|
||||||
|
expect(results[0].channels.length).toEqual(0);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update ios linking two existing objects', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{deviceToken: t}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].installationId).toEqual(installId);
|
||||||
|
expect(results[0].deviceToken).toEqual(t);
|
||||||
|
expect(results[0].deviceType).toEqual('ios');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update is linking two existing objects w/ increment', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation',
|
||||||
|
{deviceToken: t}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'score': {
|
||||||
|
'__op': 'Increment',
|
||||||
|
'amount': 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
results[0]['_id'], input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].installationId).toEqual(installId);
|
||||||
|
expect(results[0].deviceToken).toEqual(t);
|
||||||
|
expect(results[0].deviceType).toEqual('ios');
|
||||||
|
expect(results[0].score).toEqual(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update is linking two existing with installation id', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
var installObj;
|
||||||
|
var tokenObj;
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
installObj = results[0];
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {deviceToken: t}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
tokenObj = results[0];
|
||||||
|
input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
installObj._id, input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].installationId).toEqual(installId);
|
||||||
|
expect(results[0].deviceToken).toEqual(t);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update is linking two existing with installation id w/ op', (done) => {
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
var installObj;
|
||||||
|
var tokenObj;
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
installObj = results[0];
|
||||||
|
input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {deviceToken: t}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
tokenObj = results[0];
|
||||||
|
input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios',
|
||||||
|
'score': {
|
||||||
|
'__op': 'Increment',
|
||||||
|
'amount': 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return rest.update(config, auth.nobody(config), '_Installation',
|
||||||
|
installObj._id, input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {_id: tokenObj._id}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].installationId).toEqual(installId);
|
||||||
|
expect(results[0].deviceToken).toEqual(t);
|
||||||
|
expect(results[0].score).toEqual(1);
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ios merge existing same token no installation id', (done) => {
|
||||||
|
// Test creating installation when there is an existing object with the
|
||||||
|
// same device token but no installation ID. This is possible when
|
||||||
|
// developers import device tokens from another push provider; the import
|
||||||
|
// process does not generate installation IDs. When they later integrate
|
||||||
|
// the Parse SDK, their app is going to save the installation. This save
|
||||||
|
// op will have a client-generated installation ID as well as a device
|
||||||
|
// token. At this point, if the device token matches the originally-
|
||||||
|
// imported installation, then we should reuse the existing installation
|
||||||
|
// object in case the developer already added additional fields via Data
|
||||||
|
// Browser or REST API (e.g. channel targeting info).
|
||||||
|
var t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
||||||
|
var installId = '12345678-abcd-abcd-abcd-123456789abc';
|
||||||
|
var input = {
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_Installation', input)
|
||||||
|
.then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
input = {
|
||||||
|
'installationId': installId,
|
||||||
|
'deviceToken': t,
|
||||||
|
'deviceType': 'ios'
|
||||||
|
};
|
||||||
|
return rest.create(config, auth.nobody(config), '_Installation', input);
|
||||||
|
}).then(() => {
|
||||||
|
return database.mongoFind('_Installation', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(results[0].deviceToken).toEqual(t);
|
||||||
|
expect(results[0].installationId).toEqual(installId);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Look at additional tests from installation_collection_test.go:882
|
||||||
|
// TODO: Do we need to support _tombstone disabling of installations?
|
||||||
|
// TODO: Test deletion, badge increments
|
||||||
|
|
||||||
|
});
|
||||||
1739
spec/ParseObject.spec.js
Normal file
1739
spec/ParseObject.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
2075
spec/ParseQuery.spec.js
Normal file
2075
spec/ParseQuery.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
338
spec/ParseRelation.spec.js
Normal file
338
spec/ParseRelation.spec.js
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
// This is a port of the test suite:
|
||||||
|
// hungry/js/test/parse_relation_test.js
|
||||||
|
|
||||||
|
var ChildObject = Parse.Object.extend({className: "ChildObject"});
|
||||||
|
var ParentObject = Parse.Object.extend({className: "ParentObject"});
|
||||||
|
|
||||||
|
describe('Parse.Relation testing', () => {
|
||||||
|
it("simple add and remove relation", (done) => {
|
||||||
|
var child = new ChildObject();
|
||||||
|
child.set("x", 2);
|
||||||
|
var parent = new ParentObject();
|
||||||
|
parent.set("x", 4);
|
||||||
|
var relation = parent.relation("child");
|
||||||
|
|
||||||
|
child.save().then(() => {
|
||||||
|
relation.add(child);
|
||||||
|
return parent.save();
|
||||||
|
}, (e) => {
|
||||||
|
fail(e);
|
||||||
|
}).then(() => {
|
||||||
|
return relation.query().find();
|
||||||
|
}).then((list) => {
|
||||||
|
equal(list.length, 1,
|
||||||
|
"Should have gotten one element back");
|
||||||
|
equal(list[0].id, child.id,
|
||||||
|
"Should have gotten the right value");
|
||||||
|
ok(!parent.dirty("child"),
|
||||||
|
"The relation should not be dirty");
|
||||||
|
|
||||||
|
relation.remove(child);
|
||||||
|
return parent.save();
|
||||||
|
}).then(() => {
|
||||||
|
return relation.query().find();
|
||||||
|
}).then((list) => {
|
||||||
|
equal(list.length, 0,
|
||||||
|
"Delete should have worked");
|
||||||
|
ok(!parent.dirty("child"),
|
||||||
|
"The relation should not be dirty");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("query relation without schema", (done) => {
|
||||||
|
var ChildObject = Parse.Object.extend("ChildObject");
|
||||||
|
var childObjects = [];
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
childObjects.push(new ChildObject({x:i}));
|
||||||
|
};
|
||||||
|
|
||||||
|
Parse.Object.saveAll(childObjects, expectSuccess({
|
||||||
|
success: function(list) {
|
||||||
|
var ParentObject = Parse.Object.extend("ParentObject");
|
||||||
|
var parent = new ParentObject();
|
||||||
|
parent.set("x", 4);
|
||||||
|
var relation = parent.relation("child");
|
||||||
|
relation.add(childObjects[0]);
|
||||||
|
parent.save(null, expectSuccess({
|
||||||
|
success: function() {
|
||||||
|
var parentAgain = new ParentObject();
|
||||||
|
parentAgain.id = parent.id;
|
||||||
|
var relation = parentAgain.relation("child");
|
||||||
|
relation.query().find(expectSuccess({
|
||||||
|
success: function(list) {
|
||||||
|
equal(list.length, 1,
|
||||||
|
"Should have gotten one element back");
|
||||||
|
equal(list[0].id, childObjects[0].id,
|
||||||
|
"Should have gotten the right value");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("relations are constructed right from query", (done) => {
|
||||||
|
|
||||||
|
var ChildObject = Parse.Object.extend("ChildObject");
|
||||||
|
var childObjects = [];
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
childObjects.push(new ChildObject({x: i}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Parse.Object.saveAll(childObjects, {
|
||||||
|
success: function(list) {
|
||||||
|
var ParentObject = Parse.Object.extend("ParentObject");
|
||||||
|
var parent = new ParentObject();
|
||||||
|
parent.set("x", 4);
|
||||||
|
var relation = parent.relation("child");
|
||||||
|
relation.add(childObjects[0]);
|
||||||
|
parent.save(null, {
|
||||||
|
success: function() {
|
||||||
|
var query = new Parse.Query(ParentObject);
|
||||||
|
query.get(parent.id, {
|
||||||
|
success: function(object) {
|
||||||
|
var relationAgain = object.relation("child");
|
||||||
|
relationAgain.query().find({
|
||||||
|
success: function(list) {
|
||||||
|
equal(list.length, 1,
|
||||||
|
"Should have gotten one element back");
|
||||||
|
equal(list[0].id, childObjects[0].id,
|
||||||
|
"Should have gotten the right value");
|
||||||
|
ok(!parent.dirty("child"),
|
||||||
|
"The relation should not be dirty");
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
error: function(list) {
|
||||||
|
ok(false, "This shouldn't have failed");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it("compound add and remove relation", (done) => {
|
||||||
|
var ChildObject = Parse.Object.extend("ChildObject");
|
||||||
|
var childObjects = [];
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
childObjects.push(new ChildObject({x: i}));
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent;
|
||||||
|
var relation;
|
||||||
|
|
||||||
|
Parse.Object.saveAll(childObjects).then(function(list) {
|
||||||
|
var ParentObject = Parse.Object.extend('ParentObject');
|
||||||
|
parent = new ParentObject();
|
||||||
|
parent.set('x', 4);
|
||||||
|
relation = parent.relation('child');
|
||||||
|
relation.add(childObjects[0]);
|
||||||
|
relation.add(childObjects[1]);
|
||||||
|
relation.remove(childObjects[0]);
|
||||||
|
relation.add(childObjects[2]);
|
||||||
|
return parent.save();
|
||||||
|
}).then(function() {
|
||||||
|
return relation.query().find();
|
||||||
|
}).then(function(list) {
|
||||||
|
equal(list.length, 2, 'Should have gotten two elements back');
|
||||||
|
ok(!parent.dirty('child'), 'The relation should not be dirty');
|
||||||
|
relation.remove(childObjects[1]);
|
||||||
|
relation.remove(childObjects[2]);
|
||||||
|
relation.add(childObjects[1]);
|
||||||
|
relation.add(childObjects[0]);
|
||||||
|
return parent.save();
|
||||||
|
}).then(function() {
|
||||||
|
return relation.query().find();
|
||||||
|
}).then(function(list) {
|
||||||
|
equal(list.length, 2, 'Deletes and then adds should have worked');
|
||||||
|
ok(!parent.dirty('child'), 'The relation should not be dirty');
|
||||||
|
done();
|
||||||
|
}, function(err) {
|
||||||
|
ok(false, err.message);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it("queries with relations", (done) => {
|
||||||
|
|
||||||
|
var ChildObject = Parse.Object.extend("ChildObject");
|
||||||
|
var childObjects = [];
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
childObjects.push(new ChildObject({x: i}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Parse.Object.saveAll(childObjects, {
|
||||||
|
success: function() {
|
||||||
|
var ParentObject = Parse.Object.extend("ParentObject");
|
||||||
|
var parent = new ParentObject();
|
||||||
|
parent.set("x", 4);
|
||||||
|
var relation = parent.relation("child");
|
||||||
|
relation.add(childObjects[0]);
|
||||||
|
relation.add(childObjects[1]);
|
||||||
|
relation.add(childObjects[2]);
|
||||||
|
parent.save(null, {
|
||||||
|
success: function() {
|
||||||
|
var query = relation.query();
|
||||||
|
query.equalTo("x", 2);
|
||||||
|
query.find({
|
||||||
|
success: function(list) {
|
||||||
|
equal(list.length, 1,
|
||||||
|
"There should only be one element");
|
||||||
|
ok(list[0] instanceof ChildObject,
|
||||||
|
"Should be of type ChildObject");
|
||||||
|
equal(list[0].id, childObjects[2].id,
|
||||||
|
"We should have gotten back the right result");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("queries on relation fields", (done) => {
|
||||||
|
var ChildObject = Parse.Object.extend("ChildObject");
|
||||||
|
var childObjects = [];
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
childObjects.push(new ChildObject({x: i}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Parse.Object.saveAll(childObjects, {
|
||||||
|
success: function() {
|
||||||
|
var ParentObject = Parse.Object.extend("ParentObject");
|
||||||
|
var parent = new ParentObject();
|
||||||
|
parent.set("x", 4);
|
||||||
|
var relation = parent.relation("child");
|
||||||
|
relation.add(childObjects[0]);
|
||||||
|
relation.add(childObjects[1]);
|
||||||
|
relation.add(childObjects[2]);
|
||||||
|
var parent2 = new ParentObject();
|
||||||
|
parent2.set("x", 3);
|
||||||
|
var relation2 = parent2.relation("child");
|
||||||
|
relation2.add(childObjects[4]);
|
||||||
|
relation2.add(childObjects[5]);
|
||||||
|
relation2.add(childObjects[6]);
|
||||||
|
var parents = [];
|
||||||
|
parents.push(parent);
|
||||||
|
parents.push(parent2);
|
||||||
|
Parse.Object.saveAll(parents, {
|
||||||
|
success: function() {
|
||||||
|
var query = new Parse.Query(ParentObject);
|
||||||
|
var objects = [];
|
||||||
|
objects.push(childObjects[4]);
|
||||||
|
objects.push(childObjects[9]);
|
||||||
|
query.containedIn("child", objects);
|
||||||
|
query.find({
|
||||||
|
success: function(list) {
|
||||||
|
equal(list.length, 1, "There should be only one result");
|
||||||
|
equal(list[0].id, parent2.id,
|
||||||
|
"Should have gotten back the right result");
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get query on relation using un-fetched parent object", (done) => {
|
||||||
|
// Setup data model
|
||||||
|
var Wheel = Parse.Object.extend('Wheel');
|
||||||
|
var Car = Parse.Object.extend('Car');
|
||||||
|
var origWheel = new Wheel();
|
||||||
|
origWheel.save().then(function() {
|
||||||
|
var car = new Car();
|
||||||
|
var relation = car.relation('wheels');
|
||||||
|
relation.add(origWheel);
|
||||||
|
return car.save();
|
||||||
|
}).then(function(car) {
|
||||||
|
// Test starts here.
|
||||||
|
// Create an un-fetched shell car object
|
||||||
|
var unfetchedCar = new Car();
|
||||||
|
unfetchedCar.id = car.id;
|
||||||
|
var relation = unfetchedCar.relation('wheels');
|
||||||
|
var query = relation.query();
|
||||||
|
|
||||||
|
// Parent object is un-fetched, so this will call /1/classes/Car instead
|
||||||
|
// of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }.
|
||||||
|
return query.get(origWheel.id);
|
||||||
|
}).then(function(wheel) {
|
||||||
|
// Make sure this is Wheel and not Car.
|
||||||
|
strictEqual(wheel.className, 'Wheel');
|
||||||
|
strictEqual(wheel.id, origWheel.id);
|
||||||
|
}).then(function() {
|
||||||
|
done();
|
||||||
|
},function(err) {
|
||||||
|
ok(false, 'unexpected error: ' + JSON.stringify(err));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Find query on relation using un-fetched parent object", (done) => {
|
||||||
|
// Setup data model
|
||||||
|
var Wheel = Parse.Object.extend('Wheel');
|
||||||
|
var Car = Parse.Object.extend('Car');
|
||||||
|
var origWheel = new Wheel();
|
||||||
|
origWheel.save().then(function() {
|
||||||
|
var car = new Car();
|
||||||
|
var relation = car.relation('wheels');
|
||||||
|
relation.add(origWheel);
|
||||||
|
return car.save();
|
||||||
|
}).then(function(car) {
|
||||||
|
// Test starts here.
|
||||||
|
// Create an un-fetched shell car object
|
||||||
|
var unfetchedCar = new Car();
|
||||||
|
unfetchedCar.id = car.id;
|
||||||
|
var relation = unfetchedCar.relation('wheels');
|
||||||
|
var query = relation.query();
|
||||||
|
|
||||||
|
// Parent object is un-fetched, so this will call /1/classes/Car instead
|
||||||
|
// of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }.
|
||||||
|
return query.find(origWheel.id);
|
||||||
|
}).then(function(results) {
|
||||||
|
// Make sure this is Wheel and not Car.
|
||||||
|
var wheel = results[0];
|
||||||
|
strictEqual(wheel.className, 'Wheel');
|
||||||
|
strictEqual(wheel.id, origWheel.id);
|
||||||
|
}).then(function() {
|
||||||
|
done();
|
||||||
|
},function(err) {
|
||||||
|
ok(false, 'unexpected error: ' + JSON.stringify(err));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Find objects with a related object using equalTo', (done) => {
|
||||||
|
// Setup the objects
|
||||||
|
var Card = Parse.Object.extend('Card');
|
||||||
|
var House = Parse.Object.extend('House');
|
||||||
|
var card = new Card();
|
||||||
|
card.save().then(() => {
|
||||||
|
var house = new House();
|
||||||
|
var relation = house.relation('cards');
|
||||||
|
relation.add(card);
|
||||||
|
return house.save();
|
||||||
|
}).then(() => {
|
||||||
|
var query = new Parse.Query('House');
|
||||||
|
query.equalTo('cards', card);
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
62
spec/ParseRole.spec.js
Normal file
62
spec/ParseRole.spec.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// Roles are not accessible without the master key, so they are not intended
|
||||||
|
// for use by clients. We can manually test them using the master key.
|
||||||
|
|
||||||
|
describe('Parse Role testing', () => {
|
||||||
|
|
||||||
|
it('Do a bunch of basic role testing', (done) => {
|
||||||
|
|
||||||
|
var user;
|
||||||
|
var role;
|
||||||
|
|
||||||
|
createTestUser().then((x) => {
|
||||||
|
user = x;
|
||||||
|
role = new Parse.Object('_Role');
|
||||||
|
role.set('name', 'Foos');
|
||||||
|
var users = role.relation('users');
|
||||||
|
users.add(user);
|
||||||
|
return role.save({}, { useMasterKey: true });
|
||||||
|
}).then((x) => {
|
||||||
|
var query = new Parse.Query('_Role');
|
||||||
|
return query.find({ useMasterKey: true });
|
||||||
|
}).then((x) => {
|
||||||
|
expect(x.length).toEqual(1);
|
||||||
|
var relation = x[0].relation('users').query();
|
||||||
|
return relation.first({ useMasterKey: true });
|
||||||
|
}).then((x) => {
|
||||||
|
expect(x.id).toEqual(user.id);
|
||||||
|
// Here we've got a valid role and a user assigned.
|
||||||
|
// Lets create an object only the role can read/write and test
|
||||||
|
// the different scenarios.
|
||||||
|
var obj = new Parse.Object('TestObject');
|
||||||
|
var acl = new Parse.ACL();
|
||||||
|
acl.setPublicReadAccess(false);
|
||||||
|
acl.setPublicWriteAccess(false);
|
||||||
|
acl.setRoleReadAccess('Foos', true);
|
||||||
|
acl.setRoleWriteAccess('Foos', true);
|
||||||
|
obj.setACL(acl);
|
||||||
|
return obj.save();
|
||||||
|
}).then((x) => {
|
||||||
|
var query = new Parse.Query('TestObject');
|
||||||
|
return query.find({ sessionToken: user.getSessionToken() });
|
||||||
|
}).then((x) => {
|
||||||
|
expect(x.length).toEqual(1);
|
||||||
|
var objAgain = x[0];
|
||||||
|
objAgain.set('foo', 'bar');
|
||||||
|
// This should succeed:
|
||||||
|
return objAgain.save({}, {sessionToken: user.getSessionToken()});
|
||||||
|
}).then((x) => {
|
||||||
|
x.set('foo', 'baz');
|
||||||
|
// This should fail:
|
||||||
|
return x.save();
|
||||||
|
}).then((x) => {
|
||||||
|
fail('Should not have been able to save.');
|
||||||
|
}, (e) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
1595
spec/ParseUser.spec.js
Normal file
1595
spec/ParseUser.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
128
spec/RestCreate.spec.js
Normal file
128
spec/RestCreate.spec.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
// These tests check the "create" functionality of the REST API.
|
||||||
|
var auth = require('../Auth');
|
||||||
|
var cache = require('../cache');
|
||||||
|
var Config = require('../Config');
|
||||||
|
var DatabaseAdapter = require('../DatabaseAdapter');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
var rest = require('../rest');
|
||||||
|
var request = require('request');
|
||||||
|
|
||||||
|
var config = new Config('test');
|
||||||
|
var database = DatabaseAdapter.getDatabaseConnection('test');
|
||||||
|
|
||||||
|
describe('rest create', () => {
|
||||||
|
it('handles _id', (done) => {
|
||||||
|
rest.create(config, auth.nobody(config), 'Foo', {}).then(() => {
|
||||||
|
return database.mongoFind('Foo', {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var obj = results[0];
|
||||||
|
expect(typeof obj._id).toEqual('string');
|
||||||
|
expect(obj.objectId).toBeUndefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles array, object, date', (done) => {
|
||||||
|
var obj = {
|
||||||
|
array: [1, 2, 3],
|
||||||
|
object: {foo: 'bar'},
|
||||||
|
date: Parse._encode(new Date()),
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), 'MyClass', obj).then(() => {
|
||||||
|
return database.mongoFind('MyClass', {}, {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var mob = results[0];
|
||||||
|
expect(mob.array instanceof Array).toBe(true);
|
||||||
|
expect(typeof mob.object).toBe('object');
|
||||||
|
expect(mob.date instanceof Date).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles user signup', (done) => {
|
||||||
|
var user = {
|
||||||
|
username: 'asdf',
|
||||||
|
password: 'zxcv',
|
||||||
|
foo: 'bar',
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_User', user)
|
||||||
|
.then((r) => {
|
||||||
|
expect(Object.keys(r.response).length).toEqual(3);
|
||||||
|
expect(typeof r.response.objectId).toEqual('string');
|
||||||
|
expect(typeof r.response.createdAt).toEqual('string');
|
||||||
|
expect(typeof r.response.sessionToken).toEqual('string');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('test facebook signup and login', (done) => {
|
||||||
|
var data = {
|
||||||
|
authData: {
|
||||||
|
facebook: {
|
||||||
|
id: '8675309',
|
||||||
|
access_token: 'jenny'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), '_User', data)
|
||||||
|
.then((r) => {
|
||||||
|
expect(typeof r.response.objectId).toEqual('string');
|
||||||
|
expect(typeof r.response.createdAt).toEqual('string');
|
||||||
|
expect(typeof r.response.sessionToken).toEqual('string');
|
||||||
|
return rest.create(config, auth.nobody(config), '_User', data);
|
||||||
|
}).then((r) => {
|
||||||
|
expect(typeof r.response.objectId).toEqual('string');
|
||||||
|
expect(typeof r.response.createdAt).toEqual('string');
|
||||||
|
expect(typeof r.response.username).toEqual('string');
|
||||||
|
expect(typeof r.response.updatedAt).toEqual('string');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores pointers with a _p_ prefix', (done) => {
|
||||||
|
var obj = {
|
||||||
|
foo: 'bar',
|
||||||
|
aPointer: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: 'JustThePointer',
|
||||||
|
objectId: 'qwerty'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rest.create(config, auth.nobody(config), 'APointerDarkly', obj)
|
||||||
|
.then((r) => {
|
||||||
|
return database.mongoFind('APointerDarkly', {});
|
||||||
|
}).then((results) => {
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
var output = results[0];
|
||||||
|
expect(typeof output._id).toEqual('string');
|
||||||
|
expect(typeof output._p_aPointer).toEqual('string');
|
||||||
|
expect(output._p_aPointer).toEqual('JustThePointer$qwerty');
|
||||||
|
expect(output.aPointer).toBeUndefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cannot set objectId", (done) => {
|
||||||
|
var headers = {
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'rest'
|
||||||
|
};
|
||||||
|
request.post({
|
||||||
|
headers: headers,
|
||||||
|
url: 'http://localhost:8378/1/classes/TestObject',
|
||||||
|
body: JSON.stringify({
|
||||||
|
'foo': 'bar',
|
||||||
|
'objectId': 'hello'
|
||||||
|
})
|
||||||
|
}, (error, response, body) => {
|
||||||
|
var b = JSON.parse(body);
|
||||||
|
expect(b.code).toEqual(105);
|
||||||
|
expect(b.error).toEqual('objectId is an invalid field name.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
95
spec/RestQuery.spec.js
Normal file
95
spec/RestQuery.spec.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// These tests check the "find" functionality of the REST API.
|
||||||
|
var auth = require('../Auth');
|
||||||
|
var cache = require('../cache');
|
||||||
|
var Config = require('../Config');
|
||||||
|
var rest = require('../rest');
|
||||||
|
|
||||||
|
var config = new Config('test');
|
||||||
|
var nobody = auth.nobody(config);
|
||||||
|
|
||||||
|
describe('rest query', () => {
|
||||||
|
it('basic query', (done) => {
|
||||||
|
rest.create(config, nobody, 'TestObject', {}).then(() => {
|
||||||
|
return rest.find(config, nobody, 'TestObject', {});
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.results.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query with limit', (done) => {
|
||||||
|
rest.create(config, nobody, 'TestObject', {foo: 'baz'}
|
||||||
|
).then(() => {
|
||||||
|
return rest.create(config, nobody,
|
||||||
|
'TestObject', {foo: 'qux'});
|
||||||
|
}).then(() => {
|
||||||
|
return rest.find(config, nobody,
|
||||||
|
'TestObject', {}, {limit: 1});
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.results.length).toEqual(1);
|
||||||
|
expect(response.results[0].foo).toBeTruthy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Created to test a scenario in AnyPic
|
||||||
|
it('query with include', (done) => {
|
||||||
|
var photo = {
|
||||||
|
foo: 'bar'
|
||||||
|
};
|
||||||
|
var user = {
|
||||||
|
username: 'aUsername',
|
||||||
|
password: 'aPassword'
|
||||||
|
};
|
||||||
|
var activity = {
|
||||||
|
type: 'comment',
|
||||||
|
photo: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: 'TestPhoto',
|
||||||
|
objectId: ''
|
||||||
|
},
|
||||||
|
fromUser: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_User',
|
||||||
|
objectId: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var queryWhere = {
|
||||||
|
photo: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: 'TestPhoto',
|
||||||
|
objectId: ''
|
||||||
|
},
|
||||||
|
type: 'comment'
|
||||||
|
};
|
||||||
|
var queryOptions = {
|
||||||
|
include: 'fromUser',
|
||||||
|
order: 'createdAt',
|
||||||
|
limit: 30
|
||||||
|
};
|
||||||
|
rest.create(config, nobody, 'TestPhoto', photo
|
||||||
|
).then((p) => {
|
||||||
|
photo = p;
|
||||||
|
return rest.create(config, nobody, '_User', user);
|
||||||
|
}).then((u) => {
|
||||||
|
user = u.response;
|
||||||
|
activity.photo.objectId = photo.objectId;
|
||||||
|
activity.fromUser.objectId = user.objectId;
|
||||||
|
return rest.create(config, nobody,
|
||||||
|
'TestActivity', activity);
|
||||||
|
}).then(() => {
|
||||||
|
queryWhere.photo.objectId = photo.objectId;
|
||||||
|
return rest.find(config, nobody,
|
||||||
|
'TestActivity', queryWhere, queryOptions);
|
||||||
|
}).then((response) => {
|
||||||
|
var results = response.results;
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
expect(typeof results[0].objectId).toEqual('string');
|
||||||
|
expect(typeof results[0].photo).toEqual('object');
|
||||||
|
expect(typeof results[0].fromUser).toEqual('object');
|
||||||
|
expect(typeof results[0].fromUser.username).toEqual('string');
|
||||||
|
done();
|
||||||
|
}).catch((error) => { console.log(error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
134
spec/Schema.spec.js
Normal file
134
spec/Schema.spec.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// These tests check that the Schema operates correctly.
|
||||||
|
var Config = require('../Config');
|
||||||
|
var Schema = require('../Schema');
|
||||||
|
|
||||||
|
var config = new Config('test');
|
||||||
|
|
||||||
|
describe('Schema', () => {
|
||||||
|
it('can validate one object', (done) => {
|
||||||
|
config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false});
|
||||||
|
}).then((schema) => {
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
fail(error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can validate two objects in a row', (done) => {
|
||||||
|
config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0});
|
||||||
|
}).then((schema) => {
|
||||||
|
return schema.validateObject('Foo', {x: false, y: 'YY', z: 1});
|
||||||
|
}).then((schema) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects inconsistent types', (done) => {
|
||||||
|
config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.validateObject('Stuff', {bacon: 7});
|
||||||
|
}).then((schema) => {
|
||||||
|
return schema.validateObject('Stuff', {bacon: 'z'});
|
||||||
|
}).then(() => {
|
||||||
|
fail('expected invalidity');
|
||||||
|
done();
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates when new fields are added', (done) => {
|
||||||
|
config.database.loadSchema().then((schema) => {
|
||||||
|
return schema.validateObject('Stuff', {bacon: 7});
|
||||||
|
}).then((schema) => {
|
||||||
|
return schema.validateObject('Stuff', {sausage: 8});
|
||||||
|
}).then((schema) => {
|
||||||
|
return schema.validateObject('Stuff', {sausage: 'ate'});
|
||||||
|
}).then(() => {
|
||||||
|
fail('expected invalidity');
|
||||||
|
done();
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('class-level permissions test find', (done) => {
|
||||||
|
config.database.loadSchema().then((schema) => {
|
||||||
|
// Just to create a valid class
|
||||||
|
return schema.validateObject('Stuff', {foo: 'bar'});
|
||||||
|
}).then((schema) => {
|
||||||
|
return schema.setPermissions('Stuff', {
|
||||||
|
'find': {}
|
||||||
|
});
|
||||||
|
}).then((schema) => {
|
||||||
|
var query = new Parse.Query('Stuff');
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
fail('Class permissions should have rejected this query.');
|
||||||
|
done();
|
||||||
|
}, (e) => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('class-level permissions test user', (done) => {
|
||||||
|
var user;
|
||||||
|
createTestUser().then((u) => {
|
||||||
|
user = u;
|
||||||
|
return config.database.loadSchema();
|
||||||
|
}).then((schema) => {
|
||||||
|
// Just to create a valid class
|
||||||
|
return schema.validateObject('Stuff', {foo: 'bar'});
|
||||||
|
}).then((schema) => {
|
||||||
|
var find = {};
|
||||||
|
find[user.id] = true;
|
||||||
|
return schema.setPermissions('Stuff', {
|
||||||
|
'find': find
|
||||||
|
});
|
||||||
|
}).then((schema) => {
|
||||||
|
var query = new Parse.Query('Stuff');
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
done();
|
||||||
|
}, (e) => {
|
||||||
|
fail('Class permissions should have allowed this query.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('class-level permissions test get', (done) => {
|
||||||
|
var user;
|
||||||
|
var obj;
|
||||||
|
createTestUser().then((u) => {
|
||||||
|
user = u;
|
||||||
|
return config.database.loadSchema();
|
||||||
|
}).then((schema) => {
|
||||||
|
// Just to create a valid class
|
||||||
|
return schema.validateObject('Stuff', {foo: 'bar'});
|
||||||
|
}).then((schema) => {
|
||||||
|
var find = {};
|
||||||
|
var get = {};
|
||||||
|
get[user.id] = true;
|
||||||
|
return schema.setPermissions('Stuff', {
|
||||||
|
'find': find,
|
||||||
|
'get': get
|
||||||
|
});
|
||||||
|
}).then((schema) => {
|
||||||
|
obj = new Parse.Object('Stuff');
|
||||||
|
obj.set('foo', 'bar');
|
||||||
|
return obj.save();
|
||||||
|
}).then((o) => {
|
||||||
|
obj = o;
|
||||||
|
var query = new Parse.Query('Stuff');
|
||||||
|
return query.find();
|
||||||
|
}).then((results) => {
|
||||||
|
fail('Class permissions should have rejected this query.');
|
||||||
|
done();
|
||||||
|
}, (e) => {
|
||||||
|
var query = new Parse.Query('Stuff');
|
||||||
|
return query.get(obj.id).then((o) => {
|
||||||
|
done();
|
||||||
|
}, (e) => {
|
||||||
|
fail('Class permissions should have allowed this get query');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
217
spec/helper.js
Normal file
217
spec/helper.js
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
// Sets up a Parse API server for testing.
|
||||||
|
|
||||||
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
|
||||||
|
|
||||||
|
var cache = require('../cache');
|
||||||
|
var DatabaseAdapter = require('../DatabaseAdapter');
|
||||||
|
var express = require('express');
|
||||||
|
var facebook = require('../facebook');
|
||||||
|
var ParseServer = require('../index').ParseServer;
|
||||||
|
|
||||||
|
var databaseURI = process.env.DATABASE_URI;
|
||||||
|
var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js';
|
||||||
|
|
||||||
|
// Set up an API server for testing
|
||||||
|
var api = new ParseServer({
|
||||||
|
databaseURI: databaseURI,
|
||||||
|
cloud: cloudMain,
|
||||||
|
appId: 'test',
|
||||||
|
javascriptKey: 'test',
|
||||||
|
dotNetKey: 'windows',
|
||||||
|
clientKey: 'client',
|
||||||
|
restAPIKey: 'rest',
|
||||||
|
masterKey: 'test',
|
||||||
|
collectionPrefix: 'test_',
|
||||||
|
fileKey: 'test'
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
app.use('/1', api);
|
||||||
|
var port = 8378;
|
||||||
|
var server = app.listen(port);
|
||||||
|
|
||||||
|
// Set up a Parse client to talk to our test API server
|
||||||
|
var Parse = require('parse/node');
|
||||||
|
Parse.serverURL = 'http://localhost:' + port + '/1';
|
||||||
|
|
||||||
|
// This is needed because we ported a bunch of tests from the non-A+ way.
|
||||||
|
// TODO: update tests to work in an A+ way
|
||||||
|
Parse.Promise.disableAPlusCompliant();
|
||||||
|
|
||||||
|
beforeEach(function(done) {
|
||||||
|
Parse.initialize('test', 'test', 'test');
|
||||||
|
mockFacebook();
|
||||||
|
Parse.User.enableUnsafeCurrentUser();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function(done) {
|
||||||
|
Parse.User.logOut();
|
||||||
|
Parse.Promise.as().then(() => {
|
||||||
|
return clearData();
|
||||||
|
}).then(() => {
|
||||||
|
done();
|
||||||
|
}, (error) => {
|
||||||
|
console.log('error in clearData', error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var TestObject = Parse.Object.extend({
|
||||||
|
className: "TestObject"
|
||||||
|
});
|
||||||
|
var Item = Parse.Object.extend({
|
||||||
|
className: "Item"
|
||||||
|
});
|
||||||
|
var Container = Parse.Object.extend({
|
||||||
|
className: "Container"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convenience method to create a new TestObject with a callback
|
||||||
|
function create(options, callback) {
|
||||||
|
var t = new TestObject(options);
|
||||||
|
t.save(null, { success: callback });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestUser(success, error) {
|
||||||
|
var user = new Parse.User();
|
||||||
|
user.set('username', 'test');
|
||||||
|
user.set('password', 'moon-y');
|
||||||
|
var promise = user.signUp();
|
||||||
|
if (success || error) {
|
||||||
|
promise.then(function(user) {
|
||||||
|
if (success) {
|
||||||
|
success(user);
|
||||||
|
}
|
||||||
|
}, function(err) {
|
||||||
|
if (error) {
|
||||||
|
error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the tests that are known to not work.
|
||||||
|
function notWorking() {}
|
||||||
|
|
||||||
|
// Shims for compatibility with the old qunit tests.
|
||||||
|
function ok(bool, message) {
|
||||||
|
expect(bool).toBeTruthy(message);
|
||||||
|
}
|
||||||
|
function equal(a, b, message) {
|
||||||
|
expect(a).toEqual(b, message);
|
||||||
|
}
|
||||||
|
function strictEqual(a, b, message) {
|
||||||
|
expect(a).toBe(b, message);
|
||||||
|
}
|
||||||
|
function notEqual(a, b, message) {
|
||||||
|
expect(a).not.toEqual(b, message);
|
||||||
|
}
|
||||||
|
function expectSuccess(params) {
|
||||||
|
return {
|
||||||
|
success: params.success,
|
||||||
|
error: function(e) {
|
||||||
|
console.log('got error', e);
|
||||||
|
fail('failure happened in expectSuccess');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function expectError(errorCode, callback) {
|
||||||
|
return {
|
||||||
|
success: function(result) {
|
||||||
|
console.log('got result', result);
|
||||||
|
fail('expected error but got success');
|
||||||
|
},
|
||||||
|
error: function(obj, e) {
|
||||||
|
// Some methods provide 2 parameters.
|
||||||
|
e = e || obj;
|
||||||
|
if (!e) {
|
||||||
|
fail('expected a specific error but got a blank error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(e.code).toEqual(errorCode, e.message);
|
||||||
|
if (callback) {
|
||||||
|
callback(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because node doesn't have Parse._.contains
|
||||||
|
function arrayContains(arr, item) {
|
||||||
|
return -1 != arr.indexOf(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalizes a JSON object.
|
||||||
|
function normalize(obj) {
|
||||||
|
if (typeof obj !== 'object') {
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
}
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return '[' + obj.map(normalize).join(', ') + ']';
|
||||||
|
}
|
||||||
|
var answer = '{';
|
||||||
|
for (key of Object.keys(obj).sort()) {
|
||||||
|
answer += key + ': ';
|
||||||
|
answer += normalize(obj[key]);
|
||||||
|
answer += ', ';
|
||||||
|
}
|
||||||
|
answer += '}';
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserts two json structures are equal.
|
||||||
|
function jequal(o1, o2) {
|
||||||
|
expect(normalize(o1)).toEqual(normalize(o2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function range(n) {
|
||||||
|
var answer = [];
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
answer.push(i);
|
||||||
|
}
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockFacebook() {
|
||||||
|
facebook.validateUserId = function(userId, accessToken) {
|
||||||
|
if (userId === '8675309' && accessToken === 'jenny') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject();
|
||||||
|
};
|
||||||
|
facebook.validateAppId = function(appId, accessToken) {
|
||||||
|
if (accessToken === 'jenny') {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearData() {
|
||||||
|
var promises = [];
|
||||||
|
for (conn in DatabaseAdapter.dbConnections) {
|
||||||
|
promises.push(DatabaseAdapter.dbConnections[conn].deleteEverything());
|
||||||
|
}
|
||||||
|
return Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is polluting, but, it makes it way easier to directly port old tests.
|
||||||
|
global.Parse = Parse;
|
||||||
|
global.TestObject = TestObject;
|
||||||
|
global.Item = Item;
|
||||||
|
global.Container = Container;
|
||||||
|
global.create = create;
|
||||||
|
global.createTestUser = createTestUser;
|
||||||
|
global.notWorking = notWorking;
|
||||||
|
global.ok = ok;
|
||||||
|
global.equal = equal;
|
||||||
|
global.strictEqual = strictEqual;
|
||||||
|
global.notEqual = notEqual;
|
||||||
|
global.expectSuccess = expectSuccess;
|
||||||
|
global.expectError = expectError;
|
||||||
|
global.arrayContains = arrayContains;
|
||||||
|
global.jequal = jequal;
|
||||||
|
global.range = range;
|
||||||
10
spec/support/jasmine.json
Normal file
10
spec/support/jasmine.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"spec_dir": "spec",
|
||||||
|
"spec_files": [
|
||||||
|
"*spec.js"
|
||||||
|
],
|
||||||
|
"helpers": [
|
||||||
|
"helper.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
154
spec/transform.spec.js
Normal file
154
spec/transform.spec.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// These tests are unit tests designed to only test transform.js.
|
||||||
|
|
||||||
|
var transform = require('../transform');
|
||||||
|
|
||||||
|
var dummyConfig = {
|
||||||
|
schema: {
|
||||||
|
data: {},
|
||||||
|
getExpectedType: function(className, key) {
|
||||||
|
if (key == 'userPointer') {
|
||||||
|
return '*_User';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
describe('transformCreate', () => {
|
||||||
|
|
||||||
|
it('a basic number', (done) => {
|
||||||
|
var input = {five: 5};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
jequal(input, output);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('built-in timestamps', (done) => {
|
||||||
|
var input = {
|
||||||
|
createdAt: "2015-10-06T21:24:50.332Z",
|
||||||
|
updatedAt: "2015-10-06T21:24:50.332Z"
|
||||||
|
};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
expect(output._created_at instanceof Date).toBe(true);
|
||||||
|
expect(output._updated_at instanceof Date).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array of pointers', (done) => {
|
||||||
|
var pointer = {
|
||||||
|
__type: 'Pointer',
|
||||||
|
objectId: 'myId',
|
||||||
|
className: 'Blah',
|
||||||
|
};
|
||||||
|
var out = transform.transformCreate(dummyConfig, null, {pointers: [pointer]});
|
||||||
|
jequal([pointer], out.pointers);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a delete op', (done) => {
|
||||||
|
var input = {deleteMe: {__op: 'Delete'}};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
jequal(output, {});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('basic ACL', (done) => {
|
||||||
|
var input = {ACL: {'0123': {'read': true, 'write': true}}};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
// This just checks that it doesn't crash, but it should check format.
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transformWhere', () => {
|
||||||
|
it('objectId', (done) => {
|
||||||
|
var out = transform.transformWhere(dummyConfig, null, {objectId: 'foo'});
|
||||||
|
expect(out._id).toEqual('foo');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('objectId in a list', (done) => {
|
||||||
|
var input = {
|
||||||
|
objectId: {'$in': ['one', 'two', 'three']},
|
||||||
|
};
|
||||||
|
var output = transform.transformWhere(dummyConfig, null, input);
|
||||||
|
jequal(input.objectId, output._id);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('untransformObject', () => {
|
||||||
|
it('built-in timestamps', (done) => {
|
||||||
|
var input = {createdAt: new Date(), updatedAt: new Date()};
|
||||||
|
var output = transform.untransformObject(dummyConfig, null, input);
|
||||||
|
expect(typeof output.createdAt).toEqual('string');
|
||||||
|
expect(typeof output.updatedAt).toEqual('string');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transformKey', () => {
|
||||||
|
it('throws out _password', (done) => {
|
||||||
|
try {
|
||||||
|
transform.transformKey(dummyConfig, '_User', '_password');
|
||||||
|
fail('should have thrown');
|
||||||
|
} catch (e) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transform schema key changes', () => {
|
||||||
|
|
||||||
|
it('changes new pointer key', (done) => {
|
||||||
|
var input = {
|
||||||
|
somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'}
|
||||||
|
};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
expect(typeof output._p_somePointer).toEqual('string');
|
||||||
|
expect(output._p_somePointer).toEqual('Micro$oft');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes existing pointer keys', (done) => {
|
||||||
|
var input = {
|
||||||
|
userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'}
|
||||||
|
};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
expect(typeof output._p_userPointer).toEqual('string');
|
||||||
|
expect(output._p_userPointer).toEqual('_User$qwerty');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes ACL storage to _rperm and _wperm', (done) => {
|
||||||
|
var input = {
|
||||||
|
ACL: {
|
||||||
|
"*": { "read": true },
|
||||||
|
"Kevin": { "write": true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var output = transform.transformCreate(dummyConfig, null, input);
|
||||||
|
expect(typeof output._rperm).toEqual('object');
|
||||||
|
expect(typeof output._wperm).toEqual('object');
|
||||||
|
expect(output.ACL).toBeUndefined();
|
||||||
|
expect(output._rperm[0]).toEqual('*');
|
||||||
|
expect(output._wperm[0]).toEqual('Kevin');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('untransforms from _rperm and _wperm to ACL', (done) => {
|
||||||
|
var input = {
|
||||||
|
_rperm: ["*"],
|
||||||
|
_wperm: ["Kevin"]
|
||||||
|
};
|
||||||
|
var output = transform.untransformObject(dummyConfig, null, input);
|
||||||
|
expect(typeof output.ACL).toEqual('object');
|
||||||
|
expect(output._rperm).toBeUndefined();
|
||||||
|
expect(output._wperm).toBeUndefined();
|
||||||
|
expect(output.ACL['*']['read']).toEqual(true);
|
||||||
|
expect(output.ACL['Kevin']['write']).toEqual(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
73
testing-routes.js
Normal file
73
testing-routes.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// testing-routes.js
|
||||||
|
|
||||||
|
var express = require('express'),
|
||||||
|
cache = require('./cache'),
|
||||||
|
middlewares = require('./middlewares'),
|
||||||
|
rack = require('hat').rack();
|
||||||
|
|
||||||
|
var router = express.Router();
|
||||||
|
|
||||||
|
// creates a unique app in the cache, with a collection prefix
|
||||||
|
function createApp(req, res) {
|
||||||
|
var appId = rack();
|
||||||
|
cache.apps[appId] = {
|
||||||
|
'collectionPrefix': appId + '_',
|
||||||
|
'masterKey': 'master'
|
||||||
|
};
|
||||||
|
var keys = {
|
||||||
|
'application_id': appId,
|
||||||
|
'client_key': 'unused',
|
||||||
|
'windows_key': 'unused',
|
||||||
|
'javascript_key': 'unused',
|
||||||
|
'webhook_key': 'unused',
|
||||||
|
'rest_api_key': 'unused',
|
||||||
|
'master_key': 'master'
|
||||||
|
};
|
||||||
|
res.status(200).send(keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletes all collections with the collectionPrefix of the app
|
||||||
|
function clearApp(req, res) {
|
||||||
|
if (!req.auth.isMaster) {
|
||||||
|
return res.status(401).send({"error": "unauthorized"});
|
||||||
|
}
|
||||||
|
req.database.deleteEverything().then(() => {
|
||||||
|
res.status(200).send({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletes all collections and drops the app from cache
|
||||||
|
function dropApp(req, res) {
|
||||||
|
if (!req.auth.isMaster) {
|
||||||
|
return res.status(401).send({"error": "unauthorized"});
|
||||||
|
}
|
||||||
|
req.database.deleteEverything().then(() => {
|
||||||
|
delete cache.apps[req.config.applicationId];
|
||||||
|
res.status(200).send({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets just return a success response and see what happens.
|
||||||
|
function notImplementedYet(req, res) {
|
||||||
|
res.status(200).send({});
|
||||||
|
}
|
||||||
|
|
||||||
|
router.post('/rest_clear_app',
|
||||||
|
middlewares.handleParseHeaders, clearApp);
|
||||||
|
router.post('/rest_block',
|
||||||
|
middlewares.handleParseHeaders, notImplementedYet);
|
||||||
|
router.post('/rest_mock_v8_client',
|
||||||
|
middlewares.handleParseHeaders, notImplementedYet);
|
||||||
|
router.post('/rest_unmock_v8_client',
|
||||||
|
middlewares.handleParseHeaders, notImplementedYet);
|
||||||
|
router.post('/rest_verify_analytics',
|
||||||
|
middlewares.handleParseHeaders, notImplementedYet);
|
||||||
|
router.post('/rest_create_app', createApp);
|
||||||
|
router.post('/rest_drop_app',
|
||||||
|
middlewares.handleParseHeaders, dropApp);
|
||||||
|
router.post('/rest_configure_app',
|
||||||
|
middlewares.handleParseHeaders, notImplementedYet);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
router: router
|
||||||
|
};
|
||||||
717
transform.js
Normal file
717
transform.js
Normal file
@@ -0,0 +1,717 @@
|
|||||||
|
var mongodb = require('mongodb');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
// TODO: Turn this into a helper library for the database adapter.
|
||||||
|
|
||||||
|
// Transforms a key-value pair from REST API form to Mongo form.
|
||||||
|
// This is the main entry point for converting anything from REST form
|
||||||
|
// to Mongo form; no conversion should happen that doesn't pass
|
||||||
|
// through this function.
|
||||||
|
// Schema should already be loaded.
|
||||||
|
//
|
||||||
|
// There are several options that can help transform:
|
||||||
|
//
|
||||||
|
// query: true indicates that query constraints like $lt are allowed in
|
||||||
|
// the value.
|
||||||
|
//
|
||||||
|
// update: true indicates that __op operators like Add and Delete
|
||||||
|
// in the value are converted to a mongo update form. Otherwise they are
|
||||||
|
// converted to static data.
|
||||||
|
//
|
||||||
|
// validate: true indicates that key names are to be validated.
|
||||||
|
//
|
||||||
|
// Returns an object with {key: key, value: value}.
|
||||||
|
function transformKeyValue(schema, className, restKey, restValue, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
// Check if the schema is known since it's a built-in field.
|
||||||
|
var key = restKey;
|
||||||
|
var timeField = false;
|
||||||
|
switch(key) {
|
||||||
|
case 'objectId':
|
||||||
|
case '_id':
|
||||||
|
key = '_id';
|
||||||
|
break;
|
||||||
|
case 'createdAt':
|
||||||
|
case '_created_at':
|
||||||
|
key = '_created_at';
|
||||||
|
timeField = true;
|
||||||
|
break;
|
||||||
|
case 'updatedAt':
|
||||||
|
case '_updated_at':
|
||||||
|
key = '_updated_at';
|
||||||
|
timeField = true;
|
||||||
|
break;
|
||||||
|
case 'sessionToken':
|
||||||
|
case '_session_token':
|
||||||
|
key = '_session_token';
|
||||||
|
break;
|
||||||
|
case '_rperm':
|
||||||
|
case '_wperm':
|
||||||
|
return {key: key, value: restValue};
|
||||||
|
break;
|
||||||
|
case 'authData.anonymous.id':
|
||||||
|
if (options.query) {
|
||||||
|
return {key: '_auth_data_anonymous.id', value: restValue};
|
||||||
|
}
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
|
'can only query on ' + key);
|
||||||
|
break;
|
||||||
|
case 'authData.facebook.id':
|
||||||
|
if (options.query) {
|
||||||
|
// Special-case auth data.
|
||||||
|
return {key: '_auth_data_facebook.id', value: restValue};
|
||||||
|
}
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
|
'can only query on ' + key);
|
||||||
|
break;
|
||||||
|
case '$or':
|
||||||
|
if (!options.query) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
|
'you can only use $or in queries');
|
||||||
|
}
|
||||||
|
if (!(restValue instanceof Array)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||||
|
'bad $or format - use an array value');
|
||||||
|
}
|
||||||
|
var mongoSubqueries = restValue.map((s) => {
|
||||||
|
return transformWhere(schema, className, s);
|
||||||
|
});
|
||||||
|
return {key: '$or', value: mongoSubqueries};
|
||||||
|
case '$and':
|
||||||
|
if (!options.query) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
|
'you can only use $and in queries');
|
||||||
|
}
|
||||||
|
if (!(restValue instanceof Array)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||||
|
'bad $and format - use an array value');
|
||||||
|
}
|
||||||
|
var mongoSubqueries = restValue.map((s) => {
|
||||||
|
return transformWhere(schema, className, s);
|
||||||
|
});
|
||||||
|
return {key: '$and', value: mongoSubqueries};
|
||||||
|
default:
|
||||||
|
if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME,
|
||||||
|
'invalid key name: ' + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special schema key changes
|
||||||
|
// TODO: it seems like this is likely to have edge cases where
|
||||||
|
// pointer types are missed
|
||||||
|
var expected = undefined;
|
||||||
|
if (schema && schema.getExpectedType) {
|
||||||
|
expected = schema.getExpectedType(className, key);
|
||||||
|
}
|
||||||
|
if ((expected && expected[0] == '*') ||
|
||||||
|
(!expected && restValue && restValue.__type == 'Pointer')) {
|
||||||
|
key = '_p_' + key;
|
||||||
|
}
|
||||||
|
var inArray = (expected === 'array');
|
||||||
|
|
||||||
|
// Handle query constraints
|
||||||
|
if (options.query) {
|
||||||
|
value = transformConstraint(restValue, inArray);
|
||||||
|
if (value !== CannotTransform) {
|
||||||
|
return {key: key, value: value};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inArray && options.query && !(restValue instanceof Array)) {
|
||||||
|
return {
|
||||||
|
key: key, value: [restValue]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle atomic values
|
||||||
|
var value = transformAtom(restValue, false, options);
|
||||||
|
if (value !== CannotTransform) {
|
||||||
|
if (timeField && (typeof value === 'string')) {
|
||||||
|
value = new Date(value);
|
||||||
|
}
|
||||||
|
return {key: key, value: value};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACLs are handled before this method is called
|
||||||
|
// If an ACL key still exists here, something is wrong.
|
||||||
|
if (key === 'ACL') {
|
||||||
|
throw 'There was a problem transforming an ACL.';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Handle arrays
|
||||||
|
if (restValue instanceof Array) {
|
||||||
|
if (options.query) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'cannot use array as query param');
|
||||||
|
}
|
||||||
|
value = restValue.map((restObj) => {
|
||||||
|
var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true });
|
||||||
|
return out.value;
|
||||||
|
});
|
||||||
|
return {key: key, value: value};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle update operators
|
||||||
|
value = transformUpdateOperator(restValue, !options.update);
|
||||||
|
if (value !== CannotTransform) {
|
||||||
|
return {key: key, value: value};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle normal objects by recursing
|
||||||
|
value = {};
|
||||||
|
for (var subRestKey in restValue) {
|
||||||
|
var subRestValue = restValue[subRestKey];
|
||||||
|
var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true });
|
||||||
|
// For recursed objects, keep the keys in rest format
|
||||||
|
value[subRestKey] = out.value;
|
||||||
|
}
|
||||||
|
return {key: key, value: value};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main exposed method to help run queries.
|
||||||
|
// restWhere is the "where" clause in REST API form.
|
||||||
|
// Returns the mongo form of the query.
|
||||||
|
// Throws a Parse.Error if the input query is invalid.
|
||||||
|
function transformWhere(schema, className, restWhere) {
|
||||||
|
var mongoWhere = {};
|
||||||
|
if (restWhere['ACL']) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||||
|
'Cannot query on ACL.');
|
||||||
|
}
|
||||||
|
for (var restKey in restWhere) {
|
||||||
|
var out = transformKeyValue(schema, className, restKey, restWhere[restKey],
|
||||||
|
{query: true, validate: true});
|
||||||
|
mongoWhere[out.key] = out.value;
|
||||||
|
}
|
||||||
|
return mongoWhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main exposed method to create new objects.
|
||||||
|
// restCreate is the "create" clause in REST API form.
|
||||||
|
// Returns the mongo form of the object.
|
||||||
|
function transformCreate(schema, className, restCreate) {
|
||||||
|
var mongoCreate = transformACL(restCreate);
|
||||||
|
for (var restKey in restCreate) {
|
||||||
|
var out = transformKeyValue(schema, className, restKey, restCreate[restKey]);
|
||||||
|
if (out.value !== undefined) {
|
||||||
|
mongoCreate[out.key] = out.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mongoCreate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main exposed method to help update old objects.
|
||||||
|
function transformUpdate(schema, className, restUpdate) {
|
||||||
|
if (!restUpdate) {
|
||||||
|
throw 'got empty restUpdate';
|
||||||
|
}
|
||||||
|
var mongoUpdate = {};
|
||||||
|
var acl = transformACL(restUpdate);
|
||||||
|
if (acl._rperm || acl._wperm) {
|
||||||
|
mongoUpdate['$set'] = {};
|
||||||
|
if (acl._rperm) {
|
||||||
|
mongoUpdate['$set']['_rperm'] = acl._rperm;
|
||||||
|
}
|
||||||
|
if (acl._wperm) {
|
||||||
|
mongoUpdate['$set']['_wperm'] = acl._wperm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var restKey in restUpdate) {
|
||||||
|
var out = transformKeyValue(schema, className, restKey, restUpdate[restKey],
|
||||||
|
{update: true});
|
||||||
|
|
||||||
|
// If the output value is an object with any $ keys, it's an
|
||||||
|
// operator that needs to be lifted onto the top level update
|
||||||
|
// object.
|
||||||
|
if (typeof out.value === 'object' && out.value !== null &&
|
||||||
|
out.value.__op) {
|
||||||
|
mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {};
|
||||||
|
mongoUpdate[out.value.__op][out.key] = out.value.arg;
|
||||||
|
} else {
|
||||||
|
mongoUpdate['$set'] = mongoUpdate['$set'] || {};
|
||||||
|
mongoUpdate['$set'][out.key] = out.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms a REST API formatted ACL object to our two-field mongo format.
|
||||||
|
// This mutates the restObject passed in to remove the ACL key.
|
||||||
|
function transformACL(restObject) {
|
||||||
|
var output = {};
|
||||||
|
if (!restObject['ACL']) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
var acl = restObject['ACL'];
|
||||||
|
var rperm = [];
|
||||||
|
var wperm = [];
|
||||||
|
for (var entry in acl) {
|
||||||
|
if (acl[entry].read) {
|
||||||
|
rperm.push(entry);
|
||||||
|
}
|
||||||
|
if (acl[entry].write) {
|
||||||
|
wperm.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rperm.length) {
|
||||||
|
output._rperm = rperm;
|
||||||
|
}
|
||||||
|
if (wperm.length) {
|
||||||
|
output._wperm = wperm;
|
||||||
|
}
|
||||||
|
delete restObject.ACL;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms a mongo format ACL to a REST API format ACL key
|
||||||
|
// This mutates the mongoObject passed in to remove the _rperm/_wperm keys
|
||||||
|
function untransformACL(mongoObject) {
|
||||||
|
var output = {};
|
||||||
|
if (!mongoObject['_rperm'] && !mongoObject['_wperm']) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
var acl = {};
|
||||||
|
var rperm = mongoObject['_rperm'] || [];
|
||||||
|
var wperm = mongoObject['_wperm'] || [];
|
||||||
|
rperm.map((entry) => {
|
||||||
|
if (!acl[entry]) {
|
||||||
|
acl[entry] = {read: true};
|
||||||
|
} else {
|
||||||
|
acl[entry]['read'] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wperm.map((entry) => {
|
||||||
|
if (!acl[entry]) {
|
||||||
|
acl[entry] = {write: true};
|
||||||
|
} else {
|
||||||
|
acl[entry]['write'] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
output['ACL'] = acl;
|
||||||
|
delete mongoObject._rperm;
|
||||||
|
delete mongoObject._wperm;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms a key used in the REST API format to its mongo format.
|
||||||
|
function transformKey(schema, className, key) {
|
||||||
|
return transformKeyValue(schema, className, key, null, {validate: true}).key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sentinel value that helper transformations return when they
|
||||||
|
// cannot perform a transformation
|
||||||
|
function CannotTransform() {}
|
||||||
|
|
||||||
|
// Helper function to transform an atom from REST format to Mongo format.
|
||||||
|
// An atom is anything that can't contain other expressions. So it
|
||||||
|
// includes things where objects are used to represent other
|
||||||
|
// datatypes, like pointers and dates, but it does not include objects
|
||||||
|
// or arrays with generic stuff inside.
|
||||||
|
// If options.inArray is true, we'll leave it in REST format.
|
||||||
|
// If options.inObject is true, we'll leave files in REST format.
|
||||||
|
// Raises an error if this cannot possibly be valid REST format.
|
||||||
|
// Returns CannotTransform if it's just not an atom, or if force is
|
||||||
|
// true, throws an error.
|
||||||
|
function transformAtom(atom, force, options) {
|
||||||
|
options = options || {};
|
||||||
|
var inArray = options.inArray;
|
||||||
|
var inObject = options.inObject;
|
||||||
|
switch(typeof atom) {
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
case 'boolean':
|
||||||
|
return atom;
|
||||||
|
|
||||||
|
case 'undefined':
|
||||||
|
case 'symbol':
|
||||||
|
case 'function':
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'cannot transform value: ' + atom);
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
if (atom instanceof Date) {
|
||||||
|
// Technically dates are not rest format, but, it seems pretty
|
||||||
|
// clear what they should be transformed to, so let's just do it.
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atom === null) {
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check validity harder for the __type-defined types
|
||||||
|
if (atom.__type == 'Pointer') {
|
||||||
|
if (!inArray && !inObject) {
|
||||||
|
return atom.className + '$' + atom.objectId;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: atom.className,
|
||||||
|
objectId: atom.objectId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (atom.__type == 'Date') {
|
||||||
|
return new Date(atom.iso);
|
||||||
|
}
|
||||||
|
if (atom.__type == 'GeoPoint') {
|
||||||
|
return [atom.longitude, atom.latitude];
|
||||||
|
}
|
||||||
|
if (atom.__type == 'Bytes') {
|
||||||
|
return new mongodb.Binary(new Buffer(atom.base64, 'base64'));
|
||||||
|
}
|
||||||
|
if (atom.__type == 'File') {
|
||||||
|
if (!inArray && !inObject) {
|
||||||
|
return atom.name;
|
||||||
|
}
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'bad atom: ' + atom);
|
||||||
|
}
|
||||||
|
return CannotTransform;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// I don't think typeof can ever let us get here
|
||||||
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR,
|
||||||
|
'really did not expect value: ' + atom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms a query constraint from REST API format to Mongo format.
|
||||||
|
// A constraint is something with fields like $lt.
|
||||||
|
// If it is not a valid constraint but it could be a valid something
|
||||||
|
// else, return CannotTransform.
|
||||||
|
// inArray is whether this is an array field.
|
||||||
|
function transformConstraint(constraint, inArray) {
|
||||||
|
if (typeof constraint !== 'object' || !constraint) {
|
||||||
|
return CannotTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keys is the constraints in reverse alphabetical order.
|
||||||
|
// This is a hack so that:
|
||||||
|
// $regex is handled before $options
|
||||||
|
// $nearSphere is handled before $maxDistance
|
||||||
|
var keys = Object.keys(constraint).sort().reverse();
|
||||||
|
var answer = {};
|
||||||
|
for (var key of keys) {
|
||||||
|
switch(key) {
|
||||||
|
case '$lt':
|
||||||
|
case '$lte':
|
||||||
|
case '$gt':
|
||||||
|
case '$gte':
|
||||||
|
case '$exists':
|
||||||
|
case '$ne':
|
||||||
|
answer[key] = transformAtom(constraint[key], true,
|
||||||
|
{inArray: inArray});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$in':
|
||||||
|
case '$nin':
|
||||||
|
var arr = constraint[key];
|
||||||
|
if (!(arr instanceof Array)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'bad ' + key + ' value');
|
||||||
|
}
|
||||||
|
answer[key] = arr.map((v) => {
|
||||||
|
return transformAtom(v, true);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$all':
|
||||||
|
var arr = constraint[key];
|
||||||
|
if (!(arr instanceof Array)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'bad ' + key + ' value');
|
||||||
|
}
|
||||||
|
answer[key] = arr.map((v) => {
|
||||||
|
return transformAtom(v, true, { inArray: true });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$regex':
|
||||||
|
var s = constraint[key];
|
||||||
|
if (typeof s !== 'string') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s);
|
||||||
|
}
|
||||||
|
answer[key] = s;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$options':
|
||||||
|
var options = constraint[key];
|
||||||
|
if (!answer['$regex'] || (typeof options !== 'string')
|
||||||
|
|| !options.match(/^[imxs]+$/)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||||
|
'got a bad $options');
|
||||||
|
}
|
||||||
|
answer[key] = options;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$nearSphere':
|
||||||
|
var point = constraint[key];
|
||||||
|
answer[key] = [point.longitude, point.latitude];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$maxDistance':
|
||||||
|
answer[key] = constraint[key];
|
||||||
|
break;
|
||||||
|
|
||||||
|
// The SDKs don't seem to use these but they are documented in the
|
||||||
|
// REST API docs.
|
||||||
|
case '$maxDistanceInRadians':
|
||||||
|
answer['$maxDistance'] = constraint[key];
|
||||||
|
break;
|
||||||
|
case '$maxDistanceInMiles':
|
||||||
|
answer['$maxDistance'] = constraint[key] / 3959;
|
||||||
|
break;
|
||||||
|
case '$maxDistanceInKilometers':
|
||||||
|
answer['$maxDistance'] = constraint[key] / 6371;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '$select':
|
||||||
|
case '$dontSelect':
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.COMMAND_UNAVAILABLE,
|
||||||
|
'the ' + key + ' constraint is not supported yet');
|
||||||
|
|
||||||
|
case '$within':
|
||||||
|
var box = constraint[key]['$box'];
|
||||||
|
if (!box || box.length != 2) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'malformatted $within arg');
|
||||||
|
}
|
||||||
|
answer[key] = {
|
||||||
|
'$box': [
|
||||||
|
[box[0].longitude, box[0].latitude],
|
||||||
|
[box[1].longitude, box[1].latitude]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (key.match(/^\$+/)) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
'bad constraint: ' + key);
|
||||||
|
}
|
||||||
|
return CannotTransform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transforms an update operator from REST format to mongo format.
|
||||||
|
// To be transformed, the input should have an __op field.
|
||||||
|
// If flatten is true, this will flatten operators to their static
|
||||||
|
// data format. For example, an increment of 2 would simply become a
|
||||||
|
// 2.
|
||||||
|
// The output for a non-flattened operator is a hash with __op being
|
||||||
|
// the mongo op, and arg being the argument.
|
||||||
|
// The output for a flattened operator is just a value.
|
||||||
|
// Returns CannotTransform if this cannot transform it.
|
||||||
|
// Returns undefined if this should be a no-op.
|
||||||
|
function transformUpdateOperator(operator, flatten) {
|
||||||
|
if (typeof operator !== 'object' || !operator.__op) {
|
||||||
|
return CannotTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(operator.__op) {
|
||||||
|
case 'Delete':
|
||||||
|
if (flatten) {
|
||||||
|
return undefined;
|
||||||
|
} else {
|
||||||
|
return {__op: '$unset', arg: ''};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Increment':
|
||||||
|
if (typeof operator.amount !== 'number') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'incrementing must provide a number');
|
||||||
|
}
|
||||||
|
if (flatten) {
|
||||||
|
return operator.amount;
|
||||||
|
} else {
|
||||||
|
return {__op: '$inc', arg: operator.amount};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Add':
|
||||||
|
case 'AddUnique':
|
||||||
|
if (!(operator.objects instanceof Array)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'objects to add must be an array');
|
||||||
|
}
|
||||||
|
var toAdd = operator.objects.map((obj) => {
|
||||||
|
return transformAtom(obj, true, { inArray: true });
|
||||||
|
});
|
||||||
|
if (flatten) {
|
||||||
|
return toAdd;
|
||||||
|
} else {
|
||||||
|
var mongoOp = {
|
||||||
|
Add: '$push',
|
||||||
|
AddUnique: '$addToSet'
|
||||||
|
}[operator.__op];
|
||||||
|
return {__op: mongoOp, arg: {'$each': toAdd}};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Remove':
|
||||||
|
if (!(operator.objects instanceof Array)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||||
|
'objects to remove must be an array');
|
||||||
|
}
|
||||||
|
var toRemove = operator.objects.map((obj) => {
|
||||||
|
return transformAtom(obj, true, { inArray: true });
|
||||||
|
});
|
||||||
|
if (flatten) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return {__op: '$pullAll', arg: toRemove};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.COMMAND_UNAVAILABLE,
|
||||||
|
'the ' + operator.__op + ' op is not supported yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Converts from a mongo-format object to a REST-format object.
|
||||||
|
// Does not strip out anything based on a lack of authentication.
|
||||||
|
function untransformObject(schema, className, mongoObject) {
|
||||||
|
switch(typeof mongoObject) {
|
||||||
|
case 'string':
|
||||||
|
case 'number':
|
||||||
|
case 'boolean':
|
||||||
|
return mongoObject;
|
||||||
|
case 'undefined':
|
||||||
|
case 'symbol':
|
||||||
|
case 'function':
|
||||||
|
throw 'bad value in untransformObject';
|
||||||
|
case 'object':
|
||||||
|
if (mongoObject === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mongoObject instanceof Array) {
|
||||||
|
return mongoObject.map((o) => {
|
||||||
|
return untransformObject(schema, className, o);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mongoObject instanceof Date) {
|
||||||
|
return Parse._encode(mongoObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mongoObject instanceof mongodb.Binary) {
|
||||||
|
return {
|
||||||
|
__type: 'Bytes',
|
||||||
|
base64: mongoObject.buffer.toString('base64')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var restObject = untransformACL(mongoObject);
|
||||||
|
for (var key in mongoObject) {
|
||||||
|
switch(key) {
|
||||||
|
case '_id':
|
||||||
|
restObject['objectId'] = '' + mongoObject[key];
|
||||||
|
break;
|
||||||
|
case '_hashed_password':
|
||||||
|
restObject['password'] = mongoObject[key];
|
||||||
|
break;
|
||||||
|
case '_acl':
|
||||||
|
case '_perishable_token':
|
||||||
|
break;
|
||||||
|
case '_session_token':
|
||||||
|
restObject['sessionToken'] = mongoObject[key];
|
||||||
|
break;
|
||||||
|
case 'updatedAt':
|
||||||
|
case '_updated_at':
|
||||||
|
restObject['updatedAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||||
|
break;
|
||||||
|
case 'createdAt':
|
||||||
|
case '_created_at':
|
||||||
|
restObject['createdAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||||
|
break;
|
||||||
|
case '_auth_data_anonymous':
|
||||||
|
restObject['authData'] = restObject['authData'] || {};
|
||||||
|
restObject['authData']['anonymous'] = mongoObject[key];
|
||||||
|
break;
|
||||||
|
case '_auth_data_facebook':
|
||||||
|
restObject['authData'] = restObject['authData'] || {};
|
||||||
|
restObject['authData']['facebook'] = mongoObject[key];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (key.indexOf('_p_') == 0) {
|
||||||
|
var newKey = key.substring(3);
|
||||||
|
var expected;
|
||||||
|
if (schema && schema.getExpectedType) {
|
||||||
|
expected = schema.getExpectedType(className, newKey);
|
||||||
|
}
|
||||||
|
if (!expected) {
|
||||||
|
console.log(
|
||||||
|
'Found a pointer column not in the schema, dropping it.',
|
||||||
|
className, newKey);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (expected && expected[0] != '*') {
|
||||||
|
console.log('Found a pointer in a non-pointer column, dropping it.', className, key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var objData = mongoObject[key].split('$');
|
||||||
|
var newClass = (expected ? expected.substring(1) : objData[0]);
|
||||||
|
if (objData[0] !== newClass) {
|
||||||
|
throw 'pointer to incorrect className';
|
||||||
|
}
|
||||||
|
restObject[newKey] = {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: objData[0],
|
||||||
|
objectId: objData[1]
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
} else if (key[0] == '_' && key != '__type') {
|
||||||
|
throw ('bad key in untransform: ' + key);
|
||||||
|
} else {
|
||||||
|
var expected = schema.getExpectedType(className, key);
|
||||||
|
if (expected == 'file') {
|
||||||
|
restObject[key] = {
|
||||||
|
__type: 'File',
|
||||||
|
name: mongoObject[key]
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (expected == 'geopoint') {
|
||||||
|
restObject[key] = {
|
||||||
|
__type: 'GeoPoint',
|
||||||
|
latitude: mongoObject[key][1],
|
||||||
|
longitude: mongoObject[key][0]
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restObject[key] = untransformObject(schema, className,
|
||||||
|
mongoObject[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return restObject;
|
||||||
|
default:
|
||||||
|
throw 'unknown js type';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
transformKey: transformKey,
|
||||||
|
transformCreate: transformCreate,
|
||||||
|
transformUpdate: transformUpdate,
|
||||||
|
transformWhere: transformWhere,
|
||||||
|
untransformObject: untransformObject
|
||||||
|
};
|
||||||
|
|
||||||
99
triggers.js
Normal file
99
triggers.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// triggers.js
|
||||||
|
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
|
||||||
|
var Types = {
|
||||||
|
beforeSave: 'beforeSave',
|
||||||
|
afterSave: 'afterSave',
|
||||||
|
beforeDelete: 'beforeDelete',
|
||||||
|
afterDelete: 'afterDelete'
|
||||||
|
};
|
||||||
|
|
||||||
|
var getTrigger = function(className, triggerType) {
|
||||||
|
if (Parse.Cloud.Triggers
|
||||||
|
&& Parse.Cloud.Triggers[triggerType]
|
||||||
|
&& Parse.Cloud.Triggers[triggerType][className]) {
|
||||||
|
return Parse.Cloud.Triggers[triggerType][className];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
var getRequestObject = function(triggerType, auth, parseObject, originalParseObject) {
|
||||||
|
var request = {
|
||||||
|
triggerName: triggerType,
|
||||||
|
object: parseObject,
|
||||||
|
master: false
|
||||||
|
};
|
||||||
|
if (originalParseObject) {
|
||||||
|
request.original = originalParseObject;
|
||||||
|
}
|
||||||
|
if (!auth) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
if (auth.isMaster) {
|
||||||
|
request['master'] = true;
|
||||||
|
}
|
||||||
|
if (auth.user) {
|
||||||
|
request['user'] = auth.user;
|
||||||
|
}
|
||||||
|
// TODO: Add installation to Auth?
|
||||||
|
if (auth.installationId) {
|
||||||
|
request['installationId'] = auth.installationId;
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates the response object, and uses the request object to pass data
|
||||||
|
// The API will call this with REST API formatted objects, this will
|
||||||
|
// transform them to Parse.Object instances expected by Cloud Code.
|
||||||
|
// Any changes made to the object in a beforeSave will be included.
|
||||||
|
var getResponseObject = function(request, resolve, reject) {
|
||||||
|
return {
|
||||||
|
success: function() {
|
||||||
|
var response = {};
|
||||||
|
if (request.triggerName === Types.beforeSave) {
|
||||||
|
response['object'] = request.object.toJSON();
|
||||||
|
}
|
||||||
|
return resolve(response);
|
||||||
|
},
|
||||||
|
error: function(error) {
|
||||||
|
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// To be used as part of the promise chain when saving/deleting an object
|
||||||
|
// Will resolve successfully if no trigger is configured
|
||||||
|
// Resolves to an object, empty or containing an object key. A beforeSave
|
||||||
|
// trigger will set the object key to the rest format object to save.
|
||||||
|
// originalParseObject is optional, we only need that for befote/afterSave functions
|
||||||
|
var maybeRunTrigger = function(triggerType, auth, parseObject, originalParseObject) {
|
||||||
|
if (!parseObject) {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
var trigger = getTrigger(parseObject.className, triggerType);
|
||||||
|
if (!trigger) return resolve({});
|
||||||
|
var request = getRequestObject(triggerType, auth, parseObject, originalParseObject);
|
||||||
|
var response = getResponseObject(request, resolve, reject);
|
||||||
|
trigger(request, response);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Converts a REST-format object to a Parse.Object
|
||||||
|
// data is either className or an object
|
||||||
|
function inflate(data, restObject) {
|
||||||
|
var copy = typeof data == 'object' ? data : {className: data};
|
||||||
|
for (var key in restObject) {
|
||||||
|
copy[key] = restObject[key];
|
||||||
|
}
|
||||||
|
return Parse.Object.fromJSON(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getTrigger: getTrigger,
|
||||||
|
getRequestObject: getRequestObject,
|
||||||
|
inflate: inflate,
|
||||||
|
maybeRunTrigger: maybeRunTrigger,
|
||||||
|
Types: Types
|
||||||
|
};
|
||||||
180
users.js
Normal file
180
users.js
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// These methods handle the User-related routes.
|
||||||
|
|
||||||
|
var mongodb = require('mongodb');
|
||||||
|
var Parse = require('parse/node').Parse;
|
||||||
|
var rack = require('hat').rack();
|
||||||
|
|
||||||
|
var Auth = require('./Auth');
|
||||||
|
var crypto = require('./crypto');
|
||||||
|
var facebook = require('./facebook');
|
||||||
|
var PromiseRouter = require('./PromiseRouter');
|
||||||
|
var rest = require('./rest');
|
||||||
|
var RestWrite = require('./RestWrite');
|
||||||
|
|
||||||
|
var router = new PromiseRouter();
|
||||||
|
|
||||||
|
// Returns a promise for a {status, response, location} object.
|
||||||
|
function handleCreate(req) {
|
||||||
|
return rest.create(req.config, req.auth,
|
||||||
|
'_User', req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleLogIn(req) {
|
||||||
|
|
||||||
|
// Use query parameters instead if provided in url
|
||||||
|
if (!req.body.username && req.query.username) {
|
||||||
|
req.body = req.query;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use the right error codes / descriptions.
|
||||||
|
if (!req.body.username) {
|
||||||
|
throw new Parse.Error(Parse.Error.USERNAME_MISSING,
|
||||||
|
'username is required.');
|
||||||
|
}
|
||||||
|
if (!req.body.password) {
|
||||||
|
throw new Parse.Error(Parse.Error.PASSWORD_MISSING,
|
||||||
|
'password is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var user;
|
||||||
|
return req.database.find('_User', {username: req.body.username})
|
||||||
|
.then((results) => {
|
||||||
|
if (!results.length) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Invalid username/password.');
|
||||||
|
}
|
||||||
|
user = results[0];
|
||||||
|
return crypto.compare(req.body.password, user.password);
|
||||||
|
}).then((correct) => {
|
||||||
|
if (!correct) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Invalid username/password.');
|
||||||
|
}
|
||||||
|
var token = 'r:' + rack();
|
||||||
|
user.sessionToken = token;
|
||||||
|
delete user.password;
|
||||||
|
|
||||||
|
var sessionData = {
|
||||||
|
sessionToken: token,
|
||||||
|
user: {
|
||||||
|
__type: 'Pointer',
|
||||||
|
className: '_User',
|
||||||
|
objectId: user.objectId
|
||||||
|
},
|
||||||
|
createdWith: {
|
||||||
|
'action': 'login',
|
||||||
|
'authProvider': 'password'
|
||||||
|
},
|
||||||
|
restricted: false,
|
||||||
|
expiresAt: 0,
|
||||||
|
installationId: req.info.installationId
|
||||||
|
};
|
||||||
|
var create = new RestWrite(req.config, Auth.master(req.config),
|
||||||
|
'_Session', null, sessionData);
|
||||||
|
return create.execute();
|
||||||
|
}).then(() => {
|
||||||
|
return {response: user};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise that resolves to a {response} object.
|
||||||
|
// TODO: share code with classes.js
|
||||||
|
function handleFind(req) {
|
||||||
|
var options = {};
|
||||||
|
if (req.body.skip) {
|
||||||
|
options.skip = Number(req.body.skip);
|
||||||
|
}
|
||||||
|
if (req.body.limit) {
|
||||||
|
options.limit = Number(req.body.limit);
|
||||||
|
}
|
||||||
|
if (req.body.order) {
|
||||||
|
options.order = String(req.body.order);
|
||||||
|
}
|
||||||
|
if (req.body.count) {
|
||||||
|
options.count = true;
|
||||||
|
}
|
||||||
|
if (typeof req.body.keys == 'string') {
|
||||||
|
options.keys = req.body.keys;
|
||||||
|
}
|
||||||
|
if (req.body.include) {
|
||||||
|
options.include = String(req.body.include);
|
||||||
|
}
|
||||||
|
if (req.body.redirectClassNameForKey) {
|
||||||
|
options.redirectClassNameForKey = String(req.body.redirectClassNameForKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest.find(req.config, req.auth,
|
||||||
|
'_User', req.body.where, options)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a promise for a {response} object.
|
||||||
|
function handleGet(req) {
|
||||||
|
return rest.find(req.config, req.auth, '_User',
|
||||||
|
{objectId: req.params.objectId})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
} else {
|
||||||
|
return {response: response.results[0]};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMe(req) {
|
||||||
|
if (!req.info || !req.info.sessionToken) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
}
|
||||||
|
return rest.find(req.config, Auth.master(req.config), '_Session',
|
||||||
|
{_session_token: req.info.sessionToken},
|
||||||
|
{include: 'user'})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.results || response.results.length == 0 ||
|
||||||
|
!response.results[0].user) {
|
||||||
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||||
|
'Object not found.');
|
||||||
|
} else {
|
||||||
|
var user = response.results[0].user;
|
||||||
|
return {response: user};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(req) {
|
||||||
|
return rest.del(req.config, req.auth,
|
||||||
|
req.params.className, req.params.objectId)
|
||||||
|
.then(() => {
|
||||||
|
return {response: {}};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdate(req) {
|
||||||
|
return rest.update(req.config, req.auth, '_User',
|
||||||
|
req.params.objectId, req.body)
|
||||||
|
.then((response) => {
|
||||||
|
return {response: response};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function notImplementedYet(req) {
|
||||||
|
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
|
||||||
|
'This path is not implemented yet.');
|
||||||
|
}
|
||||||
|
|
||||||
|
router.route('POST', '/users', handleCreate);
|
||||||
|
router.route('GET', '/login', handleLogIn);
|
||||||
|
router.route('GET', '/users/me', handleMe);
|
||||||
|
router.route('GET', '/users/:objectId', handleGet);
|
||||||
|
router.route('PUT', '/users/:objectId', handleUpdate);
|
||||||
|
router.route('GET', '/users', handleFind);
|
||||||
|
router.route('DELETE', '/users/:objectId', handleDelete);
|
||||||
|
|
||||||
|
router.route('POST', '/requestPasswordReset', notImplementedYet);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
Reference in New Issue
Block a user