Unique indexes (#1971)

* Add unique indexing

* Add unique indexing for username/email

* WIP

* Finish unique indexes

* Notes on how to upgrade to 2.3.0 safely

* index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)"

* reconfigure username/email tests

* Start dealing with test shittyness

* Remove tests for files that we are removing

* most tests passing

* fix failing test

* Make specific server config for tests async

* Fix more tests

* fix more tests

* Fix another test

* fix more tests

* Fix email validation

* move some stuff around

* Destroy server to ensure all connections are gone

* Fix broken cloud code

* Save callback to variable

* no need to delete non existant cloud

* undo

* Fix all tests where connections are left open after server closes.

* Fix issues caused by missing gridstore adapter

* Update guide for 2.3.0 and fix final tests

* use strict

* don't use features that won't work in node 4

* Fix syntax error

* Fix typos

* Add duplicate finding command

* Update 2.3.0.md
This commit is contained in:
Drew
2016-06-10 20:27:21 -07:00
committed by GitHub
parent 6415a35433
commit 7e868b2dcc
37 changed files with 1727 additions and 1517 deletions

View File

@@ -1,6 +1,5 @@
export function loadAdapter(adapter, defaultAdapter, options) {
if (!adapter)
{
if (!adapter) {
if (!defaultAdapter) {
return options;
}

View File

@@ -83,6 +83,18 @@ export default class MongoCollection {
return this._mongoCollection.deleteMany(query);
}
_ensureSparseUniqueIndexInBackground(indexRequest) {
return new Promise((resolve, reject) => {
this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error, indexName) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
drop() {
return this._mongoCollection.drop();
}

View File

@@ -148,7 +148,7 @@ class MongoSchemaCollection {
if (results.length === 1) {
return mongoSchemaToParseSchema(results[0]);
} else {
return Promise.reject();
throw undefined;
}
});
}
@@ -175,9 +175,9 @@ class MongoSchemaCollection {
.then(result => mongoSchemaToParseSchema(result.ops[0]))
.catch(error => {
if (error.code === 11000) { //Mongo's duplicate key error
return Promise.reject();
throw undefined;
}
return Promise.reject(error);
throw error;
});
}
@@ -207,17 +207,17 @@ class MongoSchemaCollection {
if (type.type === 'GeoPoint') {
// Make sure there are not other geopoint fields
if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) {
return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.'));
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.');
}
}
return Promise.resolve();
return;
}, error => {
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
// If some other error, reject with it.
if (error === undefined) {
return Promise.resolve();
return;
}
throw Promise.reject(error);
throw error;
})
.then(() => {
// We use $exists and $set to avoid overwriting the field type if it

View File

@@ -65,6 +65,7 @@ export class MongoStorageAdapter {
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(database => {
this.database = database;
});
return this.connectionPromise;
}
@@ -102,9 +103,9 @@ export class MongoStorageAdapter {
.catch(error => {
// 'ns not found' means collection was already gone. Ignore deletion attempt.
if (error.message == 'ns not found') {
return Promise.resolve();
return;
}
return Promise.reject(error);
throw error;
});
}
@@ -180,7 +181,7 @@ export class MongoStorageAdapter {
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided');
}
return Promise.reject(error);
throw error;
});
}
@@ -236,6 +237,28 @@ export class MongoStorageAdapter {
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)));
}
// Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
// currently know which fields are nullable and which aren't, we ignore that criteria.
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
// which is why we use sparse indexes.
ensureUniqueness(className, fieldNames, schema) {
let indexCreationRequest = {};
let mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
mongoFieldNames.forEach(fieldName => {
indexCreationRequest[fieldName] = 1;
});
return this.adaptiveCollection(className)
.then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest))
.catch(error => {
if (error.code === 11000) {
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.');
} else {
throw error;
}
});
}
// Used in tests
_rawFind(className, query) {
return this.adaptiveCollection(className).then(collection => collection.find(query));

View File

@@ -16,7 +16,6 @@ function removeTrailingSlash(str) {
export class Config {
constructor(applicationId: string, mount: string) {
let DatabaseAdapter = require('./DatabaseAdapter');
let cacheInfo = AppCache.get(applicationId);
if (!cacheInfo) {
return;
@@ -32,7 +31,7 @@ export class Config {
this.fileKey = cacheInfo.fileKey;
this.facebookAppIds = cacheInfo.facebookAppIds;
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
this.database = cacheInfo.databaseController;
this.serverURL = cacheInfo.serverURL;
this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL);
@@ -55,24 +54,31 @@ export class Config {
this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset;
}
static validate(options) {
static validate({
verifyUserEmails,
appName,
publicServerURL,
revokeSessionOnPasswordReset,
expireInactiveSessions,
sessionLength,
}) {
this.validateEmailConfiguration({
verifyUserEmails: options.verifyUserEmails,
appName: options.appName,
publicServerURL: options.publicServerURL
verifyUserEmails: verifyUserEmails,
appName: appName,
publicServerURL: publicServerURL
})
if (typeof options.revokeSessionOnPasswordReset !== 'boolean') {
if (typeof revokeSessionOnPasswordReset !== 'boolean') {
throw 'revokeSessionOnPasswordReset must be a boolean value';
}
if (options.publicServerURL) {
if (!options.publicServerURL.startsWith("http://") && !options.publicServerURL.startsWith("https://")) {
if (publicServerURL) {
if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) {
throw "publicServerURL should be a valid HTTPS URL starting with https://"
}
}
this.validateSessionConfiguration(options.sessionLength, options.expireInactiveSessions);
this.validateSessionConfiguration(sessionLength, expireInactiveSessions);
}
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {

View File

@@ -61,19 +61,12 @@ function DatabaseController(adapter, { skipValidation } = {}) {
// it. Instead, use loadSchema to get a schema.
this.schemaPromise = null;
this.skipValidation = !!skipValidation;
this.connect();
}
DatabaseController.prototype.WithoutValidation = function() {
return new DatabaseController(this.adapter, {collectionPrefix: this.collectionPrefix, skipValidation: true});
}
// Connects to the database. Returns a promise that resolves when the
// connection is successful.
DatabaseController.prototype.connect = function() {
return this.adapter.connect();
};
DatabaseController.prototype.schemaCollection = function() {
return this.adapter.schemaCollection();
};
@@ -87,8 +80,7 @@ DatabaseController.prototype.validateClassName = function(className) {
return Promise.resolve();
}
if (!SchemaController.classNameIsValid(className)) {
const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className);
return Promise.reject(error);
return Promise.reject(new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className));
}
return Promise.resolve();
};
@@ -417,7 +409,6 @@ DatabaseController.prototype.canAddField = function(schema, className, object, a
return Promise.resolve();
}
// Deletes everything in the database matching the current collectionPrefix
// Won't delete collections in the system namespace
// Returns a promise.
DatabaseController.prototype.deleteEverything = function() {

View File

@@ -1,23 +1,20 @@
/** @flow weak */
import * as DatabaseAdapter from "../DatabaseAdapter";
import * as triggers from "../triggers";
import * as Parse from "parse/node";
import * as request from "request";
import { logger } from '../logger';
import * as triggers from "../triggers";
import * as Parse from "parse/node";
import * as request from "request";
import { logger } from '../logger';
const DefaultHooksCollectionName = "_Hooks";
export class HooksController {
_applicationId:string;
_collectionPrefix:string;
_collection;
constructor(applicationId:string, collectionPrefix:string = '', webhookKey) {
constructor(applicationId:string, databaseController, webhookKey) {
this._applicationId = applicationId;
this._collectionPrefix = collectionPrefix;
this._webhookKey = webhookKey;
this.database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix).WithoutValidation();
this.database = databaseController;
}
load() {

View File

@@ -43,7 +43,7 @@ export class UserController extends AdaptableController {
if (!this.shouldVerifyEmails) {
// Trying to verify email when not enabled
// TODO: Better error here.
return Promise.reject();
throw undefined;
}
let database = this.config.database.WithoutValidation();
return database.update('_User', {
@@ -51,7 +51,7 @@ export class UserController extends AdaptableController {
_email_verify_token: token
}, {emailVerified: true}).then(document => {
if (!document) {
return Promise.reject();
throw undefined;
}
return Promise.resolve(document);
});
@@ -64,7 +64,7 @@ export class UserController extends AdaptableController {
_perishable_token: token
}, {limit: 1}).then(results => {
if (results.length != 1) {
return Promise.reject();
throw undefined;
}
return results[0];
});
@@ -85,7 +85,7 @@ export class UserController extends AdaptableController {
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
return query.execute().then(function(result){
if (result.results.length != 1) {
return Promise.reject();
throw undefined;
}
return result.results[0];
})

View File

@@ -1,74 +1,21 @@
/** @flow weak */
// 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 MongoStorageAdapter.
import DatabaseController from './Controllers/DatabaseController';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
let dbConnections = {};
let appDatabaseURIs = {};
let appDatabaseOptions = {};
function setAppDatabaseURI(appId, uri) {
appDatabaseURIs[appId] = uri;
}
function setAppDatabaseOptions(appId: string, options: Object) {
appDatabaseOptions[appId] = options;
}
//Used by tests
function clearDatabaseSettings() {
appDatabaseURIs = {};
dbConnections = {};
appDatabaseOptions = {};
}
import AppCache from './cache';
//Used by tests
function destroyAllDataPermanently() {
if (process.env.TESTING) {
var promises = [];
for (var conn in dbConnections) {
promises.push(dbConnections[conn].deleteEverything());
}
return Promise.all(promises);
// This is super janky, but destroyAllDataPermanently is
// a janky interface, so we need to have some jankyness
// to support it
return Promise.all(Object.keys(AppCache.cache).map(appId => {
const app = AppCache.get(appId);
if (app.databaseController) {
return app.databaseController.deleteEverything();
} else {
return Promise.resolve();
}
}));
}
throw 'Only supported in test environment';
}
function getDatabaseConnection(appId: string, collectionPrefix: string) {
if (dbConnections[appId]) {
return dbConnections[appId];
}
let mongoAdapterOptions = {
collectionPrefix: collectionPrefix,
mongoOptions: appDatabaseOptions[appId],
uri: appDatabaseURIs[appId], //may be undefined if the user didn't supply a URI, in which case the default will be used
}
dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId});
return dbConnections[appId];
}
module.exports = {
getDatabaseConnection: getDatabaseConnection,
setAppDatabaseOptions: setAppDatabaseOptions,
setAppDatabaseURI: setAppDatabaseURI,
clearDatabaseSettings: clearDatabaseSettings,
destroyAllDataPermanently: destroyAllDataPermanently,
};
module.exports = { destroyAllDataPermanently };

View File

@@ -51,10 +51,17 @@ import { SessionsRouter } from './Routers/SessionsRouter';
import { UserController } from './Controllers/UserController';
import { UsersRouter } from './Routers/UsersRouter';
import DatabaseController from './Controllers/DatabaseController';
const SchemaController = require('./Controllers/SchemaController');
import ParsePushAdapter from 'parse-server-push-adapter';
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } };
// ParseServer works like a constructor of an express app.
// The args that we understand are:
// "filesAdapter": a class like GridStoreAdapter providing create, get,
@@ -88,6 +95,7 @@ class ParseServer {
masterKey = requiredParameter('You must provide a masterKey!'),
appName,
filesAdapter,
databaseAdapter,
push,
loggerAdapter,
logsFolder,
@@ -122,23 +130,34 @@ class ParseServer {
expireInactiveSessions = true,
verbose = false,
revokeSessionOnPasswordReset = true,
__indexBuildCompletionCallbackForTests = () => {},
}) {
// Initialize the node client SDK automatically
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
Parse.serverURL = serverURL;
if ((databaseOptions || databaseURI || collectionPrefix !== '') && databaseAdapter) {
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/connectionPrefix.';
} else if (!databaseAdapter) {
databaseAdapter = new MongoStorageAdapter({
uri: databaseURI,
collectionPrefix,
mongoOptions: databaseOptions,
});
} else {
databaseAdapter = loadAdapter(databaseAdapter)
}
if (!filesAdapter && !databaseURI) {
throw 'When using an explicit database adapter, you must also use and explicit filesAdapter.';
}
if (logsFolder) {
configureLogger({
logsFolder
})
}
if (databaseOptions) {
DatabaseAdapter.setAppDatabaseOptions(appId, databaseOptions);
}
DatabaseAdapter.setAppDatabaseURI(appId, databaseURI);
if (cloud) {
addParseCloud();
if (typeof cloud === 'function') {
@@ -168,10 +187,23 @@ class ParseServer {
const filesController = new FilesController(filesControllerAdapter, appId);
const pushController = new PushController(pushControllerAdapter, appId);
const loggerController = new LoggerController(loggerControllerAdapter, appId);
const hooksController = new HooksController(appId, collectionPrefix, webhookKey);
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
const liveQueryController = new LiveQueryController(liveQuery);
const cacheController = new CacheController(cacheControllerAdapter, appId);
const databaseController = new DatabaseController(databaseAdapter);
const hooksController = new HooksController(appId, databaseController, webhookKey);
let usernameUniqueness = databaseController.adapter.ensureUniqueness('_User', ['username'], requiredUserFields)
.catch(error => {
logger.warn('Unable to ensure uniqueness for usernames: ', error);
return Promise.reject();
});
let emailUniqueness = databaseController.adapter.ensureUniqueness('_User', ['email'], requiredUserFields)
.catch(error => {
logger.warn('Unabled to ensure uniqueness for user email addresses: ', error);
return Promise.reject();
})
AppCache.put(appId, {
masterKey: masterKey,
@@ -200,7 +232,8 @@ class ParseServer {
liveQueryController: liveQueryController,
sessionLength: Number(sessionLength),
expireInactiveSessions: expireInactiveSessions,
revokeSessionOnPasswordReset
revokeSessionOnPasswordReset,
databaseController,
});
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
@@ -211,6 +244,11 @@ class ParseServer {
Config.validate(AppCache.get(appId));
this.config = AppCache.get(appId);
hooksController.load();
// Note: Tests will start to fail if any validation happens after this is called.
if (process.env.TESTING) {
__indexBuildCompletionCallbackForTests(Promise.all([usernameUniqueness, emailUniqueness]));
}
}
get app() {

View File

@@ -6,8 +6,8 @@
// components that external developers may be modifying.
import express from 'express';
import url from 'url';
import log from './logger';
import url from 'url';
import log from './logger';
export default class PromiseRouter {
// Each entry should be an object with:

View File

@@ -105,9 +105,9 @@ RestWrite.prototype.getUserAndRoleACL = function() {
return this.auth.getUserRoles().then((roles) => {
roles.push(this.auth.user.id);
this.runOptions.acl = this.runOptions.acl.concat(roles);
return Promise.resolve();
return;
});
}else{
} else {
return Promise.resolve();
}
};
@@ -119,7 +119,7 @@ RestWrite.prototype.validateClientClassCreation = function() {
&& sysClass.indexOf(this.className) === -1) {
return this.config.database.collectionExists(this.className).then((hasClass) => {
if (hasClass === true) {
return Promise.resolve();
return;
}
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
@@ -309,7 +309,7 @@ RestWrite.prototype.handleAuthData = function(authData) {
}
}
}
return Promise.resolve();
return;
});
}
@@ -356,45 +356,43 @@ RestWrite.prototype.transformUser = function() {
}
return;
}
// We need to a find to check for duplicate username in case they are missing the unique index on usernames
// TODO: Check if there is a unique index, and if so, skip this query.
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(() => {
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;
});
})
.then(() => {
if (!this.data.email || this.data.email.__op === 'Delete') {
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.');
throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.');
}
// Check for email uniqueness
// Same problem for email as above for username
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();
}).then(() => {
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
return Promise.resolve();
})
});
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.');
}
// We updated the email, send a new validation
this.storage['sendVerificationEmail'] = true;
this.config.userController.setEmailVerifyToken(this.data);
});
})
};
RestWrite.prototype.createSessionTokenIfNeeded = function() {
@@ -577,7 +575,7 @@ RestWrite.prototype.handleInstallation = function() {
'deviceType may not be changed in this ' +
'operation');
}
return Promise.resolve();
return;
});
});
}
@@ -762,6 +760,36 @@ RestWrite.prototype.runDatabaseOperation = function() {
// Run a create
return this.config.database.create(this.className, this.data, this.runOptions)
.catch(error => {
if (this.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) {
throw error;
}
// If this was a failed user creation due to username or email already taken, we need to
// check whether it was username or email and return the appropriate error.
// TODO: See if we can later do this without additional queries by using named indexes.
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 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.');
}
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
});
})
.then(response => {
response.objectId = this.data.objectId;
response.createdAt = this.data.createdAt;

View File

@@ -1,9 +1,9 @@
import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import { randomHexString } from '../cryptoUtils';
import Config from '../Config';
import mime from 'mime';
import Config from '../Config';
import mime from 'mime';
export class FilesRouter {
@@ -77,8 +77,7 @@ export class FilesRouter {
res.set('Location', result.url);
res.json(result);
}).catch((err) => {
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
'Could not store file.'));
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.'));
});
}
@@ -93,4 +92,4 @@ export class FilesRouter {
'Could not delete file.'));
});
}
}
}

