DBController refactoring (#1228)

* Moves transform to MongoTransform

- Adds ACL query injection in MongoTransform

* Removes adaptiveCollection from DatabaseController

- All collections manipulations are now handled by a DBController
- Adds optional flags to configure an unsafe databaseController for direct
  access
- Adds ability to configure RestWrite with multiple writes
- Moves some transfirmations to MongoTransform as they output specific code

* Renames Unsafe to WithoutValidation
This commit is contained in:
Florent Vilmart
2016-04-14 19:24:56 -04:00
parent 51970fb470
commit 1023baf20d
17 changed files with 317 additions and 291 deletions

View File

@@ -7,18 +7,27 @@ var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
var Schema = require('./../Schema');
var transform = require('./../transform');
const deepcopy = require('deepcopy');
function DatabaseController(adapter) {
function DatabaseController(adapter, { skipValidation } = {}) {
this.adapter = adapter;
// 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.skipValidation = !!skipValidation;
this.connect();
Object.defineProperty(this, 'transform', {
get: function() {
return adapter.transform;
}
})
}
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
@@ -27,10 +36,6 @@ DatabaseController.prototype.connect = function() {
return this.adapter.connect();
};
DatabaseController.prototype.adaptiveCollection = function(className) {
return this.adapter.adaptiveCollection(className);
};
DatabaseController.prototype.schemaCollection = function() {
return this.adapter.schemaCollection();
};
@@ -44,6 +49,9 @@ DatabaseController.prototype.dropCollection = function(className) {
};
DatabaseController.prototype.validateClassName = function(className) {
if (this.skipValidation) {
return Promise.resolve();
}
if (!Schema.classNameIsValid(className)) {
const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className);
return Promise.reject(error);
@@ -113,7 +121,7 @@ DatabaseController.prototype.validateObject = function(className, object, query,
// Filters out any data that shouldn't be on this REST-formatted object.
DatabaseController.prototype.untransformObject = function(
schema, isMaster, aclGroup, className, mongoObject) {
var object = transform.untransformObject(schema, className, mongoObject);
var object = this.transform.untransformObject(schema, className, mongoObject);
if (className !== '_User') {
return object;
@@ -137,7 +145,7 @@ DatabaseController.prototype.untransformObject = function(
// 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.
DatabaseController.prototype.update = function(className, query, update, options) {
DatabaseController.prototype.update = function(className, query, update, options = {}) {
const originalUpdate = update;
// Make a copy of the object, so we don't mutate the incoming data.
@@ -158,26 +166,29 @@ DatabaseController.prototype.update = function(className, query, update, options
return Promise.resolve();
})
.then(() => this.handleRelationUpdates(className, query.objectId, update))
.then(() => this.adaptiveCollection(className))
.then(() => this.adapter.adaptiveCollection(className))
.then(collection => {
var mongoWhere = transform.transformWhere(schema, className, query);
var mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation});
if (options.acl) {
var writePerms = [
{_wperm: {'$exists': false}}
];
for (var entry of options.acl) {
writePerms.push({_wperm: {'$in': [entry]}});
}
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
mongoWhere = this.transform.addWriteACL(mongoWhere, options.acl);
}
mongoUpdate = this.transform.transformUpdate(schema, className, update, {validate: !this.skipValidation});
if (options.many) {
return collection.updateMany(mongoWhere, mongoUpdate);
}else if (options.upsert) {
return collection.upsertOne(mongoWhere, mongoUpdate);
} else {
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
}
mongoUpdate = transform.transformUpdate(schema, className, update);
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
})
.then(result => {
if (!result) {
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.'));
}
if (this.skipValidation) {
return Promise.resolve(result);
}
return sanitizeDatabaseResult(originalUpdate, result);
});
};
@@ -256,7 +267,7 @@ DatabaseController.prototype.addRelation = function(key, fromClassName, fromId,
owningId : fromId
};
let className = `_Join:${key}:${fromClassName}`;
return this.adaptiveCollection(className).then((coll) => {
return this.adapter.adaptiveCollection(className).then((coll) => {
return coll.upsertOne(doc, doc);
});
};
@@ -270,7 +281,7 @@ DatabaseController.prototype.removeRelation = function(key, fromClassName, fromI
owningId: fromId
};
let className = `_Join:${key}:${fromClassName}`;
return this.adaptiveCollection(className).then(coll => {
return this.adapter.adaptiveCollection(className).then(coll => {
return coll.deleteOne(doc);
});
};
@@ -295,18 +306,11 @@ DatabaseController.prototype.destroy = function(className, query, options = {})
}
return Promise.resolve();
})
.then(() => this.adaptiveCollection(className))
.then(() => this.adapter.adaptiveCollection(className))
.then(collection => {
let mongoWhere = transform.transformWhere(schema, className, query);
let mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation});
if (options.acl) {
var writePerms = [
{ _wperm: { '$exists': false } }
];
for (var entry of options.acl) {
writePerms.push({ _wperm: { '$in': [entry] } });
}
mongoWhere = { '$and': [mongoWhere, { '$or': writePerms }] };
mongoWhere = this.transform.addWriteACL(mongoWhere, options.acl);
}
return collection.deleteMany(mongoWhere);
})
@@ -321,7 +325,7 @@ DatabaseController.prototype.destroy = function(className, query, options = {})
// Inserts an object into the database.
// Returns a promise that resolves successfully iff the object saved.
DatabaseController.prototype.create = function(className, object, options) {
DatabaseController.prototype.create = function(className, object, options = {}) {
// Make a copy of the object, so we don't mutate the incoming data.
let originalObject = object;
object = deepcopy(object);
@@ -340,9 +344,9 @@ DatabaseController.prototype.create = function(className, object, options) {
return Promise.resolve();
})
.then(() => this.handleRelationUpdates(className, null, object))
.then(() => this.adaptiveCollection(className))
.then(() => this.adapter.adaptiveCollection(className))
.then(coll => {
var mongoObject = transform.transformCreate(schema, className, object);
var mongoObject = this.transform.transformCreate(schema, className, object);
return coll.insertOne(mongoObject);
})
.then(result => {
@@ -371,7 +375,7 @@ DatabaseController.prototype.canAddField = function(schema, className, object, a
// to avoid Mongo-format dependencies.
// Returns a promise that resolves to a list of items.
DatabaseController.prototype.mongoFind = function(className, query, options = {}) {
return this.adaptiveCollection(className)
return this.adapter.adaptiveCollection(className)
.then(collection => collection.find(query, options));
};
@@ -404,7 +408,7 @@ function keysForQuery(query) {
// Returns a promise for a list of related ids given an owning id.
// className here is the owning className.
DatabaseController.prototype.relatedIds = function(className, key, owningId) {
return this.adaptiveCollection(joinTableName(className, key))
return this.adapter.adaptiveCollection(joinTableName(className, key))
.then(coll => coll.find({owningId : owningId}))
.then(results => results.map(r => r.relatedId));
};
@@ -412,7 +416,7 @@ DatabaseController.prototype.relatedIds = function(className, key, owningId) {
// Returns a promise for a list of owning ids given some related ids.
// className here is the owning className.
DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
return this.adaptiveCollection(joinTableName(className, key))
return this.adapter.adaptiveCollection(joinTableName(className, key))
.then(coll => coll.find({ relatedId: { '$in': relatedIds } }))
.then(results => results.map(r => r.owningId));
};
@@ -597,7 +601,7 @@ DatabaseController.prototype.find = function(className, query, options = {}) {
if (options.sort) {
mongoOptions.sort = {};
for (let key in options.sort) {
let mongoKey = transform.transformKey(schema, className, key);
let mongoKey = this.transform.transformKey(schema, className, key);
mongoOptions.sort[mongoKey] = options.sort[key];
}
}
@@ -612,18 +616,11 @@ DatabaseController.prototype.find = function(className, query, options = {}) {
})
.then(() => this.reduceRelationKeys(className, query))
.then(() => this.reduceInRelation(className, query, schema))
.then(() => this.adaptiveCollection(className))
.then(() => this.adapter.adaptiveCollection(className))
.then(collection => {
let mongoWhere = transform.transformWhere(schema, className, query);
let mongoWhere = this.transform.transformWhere(schema, className, query);
if (!isMaster) {
let orParts = [
{"_rperm" : { "$exists": false }},
{"_rperm" : { "$in" : ["*"]}}
];
for (let acl of aclGroup) {
orParts.push({"_rperm" : { "$in" : [acl]}});
}
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
mongoWhere = this.transform.addReadACL(mongoWhere, aclGroup);
}
if (options.count) {
delete mongoOptions.limit;
@@ -640,6 +637,25 @@ DatabaseController.prototype.find = function(className, query, options = {}) {
});
};
DatabaseController.prototype.deleteSchema = function(className) {
return this.collectionExists(className)
.then(exist => {
if (!exist) {
return Promise.resolve();
}
return this.adapter.adaptiveCollection(className)
.then(collection => {
return collection.count()
.then(count => {
if (count > 0) {
throw new Parse.Error(255, `Class ${className} is not empty, contains ${count} objects, cannot drop schema.`);
}
return collection.drop();
})
})
})
}
function joinTableName(className, key) {
return `_Join:${key}:${className}`;
}

View File

@@ -15,6 +15,7 @@ export class HooksController {
constructor(applicationId:string, collectionPrefix:string = '') {
this._applicationId = applicationId;
this._collectionPrefix = collectionPrefix;
this.database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix).WithoutValidation();
}
load() {
@@ -26,18 +27,6 @@ export class HooksController {
});
}
getCollection() {
if (this._collection) {
return Promise.resolve(this._collection)
}
let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix);
return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => {
this._collection = collection;
return collection;
});
}
getFunction(functionName) {
return this._getHooks({ functionName: functionName }, 1).then(results => results[0]);
}
@@ -64,17 +53,13 @@ export class HooksController {
return this._removeHooks({ className: className, triggerName: triggerName });
}
_getHooks(query, limit) {
_getHooks(query = {}, limit) {
let options = limit ? { limit: limit } : undefined;
return this.getCollection().then(collection => collection.find(query, options));
return this.database.find(DefaultHooksCollectionName, query);
}
_removeHooks(query) {
return this.getCollection().then(collection => {
return collection.deleteMany(query);
}).then(() => {
return {};
});
return this.database.destroy(DefaultHooksCollectionName, query);
}
saveHook(hook) {
@@ -86,11 +71,9 @@ export class HooksController {
} else {
throw new Parse.Error(143, "invalid hook declaration");
}
return this.getCollection()
.then(collection => collection.upsertOne(query, hook))
.then(() => {
return hook;
});
return this.database.update(DefaultHooksCollectionName, query, hook, {upsert: true}).then(() => {
return Promise.resolve(hook);
})
}
addHookToTriggers(hook) {

View File

@@ -5,6 +5,8 @@ import AdaptableController from './AdaptableController';
import { PushAdapter } from '../Adapters/Push/PushAdapter';
import deepcopy from 'deepcopy';
import RestQuery from '../RestQuery';
import RestWrite from '../RestWrite';
import { master } from '../Auth';
import pushStatusHandler from '../pushStatusHandler';
const FEATURE_NAME = 'push';
@@ -54,30 +56,25 @@ export class PushController extends AdaptableController {
}
if (body.data && body.data.badge) {
let badge = body.data.badge;
let op = {};
let restUpdate = {};
if (typeof badge == 'string' && badge.toLowerCase() === 'increment') {
op = { $inc: { badge: 1 } }
restUpdate = { badge: { __op: 'Increment', amount: 1 } }
} else if (Number(badge)) {
op = { $set: { badge: badge } }
restUpdate = { badge: badge }
} else {
throw "Invalid value for badge, expected number or 'Increment'";
}
let updateWhere = deepcopy(where);
badgeUpdate = () => {
let badgeQuery = new RestQuery(config, auth, '_Installation', updateWhere);
return badgeQuery.buildRestWhere().then(() => {
let restWhere = deepcopy(badgeQuery.restWhere);
// Force iOS only devices
if (!restWhere['$and']) {
restWhere['$and'] = [badgeQuery.restWhere];
}
restWhere['$and'].push({
'deviceType': 'ios'
});
return config.database.adaptiveCollection("_Installation")
.then(coll => coll.updateMany(restWhere, op));
})
updateWhere.deviceType = 'ios';
// Build a real RestQuery so we can use it in RestWrite
let restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
return restQuery.buildRestWhere().then(() => {
let write = new RestWrite(config, master(config), '_Installation', restQuery.restWhere, restUpdate);
write.runOptions.many = true;
return write.execute();
});
}
}
let pushStatus = pushStatusHandler(config);

View File

@@ -45,38 +45,29 @@ export class UserController extends AdaptableController {
// TODO: Better error here.
return Promise.reject();
}
return this.config.database
.adaptiveCollection('_User')
.then(collection => {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate({
username: username,
_email_verify_token: token
}, {$set: {emailVerified: true}});
})
.then(document => {
if (!document) {
return Promise.reject();
}
return document;
});
let database = this.config.database.WithoutValidation();
return database.update('_User', {
username: username,
_email_verify_token: token
}, {emailVerified: true}).then(document => {
if (!document) {
return Promise.reject();
}
return Promise.resolve(document);
});
}
checkResetTokenValidity(username, token) {
return this.config.database.adaptiveCollection('_User')
.then(collection => {
return collection.find({
username: username,
_perishable_token: token
}, { limit: 1 });
})
.then(results => {
if (results.length != 1) {
return Promise.reject();
}
return results[0];
});
let database = this.config.database.WithoutValidation();
return database.find('_User', {
username: username,
_perishable_token: token
}, {limit: 1}).then(results => {
if (results.length != 1) {
return Promise.reject();
}
return results[0];
});
}
getUserIfNeeded(user) {
@@ -124,15 +115,8 @@ export class UserController extends AdaptableController {
setPasswordResetToken(email) {
let token = randomString(25);
return this.config.database
.adaptiveCollection('_User')
.then(collection => {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate(
{ email: email}, // query
{ $set: { _perishable_token: token } } // update
);
});
let database = this.config.database.WithoutValidation();
return database.update('_User', {email: email}, {_perishable_token: token});
}
sendPasswordResetEmail(email) {
@@ -166,14 +150,11 @@ export class UserController extends AdaptableController {
updatePassword(username, token, password, config) {
return this.checkResetTokenValidity(username, token).then((user) => {
return updateUserPassword(user._id, password, this.config);
return updateUserPassword(user.objectId, password, this.config);
}).then(() => {
// clear reset password token
return this.config.database.adaptiveCollection('_User').then(function (collection) {
// Need direct database access because verification token is not a parse field
return collection.findOneAndUpdate({ username: username },// query
{ $unset: { _perishable_token: null } } // update
);
return this.config.database.WithoutValidation().update('_User', { username }, {
_perishable_token: {__op: 'Delete'}
});
});
}