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:
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user