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:
@@ -1,6 +1,5 @@
|
||||
export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
if (!adapter)
|
||||
{
|
||||
if (!adapter) {
|
||||
if (!defaultAdapter) {
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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];
|
||||
})
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:
|
||||
|
||||
100
src/RestWrite.js
100
src/RestWrite.js
@@ -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;
|
||||
|
||||
@@ -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.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user