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:
@@ -1,7 +1,7 @@
|
||||
// These tests are unit tests designed to only test transform.js.
|
||||
"use strict";
|
||||
|
||||
let transform = require('../src/transform');
|
||||
let transform = require('../src/Adapters/Storage/Mongo/MongoTransform');
|
||||
let dd = require('deep-diff');
|
||||
let mongodb = require('mongodb');
|
||||
|
||||
@@ -7,7 +7,7 @@ let Config = require('../src/Config');
|
||||
describe('a GlobalConfig', () => {
|
||||
beforeEach(done => {
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_GlobalConfig')
|
||||
config.database.adapter.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } }))
|
||||
.then(() => { done(); });
|
||||
});
|
||||
@@ -43,6 +43,35 @@ describe('a GlobalConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('properly handles delete op', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
body : { params: { companies: {__op: 'Delete'}, foo: 'bar' } },
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.result).toEqual(true);
|
||||
request.get({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
json : true,
|
||||
headers: {
|
||||
'X-Parse-Application-Id': 'test',
|
||||
'X-Parse-Master-Key' : 'test'
|
||||
}
|
||||
}, (error, response, body) => {
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(body.params.companies).toBeUndefined();
|
||||
expect(body.params.foo).toBe('bar');
|
||||
expect(Object.keys(body.params).length).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fail to update if master key is missing', (done) => {
|
||||
request.put({
|
||||
url : 'http://localhost:8378/1/config',
|
||||
@@ -61,7 +90,7 @@ describe('a GlobalConfig', () => {
|
||||
|
||||
it('failed getting config when it is missing', (done) => {
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_GlobalConfig')
|
||||
config.database.adapter.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.deleteOne({ '_id': 1 }))
|
||||
.then(() => {
|
||||
request.get({
|
||||
|
||||
@@ -263,7 +263,7 @@ describe('Hooks', () => {
|
||||
return hooksController.load()
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail();
|
||||
fail('Should properly create all hooks');
|
||||
done();
|
||||
}).then(function() {
|
||||
for (var i=0; i<5; i++) {
|
||||
@@ -273,7 +273,7 @@ describe('Hooks', () => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail();
|
||||
fail('should properly load all hooks');
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
@@ -184,7 +184,6 @@ describe('PushController', () => {
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
@@ -233,7 +232,6 @@ describe('PushController', () => {
|
||||
}).then((result) => {
|
||||
done();
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
fail("should not fail");
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -266,7 +266,10 @@ describe("Email Verification", () => {
|
||||
.then((user) => {
|
||||
return user.save();
|
||||
}).then((user) => {
|
||||
return Parse.User.requestPasswordReset("cool_guy@parse.com");
|
||||
return Parse.User.requestPasswordReset("cool_guy@parse.com").catch((err) => {
|
||||
fail('Should not fail requesting a password');
|
||||
done();
|
||||
})
|
||||
}).then(() => {
|
||||
expect(calls).toBe(2);
|
||||
done();
|
||||
@@ -551,7 +554,7 @@ describe("Password Reset", () => {
|
||||
Parse.User.requestPasswordReset('user@parse.com', {
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
fail("Should not fail");
|
||||
fail("Should not fail requesting a password");
|
||||
done();
|
||||
}
|
||||
});
|
||||
@@ -628,7 +631,7 @@ describe("Password Reset", () => {
|
||||
|
||||
Parse.User.logIn("zxcv", "hello").then(function(user){
|
||||
let config = new Config('test');
|
||||
config.database.adaptiveCollection('_User')
|
||||
config.database.adapter.adaptiveCollection('_User')
|
||||
.then(coll => coll.find({ 'username': 'zxcv' }, { limit: 1 }))
|
||||
.then((results) => {
|
||||
// _perishable_token should be unset after reset password
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
import * as transform from './MongoTransform';
|
||||
|
||||
function mongoFieldToParseSchemaField(type) {
|
||||
if (type[0] === '*') {
|
||||
@@ -200,6 +201,10 @@ class MongoSchemaCollection {
|
||||
update = {'$set': update};
|
||||
return this.upsertSchema(className, query, update);
|
||||
}
|
||||
|
||||
get transform() {
|
||||
return transform;
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for testing reasons and because we haven't moved all mongo schema format
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoSchemaCollection from './MongoSchemaCollection';
|
||||
import {parse as parseUrl, format as formatUrl} from '../../../vendor/mongodbUrl';
|
||||
import * as transform from './MongoTransform';
|
||||
import _ from 'lodash';
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
@@ -57,7 +58,7 @@ export class MongoStorageAdapter {
|
||||
|
||||
schemaCollection() {
|
||||
return this.connect()
|
||||
.then(() => this.adaptiveCollection(this._collectionPrefix + MongoSchemaCollectionName))
|
||||
.then(() => this.adaptiveCollection(MongoSchemaCollectionName))
|
||||
.then(collection => new MongoSchemaCollection(collection));
|
||||
}
|
||||
|
||||
@@ -125,6 +126,10 @@ export class MongoStorageAdapter {
|
||||
.then(updateResult => this.schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate));
|
||||
}
|
||||
|
||||
get transform() {
|
||||
return transform;
|
||||
}
|
||||
}
|
||||
|
||||
export default MongoStorageAdapter;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import log from './logger';
|
||||
import log from '../../../logger';
|
||||
import _ from 'lodash';
|
||||
var mongodb = require('mongodb');
|
||||
var Parse = require('parse/node').Parse;
|
||||
@@ -185,14 +185,16 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
// restWhere is the "where" clause in REST API form.
|
||||
// Returns the mongo form of the query.
|
||||
// Throws a Parse.Error if the input query is invalid.
|
||||
function transformWhere(schema, className, restWhere) {
|
||||
function transformWhere(schema, className, restWhere, options = {validate: true}) {
|
||||
let mongoWhere = {};
|
||||
if (restWhere['ACL']) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
|
||||
}
|
||||
let transformKeyOptions = {query: true};
|
||||
transformKeyOptions.validate = options.validate;
|
||||
for (let restKey in restWhere) {
|
||||
let out = transformKeyValue(schema, className, restKey, restWhere[restKey],
|
||||
{query: true, validate: true});
|
||||
transformKeyOptions);
|
||||
mongoWhere[out.key] = out.value;
|
||||
}
|
||||
return mongoWhere;
|
||||
@@ -767,6 +769,87 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
|
||||
}
|
||||
}
|
||||
|
||||
function transformSelect(selectObject, key ,objects) {
|
||||
var values = [];
|
||||
for (var result of objects) {
|
||||
values.push(result[key]);
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
selectObject['$in'] = selectObject['$in'].concat(values);
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
function transformDontSelect(dontSelectObject, key, objects) {
|
||||
var values = [];
|
||||
for (var result of objects) {
|
||||
values.push(result[key]);
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
function transformInQuery(inQueryObject, className, results) {
|
||||
var values = [];
|
||||
for (var result of results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
if (Array.isArray(inQueryObject['$in'])) {
|
||||
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
|
||||
} else {
|
||||
inQueryObject['$in'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
function transformNotInQuery(notInQueryObject, className, results) {
|
||||
var values = [];
|
||||
for (var result of results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
if (Array.isArray(notInQueryObject['$nin'])) {
|
||||
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
|
||||
} else {
|
||||
notInQueryObject['$nin'] = values;
|
||||
}
|
||||
}
|
||||
|
||||
function addWriteACL(mongoWhere, acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
}
|
||||
return {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
|
||||
function addReadACL(mongoWhere, acl) {
|
||||
var orParts = [
|
||||
{"_rperm" : { "$exists": false }},
|
||||
{"_rperm" : { "$in" : ["*"]}}
|
||||
];
|
||||
for (var entry of acl) {
|
||||
orParts.push({"_rperm" : { "$in" : [entry]}});
|
||||
}
|
||||
return {'$and': [mongoWhere, {'$or': orParts}]};
|
||||
}
|
||||
|
||||
var DateCoder = {
|
||||
JSONToDatabase(json) {
|
||||
return new Date(json.iso);
|
||||
@@ -856,9 +939,15 @@ var FileCoder = {
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
transformKey: transformKey,
|
||||
transformCreate: transformCreate,
|
||||
transformUpdate: transformUpdate,
|
||||
transformWhere: transformWhere,
|
||||
untransformObject: untransformObject
|
||||
transformKey,
|
||||
transformCreate,
|
||||
transformUpdate,
|
||||
transformWhere,
|
||||
transformSelect,
|
||||
transformDontSelect,
|
||||
transformInQuery,
|
||||
transformNotInQuery,
|
||||
addReadACL,
|
||||
addWriteACL,
|
||||
untransformObject
|
||||
};
|
||||
@@ -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 = this.transform.addWriteACL(mongoWhere, options.acl);
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||
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);
|
||||
}
|
||||
})
|
||||
.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'
|
||||
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();
|
||||
});
|
||||
return config.database.adaptiveCollection("_Installation")
|
||||
.then(coll => coll.updateMany(restWhere, op));
|
||||
})
|
||||
}
|
||||
}
|
||||
let pushStatus = pushStatusHandler(config);
|
||||
|
||||
@@ -45,33 +45,24 @@ 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({
|
||||
let database = this.config.database.WithoutValidation();
|
||||
return database.update('_User', {
|
||||
username: username,
|
||||
_email_verify_token: token
|
||||
}, {$set: {emailVerified: true}});
|
||||
})
|
||||
.then(document => {
|
||||
}, {emailVerified: true}).then(document => {
|
||||
if (!document) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return document;
|
||||
return Promise.resolve(document);
|
||||
});
|
||||
}
|
||||
|
||||
checkResetTokenValidity(username, token) {
|
||||
return this.config.database.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
return collection.find({
|
||||
let database = this.config.database.WithoutValidation();
|
||||
return database.find('_User', {
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, { limit: 1 });
|
||||
})
|
||||
.then(results => {
|
||||
}, {limit: 1}).then(results => {
|
||||
if (results.length != 1) {
|
||||
return Promise.reject();
|
||||
}
|
||||
@@ -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'}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,20 +213,7 @@ RestQuery.prototype.replaceInQuery = function() {
|
||||
this.config, this.auth, inQueryValue.className,
|
||||
inQueryValue.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: subquery.className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
if (Array.isArray(inQueryObject['$in'])) {
|
||||
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
|
||||
} else {
|
||||
inQueryObject['$in'] = values;
|
||||
}
|
||||
this.config.database.transform.transformInQuery(inQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
return this.replaceInQuery();
|
||||
});
|
||||
@@ -257,21 +244,7 @@ RestQuery.prototype.replaceNotInQuery = function() {
|
||||
this.config, this.auth, notInQueryValue.className,
|
||||
notInQueryValue.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: subquery.className,
|
||||
objectId: result.objectId
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
if (Array.isArray(notInQueryObject['$nin'])) {
|
||||
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
|
||||
} else {
|
||||
notInQueryObject['$nin'] = values;
|
||||
}
|
||||
|
||||
this.config.database.transform.transformNotInQuery(notInQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
return this.replaceNotInQuery();
|
||||
});
|
||||
@@ -308,17 +281,7 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
this.config, this.auth, selectValue.query.className,
|
||||
selectValue.query.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push(result[selectValue.key]);
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
selectObject['$in'] = selectObject['$in'].concat(values);
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
|
||||
this.config.database.transform.transformSelect(selectObject, selectValue.key, response.results);
|
||||
// Keep replacing $select clauses
|
||||
return this.replaceSelect();
|
||||
})
|
||||
@@ -353,17 +316,7 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
this.config, this.auth, dontSelectValue.query.className,
|
||||
dontSelectValue.query.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
var values = [];
|
||||
for (var result of response.results) {
|
||||
values.push(result[dontSelectValue.key]);
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
|
||||
this.config.database.transform.transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
|
||||
// Keep replacing $dontSelect clauses
|
||||
return this.replaceDontSelect();
|
||||
})
|
||||
|
||||
@@ -5,9 +5,8 @@ import * as middleware from "../middlewares";
|
||||
|
||||
export class GlobalConfigRouter extends PromiseRouter {
|
||||
getGlobalConfig(req) {
|
||||
return req.config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.find({ '_id': 1 }, { limit: 1 }))
|
||||
.then(results => {
|
||||
let database = req.config.database.WithoutValidation();
|
||||
return database.find('_GlobalConfig', { '_id': 1 }, { limit: 1 }).then((results) => {
|
||||
if (results.length != 1) {
|
||||
// If there is no config in the database - return empty config.
|
||||
return { response: { params: {} } };
|
||||
@@ -18,20 +17,16 @@ export class GlobalConfigRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
updateGlobalConfig(req) {
|
||||
const params = req.body.params;
|
||||
let params = req.body.params;
|
||||
// Transform in dot notation to make sure it works
|
||||
const update = Object.keys(params).reduce((acc, key) => {
|
||||
if(params[key] && params[key].__op && params[key].__op === "Delete") {
|
||||
if (!acc.$unset) acc.$unset = {};
|
||||
acc.$unset[`params.${key}`] = "";
|
||||
} else {
|
||||
if (!acc.$set) acc.$set = {};
|
||||
acc.$set[`params.${key}`] = params[key];
|
||||
}
|
||||
acc[`params.${key}`] = params[key];
|
||||
return acc;
|
||||
}, {});
|
||||
return req.config.database.adaptiveCollection('_GlobalConfig')
|
||||
.then(coll => coll.upsertOne({ _id: 1 }, update))
|
||||
.then(() => ({ response: { result: true } }));
|
||||
let database = req.config.database.WithoutValidation();
|
||||
return database.update('_GlobalConfig', {_id: 1}, update, {upsert: true}).then(() => {
|
||||
return Promise.resolve({ response: { result: true } });
|
||||
});
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
|
||||
@@ -103,22 +103,7 @@ function deleteSchema(req) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, Schema.invalidClassNameMessage(req.params.className));
|
||||
}
|
||||
|
||||
return req.config.database.collectionExists(req.params.className)
|
||||
.then(exist => {
|
||||
if (!exist) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return req.config.database.adaptiveCollection(req.params.className)
|
||||
.then(collection => {
|
||||
return collection.count()
|
||||
.then(count => {
|
||||
if (count > 0) {
|
||||
throw new Parse.Error(255, `Class ${req.params.className} is not empty, contains ${count} objects, cannot drop schema.`);
|
||||
}
|
||||
return collection.drop();
|
||||
})
|
||||
})
|
||||
})
|
||||
return req.config.database.deleteSchema(req.params.className)
|
||||
.then(() => {
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
// TODO: hide all schema logic inside the database adapter.
|
||||
|
||||
const Parse = require('parse/node').Parse;
|
||||
const transform = require('./transform');
|
||||
import MongoSchemaCollection from './Adapters/Storage/Mongo/MongoSchemaCollection';
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -429,7 +428,7 @@ class Schema {
|
||||
validateField(className, fieldName, type, freeze) {
|
||||
return this.reloadData().then(() => {
|
||||
// Just to check that the fieldName is valid
|
||||
transform.transformKey(this, className, fieldName);
|
||||
this._collection.transform.transformKey(this, className, fieldName);
|
||||
|
||||
if( fieldName.indexOf(".") > 0 ) {
|
||||
// subdocument key (x.y) => ok if x is of type 'object'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { md5Hash, newObjectId } from './cryptoUtils';
|
||||
import { logger } from './logger';
|
||||
|
||||
const PUSH_STATUS_COLLECTION = '_PushStatus';
|
||||
|
||||
export function flatten(array) {
|
||||
return array.reduce((memo, element) => {
|
||||
if (Array.isArray(element)) {
|
||||
@@ -17,9 +19,7 @@ export default function pushStatusHandler(config) {
|
||||
let initialPromise;
|
||||
let pushStatus;
|
||||
let objectId = newObjectId();
|
||||
let collection = function() {
|
||||
return config.database.adaptiveCollection('_PushStatus');
|
||||
}
|
||||
let database = config.database.WithoutValidation();
|
||||
|
||||
let setInitial = function(body = {}, where, options = {source: 'rest'}) {
|
||||
let now = new Date();
|
||||
@@ -41,24 +41,20 @@ export default function pushStatusHandler(config) {
|
||||
_wperm: [],
|
||||
_rperm: []
|
||||
}
|
||||
initialPromise = collection().then((collection) => {
|
||||
return collection.insertOne(object);
|
||||
}).then((res) => {
|
||||
|
||||
return database.create(PUSH_STATUS_COLLECTION, object).then(() => {
|
||||
pushStatus = {
|
||||
objectId
|
||||
};
|
||||
return Promise.resolve(pushStatus);
|
||||
})
|
||||
return initialPromise;
|
||||
});
|
||||
}
|
||||
|
||||
let setRunning = function(installations) {
|
||||
logger.verbose('sending push to %d installations', installations.length);
|
||||
return initialPromise.then(() => {
|
||||
return collection();
|
||||
}).then((collection) => {
|
||||
return collection.updateOne({status:"pending", _id: objectId}, {$set: {status: "running"}});
|
||||
});
|
||||
return database.update(PUSH_STATUS_COLLECTION,
|
||||
{status:"pending", objectId: objectId},
|
||||
{status: "running"});
|
||||
}
|
||||
|
||||
let complete = function(results) {
|
||||
@@ -91,11 +87,7 @@ export default function pushStatusHandler(config) {
|
||||
}, update);
|
||||
}
|
||||
logger.verbose('sent push! %d success, %d failures', update.numSent, update.numFailed);
|
||||
return initialPromise.then(() => {
|
||||
return collection();
|
||||
}).then((collection) => {
|
||||
return collection.updateOne({status:"running", _id: objectId}, {$set: update});
|
||||
});
|
||||
return database.update('_PushStatus', {status:"running", objectId }, update);
|
||||
}
|
||||
|
||||
let fail = function(err) {
|
||||
@@ -104,11 +96,7 @@ export default function pushStatusHandler(config) {
|
||||
status: 'failed'
|
||||
}
|
||||
logger.error('error while sending push', err);
|
||||
return initialPromise.then(() => {
|
||||
return collection();
|
||||
}).then((collection) => {
|
||||
return collection.updateOne({_id: objectId}, {$set: update});
|
||||
});
|
||||
return database.update('_PushStatus', { objectId }, update);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
Reference in New Issue
Block a user