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}`;
}