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

@@ -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');

View File

@@ -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({

View File

@@ -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();
})
});

View File

@@ -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();
});

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

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

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

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'
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);

View File

@@ -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'}
});
});
}

View File

@@ -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();
})

View File

@@ -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() {

View File

@@ -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

View File

@@ -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'

View File

@@ -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({