View File

@@ -1,6 +1,5 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import { HooksController } from '../Controllers/HooksController';
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import * as middleware from "../middlewares";
export class HooksRouter extends PromiseRouter {
@@ -26,7 +25,7 @@ export class HooksRouter extends PromiseRouter {
return Promise.resolve({response: foundFunction});
});
}
return hooksController.getFunctions().then((functions) => {
return { response: functions || [] };
}, (err) => {
@@ -37,7 +36,7 @@ export class HooksRouter extends PromiseRouter {
handleGetTriggers(req) {
var hooksController = req.config.hooksController;
if (req.params.className && req.params.triggerName) {
return hooksController.getTrigger(req.params.className, req.params.triggerName).then((foundTrigger) => {
if (!foundTrigger) {
throw new Parse.Error(143,`class ${req.params.className} does not exist`);
@@ -45,7 +44,7 @@ export class HooksRouter extends PromiseRouter {
return Promise.resolve({response: foundTrigger});
});
}
return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] }));
}
@@ -73,10 +72,10 @@ export class HooksRouter extends PromiseRouter {
hook.url = req.body.url
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
}
return this.updateHook(hook, req.config);
}
handlePut(req) {
var body = req.body;
if (body.__op == "Delete") {
@@ -85,7 +84,7 @@ export class HooksRouter extends PromiseRouter {
return this.handleUpdate(req);
}
}
mountRoutes() {
this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));

View File

@@ -69,8 +69,8 @@ function del(config, auth, className, objectId) {
}).then(() => {
if (!auth.isMaster) {
return auth.getUserRoles();
}else{
return Promise.resolve();
} else {
return;
}
}).then(() => {
var options = {};
@@ -87,7 +87,7 @@ function del(config, auth, className, objectId) {
}, options);
}).then(() => {
triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config);
return Promise.resolve();
return;
});
}

View File

@@ -14,6 +14,7 @@ function createApp(req, res) {
var appId = cryptoUtils.randomHexString(32);
ParseServer({
databaseURI: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase',
appId: appId,
masterKey: 'master',
serverURL: Parse.serverURL,