From 6cfcb4ddf82ce00327105494a78fc27aa691ee8b Mon Sep 17 00:00:00 2001 From: North Date: Thu, 12 May 2016 06:22:29 +0800 Subject: [PATCH 01/63] Fix #1755 (#1756) * Add condition at limit = 0 * Add tests for installations with limit and count parameters --- spec/InstallationsRouter.spec.js | 96 ++++++++++++++++++++++++++++++ src/Routers/InstallationsRouter.js | 2 +- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 924e799fd9..82416aa42f 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -71,4 +71,100 @@ describe('InstallationsRouter', () => { done(); }); }); + + it('query installations with limit = 0', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + limit: 0 + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var response = res.response; + expect(response.results.length).toEqual(0); + done(); + }); + }); + + it('query installations with count = 1', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + count: 1 + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var response = res.response; + expect(response.results.length).toEqual(2); + expect(response.count).toEqual(2); + done(); + }); + }); + + it('query installations with limit = 0 and count = 1', (done) => { + var androidDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abc', + 'deviceType': 'android' + }; + var iosDeviceRequest = { + 'installationId': '12345678-abcd-abcd-abcd-123456789abd', + 'deviceType': 'ios' + }; + var request = { + config: config, + auth: auth.master(config), + body: {}, + query: { + limit: 0, + count: 1 + } + }; + + var router = new InstallationsRouter(); + rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + .then(() => { + return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); + }).then(() => { + return router.handleFind(request); + }).then((res) => { + var response = res.response; + expect(response.results.length).toEqual(0); + expect(response.count).toEqual(2); + done(); + }); + }); }); diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 372bba8140..4a9efc3f64 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -12,7 +12,7 @@ export class InstallationsRouter extends ClassesRouter { if (body.skip) { options.skip = Number(body.skip); } - if (body.limit) { + if (body.limit || body.limit === 0) { options.limit = Number(body.limit); } if (body.order) { From 19e7407f554e14aae8f928ffc159bd7e24d02637 Mon Sep 17 00:00:00 2001 From: Marco Cheung Date: Thu, 12 May 2016 08:24:15 +0800 Subject: [PATCH 02/63] Return correct error when violating unique index (#1763) --- spec/ParseAPI.spec.js | 22 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 9 +++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 084b7fe337..61efe8b397 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -5,6 +5,7 @@ var DatabaseAdapter = require('../src/DatabaseAdapter'); var request = require('request'); const Parse = require("parse/node"); +let Config = require('../src/Config'); describe('miscellaneous', function() { it('create a GameScore object', function(done) { @@ -1387,4 +1388,25 @@ describe('miscellaneous', function() { }) }); }); + + it('fail when create duplicate value in unique field', (done) => { + let obj = new Parse.Object('UniqueField'); + obj.set('unique', 'value'); + obj.save().then(() => { + expect(obj.id).not.toBeUndefined(); + let config = new Config('test'); + return config.database.adapter.adaptiveCollection('UniqueField') + }).then(collection => { + return collection._mongoCollection.createIndex({ 'unique': 1 }, { unique: true }) + }).then(() => { + let obj = new Parse.Object('UniqueField'); + obj.set('unique', 'value'); + return obj.save() + }).then(() => { + return Promise.reject(); + }, error => { + expect(error.code === Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index d61df40cbc..81af4e970c 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -166,7 +166,14 @@ export class MongoStorageAdapter { createObject(className, object, schemaController, parseFormatSchema) { const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema); return this.adaptiveCollection(className) - .then(collection => collection.insertOne(mongoObject)); + .then(collection => collection.insertOne(mongoObject)) + .catch(error => { + if (error.code === 11000) { // Duplicate value + throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided'); + } + return Promise.reject(error); + }); } // Remove all objects that match the given parse query. Parse Query should be in Parse Format. From c2cfa14627971d10e9c4039e6c92ee073701e2b0 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Thu, 12 May 2016 16:30:38 -0700 Subject: [PATCH 03/63] Tell dashboard that the feature exits --- src/Routers/FeaturesRouter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index cda604d41d..9c3ad7619b 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -39,6 +39,7 @@ export class FeaturesRouter extends PromiseRouter { clearAllDataFromClass: false, exportClass: false, editClassLevelPermissions: true, + editPointerPermissions: true, }, }; From d0c3535a395abbd95fbe58594a2e73b3b2b042e4 Mon Sep 17 00:00:00 2001 From: Marco Cheung Date: Sat, 14 May 2016 01:51:01 +0800 Subject: [PATCH 04/63] Fix error when unset user email (#1778) --- spec/ParseUser.spec.js | 16 ++++++++++++++++ src/RestWrite.js | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 0873f4426f..ce9763af89 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1834,6 +1834,22 @@ describe('Parse.User testing', () => { }); }); + it('unset user email', (done) => { + var user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'test'); + user.set('email', 'test@test.com'); + user.signUp().then(() => { + user.unset('email'); + return user.save(); + }).then(() => { + return Parse.User.logIn('test', 'test'); + }).then((user) => { + expect(user.getEmail()).toBeUndefined(); + done(); + }); + }); + it('create session from user', (done) => { Parse.Promise.as().then(() => { return Parse.User.signUp("finn", "human", { foo: "bar" }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 03138e44f4..ecb92a85e4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -356,7 +356,7 @@ RestWrite.prototype.transformUser = function() { return Promise.resolve(); }); }).then(() => { - if (!this.data.email) { + if (!this.data.email || this.data.email.__op === 'Delete') { return; } // Validate basic email address format From e4998c256a516d4a69553f43db5f95ec96dd257a Mon Sep 17 00:00:00 2001 From: Drew Date: Fri, 13 May 2016 15:28:14 -0700 Subject: [PATCH 05/63] Move field name validation logic out of mongo (#1752) * Remove transformKey(...) * Move validation logic into Parse Server and out of Mongo Adapter * Fix nits --- spec/MongoTransform.spec.js | 11 ----------- src/Adapters/Storage/Mongo/MongoTransform.js | 13 +------------ src/Controllers/DatabaseController.js | 19 ++++++++++++++++--- src/Controllers/SchemaController.js | 12 +++++++----- 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index bae5805fad..755187dd9e 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -191,17 +191,6 @@ describe('untransformObject', () => { }); }); -describe('transformKey', () => { - it('throws out _password', (done) => { - try { - transform.transformKey(dummySchema, '_User', '_password'); - fail('should have thrown'); - } catch (e) { - done(); - } - }); -}); - describe('transform schema key changes', () => { it('changes new pointer key', (done) => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0013a39d0e..d445e7cec2 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -15,14 +15,11 @@ var Parse = require('parse/node').Parse; // in the value are converted to a mongo update form. Otherwise they are // converted to static data. // -// validate: true indicates that key names are to be validated. -// // Returns an object with {key: key, value: value}. function transformKeyValue(schema, className, restKey, restValue, { inArray, inObject, update, - validate, } = {}) { // Check if the schema is known since it's a built-in field. var key = restKey; @@ -71,9 +68,6 @@ function transformKeyValue(schema, className, restKey, restValue, { if (authDataMatch) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + key); } - if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key); - } } // Handle special schema key changes @@ -454,11 +448,6 @@ function untransformACL(mongoObject) { return output; } -// Transforms a key used in the REST API format to its mongo format. -function transformKey(schema, className, key) { - return transformKeyValue(schema, className, key, null, {validate: true}).key; -} - // A sentinel value that helper transformations return when they // cannot perform a transformation function CannotTransform() {} @@ -1038,7 +1027,7 @@ var FileCoder = { }; module.exports = { - transformKey, + transformKeyValue, parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index de78895ed5..ab90fc3bfd 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -618,9 +618,22 @@ DatabaseController.prototype.find = function(className, query, { .then(schemaController => { if (sort) { mongoOptions.sort = {}; - for (let key in sort) { - let mongoKey = this.transform.transformKey(schemaController, className, key); - mongoOptions.sort[mongoKey] = sort[key]; + for (let fieldName in sort) { + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behaviour here. + if (fieldName === '_created_at') { + fieldName = 'createdAt'; + sort['createdAt'] = sort['_created_at']; + } else if (fieldName === '_updated_at') { + fieldName = 'updatedAt'; + sort['updatedAt'] = sort['_updated_at']; + } + + if (!SchemaController.fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + const mongoKey = this.transform.transformKeyValue(schemaController, className, fieldName, null).key; + mongoOptions.sort[mongoKey] = sort[fieldName]; } } return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index af56610b99..8c78d1cf57 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -253,7 +253,7 @@ class SchemaController { this.data[schema.className] = schema.fields; this.perms[schema.className] = schema.classLevelPermissions; }); - + // Inject the in-memory classes volatileClasses.forEach(className => { this.data[className] = injectDefaultSchema({ @@ -466,15 +466,16 @@ class SchemaController { // If 'freeze' is true, refuse to update the schema for this field. validateField(className, fieldName, type, freeze) { return this.reloadData().then(() => { - // Just to check that the fieldName is valid - this._collection.transform.transformKey(this, className, fieldName); - - if( fieldName.indexOf(".") > 0 ) { + if (fieldName.indexOf(".") > 0) { // subdocument key (x.y) => ok if x is of type 'object' fieldName = fieldName.split(".")[ 0 ]; type = 'Object'; } + if (!fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + let expected = this.data[className][fieldName]; if (expected) { expected = (expected === 'map' ? 'Object' : expected); @@ -847,6 +848,7 @@ function getObjectType(obj) { export { load, classNameIsValid, + fieldNameIsValid, invalidClassNameMessage, buildMergedSchemaObject, systemClasses, From 1854928fe7c3e05be3524d9f647120969bcd46c0 Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Fri, 13 May 2016 18:17:22 -0700 Subject: [PATCH 06/63] Add test to ensure you can set ACL in beforeSave (#1772) --- spec/ParseAPI.spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 61efe8b397..68e0544bb6 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -225,6 +225,34 @@ describe('miscellaneous', function() { }); }); + it('test beforeSave set object acl success', function(done) { + var acl = new Parse.ACL({ + '*': { read: true, write: false } + }); + Parse.Cloud.beforeSave('BeforeSaveAddACL', function(req, res) { + req.object.setACL(acl); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveAddACL'); + obj.set('lol', true); + obj.save().then(function() { + Parse.Cloud._removeHook('Triggers', 'beforeSave', 'BeforeSaveAddACL'); + var query = new Parse.Query('BeforeSaveAddACL'); + query.get(obj.id).then(function(objAgain) { + expect(objAgain.get('lol')).toBeTruthy(); + expect(objAgain.getACL().equals(acl)); + done(); + }, function(error) { + fail(error); + done(); + }); + }, function(error) { + fail(error); + done(); + }); + }); + it('test beforeSave returns value on create and update', (done) => { var obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bing'); From 3b4ae2d0a0025ae08b0b30e9b1f7ca9af882c0d9 Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Mon, 16 May 2016 14:41:25 -0700 Subject: [PATCH 07/63] Write old ACL format in _acl in addition to new format (#1810) --- spec/MongoTransform.spec.js | 15 +++++++++++++++ src/Adapters/Storage/Mongo/MongoTransform.js | 12 +++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 755187dd9e..905b7647c1 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -233,6 +233,21 @@ describe('transform schema key changes', () => { done(); }); + it('writes the old ACL format in addition to rperm and wperm', (done) => { + var input = { + ACL: { + "*": { "read": true }, + "Kevin": { "write": true } + } + }; + + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + expect(typeof output._acl).toEqual('object'); + expect(output._acl["Kevin"].w).toBeTruthy(); + expect(output._acl["Kevin"].r).toBeUndefined(); + done(); + }) + it('untransforms from _rperm and _wperm to ACL', (done) => { var input = { _rperm: ["*"], diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index d445e7cec2..0cf09dbbee 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -348,7 +348,7 @@ function transformUpdate(schema, className, restUpdate) { var mongoUpdate = {}; var acl = transformACL(restUpdate); - if (acl._rperm || acl._wperm) { + if (acl._rperm || acl._wperm || acl._acl) { mongoUpdate['$set'] = {}; if (acl._rperm) { mongoUpdate['$set']['_rperm'] = acl._rperm; @@ -356,6 +356,9 @@ function transformUpdate(schema, className, restUpdate) { if (acl._wperm) { mongoUpdate['$set']['_wperm'] = acl._wperm; } + if (acl._acl) { + mongoUpdate['$set']['_acl'] = acl._acl; + } } for (var restKey in restUpdate) { @@ -404,16 +407,23 @@ function transformACL(restObject) { var acl = restObject['ACL']; var rperm = []; var wperm = []; + var _acl = {}; // old format + for (var entry in acl) { if (acl[entry].read) { rperm.push(entry); + _acl[entry] = _acl[entry] || {}; + _acl[entry]['r'] = true; } if (acl[entry].write) { wperm.push(entry); + _acl[entry] = _acl[entry] || {}; + _acl[entry]['w'] = true; } } output._rperm = rperm; output._wperm = wperm; + output._acl = _acl; delete restObject.ACL; return output; } From b40e16647bc811776107bbb1e5e6b2b3c1142112 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 16 May 2016 15:32:36 -0700 Subject: [PATCH 08/63] Changelog for version 2.2.10 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cecb204534..2d1dcf2a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Parse Server Changelog +### 2.2.10 (5/15/2016) + +* Fix: Write legacy ACLs to Mongo so that clients that still go through Parse.com can read them, thanks to [Tyler Brock](https://github.com/TylerBrock) and [carmenlau](https://github.com/carmenlau) +* Fix: Querying installations with limit = 0 and count = 1 now works, thanks to [ssk7833](https://github.com/ssk7833) +* Fix: Return correct error when violating unique index, thanks to [Marco Cheung](https://github.com/Marco129) +* Fix: Allow unsetting user's email, thanks to [Marco Cheung](https://github.com/Marco129) +* New: Support for Node 6.1 + ### 2.2.9 (5/9/2016) * Fix: Fix a regression that caused Parse Server to crash when a null parameter is passed to a Cloud function diff --git a/package.json b/package.json index 264f57ab6b..ac8bc7e037 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "2.2.9", + "version": "2.2.10", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 40965186c05015c337b6be6e4beca8790fee6189 Mon Sep 17 00:00:00 2001 From: Marco Cheung Date: Wed, 18 May 2016 04:15:44 +0800 Subject: [PATCH 09/63] Mask sensitive information when logging (#1790) --- spec/FileLoggerAdapter.spec.js | 55 ++++++++++++++++++++++++++++++++++ src/PromiseRouter.js | 36 ++++++++++++++++++++-- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/spec/FileLoggerAdapter.spec.js b/spec/FileLoggerAdapter.spec.js index fb4d6d5572..f259422df6 100644 --- a/spec/FileLoggerAdapter.spec.js +++ b/spec/FileLoggerAdapter.spec.js @@ -1,5 +1,8 @@ +'use strict'; + var FileLoggerAdapter = require('../src/Adapters/Logger/FileLoggerAdapter').FileLoggerAdapter; var Parse = require('parse/node').Parse; +var request = require('request'); describe('info logs', () => { @@ -45,3 +48,55 @@ describe('error logs', () => { }); }); }); + +describe('verbose logs', () => { + + it("mask sensitive information in _User class", (done) => { + let customConfig = Object.assign({}, defaultConfiguration, {verbose: true}); + setServerConfiguration(customConfig); + createTestUser().then(() => { + let fileLoggerAdapter = new FileLoggerAdapter(); + return fileLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose' + }); + }).then((results) => { + expect(results[1].message.includes('"password": "********"')).toEqual(true); + var headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.get({ + headers: headers, + url: 'http://localhost:8378/1/login?username=test&password=moon-y' + }, (error, response, body) => { + let fileLoggerAdapter = new FileLoggerAdapter(); + return fileLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose' + }).then((results) => { + expect(results[1].message.includes('password=********')).toEqual(true); + done(); + }); + }); + }); + }); + + it("should not mask information in non _User class", (done) => { + let obj = new Parse.Object('users'); + obj.set('password', 'pw'); + obj.save().then(() => { + let fileLoggerAdapter = new FileLoggerAdapter(); + return fileLoggerAdapter.query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose' + }); + }).then((results) => { + expect(results[1].message.includes('"password": "pw"')).toEqual(true); + done(); + }); + }); +}); diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index 099b2474b4..b159ef0768 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -6,6 +6,7 @@ // components that external developers may be modifying. import express from 'express'; +import url from 'url'; import log from './logger'; export default class PromiseRouter { @@ -154,8 +155,8 @@ export default class PromiseRouter { function makeExpressHandler(promiseHandler) { return function(req, res, next) { try { - log.verbose(req.method, req.originalUrl, req.headers, - JSON.stringify(req.body, null, 2)); + log.verbose(req.method, maskSensitiveUrl(req), req.headers, + JSON.stringify(maskSensitiveBody(req), null, 2)); promiseHandler(req).then((result) => { if (!result.response && !result.location && !result.text) { log.error('the handler did not include a "response" or a "location" field'); @@ -194,3 +195,34 @@ function makeExpressHandler(promiseHandler) { } } } + +function maskSensitiveBody(req) { + let maskBody = Object.assign({}, req.body); + let shouldMaskBody = (req.method === 'POST' && req.originalUrl.endsWith('/users') + && !req.originalUrl.includes('classes')) || + (req.method === 'PUT' && /users\/\w+$/.test(req.originalUrl) + && !req.originalUrl.includes('classes')) || + (req.originalUrl.includes('classes/_User')); + if (shouldMaskBody) { + for (let key of Object.keys(maskBody)) { + if (key == 'password') { + maskBody[key] = '********'; + break; + } + } + } + return maskBody; +} + +function maskSensitiveUrl(req) { + let maskUrl = req.originalUrl.toString(); + let shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login') + && !req.originalUrl.includes('classes'); + if (shouldMaskUrl) { + let password = url.parse(req.originalUrl, true).query.password; + if (password) { + maskUrl = maskUrl.replace('password=' + password, 'password=********') + } + } + return maskUrl; +} From 5d887e18f0dfdb22c8b4a70aa764f502ede2b881 Mon Sep 17 00:00:00 2001 From: KartikeyaRokde Date: Wed, 18 May 2016 05:32:28 +0530 Subject: [PATCH 10/63] FIX #1572 - Accepting LOGS_FOLDER as env variable (#1757) * FIX #1572 - Accepting LOGS_FOLDER as env variable * Changed env variable LOGS_FOLDER to PARSE_SERVER_LOGS_FOLDER * Added Note for starting parse-server with PARSE_SERVER_LOGS_FOLDER env variable --- README.md | 2 ++ src/logger.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index de6b43f74c..586cc9d3b9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ That's it! You are now running a standalone version of Parse Server on your mach **Using a remote MongoDB?** Pass the `--databaseURI DATABASE_URI` parameter when starting `parse-server`. Learn more about configuring Parse Server [here](#configuration). For a full list of available options, run `parse-server --help`. +**Want logs to be in placed in other folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY` + ### Saving your first object Now that you're running Parse Server, it is time to save your first object. We'll use the [REST API](https://parse.com/docs/rest/guide), but you can easily do the same using any of the [Parse SDKs](https://parseplatform.github.io/#sdks). Run the following: diff --git a/src/logger.js b/src/logger.js index e0556bf7d9..d5b81e9ecf 100644 --- a/src/logger.js +++ b/src/logger.js @@ -9,6 +9,8 @@ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { LOGS_FOLDER = './test_logs/' } +LOGS_FOLDER = process.env.PARSE_SERVER_LOGS_FOLDER || LOGS_FOLDER; + let currentLogsFolder = LOGS_FOLDER; function generateTransports(level) { From 8c09c3dae1b913b336e5a55c7066e3de7b522087 Mon Sep 17 00:00:00 2001 From: Blayne Chard Date: Wed, 18 May 2016 12:12:30 +1200 Subject: [PATCH 11/63] Adding Caching Adapter, allows caching of _Role and _User queries (fixes #168) (#1664) * Adding Caching Adapter, allows caching of _Role and _User queries. --- spec/CacheController.spec.js | 74 ++++++++++++ spec/InMemoryCache.spec.js | 74 ++++++++++++ spec/InMemoryCacheAdapter.spec.js | 59 ++++++++++ spec/helper.js | 2 +- src/Adapters/Cache/CacheAdapter.js | 27 +++++ src/Adapters/Cache/InMemoryCache.js | 66 +++++++++++ src/Adapters/Cache/InMemoryCacheAdapter.js | 36 ++++++ src/Auth.js | 131 ++++++++++++--------- src/Config.js | 5 +- src/Controllers/CacheController.js | 75 ++++++++++++ src/DatabaseAdapter.js | 2 +- src/ParseServer.js | 19 ++- src/RestWrite.js | 27 ++--- src/cache.js | 37 +----- src/middlewares.js | 12 +- src/rest.js | 8 +- src/testing-routes.js | 4 +- src/triggers.js | 2 +- 18 files changed, 526 insertions(+), 134 deletions(-) create mode 100644 spec/CacheController.spec.js create mode 100644 spec/InMemoryCache.spec.js create mode 100644 spec/InMemoryCacheAdapter.spec.js create mode 100644 src/Adapters/Cache/CacheAdapter.js create mode 100644 src/Adapters/Cache/InMemoryCache.js create mode 100644 src/Adapters/Cache/InMemoryCacheAdapter.js create mode 100644 src/Controllers/CacheController.js diff --git a/spec/CacheController.spec.js b/spec/CacheController.spec.js new file mode 100644 index 0000000000..1e02d59e2f --- /dev/null +++ b/spec/CacheController.spec.js @@ -0,0 +1,74 @@ +var CacheController = require('../src/Controllers/CacheController.js').default; + +describe('CacheController', function() { + var FakeCacheAdapter; + var FakeAppID = 'foo'; + var KEY = 'hello'; + + beforeEach(() => { + FakeCacheAdapter = { + get: () => Promise.resolve(null), + put: jasmine.createSpy('put'), + del: jasmine.createSpy('del'), + clear: jasmine.createSpy('clear') + } + + spyOn(FakeCacheAdapter, 'get').and.callThrough(); + }); + + + it('should expose role and user caches', (done) => { + var cache = new CacheController(FakeCacheAdapter, FakeAppID); + + expect(cache.role).not.toEqual(null); + expect(cache.role.get).not.toEqual(null); + expect(cache.user).not.toEqual(null); + expect(cache.user.get).not.toEqual(null); + + done(); + }); + + + ['role', 'user'].forEach((cacheName) => { + it('should prefix ' + cacheName + ' cache', () => { + var cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName]; + + cache.put(KEY, 'world'); + var firstPut = FakeCacheAdapter.put.calls.first(); + expect(firstPut.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':')); + + cache.get(KEY); + var firstGet = FakeCacheAdapter.get.calls.first(); + expect(firstGet.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':')); + + cache.del(KEY); + var firstDel = FakeCacheAdapter.del.calls.first(); + expect(firstDel.args[0]).toEqual([FakeAppID, cacheName, KEY].join(':')); + }); + }); + + it('should clear the entire cache', () => { + var cache = new CacheController(FakeCacheAdapter, FakeAppID); + + cache.clear(); + expect(FakeCacheAdapter.clear.calls.count()).toEqual(1); + + cache.user.clear(); + expect(FakeCacheAdapter.clear.calls.count()).toEqual(2); + + cache.role.clear(); + expect(FakeCacheAdapter.clear.calls.count()).toEqual(3); + }); + + it('should handle cache rejections', (done) => { + + FakeCacheAdapter.get = () => Promise.reject(); + + var cache = new CacheController(FakeCacheAdapter, FakeAppID); + + cache.get('foo').then(done, () => { + fail('Promise should not be rejected.'); + }); + }); + +}); diff --git a/spec/InMemoryCache.spec.js b/spec/InMemoryCache.spec.js new file mode 100644 index 0000000000..3c0fb47bbb --- /dev/null +++ b/spec/InMemoryCache.spec.js @@ -0,0 +1,74 @@ +const InMemoryCache = require('../src/Adapters/Cache/InMemoryCache').default; + + +describe('InMemoryCache', function() { + var BASE_TTL = { + ttl: 10 + }; + var NO_EXPIRE_TTL = { + ttl: NaN + }; + var KEY = 'hello'; + var KEY_2 = KEY + '_2'; + + var VALUE = 'world'; + + + function wait(sleep) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, sleep); + }) + } + + it('should destroy a expire items in the cache', (done) => { + var cache = new InMemoryCache(BASE_TTL); + + cache.put(KEY, VALUE); + + var value = cache.get(KEY); + expect(value).toEqual(VALUE); + + wait(BASE_TTL.ttl * 5).then(() => { + value = cache.get(KEY) + expect(value).toEqual(null); + done(); + }); + }); + + it('should delete items', (done) => { + var cache = new InMemoryCache(NO_EXPIRE_TTL); + cache.put(KEY, VALUE); + cache.put(KEY_2, VALUE); + expect(cache.get(KEY)).toEqual(VALUE); + expect(cache.get(KEY_2)).toEqual(VALUE); + + cache.del(KEY); + expect(cache.get(KEY)).toEqual(null); + expect(cache.get(KEY_2)).toEqual(VALUE); + + cache.del(KEY_2); + expect(cache.get(KEY)).toEqual(null); + expect(cache.get(KEY_2)).toEqual(null); + done(); + }); + + it('should clear all items', (done) => { + var cache = new InMemoryCache(NO_EXPIRE_TTL); + cache.put(KEY, VALUE); + cache.put(KEY_2, VALUE); + + expect(cache.get(KEY)).toEqual(VALUE); + expect(cache.get(KEY_2)).toEqual(VALUE); + cache.clear(); + + expect(cache.get(KEY)).toEqual(null); + expect(cache.get(KEY_2)).toEqual(null); + done(); + }); + + it('should deafult TTL to 5 seconds', () => { + var cache = new InMemoryCache({}); + expect(cache.ttl).toEqual(5 * 1000); + }); + +}); diff --git a/spec/InMemoryCacheAdapter.spec.js b/spec/InMemoryCacheAdapter.spec.js new file mode 100644 index 0000000000..405da6f7ad --- /dev/null +++ b/spec/InMemoryCacheAdapter.spec.js @@ -0,0 +1,59 @@ +var InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').default; + +describe('InMemoryCacheAdapter', function() { + var KEY = 'hello'; + var VALUE = 'world'; + + function wait(sleep) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, sleep); + }) + } + + it('should expose promisifyed methods', (done) => { + var cache = new InMemoryCacheAdapter({ + ttl: NaN + }); + + var noop = () => {}; + + // Verify all methods return promises. + Promise.all([ + cache.put(KEY, VALUE), + cache.del(KEY), + cache.get(KEY), + cache.clear() + ]).then(() => { + done(); + }); + }); + + it('should get/set/clear', (done) => { + var cache = new InMemoryCacheAdapter({ + ttl: NaN + }); + + cache.put(KEY, VALUE) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(VALUE)) + .then(() => cache.clear()) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(null)) + .then(done); + }); + + it('should expire after ttl', (done) => { + var cache = new InMemoryCacheAdapter({ + ttl: 10 + }); + + cache.put(KEY, VALUE) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(VALUE)) + .then(wait.bind(null, 50)) + .then(() => cache.get(KEY)) + .then((value) => expect(value).toEqual(null)) + .then(done); + }) + +}); diff --git a/spec/helper.js b/spec/helper.js index 0d6379fbf8..aa63ff0f4d 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -63,7 +63,7 @@ const setServerConfiguration = configuration => { DatabaseAdapter.clearDatabaseSettings(); currentConfiguration = configuration; server.close(); - cache.clearCache(); + cache.clear(); app = express(); api = new ParseServer(configuration); app.use('/1', api); diff --git a/src/Adapters/Cache/CacheAdapter.js b/src/Adapters/Cache/CacheAdapter.js new file mode 100644 index 0000000000..7d65381763 --- /dev/null +++ b/src/Adapters/Cache/CacheAdapter.js @@ -0,0 +1,27 @@ +export class CacheAdapter { + /** + * Get a value in the cache + * @param key Cache key to get + * @return Promise that will eventually resolve to the value in the cache. + */ + get(key) {} + + /** + * Set a value in the cache + * @param key Cache key to set + * @param value Value to set the key + * @param ttl Optional TTL + */ + put(key, value, ttl) {} + + /** + * Remove a value from the cache. + * @param key Cache key to remove + */ + del(key) {} + + /** + * Empty a cache + */ + clear() {} +} diff --git a/src/Adapters/Cache/InMemoryCache.js b/src/Adapters/Cache/InMemoryCache.js new file mode 100644 index 0000000000..37eeb43b02 --- /dev/null +++ b/src/Adapters/Cache/InMemoryCache.js @@ -0,0 +1,66 @@ +const DEFAULT_CACHE_TTL = 5 * 1000; + + +export class InMemoryCache { + constructor({ + ttl = DEFAULT_CACHE_TTL + }) { + this.ttl = ttl; + this.cache = Object.create(null); + } + + get(key) { + let record = this.cache[key]; + if (record == null) { + return null; + } + + // Has Record and isnt expired + if (isNaN(record.expire) || record.expire >= Date.now()) { + return record.value; + } + + // Record has expired + delete this.cache[key]; + return null; + } + + put(key, value, ttl = this.ttl) { + if (ttl < 0 || isNaN(ttl)) { + ttl = NaN; + } + + var record = { + value: value, + expire: ttl + Date.now() + } + + if (!isNaN(record.expire)) { + record.timeout = setTimeout(() => { + this.del(key); + }, ttl); + } + + this.cache[key] = record; + } + + del(key) { + var record = this.cache[key]; + if (record == null) { + return; + } + + if (record.timeout) { + clearTimeout(record.timeout); + } + + delete this.cache[key]; + } + + clear() { + this.cache = Object.create(null); + } + +} + +export default InMemoryCache; diff --git a/src/Adapters/Cache/InMemoryCacheAdapter.js b/src/Adapters/Cache/InMemoryCacheAdapter.js new file mode 100644 index 0000000000..09e1c12a11 --- /dev/null +++ b/src/Adapters/Cache/InMemoryCacheAdapter.js @@ -0,0 +1,36 @@ +import {InMemoryCache} from './InMemoryCache'; + +export class InMemoryCacheAdapter { + + constructor(ctx) { + this.cache = new InMemoryCache(ctx) + } + + get(key) { + return new Promise((resolve, reject) => { + let record = this.cache.get(key); + if (record == null) { + return resolve(null); + } + + return resolve(JSON.parse(record)); + }) + } + + put(key, value, ttl) { + this.cache.put(key, JSON.stringify(value), ttl); + return Promise.resolve(); + } + + del(key) { + this.cache.del(key); + return Promise.resolve(); + } + + clear() { + this.cache.clear(); + return Promise.resolve(); + } +} + +export default InMemoryCacheAdapter; diff --git a/src/Auth.js b/src/Auth.js index f21bdc763f..8f21567903 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -2,8 +2,6 @@ var deepcopy = require('deepcopy'); var Parse = require('parse/node').Parse; var RestQuery = require('./RestQuery'); -import cache from './cache'; - // An Auth object tells you who is requesting something and whether // the master key was used. // userObject is a Parse.User and can be null if there's no user. @@ -42,36 +40,42 @@ function nobody(config) { return new Auth({ config, isMaster: false }); } + // Returns a promise that resolves to an Auth object var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) { - var cachedUser = cache.users.get(sessionToken); - if (cachedUser) { - return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser })); - } - var restOptions = { - limit: 1, - include: 'user' - }; - var query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions); - return query.execute().then((response) => { - var results = response.results; - if (results.length !== 1 || !results[0]['user']) { - return nobody(config); + return config.cacheController.user.get(sessionToken).then((userJSON) => { + if (userJSON) { + let cachedUser = Parse.Object.fromJSON(userJSON); + return Promise.resolve(new Auth({config, isMaster: false, installationId, user: cachedUser})); } - var now = new Date(), + var restOptions = { + limit: 1, + include: 'user' + }; + + var query = new RestQuery(config, master(config), '_Session', {sessionToken}, restOptions); + return query.execute().then((response) => { + var results = response.results; + if (results.length !== 1 || !results[0]['user']) { + return nobody(config); + } + + var now = new Date(), expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; - if(expiresAt < now) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token is expired.'); - } - var obj = results[0]['user']; - delete obj.password; - obj['className'] = '_User'; - obj['sessionToken'] = sessionToken; - let userObject = Parse.Object.fromJSON(obj); - cache.users.set(sessionToken, userObject); - return new Auth({ config, isMaster: false, installationId, user: userObject }); + if (expiresAt < now) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, + 'Session token is expired.'); + } + var obj = results[0]['user']; + delete obj.password; + obj['className'] = '_User'; + obj['sessionToken'] = sessionToken; + config.cacheController.user.put(sessionToken, obj); + + let userObject = Parse.Object.fromJSON(obj); + return new Auth({config, isMaster: false, installationId, user: userObject}); + }); }); }; @@ -92,39 +96,50 @@ Auth.prototype.getUserRoles = function() { // Iterates through the role tree and compiles a users roles Auth.prototype._loadRoles = function() { - var restWhere = { - 'users': { - __type: 'Pointer', - className: '_User', - objectId: this.user.id - } - }; - // First get the role ids this user is directly a member of - var query = new RestQuery(this.config, master(this.config), '_Role', - restWhere, {}); - return query.execute().then((response) => { - var results = response.results; - if (!results.length) { - this.userRoles = []; - this.fetchedRoles = true; - this.rolePromise = null; - return Promise.resolve(this.userRoles); + var cacheAdapter = this.config.cacheController; + return cacheAdapter.role.get(this.user.id).then((cachedRoles) => { + if (cachedRoles != null) { + this.fetchedroles = true; + return Promise.resolve(cachedRoles); } - var rolesMap = results.reduce((m, r) => { - m.names.push(r.name); - m.ids.push(r.objectId); - return m; - }, {ids: [], names: []}); - // run the recursive finding - return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) - .then((roleNames) => { - this.userRoles = roleNames.map((r) => { - return 'role:' + r; - }); - this.fetchedRoles = true; - this.rolePromise = null; - return Promise.resolve(this.userRoles); + var restWhere = { + 'users': { + __type: 'Pointer', + className: '_User', + objectId: this.user.id + } + }; + // First get the role ids this user is directly a member of + var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); + return query.execute().then((response) => { + var results = response.results; + if (!results.length) { + this.userRoles = []; + this.fetchedRoles = true; + this.rolePromise = null; + + cacheAdapter.role.put(this.user.id, this.userRoles); + return Promise.resolve(this.userRoles); + } + var rolesMap = results.reduce((m, r) => { + m.names.push(r.name); + m.ids.push(r.objectId); + return m; + }, {ids: [], names: []}); + + // run the recursive finding + return this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names) + .then((roleNames) => { + this.userRoles = roleNames.map((r) => { + return 'role:' + r; + }); + this.fetchedRoles = true; + this.rolePromise = null; + + cacheAdapter.role.put(this.user.id, this.userRoles); + return Promise.resolve(this.userRoles); + }); }); }); }; diff --git a/src/Config.js b/src/Config.js index badff9da40..faaa6c2235 100644 --- a/src/Config.js +++ b/src/Config.js @@ -2,7 +2,7 @@ // configured. // mount is the URL for the root of the API; includes http, domain, etc. -import cache from './cache'; +import AppCache from './cache'; function removeTrailingSlash(str) { if (!str) { @@ -17,7 +17,7 @@ function removeTrailingSlash(str) { export class Config { constructor(applicationId: string, mount: string) { let DatabaseAdapter = require('./DatabaseAdapter'); - let cacheInfo = cache.apps.get(applicationId); + let cacheInfo = AppCache.get(applicationId); if (!cacheInfo) { return; } @@ -38,6 +38,7 @@ export class Config { this.verifyUserEmails = cacheInfo.verifyUserEmails; this.appName = cacheInfo.appName; + this.cacheController = cacheInfo.cacheController; this.hooksController = cacheInfo.hooksController; this.filesController = cacheInfo.filesController; this.pushController = cacheInfo.pushController; diff --git a/src/Controllers/CacheController.js b/src/Controllers/CacheController.js new file mode 100644 index 0000000000..27dc4936f7 --- /dev/null +++ b/src/Controllers/CacheController.js @@ -0,0 +1,75 @@ +import AdaptableController from './AdaptableController'; +import CacheAdapter from '../Adapters/Cache/CacheAdapter'; + +const KEY_SEPARATOR_CHAR = ':'; + +function joinKeys(...keys) { + return keys.join(KEY_SEPARATOR_CHAR); +} + +/** + * Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type. + * + * eg "Role" or "Session" + */ +export class SubCache { + constructor(prefix, cacheController) { + this.prefix = prefix; + this.cache = cacheController; + } + + get(key) { + let cacheKey = joinKeys(this.prefix, key); + return this.cache.get(cacheKey); + } + + put(key, value, ttl) { + let cacheKey = joinKeys(this.prefix, key); + return this.cache.put(cacheKey, value, ttl); + } + + del(key) { + let cacheKey = joinKeys(this.prefix, key); + return this.cache.del(cacheKey); + } + + clear() { + return this.cache.clear(); + } +} + + +export class CacheController extends AdaptableController { + + constructor(adapter, appId, options = {}) { + super(adapter, appId, options); + + this.role = new SubCache('role', this); + this.user = new SubCache('user', this); + } + + get(key) { + let cacheKey = joinKeys(this.appId, key); + return this.adapter.get(cacheKey).then(null, () => Promise.resolve(null)); + } + + put(key, value, ttl) { + let cacheKey = joinKeys(this.appId, key); + return this.adapter.put(cacheKey, value, ttl); + } + + del(key) { + let cacheKey = joinKeys(this.appId, key); + return this.adapter.del(cacheKey); + } + + clear() { + return this.adapter.clear(); + } + + expectedAdapterType() { + return CacheAdapter; + } +} + +export default CacheController; diff --git a/src/DatabaseAdapter.js b/src/DatabaseAdapter.js index 73bc09334f..25ce69b422 100644 --- a/src/DatabaseAdapter.js +++ b/src/DatabaseAdapter.js @@ -60,7 +60,7 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) { uri: appDatabaseURIs[appId], //may be undefined if the user didn't supply a URI, in which case the default will be used } - dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions)); + dbConnections[appId] = new DatabaseController(new MongoStorageAdapter(mongoAdapterOptions), {appId: appId}); return dbConnections[appId]; } diff --git a/src/ParseServer.js b/src/ParseServer.js index baeb1d69e5..85b2b40c02 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -15,8 +15,8 @@ if (!global._babelPolyfill) { } import { logger, - configureLogger } from './logger'; -import cache from './cache'; + configureLogger } from './logger'; +import AppCache from './cache'; import Config from './Config'; import parseServerPackage from '../package.json'; import PromiseRouter from './PromiseRouter'; @@ -24,6 +24,8 @@ import requiredParameter from './requiredParameter'; import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { ClassesRouter } from './Routers/ClassesRouter'; import { FeaturesRouter } from './Routers/FeaturesRouter'; +import { InMemoryCacheAdapter } from './Adapters/Cache/InMemoryCacheAdapter'; +import { CacheController } from './Controllers/CacheController'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FilesController } from './Controllers/FilesController'; import { FilesRouter } from './Routers/FilesRouter'; @@ -104,6 +106,7 @@ class ParseServer { serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb', verifyUserEmails = false, + cacheAdapter, emailAdapter, publicServerURL, customPages = { @@ -156,6 +159,8 @@ class ParseServer { const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const emailControllerAdapter = loadAdapter(emailAdapter); + const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId}); + // We pass the options and the base class for the adatper, // Note that passing an instance would work too const filesController = new FilesController(filesControllerAdapter, appId); @@ -164,8 +169,9 @@ class ParseServer { const hooksController = new HooksController(appId, collectionPrefix); const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails }); const liveQueryController = new LiveQueryController(liveQuery); + const cacheController = new CacheController(cacheControllerAdapter, appId); - cache.apps.set(appId, { + AppCache.put(appId, { masterKey: masterKey, serverURL: serverURL, collectionPrefix: collectionPrefix, @@ -175,6 +181,7 @@ class ParseServer { restAPIKey: restAPIKey, fileKey: fileKey, facebookAppIds: facebookAppIds, + cacheController: cacheController, filesController: filesController, pushController: pushController, loggerController: loggerController, @@ -195,11 +202,11 @@ class ParseServer { // To maintain compatibility. TODO: Remove in some version that breaks backwards compatability if (process.env.FACEBOOK_APP_ID) { - cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); + AppCache.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID); } - Config.validate(cache.apps.get(appId)); - this.config = cache.apps.get(appId); + Config.validate(AppCache.get(appId)); + this.config = AppCache.get(appId); hooksController.load(); } diff --git a/src/RestWrite.js b/src/RestWrite.js index ecb92a85e4..f6e758f6bf 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -2,7 +2,6 @@ // that writes to the database. // This could be either a "create" or an "update". -import cache from './cache'; var SchemaController = require('./Controllers/SchemaController'); var deepcopy = require('deepcopy'); @@ -310,6 +309,7 @@ RestWrite.prototype.handleAuthData = function(authData) { }); } + // The non-third-party parts of User transformation RestWrite.prototype.transformUser = function() { if (this.className !== '_User') { @@ -320,7 +320,8 @@ RestWrite.prototype.transformUser = function() { // If we're updating a _User object, clear the user cache for the session if (this.query && this.auth.user && this.auth.user.getSessionToken()) { - cache.users.remove(this.auth.user.getSessionToken()); + let cacheAdapter = this.config.cacheController; + cacheAdapter.user.del(this.auth.user.getSessionToken()); } return promise.then(() => { @@ -441,24 +442,6 @@ RestWrite.prototype.handleFollowup = function() { } }; -// Handles the _Role class specialness. -// Does nothing if this isn't a role object. -RestWrite.prototype.handleRole = function() { - if (this.response || this.className !== '_Role') { - return; - } - - if (!this.auth.user && !this.auth.isMaster) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token required.'); - } - - if (!this.data.name) { - throw new Parse.Error(Parse.Error.INVALID_ROLE_NAME, - 'Invalid role name.'); - } -}; - // Handles the _Session class specialness. // Does nothing if this isn't an installation object. RestWrite.prototype.handleSession = function() { @@ -716,6 +699,10 @@ RestWrite.prototype.runDatabaseOperation = function() { return; } + if (this.className === '_Role') { + this.config.cacheController.role.clear(); + } + if (this.className === '_User' && this.query && !this.auth.couldUpdateUserId(this.query.objectId)) { diff --git a/src/cache.js b/src/cache.js index 8893f29b1b..96b00b4534 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,35 +1,4 @@ -/** @flow weak */ +import {InMemoryCache} from './Adapters/Cache/InMemoryCache'; -export function CacheStore() { - let dataStore: {[id:KeyType]:ValueType} = {}; - return { - get: (key: KeyType): ValueType => { - return dataStore[key]; - }, - set(key: KeyType, value: ValueType): void { - dataStore[key] = value; - }, - remove(key: KeyType): void { - delete dataStore[key]; - }, - clear(): void { - dataStore = {}; - } - }; -} - -const apps = CacheStore(); -const users = CacheStore(); - -//So far used only in tests -export function clearCache(): void { - apps.clear(); - users.clear(); -} - -export default { - apps, - users, - clearCache, - CacheStore -}; +export var AppCache = new InMemoryCache({ttl: NaN}); +export default AppCache; diff --git a/src/middlewares.js b/src/middlewares.js index 10115d6853..c500609363 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -1,5 +1,5 @@ -import cache from './cache'; -import log from './logger'; +import AppCache from './cache'; +import log from './logger'; var Parse = require('parse/node').Parse; @@ -36,7 +36,7 @@ function handleParseHeaders(req, res, next) { var fileViaJSON = false; - if (!info.appId || !cache.apps.get(info.appId)) { + if (!info.appId || !AppCache.get(info.appId)) { // See if we can find the app id on the body. if (req.body instanceof Buffer) { // The only chance to find the app id is if this is a file @@ -51,8 +51,8 @@ function handleParseHeaders(req, res, next) { if (req.body && req.body._ApplicationId && - cache.apps.get(req.body._ApplicationId) && - (!info.masterKey || cache.apps.get(req.body._ApplicationId).masterKey === info.masterKey) + AppCache.get(req.body._ApplicationId) && + (!info.masterKey || AppCache.get(req.body._ApplicationId).masterKey === info.masterKey) ) { info.appId = req.body._ApplicationId; info.javascriptKey = req.body._JavaScriptKey || ''; @@ -87,7 +87,7 @@ function handleParseHeaders(req, res, next) { req.body = new Buffer(base64, 'base64'); } - info.app = cache.apps.get(info.appId); + info.app = AppCache.get(info.appId); req.config = new Config(info.appId, mount); req.info = info; diff --git a/src/rest.js b/src/rest.js index 60f017213e..d1894165a6 100644 --- a/src/rest.js +++ b/src/rest.js @@ -8,8 +8,7 @@ // things. var Parse = require('parse/node').Parse; -import cache from './cache'; -import Auth from './Auth'; +import Auth from './Auth'; var RestQuery = require('./RestQuery'); var RestWrite = require('./RestWrite'); @@ -48,7 +47,9 @@ function del(config, auth, className, objectId) { .then((response) => { if (response && response.results && response.results.length) { response.results[0].className = className; - cache.users.remove(response.results[0].sessionToken); + + var cacheAdapter = config.cacheController; + cacheAdapter.user.del(response.results[0].sessionToken); inflatedObject = Parse.Object.fromJSON(response.results[0]); // Notify LiveQuery server if possible config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject); @@ -96,6 +97,7 @@ function create(config, auth, className, restObject) { // Usually, this is just updatedAt. function update(config, auth, className, objectId, restObject) { enforceRoleSecurity('update', className, auth); + return Promise.resolve().then(() => { if (triggers.getTrigger(className, triggers.Types.beforeSave, config.applicationId) || triggers.getTrigger(className, triggers.Types.afterSave, config.applicationId) || diff --git a/src/testing-routes.js b/src/testing-routes.js index 04b5cf83ed..eee022d9f9 100644 --- a/src/testing-routes.js +++ b/src/testing-routes.js @@ -1,5 +1,5 @@ // testing-routes.js -import cache from './cache'; +import AppCache from './cache'; import * as middlewares from './middlewares'; import { ParseServer } from './index'; import { Parse } from 'parse/node'; @@ -47,7 +47,7 @@ function dropApp(req, res) { return res.status(401).send({ "error": "unauthorized" }); } return req.config.database.deleteEverything().then(() => { - cache.apps.remove(req.config.applicationId); + AppCache.del(req.config.applicationId); res.status(200).send({}); }); } diff --git a/src/triggers.js b/src/triggers.js index 7ab1ea1902..c7c9ba2c1c 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,6 @@ // triggers.js import Parse from 'parse/node'; -import cache from './cache'; +import AppCache from './cache'; export const Types = { beforeSave: 'beforeSave', From d7d46998322fc8f241863a57b1295986ae3730ab Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Wed, 18 May 2016 12:33:55 -0700 Subject: [PATCH 12/63] Fix logic for missing geo index error message check (#1824) --- src/Adapters/Storage/Mongo/MongoCollection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index e29b68754b..bf41582b19 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -17,7 +17,7 @@ export default class MongoCollection { return this._rawFind(query, { skip, limit, sort }) .catch(error => { // Check for "no geoindex" error - if (error.code != 17007 || !error.message.match(/unable to find index for .geoNear/)) { + if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { throw error; } // Figure out what key needs an index From 4d4361451cdefa2592b92e8fd54c459b03190205 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 18 May 2016 18:14:54 -0700 Subject: [PATCH 13/63] Refactor MongoTransform.js (#1823) * Split transformAtom into transfromTopLevelAtom and transformInteriorAtom * Use single param for inArray and inObject * Tidyness in transformKeyValue * Add transformInteriorKeyValue * Remove update from tranformInteriorKeyValue * Split out transform update * Move validation out of transfromUpdate * Remove force paramater from transformTopLevelAtom throw error after if necessary * Turn transformKeyValue into transfromKey since it is only used for that purpose * Remove unnecessary stuff from transformKey * convert transformKey to use parse format schema * interior keys fixes * Add test for interior keys with special names * Correct validation of inner keys --- spec/ParseAPI.spec.js | 32 +++ src/Adapters/Storage/Mongo/MongoTransform.js | 251 +++++++++---------- src/Controllers/DatabaseController.js | 99 ++++---- 3 files changed, 205 insertions(+), 177 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 68e0544bb6..5f44dca79f 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1437,4 +1437,36 @@ describe('miscellaneous', function() { done(); }); }); + + it('doesnt convert interior keys of objects that use special names', done => { + let obj = new Parse.Object('Obj'); + obj.set('val', { createdAt: 'a', updatedAt: 1 }); + obj.save() + .then(obj => new Parse.Query('Obj').get(obj.id)) + .then(obj => { + expect(obj.get('val').createdAt).toEqual('a'); + expect(obj.get('val').updatedAt).toEqual(1); + done(); + }); + }); + + it('bans interior keys containing . or $', done => { + new Parse.Object('Obj').save({innerObj: {'key with a $': 'fails'}}) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({innerObj: {'key with a .': 'fails'}}); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({innerObj: {innerInnerObj: {'key with $': 'fails'}}}); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({innerObj: {innerInnerObj: {'key with .': 'fails'}}}); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + done(); + }) + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0cf09dbbee..31c3a9192c 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -3,24 +3,23 @@ import _ from 'lodash'; var mongodb = require('mongodb'); var Parse = require('parse/node').Parse; -// Transforms a key-value pair from REST API form to Mongo form. -// This is the main entry point for converting anything from REST form -// to Mongo form; no conversion should happen that doesn't pass -// through this function. -// Schema should already be loaded. -// -// There are several options that can help transform: -// -// update: true indicates that __op operators like Add and Delete -// in the value are converted to a mongo update form. Otherwise they are -// converted to static data. -// -// Returns an object with {key: key, value: value}. -function transformKeyValue(schema, className, restKey, restValue, { - inArray, - inObject, - update, -} = {}) { +const transformKey = (className, fieldName, schema) => { + // Check if the schema is known since it's a built-in field. + switch(fieldName) { + case 'objectId': return '_id'; + case 'createdAt': return '_created_at'; + case 'updatedAt': return '_updated_at'; + case 'sessionToken': return '_session_token'; + } + + if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') { + fieldName = '_p_' + fieldName; + } + + return fieldName; +} + +const transformKeyValueForUpdate = (schema, className, restKey, restValue) => { // Check if the schema is known since it's a built-in field. var key = restKey; var timeField = false; @@ -77,51 +76,60 @@ function transformKeyValue(schema, className, restKey, restValue, { if (schema && schema.getExpectedType) { expected = schema.getExpectedType(className, key); } - if ((expected && expected.type == 'Pointer') || - (!expected && restValue && restValue.__type == 'Pointer')) { + if ((expected && expected.type == 'Pointer') || (!expected && restValue && restValue.__type == 'Pointer')) { key = '_p_' + key; } - var expectedTypeIsArray = (expected && expected.type === 'Array'); // Handle atomic values - var value = transformAtom(restValue, false, { inArray, inObject }); + var value = transformTopLevelAtom(restValue); if (value !== CannotTransform) { if (timeField && (typeof value === 'string')) { value = new Date(value); } - return {key: key, value: value}; + return {key, value}; } - // ACLs are handled before this method is called - // If an ACL key still exists here, something is wrong. - if (key === 'ACL') { - throw 'There was a problem transforming an ACL.'; + // Handle arrays + if (restValue instanceof Array) { + value = restValue.map(transformInteriorValue); + return {key, value}; + } + + // Handle update operators + if (typeof restValue === 'object' && '__op' in restValue) { + return {key, value: transformUpdateOperator(restValue, false)}; + } + + // Handle normal objects by recursing + if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + value = _.mapValues(restValue, transformInteriorValue); + return {key, value}; +} + +const transformInteriorValue = restValue => { + if (typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + // Handle atomic values + var value = transformInteriorAtom(restValue); + if (value !== CannotTransform) { + return value; } // Handle arrays if (restValue instanceof Array) { - value = restValue.map((restObj) => { - var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); - return out.value; - }); - return {key: key, value: value}; + return restValue.map(transformInteriorValue); } // Handle update operators - value = transformUpdateOperator(restValue, !update); - if (value !== CannotTransform) { - return {key: key, value: value}; + if (typeof restValue === 'object' && '__op' in restValue) { + return transformUpdateOperator(restValue, true); } // Handle normal objects by recursing - value = {}; - for (var subRestKey in restValue) { - var subRestValue = restValue[subRestKey]; - var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); - // For recursed objects, keep the keys in rest format - value[subRestKey] = out.value; - } - return {key: key, value: value}; + return _.mapValues(restValue, transformInteriorValue); } const valueAsDate = value => { @@ -205,8 +213,8 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, schema } // Handle atomic values - if (transformAtom(value, false) !== CannotTransform) { - return {key, value: transformAtom(value, false)}; + if (transformTopLevelAtom(value) !== CannotTransform) { + return {key, value: transformTopLevelAtom(value)}; } else { throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`); } @@ -241,15 +249,15 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( switch(restKey) { case 'objectId': return {key: '_id', value: restValue}; case 'createdAt': - transformedValue = transformAtom(restValue, false); + transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: '_created_at', value: coercedToDate}; case 'updatedAt': - transformedValue = transformAtom(restValue, false); + transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: '_updated_at', value: coercedToDate}; case 'expiresAt': - transformedValue = transformAtom(restValue, false); + transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue return {key: 'expiresAt', value: coercedToDate}; case '_rperm': @@ -268,7 +276,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( return {key: restKey, value: restValue}; } } - //skip straight to transformAtom for Bytes, they don't show up in the schema for some reason + //skip straight to transformTopLevelAtom for Bytes, they don't show up in the schema for some reason if (restValue && restValue.__type !== 'Bytes') { //Note: We may not know the type of a field here, as the user could be saving (null) to a field //That never existed before, meaning we can't infer the type. @@ -278,7 +286,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( } // Handle atomic values - var value = transformAtom(restValue, false, { inArray: false, inObject: false }); + var value = transformTopLevelAtom(restValue); if (value !== CannotTransform) { return {key: restKey, value: value}; } @@ -291,28 +299,21 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( // Handle arrays if (restValue instanceof Array) { - value = restValue.map((restObj) => { - var out = transformKeyValue(schema, className, restKey, restObj, { inArray: true }); - return out.value; - }); + value = restValue.map(transformInteriorValue); return {key: restKey, value: value}; } // Handle update operators. TODO: handle within Parse Server. DB adapter shouldn't see update operators in creates. - value = transformUpdateOperator(restValue, true); - if (value !== CannotTransform) { - return {key: restKey, value: value}; + if (typeof restValue === 'object' && '__op' in restValue) { + return {key: restKey, value: transformUpdateOperator(restValue, true)}; } // Handle normal objects by recursing - value = {}; - for (var subRestKey in restValue) { - var subRestValue = restValue[subRestKey]; - var out = transformKeyValue(schema, className, subRestKey, subRestValue, { inObject: true }); - // For recursed objects, keep the keys in rest format - value[subRestKey] = out.value; + if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); } - return {key: restKey, value: value}; + value = _.mapValues(restValue, transformInteriorValue); + return {key: restKey, value}; } // Main exposed method to create new objects. @@ -362,13 +363,12 @@ function transformUpdate(schema, className, restUpdate) { } for (var restKey in restUpdate) { - var out = transformKeyValue(schema, className, restKey, restUpdate[restKey], {update: true}); + var out = transformKeyValueForUpdate(schema, className, restKey, restUpdate[restKey]); // If the output value is an object with any $ keys, it's an // operator that needs to be lifted onto the top level update // object. - if (typeof out.value === 'object' && out.value !== null && - out.value.__op) { + if (typeof out.value === 'object' && out.value !== null && out.value.__op) { mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {}; mongoUpdate[out.value.__op][out.key] = out.value.arg; } else { @@ -462,20 +462,33 @@ function untransformACL(mongoObject) { // cannot perform a transformation function CannotTransform() {} +const transformInteriorAtom = atom => { + // TODO: check validity harder for the __type-defined types + if (typeof atom === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') { + return { + __type: 'Pointer', + className: atom.className, + objectId: atom.objectId + }; + } else if (typeof atom === 'function' || typeof atom === 'symbol') { + throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`); + } else if (DateCoder.isValidJSON(atom)) { + return DateCoder.JSONToDatabase(atom); + } else if (BytesCoder.isValidJSON(atom)) { + return BytesCoder.JSONToDatabase(atom); + } else { + return atom; + } +} + // Helper function to transform an atom from REST format to Mongo format. // An atom is anything that can't contain other expressions. So it // includes things where objects are used to represent other // datatypes, like pointers and dates, but it does not include objects // or arrays with generic stuff inside. -// If options.inArray is true, we'll leave it in REST format. -// If options.inObject is true, we'll leave files in REST format. // Raises an error if this cannot possibly be valid REST format. -// Returns CannotTransform if it's just not an atom, or if force is -// true, throws an error. -function transformAtom(atom, force, { - inArray, - inObject, -} = {}) { +// Returns CannotTransform if it's just not an atom +function transformTopLevelAtom(atom) { switch(typeof atom) { case 'string': case 'number': @@ -499,14 +512,7 @@ function transformAtom(atom, force, { // TODO: check validity harder for the __type-defined types if (atom.__type == 'Pointer') { - if (!inArray && !inObject) { - return `${atom.className}$${atom.objectId}`; - } - return { - __type: 'Pointer', - className: atom.className, - objectId: atom.objectId - }; + return `${atom.className}$${atom.objectId}`; } if (DateCoder.isValidJSON(atom)) { return DateCoder.JSONToDatabase(atom); @@ -515,17 +521,10 @@ function transformAtom(atom, force, { return BytesCoder.JSONToDatabase(atom); } if (GeoPointCoder.isValidJSON(atom)) { - return (inArray || inObject ? atom : GeoPointCoder.JSONToDatabase(atom)); + return GeoPointCoder.JSONToDatabase(atom); } if (FileCoder.isValidJSON(atom)) { - return (inArray || inObject ? atom : FileCoder.JSONToDatabase(atom)); - } - if (inArray || inObject) { - return atom; - } - - if (force) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`); + return FileCoder.JSONToDatabase(atom); } return CannotTransform; @@ -560,19 +559,24 @@ function transformConstraint(constraint, inArray) { case '$exists': case '$ne': case '$eq': - answer[key] = transformAtom(constraint[key], true, - {inArray: inArray}); + answer[key] = inArray ? transformInteriorAtom(constraint[key]) : transformTopLevelAtom(constraint[key]); + if (answer[key] === CannotTransform) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`); + } break; case '$in': case '$nin': var arr = constraint[key]; if (!(arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'bad ' + key + ' value'); + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); } - answer[key] = arr.map((v) => { - return transformAtom(v, true, { inArray: inArray }); + answer[key] = arr.map(value => { + let result = inArray ? transformInteriorAtom(value) : transformTopLevelAtom(value); + if (result === CannotTransform) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${atom}`); + } + return result; }); break; @@ -582,9 +586,7 @@ function transformConstraint(constraint, inArray) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); } - answer[key] = arr.map((v) => { - return transformAtom(v, true, { inArray: true }); - }); + answer[key] = arr.map(transformInteriorAtom); break; case '$regex': @@ -667,14 +669,14 @@ function transformConstraint(constraint, inArray) { // The output for a non-flattened operator is a hash with __op being // the mongo op, and arg being the argument. // The output for a flattened operator is just a value. -// Returns CannotTransform if this cannot transform it. // Returns undefined if this should be a no-op. -function transformUpdateOperator(operator, flatten) { - if (typeof operator !== 'object' || !operator.__op) { - return CannotTransform; - } - switch(operator.__op) { +function transformUpdateOperator({ + __op, + amount, + objects, +}, flatten) { + switch(__op) { case 'Delete': if (flatten) { return undefined; @@ -683,43 +685,36 @@ function transformUpdateOperator(operator, flatten) { } case 'Increment': - if (typeof operator.amount !== 'number') { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'incrementing must provide a number'); + if (typeof amount !== 'number') { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'incrementing must provide a number'); } if (flatten) { - return operator.amount; + return amount; } else { - return {__op: '$inc', arg: operator.amount}; + return {__op: '$inc', arg: amount}; } case 'Add': case 'AddUnique': - if (!(operator.objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'objects to add must be an array'); + if (!(objects instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); } - var toAdd = operator.objects.map((obj) => { - return transformAtom(obj, true, { inArray: true }); - }); + var toAdd = objects.map(transformInteriorAtom); if (flatten) { return toAdd; } else { var mongoOp = { Add: '$push', AddUnique: '$addToSet' - }[operator.__op]; + }[__op]; return {__op: mongoOp, arg: {'$each': toAdd}}; } case 'Remove': - if (!(operator.objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'objects to remove must be an array'); + if (!(objects instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to remove must be an array'); } - var toRemove = operator.objects.map((obj) => { - return transformAtom(obj, true, { inArray: true }); - }); + var toRemove = objects.map(transformInteriorAtom); if (flatten) { return []; } else { @@ -727,9 +722,7 @@ function transformUpdateOperator(operator, flatten) { } default: - throw new Parse.Error( - Parse.Error.COMMAND_UNAVAILABLE, - 'the ' + operator.__op + ' op is not supported yet'); + throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, `The ${__op} operator is not supported yet.`); } } @@ -1037,7 +1030,7 @@ var FileCoder = { }; module.exports = { - transformKeyValue, + transformKey, parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ab90fc3bfd..fefd5afe6b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -616,64 +616,67 @@ DatabaseController.prototype.find = function(className, query, { let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'; return this.loadSchema() .then(schemaController => { - if (sort) { - mongoOptions.sort = {}; - for (let fieldName in sort) { - // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, - // so duplicate that behaviour here. - if (fieldName === '_created_at') { - fieldName = 'createdAt'; - sort['createdAt'] = sort['_created_at']; - } else if (fieldName === '_updated_at') { - fieldName = 'updatedAt'; - sort['updatedAt'] = sort['_updated_at']; - } + return schemaController.getOneSchema(className) + .catch(error => { + // If the schema doesn't exist, pretend it exists with no fields. This behaviour + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; + } + throw error; + }) + .then(schema => { + if (sort) { + mongoOptions.sort = {}; + for (let fieldName in sort) { + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behaviour here. + if (fieldName === '_created_at') { + fieldName = 'createdAt'; + sort['createdAt'] = sort['_created_at']; + } else if (fieldName === '_updated_at') { + fieldName = 'updatedAt'; + sort['updatedAt'] = sort['_updated_at']; + } - if (!SchemaController.fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + if (!SchemaController.fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`); + } + const mongoKey = this.transform.transformKey(className, fieldName, schema); + mongoOptions.sort[mongoKey] = sort[fieldName]; } - const mongoKey = this.transform.transformKeyValue(schemaController, className, fieldName, null).key; - mongoOptions.sort[mongoKey] = sort[fieldName]; } - } - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) - .then(() => this.reduceRelationKeys(className, query)) - .then(() => this.reduceInRelation(className, query, schemaController)) - .then(() => this.adapter.adaptiveCollection(className)) - .then(collection => { - if (!isMaster) { - query = this.addPointerPermissions(schemaController, className, op, query, aclGroup); - } - if (!query) { - if (op == 'get') { - return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.')); - } else { - return Promise.resolve([]); + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) + .then(() => this.reduceRelationKeys(className, query)) + .then(() => this.reduceInRelation(className, query, schemaController)) + .then(() => this.adapter.adaptiveCollection(className)) + .then(collection => { + if (!isMaster) { + query = this.addPointerPermissions(schemaController, className, op, query, aclGroup); } - } - if (!isMaster) { - query = addReadACL(query, aclGroup); - } - return schemaController.getOneSchema(className) - .catch(error => { - // If the schema doesn't exist, pretend it exists with no fields. This behaviour - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; + if (!query) { + if (op == 'get') { + return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.')); + } else { + return Promise.resolve([]); + } } - throw error; - }) - .then(parseFormatSchema => { - let mongoWhere = this.transform.transformWhere(className, query, {}, parseFormatSchema); + if (!isMaster) { + query = addReadACL(query, aclGroup); + } + let mongoWhere = this.transform.transformWhere(className, query, {}, schema); if (count) { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); } else { return collection.find(mongoWhere, mongoOptions) - .then((mongoResults) => { - return mongoResults.map((r) => { - return this.untransformObject(schemaController, isMaster, aclGroup, className, r); + .then(mongoResults => { + return mongoResults.map(result => { + return this.untransformObject(schemaController, isMaster, aclGroup, className, result); }); }); } From 0d856c1f23cccfce69a695edff99813928bc23d7 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 17 May 2016 00:56:04 -0700 Subject: [PATCH 14/63] remove extra lines --- src/Adapters/Storage/Mongo/MongoTransform.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 31c3a9192c..230929bd1d 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -112,6 +112,7 @@ const transformInteriorValue = restValue => { if (typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); } + // Handle atomic values var value = transformInteriorAtom(restValue); if (value !== CannotTransform) { From b24ff151d5586750d718352709701b5fbb4874f8 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 17 May 2016 15:57:25 -0700 Subject: [PATCH 15/63] Correct validation of inner keys --- src/Adapters/Storage/Mongo/MongoTransform.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 230929bd1d..31c3a9192c 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -112,7 +112,6 @@ const transformInteriorValue = restValue => { if (typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); } - // Handle atomic values var value = transformInteriorAtom(restValue); if (value !== CannotTransform) { From 66b8a8474e83ba40d4af9b82d03175ca03ac6606 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 13:35:49 -0700 Subject: [PATCH 16/63] Lift query key validation out of transformQueryKeyValue --- src/Adapters/Storage/Mongo/MongoTransform.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 31c3a9192c..5be57583e6 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -141,7 +141,7 @@ const valueAsDate = value => { return false; } -function transformQueryKeyValue(className, key, value, { validate } = {}, schema) { +function transformQueryKeyValue(className, key, value, schema) { switch(key) { case 'createdAt': if (valueAsDate(value)) { @@ -184,9 +184,6 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, schema // Special-case auth data. return {key: `_auth_data_${provider}.id`, value}; } - if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key); - } } const expectedTypeIsArray = @@ -224,13 +221,17 @@ function transformQueryKeyValue(className, key, value, { validate } = {}, schema // 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. +const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; function transformWhere(className, restWhere, { validate = true } = {}, schema) { let mongoWhere = {}; if (restWhere['ACL']) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } for (let restKey in restWhere) { - let out = transformQueryKeyValue(className, restKey, restWhere[restKey], { validate }, schema); + if (validate && !specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); + } + let out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema); mongoWhere[out.key] = out.value; } return mongoWhere; From 559205bc64d4983116fcc335bdc598c4135cf1bb Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 13:49:31 -0700 Subject: [PATCH 17/63] Lift no-query-ACL validation out of transformWhere --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 3 +++ src/Adapters/Storage/Mongo/MongoTransform.js | 9 ++++++--- src/Controllers/DatabaseController.js | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 81af4e970c..9096d4b851 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -184,6 +184,9 @@ export class MongoStorageAdapter { deleteObjectsByQuery(className, query, validate, schema) { return this.adaptiveCollection(className) .then(collection => { + if (query.ACL) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } let mongoWhere = transform.transformWhere(className, query, { validate }, schema); return collection.deleteMany(mongoWhere) }) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 5be57583e6..a7aee55a56 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -170,11 +170,17 @@ function transformQueryKeyValue(className, key, value, schema) { if (!(value instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $or format - use an array value'); } + if (value.some(subQuery => subQuery.ACL)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))}; case '$and': if (!(value instanceof Array)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $and format - use an array value'); } + if (value.some(subQuery => subQuery.ACL)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))}; default: // Other auth data @@ -224,9 +230,6 @@ function transformQueryKeyValue(className, key, value, schema) { const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; function transformWhere(className, restWhere, { validate = true } = {}, schema) { let mongoWhere = {}; - if (restWhere['ACL']) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - } for (let restKey in restWhere) { if (validate && !specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index fefd5afe6b..ac0ada29ec 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -184,6 +184,9 @@ DatabaseController.prototype.update = function(className, query, update, { throw error; }) .then(parseFormatSchema => { + if (query.ACL) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } var mongoWhere = this.transform.transformWhere(className, query, {validate: !this.skipValidation}, parseFormatSchema); mongoUpdate = this.transform.transformUpdate( schemaController, @@ -668,6 +671,9 @@ DatabaseController.prototype.find = function(className, query, { if (!isMaster) { query = addReadACL(query, aclGroup); } + if (query.ACL) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } let mongoWhere = this.transform.transformWhere(className, query, {}, schema); if (count) { delete mongoOptions.limit; From ea0921351159a2938f85a4c89b681ba1f56fe980 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 16:26:59 -0700 Subject: [PATCH 18/63] lift query key validation out of transformWhere --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 6 +++++- src/Adapters/Storage/Mongo/MongoTransform.js | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 9096d4b851..949ef1cc58 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -25,6 +25,7 @@ const storageAdapterAllCollections = mongoAdapter => { }); } +const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; export class MongoStorageAdapter { // Private _uri: string; @@ -187,7 +188,10 @@ export class MongoStorageAdapter { if (query.ACL) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } - let mongoWhere = transform.transformWhere(className, query, { validate }, schema); + if (validate && Object.keys(query).some(restKey => !specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/))) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); + } + let mongoWhere = transform.transformWhere(className, query, schema); return collection.deleteMany(mongoWhere) }) .then(({ result }) => { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index a7aee55a56..279888c9a3 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -172,6 +172,11 @@ function transformQueryKeyValue(className, key, value, schema) { } if (value.some(subQuery => subQuery.ACL)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + Object.keys(subQuery).forEach(restKey => { + if (!specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); + } + }); } return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))}; case '$and': @@ -180,6 +185,11 @@ function transformQueryKeyValue(className, key, value, schema) { } if (value.some(subQuery => subQuery.ACL)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + Object.keys(subQuery).forEach(restKey => { + if (!specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); + } + }); } return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))}; default: From 15fc186a517db3ed7f7d4f0e1550e5b1e9d13c01 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 18:05:04 -0700 Subject: [PATCH 19/63] Extract query validation logic --- .../Storage/Mongo/MongoStorageAdapter.js | 7 +-- src/Adapters/Storage/Mongo/MongoTransform.js | 60 ++++++++++--------- src/Controllers/DatabaseController.js | 12 ++-- src/Routers/GlobalConfigRouter.js | 2 +- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 949ef1cc58..d6b0f1ab71 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -185,12 +185,7 @@ export class MongoStorageAdapter { deleteObjectsByQuery(className, query, validate, schema) { return this.adaptiveCollection(className) .then(collection => { - if (query.ACL) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - } - if (validate && Object.keys(query).some(restKey => !specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/))) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); - } + transform.validateQuery(query); let mongoWhere = transform.transformWhere(className, query, schema); return collection.deleteMany(mongoWhere) }) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 279888c9a3..e92cacee06 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -167,31 +167,9 @@ function transformQueryKeyValue(className, key, value, schema) { case '_perishable_token': case '_email_verify_token': return {key, value} case '$or': - if (!(value instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $or format - use an array value'); - } - if (value.some(subQuery => subQuery.ACL)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - Object.keys(subQuery).forEach(restKey => { - if (!specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); - } - }); - } - return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))}; + return {key: '$or', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; case '$and': - if (!(value instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad $and format - use an array value'); - } - if (value.some(subQuery => subQuery.ACL)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - Object.keys(subQuery).forEach(restKey => { - if (!specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); - } - }); - } - return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, {}, schema))}; + return {key: '$and', value: value.map(subQuery => transformWhere(className, subQuery, schema))}; default: // Other auth data const authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); @@ -233,17 +211,42 @@ function transformQueryKeyValue(className, key, value, schema) { } } +const validateQuery = query => { + if (query.ACL) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } + + if (query.$or) { + if (query.$or instanceof Array) { + query.$or.forEach(validateQuery); + } else { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); + } + } + + if (query.$and) { + if (query.$and instanceof Array) { + query.$and.forEach(validateQuery); + } else { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); + } + } + + Object.keys(query).forEach(key => { + if (!specialQuerykeys.includes(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); + } + }); +} + // Main exposed method to help run queries. // 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. const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; -function transformWhere(className, restWhere, { validate = true } = {}, schema) { +function transformWhere(className, restWhere, schema) { let mongoWhere = {}; for (let restKey in restWhere) { - if (validate && !specialQuerykeys.includes(restKey) && !restKey.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${restKey}`); - } let out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema); mongoWhere[out.key] = out.value; } @@ -1045,6 +1048,7 @@ var FileCoder = { module.exports = { transformKey, + validateQuery, parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ac0ada29ec..2657806b0e 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -184,10 +184,8 @@ DatabaseController.prototype.update = function(className, query, update, { throw error; }) .then(parseFormatSchema => { - if (query.ACL) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - } - var mongoWhere = this.transform.transformWhere(className, query, {validate: !this.skipValidation}, parseFormatSchema); + this.transform.validateQuery(query); + var mongoWhere = this.transform.transformWhere(className, query, parseFormatSchema); mongoUpdate = this.transform.transformUpdate( schemaController, className, @@ -671,10 +669,8 @@ DatabaseController.prototype.find = function(className, query, { if (!isMaster) { query = addReadACL(query, aclGroup); } - if (query.ACL) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - } - let mongoWhere = this.transform.transformWhere(className, query, {}, schema); + this.transform.validateQuery(query); + let mongoWhere = this.transform.transformWhere(className, query, schema); if (count) { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index ab49852273..5ab89b0b61 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -24,7 +24,7 @@ export class GlobalConfigRouter extends PromiseRouter { return acc; }, {}); let database = req.config.database.WithoutValidation(); - return database.update('_GlobalConfig', {_id: 1}, update, {upsert: true}).then(() => { + return database.update('_GlobalConfig', {objectId: 1}, update, {upsert: true}).then(() => { return Promise.resolve({ response: { result: true } }); }); } From 643bdc8227bb9e1d67f02af4a627a87b01b84856 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 18:10:57 -0700 Subject: [PATCH 20/63] Move query validation out of mongo adapter --- .../Storage/Mongo/MongoStorageAdapter.js | 1 - src/Adapters/Storage/Mongo/MongoTransform.js | 31 ----------------- src/Controllers/DatabaseController.js | 34 +++++++++++++++++-- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index d6b0f1ab71..c860819de8 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -185,7 +185,6 @@ export class MongoStorageAdapter { deleteObjectsByQuery(className, query, validate, schema) { return this.adaptiveCollection(className) .then(collection => { - transform.validateQuery(query); let mongoWhere = transform.transformWhere(className, query, schema); return collection.deleteMany(mongoWhere) }) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index e92cacee06..33aa666b09 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -211,39 +211,9 @@ function transformQueryKeyValue(className, key, value, schema) { } } -const validateQuery = query => { - if (query.ACL) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - } - - if (query.$or) { - if (query.$or instanceof Array) { - query.$or.forEach(validateQuery); - } else { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); - } - } - - if (query.$and) { - if (query.$and instanceof Array) { - query.$and.forEach(validateQuery); - } else { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); - } - } - - Object.keys(query).forEach(key => { - if (!specialQuerykeys.includes(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); - } - }); -} - // Main exposed method to help run queries. // 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. -const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; function transformWhere(className, restWhere, schema) { let mongoWhere = {}; for (let restKey in restWhere) { @@ -1048,7 +1018,6 @@ var FileCoder = { module.exports = { transformKey, - validateQuery, parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 2657806b0e..0af8fb9252 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -24,6 +24,35 @@ function addReadACL(query, acl) { return newQuery; } +const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; +const validateQuery = query => { + if (query.ACL) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } + + if (query.$or) { + if (query.$or instanceof Array) { + query.$or.forEach(validateQuery); + } else { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); + } + } + + if (query.$and) { + if (query.$and instanceof Array) { + query.$and.forEach(validateQuery); + } else { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); + } + } + + Object.keys(query).forEach(key => { + if (!specialQuerykeys.includes(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); + } + }); +} + function DatabaseController(adapter, { skipValidation } = {}) { this.adapter = adapter; @@ -174,6 +203,7 @@ DatabaseController.prototype.update = function(className, query, update, { if (acl) { query = addWriteACL(query, acl); } + validateQuery(query); return schemaController.getOneSchema(className) .catch(error => { // If the schema doesn't exist, pretend it exists with no fields. This behaviour @@ -184,7 +214,6 @@ DatabaseController.prototype.update = function(className, query, update, { throw error; }) .then(parseFormatSchema => { - this.transform.validateQuery(query); var mongoWhere = this.transform.transformWhere(className, query, parseFormatSchema); mongoUpdate = this.transform.transformUpdate( schemaController, @@ -329,6 +358,7 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {}) if (acl) { query = addWriteACL(query, acl); } + validateQuery(query); return schemaController.getOneSchema(className) .catch(error => { // If the schema doesn't exist, pretend it exists with no fields. This behaviour @@ -669,7 +699,7 @@ DatabaseController.prototype.find = function(className, query, { if (!isMaster) { query = addReadACL(query, aclGroup); } - this.transform.validateQuery(query); + validateQuery(query); let mongoWhere = this.transform.transformWhere(className, query, schema); if (count) { delete mongoOptions.limit; From 5165c80570477f1ad37dca74491cdd74254ee81b Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 18:35:02 -0700 Subject: [PATCH 21/63] Remove validate parameter from deleteObjectsByQuery --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- src/Controllers/DatabaseController.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index c860819de8..b47c32a604 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -181,8 +181,8 @@ export class MongoStorageAdapter { // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. // If there is some other error, reject with INTERNAL_SERVER_ERROR. - // Currently accepts validate for legacy reasons. Currently accepts the schema, that may not actually be necessary. - deleteObjectsByQuery(className, query, validate, schema) { + // Currently accepts the schema, that may not actually be necessary. + deleteObjectsByQuery(className, query, schema) { return this.adaptiveCollection(className) .then(collection => { let mongoWhere = transform.transformWhere(className, query, schema); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 0af8fb9252..522a2f1443 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -368,7 +368,7 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {}) } throw error; }) - .then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, query, !this.skipValidation, parseFormatSchema)) + .then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, query, parseFormatSchema)) .catch(error => { // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. if (className === "_Session" && error.code === Parse.Error.OBJECT_NOT_FOUND) { From c416cad43fc3eb1e5e5ae139f772a0ed9cb080e7 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 18 May 2016 18:59:52 -0700 Subject: [PATCH 22/63] remove extra special keys list --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index b47c32a604..e194dfe00f 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -25,7 +25,6 @@ const storageAdapterAllCollections = mongoAdapter => { }); } -const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token']; export class MongoStorageAdapter { // Private _uri: string; From 03108e634776ebeda71c0c3159f8fbedff1b796c Mon Sep 17 00:00:00 2001 From: Hussam Moqhim Date: Wed, 18 May 2016 22:06:37 -0500 Subject: [PATCH 23/63] add support for http basic auth (#1706) * add support for http basic auth * update http auth per flovilmart feedback --- spec/index.spec.js | 24 +++++++++++++++++++++++ src/middlewares.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/spec/index.spec.js b/spec/index.spec.js index d0d54401b1..f76e809ed4 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -12,6 +12,30 @@ describe('server', () => { expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!'); done(); }); + + it('support http basic authentication with masterkey', done => { + request.get({ + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Authorization': 'Basic ' + new Buffer('test:' + 'test').toString('base64') + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + done(); + }); + }); + + it('support http basic authentication with javascriptKey', done => { + request.get({ + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'Authorization': 'Basic ' + new Buffer('test:javascript-key=' + 'test').toString('base64') + } + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + done(); + }); + }); it('fails if database is unreachable', done => { setServerConfiguration({ diff --git a/src/middlewares.js b/src/middlewares.js index c500609363..d534215453 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -27,6 +27,14 @@ function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key') }; + + var basicAuth = httpAuth(req); + + if (basicAuth) { + info.appId = basicAuth.appId + info.masterKey = basicAuth.masterKey || info.masterKey; + info.javascriptKey = basicAuth.javascriptKey || info.javascriptKey; + } if (req.body) { // Unity SDK sends a _noBody key which needs to be removed. @@ -144,6 +152,45 @@ function handleParseHeaders(req, res, next) { }); } +function httpAuth(req) { + if (!(req.req || req).headers.authorization) + return ; + + var header = (req.req || req).headers.authorization; + var appId, masterKey, javascriptKey; + + // parse header + var authPrefix = 'basic '; + + var match = header.toLowerCase().indexOf(authPrefix); + + if (match == 0) { + var encodedAuth = header.substring(authPrefix.length, header.length); + var credentials = decodeBase64(encodedAuth).split(':'); + + if (credentials.length == 2) { + appId = credentials[0]; + var key = credentials[1]; + + var jsKeyPrefix = 'javascript-key='; + + var matchKey = key.indexOf(jsKeyPrefix) + if (matchKey == 0) { + javascriptKey = key.substring(jsKeyPrefix.length, key.length); + } + else { + masterKey = key; + } + } + } + + return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey}; +} + +function decodeBase64(str) { + return new Buffer(str, 'base64').toString() +} + var allowCrossDomain = function(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); From fece2a4b47f1469c6371c39bca63df85276e7d5f Mon Sep 17 00:00:00 2001 From: benishak Date: Thu, 19 May 2016 18:52:44 +0200 Subject: [PATCH 24/63] change logger.error to logger.info to prevent pm2 from crashing (#1830) --- src/pushStatusHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pushStatusHandler.js b/src/pushStatusHandler.js index dfe887f3ea..934e8fa27f 100644 --- a/src/pushStatusHandler.js +++ b/src/pushStatusHandler.js @@ -96,7 +96,7 @@ export default function pushStatusHandler(config) { status: 'failed', updatedAt: new Date() } - logger.error('error while sending push', err); + logger.info('warning: error while sending push', err); return database.update(PUSH_STATUS_COLLECTION, { objectId }, update); } From b2183680be18ffc6c5b90e2fc2260bede571c2b3 Mon Sep 17 00:00:00 2001 From: Drew Date: Thu, 19 May 2016 13:38:16 -0700 Subject: [PATCH 25/63] Refactor cloud code tests (#1837) * Move cloud code tests * Remove _removeHook calls that are no longer necessary * Use Strict --- spec/CloudCode.spec.js | 470 ++++++++++++++++++++++++++++ spec/CloudCodeLogger.spec.js | 2 - spec/ParseACL.spec.js | 1 - spec/ParseAPI.spec.js | 349 --------------------- spec/ParseRelation.spec.js | 4 - spec/ParseUser.spec.js | 8 - spec/cloud/cloudCodeAbsoluteFile.js | 3 + spec/cloud/cloudCodeRelativeFile.js | 3 + spec/cloud/main.js | 117 ------- spec/helper.js | 3 +- spec/index.spec.js | 24 +- src/cloud-code/Parse.Cloud.js | 8 +- src/triggers.js | 18 +- 13 files changed, 496 insertions(+), 514 deletions(-) create mode 100644 spec/CloudCode.spec.js create mode 100644 spec/cloud/cloudCodeAbsoluteFile.js create mode 100644 spec/cloud/cloudCodeRelativeFile.js delete mode 100644 spec/cloud/main.js diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js new file mode 100644 index 0000000000..2218a25706 --- /dev/null +++ b/spec/CloudCode.spec.js @@ -0,0 +1,470 @@ +"use strict" +const Parse = require("parse/node"); + +describe('Cloud Code', () => { + it('can load absolute cloud code file', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' + }); + Parse.Cloud.run('cloudCodeInFile', {}, result => { + expect(result).toEqual('It is possible to define cloud code in a file.'); + done(); + }); + }); + + it('can load relative cloud code file', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + masterKey: 'test', + cloud: './spec/cloud/cloudCodeAbsoluteFile.js' + }); + Parse.Cloud.run('cloudCodeInFile', {}, result => { + expect(result).toEqual('It is possible to define cloud code in a file.'); + done(); + }); + }); + + it('can create functions', done => { + Parse.Cloud.define('hello', (req, res) => { + res.success('Hello world!'); + }); + + Parse.Cloud.run('hello', {}, result => { + expect(result).toEqual('Hello world!'); + done(); + }); + }); + + it('is cleared cleared after the previous test', done => { + Parse.Cloud.run('hello', {}) + .catch(error => { + expect(error.code).toEqual(141); + done(); + }); + }); + + it('basic beforeSave rejection', function(done) { + Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) { + res.error('You shall not pass!'); + }); + + var obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj.save().then(() => { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, () => { + done(); + }) + }); + + it('basic beforeSave rejection via promise', function(done) { + Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) { + var query = new Parse.Query('Yolo'); + query.find().then(() => { + res.error('Nope'); + }, () => { + res.success(); + }); + }); + + var obj = new Parse.Object('BeforeSaveFailWithPromise'); + obj.set('foo', 'bar'); + obj.save().then(function() { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, function(error) { + expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(error.message).toEqual('Nope'); + done(); + }) + }); + + it('test beforeSave changed object success', function(done) { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bar'); + obj.save().then(function() { + var query = new Parse.Query('BeforeSaveChanged'); + query.get(obj.id).then(function(objAgain) { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }, function(error) { + fail(error); + done(); + }); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeSave returns value on create and update', (done) => { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bing'); + obj.save().then(() => { + expect(obj.get('foo')).toEqual('baz'); + obj.set('foo', 'bar'); + return obj.save().then(() => { + expect(obj.get('foo')).toEqual('baz'); + done(); + }) + }) + }); + + it('test afterSave ran and created an object', function(done) { + Parse.Cloud.afterSave('AfterSaveTest', function(req) { + var obj = new Parse.Object('AfterSaveProof'); + obj.set('proof', req.object.id); + obj.save(); + }); + + var obj = new Parse.Object('AfterSaveTest'); + obj.save(); + + setTimeout(function() { + var query = new Parse.Query('AfterSaveProof'); + query.equalTo('proof', obj.id); + query.find().then(function(results) { + expect(results.length).toEqual(1); + done(); + }, function(error) { + fail(error); + done(); + }); + }, 500); + }); + + it('test beforeSave happens on update', function(done) { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveChanged'); + obj.set('foo', 'bar'); + obj.save().then(function() { + obj.set('foo', 'bar'); + return obj.save(); + }).then(function() { + var query = new Parse.Query('BeforeSaveChanged'); + return query.get(obj.id).then(function(objAgain) { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeDelete failure', function(done) { + Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) { + res.error('Nope'); + }); + + var obj = new Parse.Object('BeforeDeleteFail'); + var id; + obj.set('foo', 'bar'); + obj.save().then(() => { + id = obj.id; + return obj.destroy(); + }).then(() => { + fail('obj.destroy() should have failed, but it succeeded'); + done(); + }, (error) => { + expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(error.message).toEqual('Nope'); + + var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id}); + return objAgain.fetch(); + }).then((objAgain) => { + if (objAgain) { + expect(objAgain.get('foo')).toEqual('bar'); + } else { + fail("unable to fetch the object ", id); + } + done(); + }, (error) => { + // We should have been able to fetch the object again + fail(error); + }); + }); + + it('basic beforeDelete rejection via promise', function(done) { + Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) { + var query = new Parse.Query('Yolo'); + query.find().then(() => { + res.error('Nope'); + }, () => { + res.success(); + }); + }); + + var obj = new Parse.Object('BeforeDeleteFailWithPromise'); + obj.set('foo', 'bar'); + obj.save().then(function() { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, function(error) { + expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(error.message).toEqual('Nope'); + + done(); + }) + }); + + it('test afterDelete ran and created an object', function(done) { + Parse.Cloud.afterDelete('AfterDeleteTest', function(req) { + var obj = new Parse.Object('AfterDeleteProof'); + obj.set('proof', req.object.id); + obj.save(); + }); + + var obj = new Parse.Object('AfterDeleteTest'); + obj.save().then(function() { + obj.destroy(); + }); + + setTimeout(function() { + var query = new Parse.Query('AfterDeleteProof'); + query.equalTo('proof', obj.id); + query.find().then(function(results) { + expect(results.length).toEqual(1); + done(); + }, function(error) { + fail(error); + done(); + }); + }, 500); + }); + + it('test cloud function return types', function(done) { + Parse.Cloud.define('foo', function(req, res) { + res.success({ + object: { + __type: 'Object', + className: 'Foo', + objectId: '123', + x: 2, + relation: { + __type: 'Object', + className: 'Bar', + objectId: '234', + x: 3 + } + }, + array: [{ + __type: 'Object', + className: 'Bar', + objectId: '345', + x: 2 + }], + a: 2 + }); + }); + + Parse.Cloud.run('foo').then((result) => { + expect(result.object instanceof Parse.Object).toBeTruthy(); + if (!result.object) { + fail("Unable to run foo"); + done(); + return; + } + expect(result.object.className).toEqual('Foo'); + expect(result.object.get('x')).toEqual(2); + var bar = result.object.get('relation'); + expect(bar instanceof Parse.Object).toBeTruthy(); + expect(bar.className).toEqual('Bar'); + expect(bar.get('x')).toEqual(3); + expect(Array.isArray(result.array)).toEqual(true); + expect(result.array[0] instanceof Parse.Object).toBeTruthy(); + expect(result.array[0].get('x')).toEqual(2); + done(); + }); + }); + + it('test cloud function should echo keys', function(done) { + Parse.Cloud.define('echoKeys', function(req, res){ + return res.success({ + applicationId: Parse.applicationId, + masterKey: Parse.masterKey, + javascriptKey: Parse.javascriptKey + }) + }); + + Parse.Cloud.run('echoKeys').then((result) => { + expect(result.applicationId).toEqual(Parse.applicationId); + expect(result.masterKey).toEqual(Parse.masterKey); + expect(result.javascriptKey).toEqual(Parse.javascriptKey); + done(); + }); + }); + + it('should properly create an object in before save', done => { + Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { + req.object.set('foo', 'baz'); + res.success(); + }); + + Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){ + var obj = new Parse.Object('BeforeSaveChanged'); + obj.save().then(() => { + res.success(obj); + }) + }) + + Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => { + expect(res.get('foo')).toEqual('baz'); + done(); + }); + }); + + it('dirtyKeys are set on update', done => { + let triggerTime = 0; + // Register a mock beforeSave hook + Parse.Cloud.beforeSave('GameScore', (req, res) => { + var object = req.object; + expect(object instanceof Parse.Object).toBeTruthy(); + expect(object.get('fooAgain')).toEqual('barAgain'); + if (triggerTime == 0) { + // Create + expect(object.get('foo')).toEqual('bar'); + } else if (triggerTime == 1) { + // Update + expect(object.dirtyKeys()).toEqual(['foo']); + expect(object.dirty('foo')).toBeTruthy(); + expect(object.get('foo')).toEqual('baz'); + } else { + res.error(); + } + triggerTime++; + res.success(); + }); + + let obj = new Parse.Object('GameScore'); + obj.set('foo', 'bar'); + obj.set('fooAgain', 'barAgain'); + obj.save().then(() => { + // We only update foo + obj.set('foo', 'baz'); + return obj.save(); + }).then(() => { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeSave unchanged success', function(done) { + Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) { + res.success(); + }); + + var obj = new Parse.Object('BeforeSaveUnchanged'); + obj.set('foo', 'bar'); + obj.save().then(function() { + done(); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test beforeDelete success', function(done) { + Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) { + res.success(); + }); + + var obj = new Parse.Object('BeforeDeleteTest'); + obj.set('foo', 'bar'); + obj.save().then(function() { + return obj.destroy(); + }).then(function() { + var objAgain = new Parse.Object('BeforeDeleteTest', obj.id); + return objAgain.fetch().then(fail, done); + }, function(error) { + fail(error); + done(); + }); + }); + + it('test save triggers get user', function(done) { + Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) { + if (req.user && req.user.id) { + res.success(); + } else { + res.error('No user present on request object for beforeSave.'); + } + }); + + Parse.Cloud.afterSave('SaveTriggerUser', function(req) { + if (!req.user || !req.user.id) { + console.log('No user present on request object for afterSave.'); + } + }); + + var user = new Parse.User(); + user.set("password", "asdf"); + user.set("email", "asdf@example.com"); + user.set("username", "zxcv"); + user.signUp(null, { + success: function() { + var obj = new Parse.Object('SaveTriggerUser'); + obj.save().then(function() { + done(); + }, function(error) { + fail(error); + done(); + }); + } + }); + }); + + it('beforeSave change propagates through the save response', (done) => { + Parse.Cloud.beforeSave('ChangingObject', function(request, response) { + request.object.set('foo', 'baz'); + response.success(); + }); + let obj = new Parse.Object('ChangingObject'); + obj.save({ foo: 'bar' }).then((objAgain) => { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }, (e) => { + fail('Should not have failed to save.'); + done(); + }); + }); + + it('test cloud function parameter validation success', (done) => { + // Register a function with validation + Parse.Cloud.define('functionWithParameterValidation', (req, res) => { + res.success('works'); + }, (request) => { + return request.params.success === 100; + }); + + Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { + done(); + }, (e) => { + fail('Validation should not have failed.'); + done(); + }); + }); +}); diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 23fc967e0c..a556762a8f 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -14,7 +14,6 @@ describe("Cloud Code Logger", () => { }); Parse.Cloud.run('loggerTest').then(() => { - Parse.Cloud._removeHook('Functions', 'logTest'); return logController.getLogs({from: Date.now() - 500, size: 1000}); }).then((res) => { expect(res.length).not.toBe(0); @@ -42,7 +41,6 @@ describe("Cloud Code Logger", () => { let obj = new Parse.Object('MyObject'); obj.save().then(() => { - Parse.Cloud._removeHook('Triggers', 'beforeSave', 'MyObject'); return logController.getLogs({from: Date.now() - 500, size: 1000}) }).then((res) => { expect(res.length).not.toBe(0); diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 79364b0ce4..ac25793b8f 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -1182,7 +1182,6 @@ describe('Parse.ACL', () => { done(); }, error => { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 5f44dca79f..21603a0956 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -164,67 +164,6 @@ describe('miscellaneous', function() { }); }); - it('test cloud function', function(done) { - Parse.Cloud.run('hello', {}, function(result) { - expect(result).toEqual('Hello world!'); - done(); - }); - }); - - it('basic beforeSave rejection', function(done) { - var obj = new Parse.Object('BeforeSaveFail'); - obj.set('foo', 'bar'); - obj.save().then(() => { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, () => { - done(); - }) - }); - - it('basic beforeSave rejection via promise', function(done) { - var obj = new Parse.Object('BeforeSaveFailWithPromise'); - obj.set('foo', 'bar'); - obj.save().then(function() { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, function(error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - done(); - }) - }); - - it('test beforeSave unchanged success', function(done) { - var obj = new Parse.Object('BeforeSaveUnchanged'); - obj.set('foo', 'bar'); - obj.save().then(function() { - done(); - }, function(error) { - fail(error); - done(); - }); - }); - - it('test beforeSave changed object success', function(done) { - var obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bar'); - obj.save().then(function() { - var query = new Parse.Query('BeforeSaveChanged'); - query.get(obj.id).then(function(objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }, function(error) { - fail(error); - done(); - }); - }, function(error) { - fail(error); - done(); - }); - }); - it('test beforeSave set object acl success', function(done) { var acl = new Parse.ACL({ '*': { read: true, write: false } @@ -237,7 +176,6 @@ describe('miscellaneous', function() { var obj = new Parse.Object('BeforeSaveAddACL'); obj.set('lol', true); obj.save().then(function() { - Parse.Cloud._removeHook('Triggers', 'beforeSave', 'BeforeSaveAddACL'); var query = new Parse.Query('BeforeSaveAddACL'); query.get(obj.id).then(function(objAgain) { expect(objAgain.get('lol')).toBeTruthy(); @@ -253,185 +191,6 @@ describe('miscellaneous', function() { }); }); - it('test beforeSave returns value on create and update', (done) => { - var obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bing'); - obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - obj.set('foo', 'bar'); - return obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - done(); - }) - }) - }); - - it('test afterSave ran and created an object', function(done) { - var obj = new Parse.Object('AfterSaveTest'); - obj.save(); - - setTimeout(function() { - var query = new Parse.Query('AfterSaveProof'); - query.equalTo('proof', obj.id); - query.find().then(function(results) { - expect(results.length).toEqual(1); - done(); - }, function(error) { - fail(error); - done(); - }); - }, 500); - }); - - it('test beforeSave happens on update', function(done) { - var obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bar'); - obj.save().then(function() { - obj.set('foo', 'bar'); - return obj.save(); - }).then(function() { - var query = new Parse.Query('BeforeSaveChanged'); - return query.get(obj.id).then(function(objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }); - }, function(error) { - fail(error); - done(); - }); - }); - - it('test beforeDelete failure', function(done) { - var obj = new Parse.Object('BeforeDeleteFail'); - var id; - obj.set('foo', 'bar'); - obj.save().then(() => { - id = obj.id; - return obj.destroy(); - }).then(() => { - fail('obj.destroy() should have failed, but it succeeded'); - done(); - }, (error) => { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - var objAgain = new Parse.Object('BeforeDeleteFail', {objectId: id}); - return objAgain.fetch(); - }).then((objAgain) => { - if (objAgain) { - expect(objAgain.get('foo')).toEqual('bar'); - } else { - fail("unable to fetch the object ", id); - } - done(); - }, (error) => { - // We should have been able to fetch the object again - fail(error); - }); - }); - - it('basic beforeDelete rejection via promise', function(done) { - var obj = new Parse.Object('BeforeDeleteFailWithPromise'); - obj.set('foo', 'bar'); - obj.save().then(function() { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, function(error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - done(); - }) - }); - - it('test beforeDelete success', function(done) { - var obj = new Parse.Object('BeforeDeleteTest'); - obj.set('foo', 'bar'); - obj.save().then(function() { - return obj.destroy(); - }).then(function() { - var objAgain = new Parse.Object('BeforeDeleteTest', obj.id); - return objAgain.fetch().then(fail, done); - }, function(error) { - fail(error); - done(); - }); - }); - - it('test afterDelete ran and created an object', function(done) { - var obj = new Parse.Object('AfterDeleteTest'); - obj.save().then(function() { - obj.destroy(); - }); - - setTimeout(function() { - var query = new Parse.Query('AfterDeleteProof'); - query.equalTo('proof', obj.id); - query.find().then(function(results) { - expect(results.length).toEqual(1); - done(); - }, function(error) { - fail(error); - done(); - }); - }, 500); - }); - - it('test save triggers get user', function(done) { - var user = new Parse.User(); - user.set("password", "asdf"); - user.set("email", "asdf@example.com"); - user.set("username", "zxcv"); - user.signUp(null, { - success: function() { - var obj = new Parse.Object('SaveTriggerUser'); - obj.save().then(function() { - done(); - }, function(error) { - fail(error); - done(); - }); - } - }); - }); - - it('test cloud function return types', function(done) { - Parse.Cloud.run('foo').then((result) => { - expect(result.object instanceof Parse.Object).toBeTruthy(); - if (!result.object) { - fail("Unable to run foo"); - done(); - return; - } - expect(result.object.className).toEqual('Foo'); - expect(result.object.get('x')).toEqual(2); - var bar = result.object.get('relation'); - expect(bar instanceof Parse.Object).toBeTruthy(); - expect(bar.className).toEqual('Bar'); - expect(bar.get('x')).toEqual(3); - expect(Array.isArray(result.array)).toEqual(true); - expect(result.array[0] instanceof Parse.Object).toBeTruthy(); - expect(result.array[0].get('x')).toEqual(2); - done(); - }); - }); - - it('test cloud function should echo keys', function(done) { - Parse.Cloud.run('echoKeys').then((result) => { - expect(result.applicationId).toEqual(Parse.applicationId); - expect(result.masterKey).toEqual(Parse.masterKey); - expect(result.javascriptKey).toEqual(Parse.javascriptKey); - done(); - }); - }); - - it('should properly create an object in before save', (done) => { - Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => { - expect(res.get('foo')).toEqual('baz'); - done(); - }); - }) - it('test rest_create_app', function(done) { var appId; Parse._request('POST', 'rest_create_app').then((res) => { @@ -456,17 +215,6 @@ describe('miscellaneous', function() { }); describe('beforeSave', () => { - beforeEach(done => { - // Make sure the required mock for all tests is unset. - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - done(); - }); - afterEach(done => { - // Make sure the required mock for all tests is unset. - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - done(); - }); - it('object is set on create and update', done => { let triggerTime = 0; // Register a mock beforeSave hook @@ -511,45 +259,6 @@ describe('miscellaneous', function() { }); }); - it('dirtyKeys are set on update', done => { - let triggerTime = 0; - // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', (req, res) => { - var object = req.object; - expect(object instanceof Parse.Object).toBeTruthy(); - expect(object.get('fooAgain')).toEqual('barAgain'); - if (triggerTime == 0) { - // Create - expect(object.get('foo')).toEqual('bar'); - } else if (triggerTime == 1) { - // Update - expect(object.dirtyKeys()).toEqual(['foo']); - expect(object.dirty('foo')).toBeTruthy(); - expect(object.get('foo')).toEqual('baz'); - } else { - res.error(); - } - triggerTime++; - res.success(); - }); - - let obj = new Parse.Object('GameScore'); - obj.set('foo', 'bar'); - obj.set('fooAgain', 'barAgain'); - obj.save().then(() => { - // We only update foo - obj.set('foo', 'baz'); - return obj.save(); - }).then(() => { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, function(error) { - fail(error); - done(); - }); - }); - it('original object is set on update', done => { let triggerTime = 0; // Register a mock beforeSave hook @@ -673,7 +382,6 @@ describe('miscellaneous', function() { return obj.save(); }).then((obj) => { expect(obj.get('point').id).toEqual(pointId); - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }) }); @@ -711,8 +419,6 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock beforeSave - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, function(error) { fail(error); @@ -764,8 +470,6 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, function(error) { console.error(error); @@ -814,8 +518,6 @@ describe('miscellaneous', function() { }).then(function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, function(error) { console.error(error); @@ -854,8 +556,6 @@ describe('miscellaneous', function() { }).then(() => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, error => { console.error(error); @@ -896,8 +596,6 @@ describe('miscellaneous', function() { }).then(() => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); done(); }, error => { console.error(error); @@ -938,8 +636,6 @@ describe('miscellaneous', function() { }).then(() => { // Make sure the checking has been triggered expect(triggerTime).toBe(2); - // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, error => { console.error(error); @@ -999,12 +695,10 @@ describe('miscellaneous', function() { }); Parse.Cloud.run('willFail').then((s) => { fail('Should not have succeeded.'); - Parse.Cloud._removeHook("Functions", "willFail"); done(); }, (e) => { expect(e.code).toEqual(141); expect(e.message).toEqual('noway'); - Parse.Cloud._removeHook("Functions", "willFail"); done(); }); }); @@ -1036,9 +730,6 @@ describe('miscellaneous', function() { }, (error, response, body) => { expect(error).toBe(null); expect(triggerTime).toEqual(2); - - Parse.Cloud._removeHook("Triggers", "beforeSave", "GameScore"); - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }); }); @@ -1075,9 +766,6 @@ describe('miscellaneous', function() { }, (error, response, body) => { expect(error).toBe(null); expect(triggerTime).toEqual(2); - - Parse.Cloud._removeHook("Triggers", "beforeDelete", "GameScore"); - Parse.Cloud._removeHook("Triggers", "afterDelete", "GameScore"); done(); }); }); @@ -1107,24 +795,6 @@ describe('miscellaneous', function() { // Make sure query string params override body params expect(res.other).toEqual('2'); expect(res.foo).toEqual("bar"); - Parse.Cloud._removeHook("Functions",'echoParams'); - done(); - }); - }); - - it('test cloud function parameter validation success', (done) => { - // Register a function with validation - Parse.Cloud.define('functionWithParameterValidation', (req, res) => { - res.success('works'); - }, (request) => { - return request.params.success === 100; - }); - - Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then((s) => { - Parse.Cloud._removeHook("Functions", "functionWithParameterValidation"); - done(); - }, (e) => { - fail('Validation should not have failed.'); done(); }); }); @@ -1139,7 +809,6 @@ describe('miscellaneous', function() { Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then((s) => { fail('Validation should not have succeeded'); - Parse.Cloud._removeHook("Functions", "functionWithParameterValidationFailure"); done(); }, (e) => { expect(e.code).toEqual(141); @@ -1156,7 +825,6 @@ describe('miscellaneous', function() { Parse.Cloud.run('func', {nullParam: null}) .then(() => { - Parse.Cloud._removeHook('Functions', 'func'); done() }, e => { fail('cloud code call failed'); @@ -1243,23 +911,6 @@ describe('miscellaneous', function() { }); }); - it('beforeSave change propagates through the save response', (done) => { - Parse.Cloud.beforeSave('ChangingObject', function(request, response) { - request.object.set('foo', 'baz'); - response.success(); - }); - let obj = new Parse.Object('ChangingObject'); - obj.save({ foo: 'bar' }).then((objAgain) => { - expect(objAgain.get('foo')).toEqual('baz'); - Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject"); - done(); - }, (e) => { - Parse.Cloud._removeHook("Triggers", "beforeSave", "ChangingObject"); - fail('Should not have failed to save.'); - done(); - }); - }); - it('dedupes an installation properly and returns updatedAt', (done) => { let headers = { 'Content-Type': 'application/json', diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index 8ff6c6c37b..6b79743bb3 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -696,20 +696,16 @@ describe('Parse.Relation testing', () => { admins.first({ useMasterKey: true }) .then(user => { if (user) { - Parse.Cloud._removeHook('Functions', 'isAdmin'); done(); } else { - Parse.Cloud._removeHook('Functions', 'isAdmin'); fail('Should have found admin user, found nothing instead'); done(); } }, error => { - Parse.Cloud._removeHook('Functions', 'isAdmin'); fail('User not admin'); done(); }) }, error => { - Parse.Cloud._removeHook('Functions', 'isAdmin'); fail('Should have found admin user, errored instead'); fail(error); done(); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index ce9763af89..f333a714e1 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1156,12 +1156,10 @@ describe('Parse.User testing', () => { Parse.User._logInWith("facebook", { success: function(innerModel) { - Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); done(); }, error: function(model, error) { ok(undefined, error); - Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); done(); } }); @@ -1584,8 +1582,6 @@ describe('Parse.User testing', () => { Parse.User._registerAuthenticationProvider(provider); Parse.User._logInWith("facebook", { success: function(model) { - Parse.Cloud._removeHook('Triggers', 'beforeSave', Parse.User.className); - Parse.Cloud._removeHook('Triggers', 'afterSave', Parse.User.className); done(); } }); @@ -2213,11 +2209,9 @@ describe('Parse.User testing', () => { }).then((user) => { expect(typeof user).toEqual('object'); expect(user.authData).toBeUndefined(); - Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User'); done(); }).catch((err) => { fail('no request should fail: ' + JSON.stringify(err)); - Parse.Cloud._removeHook('Triggers', 'beforeSave', '_User'); done(); }); }); @@ -2237,7 +2231,6 @@ describe('Parse.User testing', () => { user.set('hello', 'world'); return user.save(); }).then(() => { - Parse.Cloud._removeHook('Triggers', 'afterSave', '_User'); done(); }); }); @@ -2395,7 +2388,6 @@ describe('Parse.User testing', () => { serverURL: 'http://localhost:8378/1', appId: 'test', masterKey: 'test', - cloud: './spec/cloud/main.js', revokeSessionOnPasswordReset: false, }) request.post({ diff --git a/spec/cloud/cloudCodeAbsoluteFile.js b/spec/cloud/cloudCodeAbsoluteFile.js new file mode 100644 index 0000000000..f5fcf2b856 --- /dev/null +++ b/spec/cloud/cloudCodeAbsoluteFile.js @@ -0,0 +1,3 @@ +Parse.Cloud.define('cloudCodeInFile', (req, res) => { + res.success('It is possible to define cloud code in a file.'); +}); diff --git a/spec/cloud/cloudCodeRelativeFile.js b/spec/cloud/cloudCodeRelativeFile.js new file mode 100644 index 0000000000..f5fcf2b856 --- /dev/null +++ b/spec/cloud/cloudCodeRelativeFile.js @@ -0,0 +1,3 @@ +Parse.Cloud.define('cloudCodeInFile', (req, res) => { + res.success('It is possible to define cloud code in a file.'); +}); diff --git a/spec/cloud/main.js b/spec/cloud/main.js deleted file mode 100644 index 0785c0a624..0000000000 --- a/spec/cloud/main.js +++ /dev/null @@ -1,117 +0,0 @@ -Parse.Cloud.define('hello', function(req, res) { - res.success('Hello world!'); -}); - -Parse.Cloud.beforeSave('BeforeSaveFail', function(req, res) { - res.error('You shall not pass!'); -}); - -Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function (req, res) { - var query = new Parse.Query('Yolo'); - query.find().then(() => { - res.error('Nope'); - }, () => { - res.success(); - }); -}); - -Parse.Cloud.beforeSave('BeforeSaveUnchanged', function(req, res) { - res.success(); -}); - -Parse.Cloud.beforeSave('BeforeSaveChanged', function(req, res) { - req.object.set('foo', 'baz'); - res.success(); -}); - -Parse.Cloud.afterSave('AfterSaveTest', function(req) { - var obj = new Parse.Object('AfterSaveProof'); - obj.set('proof', req.object.id); - obj.save(); -}); - -Parse.Cloud.beforeDelete('BeforeDeleteFail', function(req, res) { - res.error('Nope'); -}); - -Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function (req, res) { - var query = new Parse.Query('Yolo'); - query.find().then(() => { - res.error('Nope'); - }, () => { - res.success(); - }); -}); - -Parse.Cloud.beforeDelete('BeforeDeleteTest', function(req, res) { - res.success(); -}); - -Parse.Cloud.afterDelete('AfterDeleteTest', function(req) { - var obj = new Parse.Object('AfterDeleteProof'); - obj.set('proof', req.object.id); - obj.save(); -}); - -Parse.Cloud.beforeSave('SaveTriggerUser', function(req, res) { - if (req.user && req.user.id) { - res.success(); - } else { - res.error('No user present on request object for beforeSave.'); - } -}); - -Parse.Cloud.afterSave('SaveTriggerUser', function(req) { - if (!req.user || !req.user.id) { - console.log('No user present on request object for afterSave.'); - } -}); - -Parse.Cloud.define('foo', function(req, res) { - res.success({ - object: { - __type: 'Object', - className: 'Foo', - objectId: '123', - x: 2, - relation: { - __type: 'Object', - className: 'Bar', - objectId: '234', - x: 3 - } - }, - array: [{ - __type: 'Object', - className: 'Bar', - objectId: '345', - x: 2 - }], - a: 2 - }); -}); - -Parse.Cloud.define('bar', function(req, res) { - res.error('baz'); -}); - -Parse.Cloud.define('requiredParameterCheck', function(req, res) { - res.success(); -}, function(params) { - return params.name; -}); - -Parse.Cloud.define('echoKeys', function(req, res){ - return res.success({ - applicationId: Parse.applicationId, - masterKey: Parse.masterKey, - javascriptKey: Parse.javascriptKey - }) -}); - -Parse.Cloud.define('createBeforeSaveChangedObject', function(req, res){ - var obj = new Parse.Object('BeforeSaveChanged'); - obj.save().then(() => { - res.success(obj); - }) -}) diff --git a/spec/helper.js b/spec/helper.js index aa63ff0f4d..049c9664fa 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -12,13 +12,11 @@ var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); var databaseURI = process.env.DATABASE_URI; -var cloudMain = process.env.CLOUD_CODE_MAIN || './spec/cloud/main.js'; var port = 8378; // Default server configuration for tests. var defaultConfiguration = { databaseURI: databaseURI, - cloud: cloudMain, serverURL: 'http://localhost:' + port + '/1', appId: 'test', javascriptKey: 'test', @@ -94,6 +92,7 @@ var mongoAdapter = new MongoStorageAdapter({ }) afterEach(function(done) { + Parse.Cloud._removeAllHooks(); mongoAdapter.getAllSchemas() .then(allSchemas => { allSchemas.forEach((schema) => { diff --git a/spec/index.spec.js b/spec/index.spec.js index f76e809ed4..6ea648423a 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -12,7 +12,7 @@ describe('server', () => { expect(setServerConfiguration.bind(undefined, { appId: 'myId', masterKey: 'mk' })).toThrow('You must provide a serverURL!'); done(); }); - + it('support http basic authentication with masterkey', done => { request.get({ url: 'http://localhost:8378/1/classes/TestObject', @@ -24,7 +24,7 @@ describe('server', () => { done(); }); }); - + it('support http basic authentication with javascriptKey', done => { request.get({ url: 'http://localhost:8378/1/classes/TestObject', @@ -199,26 +199,6 @@ describe('server', () => { }) }); - it('can load absolute cloud code file', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - cloud: __dirname + '/cloud/main.js' - }); - done(); - }); - - it('can load relative cloud code file', done => { - setServerConfiguration({ - serverURL: 'http://localhost:8378/1', - appId: 'test', - masterKey: 'test', - cloud: './spec/cloud/main.js' - }); - done(); - }); - it('can create a parse-server', done => { var parseServer = new ParseServer.default({ appId: "aTestApp", diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index e1b9ec3ab2..ca323f0ac5 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -1,4 +1,4 @@ -import { Parse } from 'parse/node'; +import { Parse } from 'parse/node'; import * as triggers from '../triggers'; function validateClassNameForTriggers(className) { @@ -40,12 +40,16 @@ ParseCloud.afterDelete = function(parseClass, handler) { var className = getClassName(parseClass); triggers.addTrigger(triggers.Types.afterDelete, className, handler, Parse.applicationId); }; - + ParseCloud._removeHook = function(category, name, type, applicationId) { applicationId = applicationId || Parse.applicationId; triggers._unregister(applicationId, category, name, type); }; +ParseCloud._removeAllHooks = () => { + triggers._unregisterAll(); +} + ParseCloud.httpRequest = require("./httpRequest"); module.exports = ParseCloud; diff --git a/src/triggers.js b/src/triggers.js index c7c9ba2c1c..0827de7a13 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,6 @@ // triggers.js -import Parse from 'parse/node'; -import AppCache from './cache'; +import Parse from 'parse/node'; +import AppCache from './cache'; export const Types = { beforeSave: 'beforeSave', @@ -49,15 +49,19 @@ export function removeTrigger(type, className, applicationId) { delete _triggerStore[applicationId].Triggers[type][className] } -export function _unregister(a,b,c,d) { - if (d) { - removeTrigger(c,d,a); - delete _triggerStore[a][b][c][d]; +export function _unregister(appId,category,className,type) { + if (type) { + removeTrigger(className,type,appId); + delete _triggerStore[appId][category][className][type]; } else { - delete _triggerStore[a][b][c]; + delete _triggerStore[appId][category][className]; } } +export function _unregisterAll() { + Object.keys(_triggerStore).forEach(appId => delete _triggerStore[appId]); +} + export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { throw "Missing ApplicationID"; From 9bad87905fcdc21e20aace4cea8174bb5b4371a6 Mon Sep 17 00:00:00 2001 From: Tyler Brock Date: Fri, 20 May 2016 14:18:41 -0700 Subject: [PATCH 26/63] Add MongoDB 3.2 to test matrix (#1842) * Add MongoDB 3.2 to test matrix - Updated mongodb-runner to support specifying storage engine - Specifying mmapv1 explictly because of new 3.2 default * Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 3 seconds * Use fixed mongodb-runner * Increase jasmine.DEFAULT_TIMEOUT_INTERVAL to 15 seconds * Update to mongodb-runner 3.3.2 upon release --- .travis.yml | 1 + package.json | 4 ++-- spec/helper.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 68052a1f5b..e6248cfd7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ env: matrix: - MONGODB_VERSION=2.6.11 - MONGODB_VERSION=3.0.8 + - MONGODB_VERSION=3.2.6 branches: only: - master diff --git a/package.json b/package.json index ac8bc7e037..c8f36227a2 100644 --- a/package.json +++ b/package.json @@ -59,13 +59,13 @@ "deep-diff": "^0.3.3", "gaze": "^1.0.0", "jasmine": "^2.3.2", - "mongodb-runner": "3.2.2", + "mongodb-runner": "^3.3.2", "nodemon": "^1.8.1" }, "scripts": { "dev": "npm run build && node bin/dev", "build": "./node_modules/.bin/babel src/ -d lib/", - "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./node_modules/.bin/mongodb-runner start", + "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 ./node_modules/.bin/mongodb-runner start", "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", "test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest", "posttest": "./node_modules/.bin/mongodb-runner stop", diff --git a/spec/helper.js b/spec/helper.js index 049c9664fa..5d804d12e9 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,6 +1,6 @@ // Sets up a Parse API server for testing. -jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; +jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; var cache = require('../src/cache').default; var DatabaseAdapter = require('../src/DatabaseAdapter'); From fab8cfdfc736f41396c33e424fe08f67eff346e2 Mon Sep 17 00:00:00 2001 From: Jeremy Pease Date: Fri, 20 May 2016 21:15:47 -0400 Subject: [PATCH 27/63] Add additional default fields to _Installation class (#1852) Fields are appVersion, appName, appIdentifier, and parseVersion. These fields are sent by Android and iOS SDKs. --- spec/MongoSchemaCollectionAdapter.spec.js | 18 ++++++++++++++++++ spec/Schema.spec.js | 4 ++++ src/Controllers/SchemaController.js | 12 ++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/spec/MongoSchemaCollectionAdapter.spec.js b/spec/MongoSchemaCollectionAdapter.spec.js index 00a5b1900c..ba22c1fab3 100644 --- a/spec/MongoSchemaCollectionAdapter.spec.js +++ b/spec/MongoSchemaCollectionAdapter.spec.js @@ -28,6 +28,15 @@ describe('MongoSchemaCollection', () => { "deviceType":"string", "channels":"array", "user":"*_User", + "pushType":"string", + "GCMSenderId":"string", + "timeZone":"string", + "localeIdentifier":"string", + "badge":"number", + "appVersion":"string", + "appName":"string", + "appIdentifier":"string", + "parseVersion":"string", })).toEqual({ className: '_Installation', fields: { @@ -36,6 +45,15 @@ describe('MongoSchemaCollection', () => { deviceType: { type: 'String' }, channels: { type: 'Array' }, user: { type: 'Pointer', targetClass: '_User' }, + pushType: { type: 'String' }, + GCMSenderId: { type: 'String' }, + timeZone: { type: 'String' }, + localeIdentifier: { type: 'String' }, + badge: { type: 'Number' }, + appVersion: { type: 'String' }, + appName: { type: 'String' }, + appIdentifier: { type: 'String' }, + parseVersion: { type: 'String' }, ACL: { type: 'ACL' }, createdAt: { type: 'Date' }, updatedAt: { type: 'Date' }, diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index d824d2b048..e30ed6da49 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -453,6 +453,10 @@ describe('SchemaController', () => { timeZone: { type: 'String' }, localeIdentifier: { type: 'String' }, badge: { type: 'Number' }, + appVersion: { type: 'String' }, + appName: { type: 'String' }, + appIdentifier: { type: 'String' }, + parseVersion: { type: 'String' }, }, classLevelPermissions: { find: { '*': true }, diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 8c78d1cf57..b3fdc7bb77 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -33,7 +33,7 @@ const defaultColumns = Object.freeze({ "email": {type:'String'}, "emailVerified": {type:'Boolean'}, }, - // The additional default columns for the _User collection (in addition to DefaultCols) + // The additional default columns for the _Installation collection (in addition to DefaultCols) _Installation: { "installationId": {type:'String'}, "deviceToken": {type:'String'}, @@ -43,15 +43,19 @@ const defaultColumns = Object.freeze({ "GCMSenderId": {type:'String'}, "timeZone": {type:'String'}, "localeIdentifier": {type:'String'}, - "badge": {type:'Number'} + "badge": {type:'Number'}, + "appVersion": {type:'String'}, + "appName": {type:'String'}, + "appIdentifier": {type:'String'}, + "parseVersion": {type:'String'}, }, - // The additional default columns for the _User collection (in addition to DefaultCols) + // The additional default columns for the _Role collection (in addition to DefaultCols) _Role: { "name": {type:'String'}, "users": {type:'Relation', targetClass:'_User'}, "roles": {type:'Relation', targetClass:'_Role'} }, - // The additional default columns for the _User collection (in addition to DefaultCols) + // The additional default columns for the _Session collection (in addition to DefaultCols) _Session: { "restricted": {type:'Boolean'}, "user": {type:'Pointer', targetClass:'_User'}, From eefa2ccac7cf5248f5be9396b124b2772cc4a07c Mon Sep 17 00:00:00 2001 From: Drew Date: Sun, 22 May 2016 06:33:59 -0700 Subject: [PATCH 28/63] Handle "bytes" type in DB. Fixes #1821. (#1866) --- src/Adapters/Storage/Mongo/MongoSchemaCollection.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index a51edc2a65..11db8a377e 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -25,6 +25,7 @@ function mongoFieldToParseSchemaField(type) { case 'array': return {type: 'Array'}; case 'geopoint': return {type: 'GeoPoint'}; case 'file': return {type: 'File'}; + case 'bytes': return {type: 'Bytes'}; } } From 392102eb97a8b8a183e635453a14affb7cb32166 Mon Sep 17 00:00:00 2001 From: Drew Date: Sun, 22 May 2016 09:59:36 -0700 Subject: [PATCH 29/63] Cache users by objectID, and clear cache when updated via master key (fixes #1836) (#1844) * Cache users by objectID, and clear cache when updated via master key * Go back to caching by session token. Clear out cache by querying _Session when user is modified with Master Key (ew, hopefully that can be improved later) * Fix issue with user updates from different sessions causing stale reads * Tests aren't transpiled... * Still not transpiled --- spec/CloudCode.spec.js | 90 +++++++++++++++++++++++++++++ src/Adapters/Cache/InMemoryCache.js | 1 - src/Auth.js | 1 - src/RestWrite.js | 22 ++++--- src/Routers/UsersRouter.js | 1 + src/middlewares.js | 20 +++---- 6 files changed, 115 insertions(+), 20 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 2218a25706..8c2802d3e5 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1,5 +1,7 @@ "use strict" const Parse = require("parse/node"); +const request = require('request'); +const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter; describe('Cloud Code', () => { it('can load absolute cloud code file', done => { @@ -467,4 +469,92 @@ describe('Cloud Code', () => { done(); }); }); + + it('doesnt receive stale user in cloud code functions after user has been updated with master key (regression test for #1836)', done => { + Parse.Cloud.define('testQuery', function(request, response) { + response.success(request.user.get('data')); + }); + + Parse.User.signUp('user', 'pass') + .then(user => { + user.set('data', 'AAA'); + return user.save(); + }) + .then(() => Parse.Cloud.run('testQuery')) + .then(result => { + expect(result).toEqual('AAA'); + Parse.User.current().set('data', 'BBB'); + return Parse.User.current().save(null, {useMasterKey: true}); + }) + .then(() => Parse.Cloud.run('testQuery')) + .then(result => { + expect(result).toEqual('BBB'); + done(); + }); + }); + + it('clears out the user cache for all sessions when the user is changed', done => { + const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 }); + setServerConfiguration(Object.assign({}, defaultConfiguration, { cacheAdapter: cacheAdapter })); + Parse.Cloud.define('checkStaleUser', (request, response) => { + response.success(request.user.get('data')); + }); + + let user = new Parse.User(); + user.set('username', 'test'); + user.set('password', 'moon-y'); + user.set('data', 'first data'); + user.signUp() + .then(user => { + let session1 = user.getSessionToken(); + request.get({ + url: 'http://localhost:8378/1/login?username=test&password=moon-y', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + }, (error, response, body) => { + let session2 = body.sessionToken; + + //Ensure both session tokens are in the cache + Parse.Cloud.run('checkStaleUser') + .then(() => { + request.post({ + url: 'http://localhost:8378/1/functions/checkStaleUser', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': session2, + } + }, (error, response, body) => { + Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)]) + .then(cachedVals => { + expect(cachedVals[0].objectId).toEqual(user.id); + expect(cachedVals[1].objectId).toEqual(user.id); + + //Change with session 1 and then read with session 2. + user.set('data', 'second data'); + user.save() + .then(() => { + request.post({ + url: 'http://localhost:8378/1/functions/checkStaleUser', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': session2, + } + }, (error, response, body) => { + expect(body.result).toEqual('second data'); + done(); + }) + }); + }); + }); + }); + }); + }); + }); }); diff --git a/src/Adapters/Cache/InMemoryCache.js b/src/Adapters/Cache/InMemoryCache.js index 37eeb43b02..2d44292a0a 100644 --- a/src/Adapters/Cache/InMemoryCache.js +++ b/src/Adapters/Cache/InMemoryCache.js @@ -53,7 +53,6 @@ export class InMemoryCache { if (record.timeout) { clearTimeout(record.timeout); } - delete this.cache[key]; } diff --git a/src/Auth.js b/src/Auth.js index 8f21567903..634a839b9a 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -72,7 +72,6 @@ var getAuthForSessionToken = function({ config, sessionToken, installationId } = obj['className'] = '_User'; obj['sessionToken'] = sessionToken; config.cacheController.user.put(sessionToken, obj); - let userObject = Parse.Object.fromJSON(obj); return new Auth({config, isMaster: false, installationId, user: userObject}); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index f6e758f6bf..be460d4c48 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -11,6 +11,7 @@ var cryptoUtils = require('./cryptoUtils'); var passwordCrypto = require('./password'); var Parse = require('parse/node'); var triggers = require('./triggers'); +import RestQuery from './RestQuery'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -318,10 +319,17 @@ RestWrite.prototype.transformUser = function() { var promise = Promise.resolve(); - // If we're updating a _User object, clear the user cache for the session - if (this.query && this.auth.user && this.auth.user.getSessionToken()) { - let cacheAdapter = this.config.cacheController; - cacheAdapter.user.del(this.auth.user.getSessionToken()); + if (this.query) { + // If we're updating a _User object, we need to clear out the cache for that user. Find all their + // session tokens, and remove them from the cache. + promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: { + __type: "Pointer", + className: "_User", + objectId: this.objectId(), + }}).execute() + .then(results => { + results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken)); + }); } return promise.then(() => { @@ -414,8 +422,7 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() { if (this.response && this.response.response) { this.response.response.sessionToken = token; } - var create = new RestWrite(this.config, Auth.master(this.config), - '_Session', null, sessionData); + var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); return create.execute(); } @@ -482,8 +489,7 @@ RestWrite.prototype.handleSession = function() { } sessionData[key] = this.data[key]; } - var create = new RestWrite(this.config, Auth.master(this.config), - '_Session', null, sessionData); + var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); return create.execute().then((results) => { if (!results.response) { throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index adba752f83..a5e6299c58 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -85,6 +85,7 @@ export class UsersRouter extends ClassesRouter { user = results[0]; return passwordCrypto.compare(req.body.password, user.password); }).then((correct) => { + if (!correct) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); } diff --git a/src/middlewares.js b/src/middlewares.js index d534215453..e46eb62557 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -27,9 +27,9 @@ function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key') }; - + var basicAuth = httpAuth(req); - + if (basicAuth) { info.appId = basicAuth.appId info.masterKey = basicAuth.masterKey || info.masterKey; @@ -156,24 +156,24 @@ function httpAuth(req) { if (!(req.req || req).headers.authorization) return ; - var header = (req.req || req).headers.authorization; - var appId, masterKey, javascriptKey; + var header = (req.req || req).headers.authorization; + var appId, masterKey, javascriptKey; // parse header var authPrefix = 'basic '; - + var match = header.toLowerCase().indexOf(authPrefix); - + if (match == 0) { var encodedAuth = header.substring(authPrefix.length, header.length); var credentials = decodeBase64(encodedAuth).split(':'); - + if (credentials.length == 2) { appId = credentials[0]; var key = credentials[1]; - + var jsKeyPrefix = 'javascript-key='; - + var matchKey = key.indexOf(jsKeyPrefix) if (matchKey == 0) { javascriptKey = key.substring(jsKeyPrefix.length, key.length); @@ -183,7 +183,7 @@ function httpAuth(req) { } } } - + return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey}; } From e93a1ad7fc7bd49c8289e92bc55c1c6ff65e457c Mon Sep 17 00:00:00 2001 From: Drew Date: Mon, 23 May 2016 09:15:32 -0700 Subject: [PATCH 30/63] Pin mongodb package Fixes #1855 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8f36227a2..c767ca7177 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lru-cache": "^4.0.0", "mailgun-js": "^0.7.7", "mime": "^1.3.4", - "mongodb": "~2.1.0", + "mongodb": "2.1.18", "multer": "^1.1.0", "parse": "^1.8.0", "parse-server-fs-adapter": "^1.0.0", From 8b43d2644fc999c575a36de3ec1d13d5343378b6 Mon Sep 17 00:00:00 2001 From: Steven Shipton Date: Mon, 23 May 2016 19:49:43 +0100 Subject: [PATCH 31/63] Change existsSync to exception handler (#1879) --- src/logger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index d5b81e9ecf..4bcd684729 100644 --- a/src/logger.js +++ b/src/logger.js @@ -46,8 +46,10 @@ export function configureLogger({logsFolder, level = winston.level}) { if (!path.isAbsolute(logsFolder)) { logsFolder = path.resolve(process.cwd(), logsFolder); } - if (!fs.existsSync(logsFolder)) { + try { fs.mkdirSync(logsFolder); + } catch (exception) { + // Ignore, assume the folder already exists } currentLogsFolder = logsFolder; From 88fa7bad92b2d6e9064e5fd92ca47b3ab310afa0 Mon Sep 17 00:00:00 2001 From: Adrian Brink Date: Tue, 24 May 2016 00:49:06 +0200 Subject: [PATCH 32/63] Update README.md (#1886) Add link to the parse-server-module repo for easier reference. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 586cc9d3b9..de489ec051 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,9 @@ $ PORT=8080 parse-server --appId APPLICATION_ID --masterKey MASTER_KEY For the full list of configurable environment variables, run `parse-server --help`. +### Available Adapters +[Parse Server Modules (Adapters)](https://github.com/parse-server-modules) + ### Configuring File Adapters Parse Server allows developers to choose from several options when hosting files: From 614e1ac8e55c1bd35d21477ecd78cdd3e5856ead Mon Sep 17 00:00:00 2001 From: Drew Date: Mon, 23 May 2016 16:31:51 -0700 Subject: [PATCH 33/63] Move query logic into mongo (#1885) * Move Parse Server logic into Parse Server and out of MongoAdapter * Move untransforming up one level * Make find() in MongoStorageAdapter * Put nested object untransforming into it's own function * Simplfy nested untransform * Don't mess with inner object keys called _auth_data_* * Prevent untransforming inner object keys named _p_* * Fix inner keys named _rperm, _wperm * Fix bugs with inner objects behaving strange when other fields have same name as key in specific circumstances * remove params from untransform nested object * Revert changes to find --- spec/MongoTransform.spec.js | 20 +-- spec/ParseAPI.spec.js | 50 ++++++ .../Storage/Mongo/MongoStorageAdapter.js | 8 + src/Adapters/Storage/Mongo/MongoTransform.js | 142 ++++++------------ src/Controllers/DatabaseController.js | 14 +- src/RestQuery.js | 68 ++++++++- 6 files changed, 181 insertions(+), 121 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 905b7647c1..ca142134f0 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -121,10 +121,10 @@ describe('transformWhere', () => { }); }); -describe('untransformObject', () => { +describe('mongoObjectToParseObject', () => { it('built-in timestamps', (done) => { var input = {createdAt: new Date(), updatedAt: new Date()}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); @@ -132,7 +132,7 @@ describe('untransformObject', () => { it('pointer', (done) => { var input = {_p_userPointer: '_User$123'}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.userPointer).toEqual('object'); expect(output.userPointer).toEqual( {__type: 'Pointer', className: '_User', objectId: '123'} @@ -142,14 +142,14 @@ describe('untransformObject', () => { it('null pointer', (done) => { var input = {_p_userPointer: null}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(output.userPointer).toBeUndefined(); done(); }); it('file', (done) => { var input = {picture: 'pic.jpg'}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.picture).toEqual('object'); expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'}); done(); @@ -157,7 +157,7 @@ describe('untransformObject', () => { it('geopoint', (done) => { var input = {location: [180, -180]}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.location).toEqual('object'); expect(output.location).toEqual( {__type: 'GeoPoint', longitude: 180, latitude: -180} @@ -167,7 +167,7 @@ describe('untransformObject', () => { it('nested array', (done) => { var input = {arr: [{_testKey: 'testValue' }]}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(Array.isArray(output.arr)).toEqual(true); expect(output.arr).toEqual([{ _testKey: 'testValue'}]); done(); @@ -185,7 +185,7 @@ describe('untransformObject', () => { }, regularKey: "some data", }]} - let output = transform.untransformObject(dummySchema, null, input); + let output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(dd(output, input)).toEqual(undefined); done(); }); @@ -253,7 +253,7 @@ describe('transform schema key changes', () => { _rperm: ["*"], _wperm: ["Kevin"] }; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.ACL).toEqual('object'); expect(output._rperm).toBeUndefined(); expect(output._wperm).toBeUndefined(); @@ -267,7 +267,7 @@ describe('transform schema key changes', () => { long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER), double: new mongodb.Double(Number.MAX_VALUE) } - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(output.long).toBe(Number.MAX_SAFE_INTEGER); expect(output.double).toBe(Number.MAX_VALUE); done(); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 21603a0956..80fe442c1f 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1120,4 +1120,54 @@ describe('miscellaneous', function() { done(); }) }); + + it('does not change inner object key names _auth_data_something', done => { + new Parse.Object('O').save({ innerObj: {_auth_data_facebook: 7}}) + .then(object => new Parse.Query('O').get(object.id)) + .then(object => { + expect(object.get('innerObj')).toEqual({_auth_data_facebook: 7}); + done(); + }); + }); + + it('does not change inner object key names _p_somethign', done => { + new Parse.Object('O').save({ innerObj: {_p_data: 7}}) + .then(object => new Parse.Query('O').get(object.id)) + .then(object => { + expect(object.get('innerObj')).toEqual({_p_data: 7}); + done(); + }); + }); + + it('does not change inner object key names _rperm, _wperm', done => { + new Parse.Object('O').save({ innerObj: {_rperm: 7, _wperm: 8}}) + .then(object => new Parse.Query('O').get(object.id)) + .then(object => { + expect(object.get('innerObj')).toEqual({_rperm: 7, _wperm: 8}); + done(); + }); + }); + + it('does not change inner objects if the key has the same name as a geopoint field on the class, and the value is an array of length 2, or if the key has the same name as a file field on the class, and the value is a string', done => { + let file = new Parse.File('myfile.txt', { base64: 'eAo=' }); + file.save() + .then(f => { + let obj = new Parse.Object('O'); + obj.set('fileField', f); + obj.set('geoField', new Parse.GeoPoint(0, 0)); + obj.set('innerObj', { + fileField: "data", + geoField: [1,2], + }); + return obj.save(); + }) + .then(object => object.fetch()) + .then(object => { + expect(object.get('innerObj')).toEqual({ + fileField: "data", + geoField: [1,2], + }); + done(); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index e194dfe00f..b4e1f84eb1 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -197,6 +197,14 @@ export class MongoStorageAdapter { }); } + // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. + // Accepts the schemaController for legacy reasons. + find(className, query, { skip, limit, sort }, schemaController) { + return this.adaptiveCollection(className) + .then(collection => collection.find(query, { skip, limit, sort })) + .then(objects => objects.map(object => transform.mongoObjectToParseObject(schemaController, className, object))); + } + get transform() { return transform; } diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 33aa666b09..3bb008f78a 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -713,25 +713,49 @@ function transformUpdateOperator({ } } -const specialKeysForUntransform = [ - '_id', - '_hashed_password', - '_acl', - '_email_verify_token', - '_perishable_token', - '_tombstone', - '_session_token', - 'updatedAt', - '_updated_at', - 'createdAt', - '_created_at', - 'expiresAt', - '_expiresAt', -]; +const nestedMongoObjectToNestedParseObject = mongoObject => { + switch(typeof mongoObject) { + case 'string': + case 'number': + case 'boolean': + return mongoObject; + case 'undefined': + case 'symbol': + case 'function': + throw 'bad value in mongoObjectToParseObject'; + case 'object': + if (mongoObject === null) { + return null; + } + if (mongoObject instanceof Array) { + return mongoObject.map(nestedMongoObjectToNestedParseObject); + } + + if (mongoObject instanceof Date) { + return Parse._encode(mongoObject); + } + + if (mongoObject instanceof mongodb.Long) { + return mongoObject.toNumber(); + } + + if (mongoObject instanceof mongodb.Double) { + return mongoObject.value; + } + + if (BytesCoder.isValidDatabaseObject(mongoObject)) { + return BytesCoder.databaseToJSON(mongoObject); + } + + return _.mapValues(mongoObject, nestedMongoObjectToNestedParseObject); + default: + throw 'unknown js type'; + } +} // Converts from a mongo-format object to a REST-format object. // Does not strip out anything based on a lack of authentication. -function untransformObject(schema, className, mongoObject, isNestedObject = false) { +const mongoObjectToParseObject = (schema, className, mongoObject) => { switch(typeof mongoObject) { case 'string': case 'number': @@ -740,15 +764,13 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals case 'undefined': case 'symbol': case 'function': - throw 'bad value in untransformObject'; + throw 'bad value in mongoObjectToParseObject'; case 'object': if (mongoObject === null) { return null; } if (mongoObject instanceof Array) { - return mongoObject.map(arrayEntry => { - return untransformObject(schema, className, arrayEntry, true); - }); + return mongoObject.map(nestedMongoObjectToNestedParseObject); } if (mongoObject instanceof Date) { @@ -769,10 +791,6 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals var restObject = untransformACL(mongoObject); for (var key in mongoObject) { - if (isNestedObject && _.includes(specialKeysForUntransform, key)) { - restObject[key] = untransformObject(schema, className, mongoObject[key], true); - continue; - } switch(key) { case '_id': restObject['objectId'] = '' + mongoObject[key]; @@ -840,7 +858,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals objectId: objData[1] }; break; - } else if (!isNestedObject && key[0] == '_' && key != '__type') { + } else if (key[0] == '_' && key != '__type') { throw ('bad key in untransform: ' + key); } else { var expectedType = schema.getExpectedType(className, key); @@ -854,80 +872,16 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals break; } } - restObject[key] = untransformObject(schema, className, mongoObject[key], true); + restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]); } } - if (!isNestedObject) { - let relationFields = schema.getRelationFields(className); - Object.assign(restObject, relationFields); - } - return restObject; + return { ...restObject, ...schema.getRelationFields(className) }; default: throw 'unknown js type'; } } -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; - } -} - var DateCoder = { JSONToDatabase(json) { return new Date(json.iso); @@ -1021,9 +975,5 @@ module.exports = { parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, - transformSelect, - transformDontSelect, - transformInQuery, - transformNotInQuery, - untransformObject + mongoObjectToParseObject, }; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 522a2f1443..67240a9128 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -146,12 +146,8 @@ DatabaseController.prototype.validateObject = function(className, object, query, }); }; -// Like transform.untransformObject but you need to provide a className. // Filters out any data that shouldn't be on this REST-formatted object. -DatabaseController.prototype.untransformObject = function( - schema, isMaster, aclGroup, className, mongoObject) { - var object = this.transform.untransformObject(schema, className, mongoObject); - +const filterSensitiveData = (isMaster, aclGroup, className, object) => { if (className !== '_User') { return object; } @@ -705,12 +701,8 @@ DatabaseController.prototype.find = function(className, query, { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); } else { - return collection.find(mongoWhere, mongoOptions) - .then(mongoResults => { - return mongoResults.map(result => { - return this.untransformObject(schemaController, isMaster, aclGroup, className, result); - }); - }); + return this.adapter.find(className, mongoWhere, mongoOptions, schemaController) + .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); }); diff --git a/src/RestQuery.js b/src/RestQuery.js index 34324f5e0e..a1d48fa7e8 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -188,6 +188,23 @@ RestQuery.prototype.validateClientClassCreation = function() { } }; +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; + } +} + // Replaces a $inQuery clause by running the subquery, if there is an // $inQuery clause. // The $inQuery clause turns into an $in with values that are just @@ -213,12 +230,29 @@ RestQuery.prototype.replaceInQuery = function() { this.config, this.auth, inQueryValue.className, inQueryValue.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformInQuery(inQueryObject, subquery.className, response.results); + transformInQuery(inQueryObject, subquery.className, response.results); // Recurse to repeat return this.replaceInQuery(); }); }; +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; + } +} + // Replaces a $notInQuery clause by running the subquery, if there is an // $notInQuery clause. // The $notInQuery clause turns into a $nin with values that are just @@ -244,12 +278,25 @@ RestQuery.prototype.replaceNotInQuery = function() { this.config, this.auth, notInQueryValue.className, notInQueryValue.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformNotInQuery(notInQueryObject, subquery.className, response.results); + transformNotInQuery(notInQueryObject, subquery.className, response.results); // Recurse to repeat return this.replaceNotInQuery(); }); }; +const 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; + } +} + // Replaces a $select clause by running the subquery, if there is a // $select clause. // The $select clause turns into an $in with values selected out of @@ -281,12 +328,25 @@ RestQuery.prototype.replaceSelect = function() { this.config, this.auth, selectValue.query.className, selectValue.query.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformSelect(selectObject, selectValue.key, response.results); + transformSelect(selectObject, selectValue.key, response.results); // Keep replacing $select clauses return this.replaceSelect(); }) }; +const 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; + } +} + // Replaces a $dontSelect clause by running the subquery, if there is a // $dontSelect clause. // The $dontSelect clause turns into an $nin with values selected out of @@ -316,7 +376,7 @@ RestQuery.prototype.replaceDontSelect = function() { this.config, this.auth, dontSelectValue.query.className, dontSelectValue.query.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); + transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); // Keep replacing $dontSelect clauses return this.replaceDontSelect(); }) From 3a9614010366c8d243f22141f1b644c1e43ebd16 Mon Sep 17 00:00:00 2001 From: Drew Date: Mon, 23 May 2016 17:13:32 -0700 Subject: [PATCH 34/63] Fix issue with pointers getting un-hydrated when there is a beforeSave (#1884) * Add failing test and simplify RestWrite * simplify and add test stubs * Fix issue --- spec/CloudCode.spec.js | 32 ++++++++++++++++++++++++ src/RestWrite.js | 57 ++++++++++++++++++++---------------------- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 8c2802d3e5..c6bc8b25c8 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -557,4 +557,36 @@ describe('Cloud Code', () => { }); }); }); + + it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => { + Parse.Cloud.beforeSave('BeforeSaveUnchanged', (req, res) => { + res.success(); + }); + + var TestObject = Parse.Object.extend("TestObject"); + var NoBeforeSaveObject = Parse.Object.extend("NoBeforeSave"); + var BeforeSaveObject = Parse.Object.extend("BeforeSaveUnchanged"); + + var aTestObject = new TestObject(); + aTestObject.set("foo", "bar"); + aTestObject.save() + .then(aTestObject => { + var aNoBeforeSaveObj = new NoBeforeSaveObject(); + aNoBeforeSaveObj.set("aTestObject", aTestObject); + expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar"); + return aNoBeforeSaveObj.save(); + }) + .then(aNoBeforeSaveObj => { + expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar"); + + var aBeforeSaveObj = new BeforeSaveObject(); + aBeforeSaveObj.set("aTestObject", aTestObject); + expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar"); + return aBeforeSaveObj.save(); + }) + .then(aBeforeSaveObj => { + expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar"); + done(); + }); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index be460d4c48..fc40a71031 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -12,6 +12,7 @@ var passwordCrypto = require('./password'); var Parse = require('parse/node'); var triggers = require('./triggers'); import RestQuery from './RestQuery'; +import _ from 'lodash'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -165,8 +166,10 @@ RestWrite.prototype.runBeforeTrigger = function() { return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config); }).then((response) => { if (response && response.object) { + if (!_.isEqual(this.data, response.object)) { + this.storage.changedByTrigger = true; + } this.data = response.object; - this.storage['changedByTrigger'] = true; // We should delete the objectId for an update write if (this.query && this.query.objectId) { delete this.data.objectId @@ -733,19 +736,16 @@ RestWrite.prototype.runDatabaseOperation = function() { this.data.ACL[this.query.objectId] = { read: true, write: true }; } // Run an update - return this.config.database.update( - this.className, this.query, this.data, this.runOptions).then((resp) => { - resp.updatedAt = this.updatedAt; - if (this.storage['changedByTrigger']) { - resp = Object.keys(this.data).reduce((memo, key) => { - memo[key] = resp[key] || this.data[key]; - return memo; - }, resp); - } - this.response = { - response: resp - }; - }); + return this.config.database.update(this.className, this.query, this.data, this.runOptions) + .then(response => { + response.updatedAt = this.updatedAt; + if (this.storage.changedByTrigger) { + Object.keys(this.data).forEach(fieldName => { + response[fieldName] = response[fieldName] || this.data[fieldName]; + }); + } + this.response = { response }; + }); } else { // Set the default ACL for the new _User if (this.className === '_User') { @@ -762,23 +762,20 @@ RestWrite.prototype.runDatabaseOperation = function() { // Run a create return this.config.database.create(this.className, this.data, this.runOptions) - .then((resp) => { - Object.assign(resp, { - objectId: this.data.objectId, - createdAt: this.data.createdAt + .then(response => { + response.objectId = this.data.objectId; + response.createdAt = this.data.createdAt; + if (this.storage.changedByTrigger) { + Object.keys(this.data).forEach(fieldName => { + response[fieldName] = response[fieldName] || this.data[fieldName]; }); - if (this.storage['changedByTrigger']) { - resp = Object.keys(this.data).reduce((memo, key) => { - memo[key] = resp[key] || this.data[key]; - return memo; - }, resp); - } - this.response = { - status: 201, - response: resp, - location: this.location() - }; - }); + } + this.response = { + status: 201, + response, + location: this.location() + }; + }); } }; From 69d5a2f87cc3f8cbf5ac00d3030d712388b70716 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Sat, 21 May 2016 19:51:06 -0700 Subject: [PATCH 35/63] Make find() in MongoStorageAdapter --- src/Adapters/Storage/Mongo/MongoCollection.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index bf41582b19..e309281884 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -1,5 +1,6 @@ let mongodb = require('mongodb'); let Collection = mongodb.Collection; +import * as transform from './MongoTransform'; export default class MongoCollection { _mongoCollection:Collection; @@ -13,25 +14,28 @@ export default class MongoCollection { // none, then build the geoindex. // This could be improved a lot but it's not clear if that's a good // idea. Or even if this behavior is a good idea. + + // Depends on the className and schemaController because mongoObjectToParseObject does. + // TODO: break this dependency find(query, { skip, limit, sort } = {}) { return this._rawFind(query, { skip, limit, sort }) - .catch(error => { - // Check for "no geoindex" error - if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { - throw error; - } - // Figure out what key needs an index - let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; - if (!key) { - throw error; - } + .catch(error => { + // Check for "no geoindex" error + if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { + throw error; + } + // Figure out what key needs an index + let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; + if (!key) { + throw error; + } - var index = {}; - index[key] = '2d'; - return this._mongoCollection.createIndex(index) - // Retry, but just once. - .then(() => this._rawFind(query, { skip, limit, sort })); - }); + var index = {}; + index[key] = '2d'; + return this._mongoCollection.createIndex(index) + // Retry, but just once. + .then(() => this._rawFind(query, { skip, limit, sort })); + }) } _rawFind(query, { skip, limit, sort } = {}) { From 9f149e6db5be402238803ac98142e63f09a75334 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Sat, 21 May 2016 20:57:31 -0700 Subject: [PATCH 36/63] Don't mess with inner object keys called _auth_data_* --- spec/ParseAPI.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 80fe442c1f..cceb818f9d 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1121,7 +1121,7 @@ describe('miscellaneous', function() { }) }); - it('does not change inner object key names _auth_data_something', done => { + it('does not change inner object keys named _auth_data_something', done => { new Parse.Object('O').save({ innerObj: {_auth_data_facebook: 7}}) .then(object => new Parse.Query('O').get(object.id)) .then(object => { From c928dcc118aa35a3c586bd71b3ba237f337b5a7c Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Sat, 21 May 2016 21:20:53 -0700 Subject: [PATCH 37/63] Prevent untransforming inner object keys named _p_* --- spec/ParseAPI.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index cceb818f9d..f8dcc05895 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1170,4 +1170,6 @@ describe('miscellaneous', function() { done(); }); }); +======= +>>>>>>> Prevent untransforming inner object keys named _p_* }); From 74ee8613d83fde62d7c5890cca1e5fe6eb498ba4 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Sat, 21 May 2016 21:43:21 -0700 Subject: [PATCH 38/63] Fix inner keys named _rperm, _wperm --- spec/ParseAPI.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index f8dcc05895..cceb818f9d 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1170,6 +1170,4 @@ describe('miscellaneous', function() { done(); }); }); -======= ->>>>>>> Prevent untransforming inner object keys named _p_* }); From fe8160449cac0eec253715cbfe2b95c58d1d84af Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 11:54:54 -0700 Subject: [PATCH 39/63] Revert changes to find --- src/Adapters/Storage/Mongo/MongoCollection.js | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index e309281884..bf41582b19 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -1,6 +1,5 @@ let mongodb = require('mongodb'); let Collection = mongodb.Collection; -import * as transform from './MongoTransform'; export default class MongoCollection { _mongoCollection:Collection; @@ -14,28 +13,25 @@ export default class MongoCollection { // none, then build the geoindex. // This could be improved a lot but it's not clear if that's a good // idea. Or even if this behavior is a good idea. - - // Depends on the className and schemaController because mongoObjectToParseObject does. - // TODO: break this dependency find(query, { skip, limit, sort } = {}) { return this._rawFind(query, { skip, limit, sort }) - .catch(error => { - // Check for "no geoindex" error - if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { - throw error; - } - // Figure out what key needs an index - let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; - if (!key) { - throw error; - } + .catch(error => { + // Check for "no geoindex" error + if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { + throw error; + } + // Figure out what key needs an index + let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; + if (!key) { + throw error; + } - var index = {}; - index[key] = '2d'; - return this._mongoCollection.createIndex(index) - // Retry, but just once. - .then(() => this._rawFind(query, { skip, limit, sort })); - }) + var index = {}; + index[key] = '2d'; + return this._mongoCollection.createIndex(index) + // Retry, but just once. + .then(() => this._rawFind(query, { skip, limit, sort })); + }); } _rawFind(query, { skip, limit, sort } = {}) { From 474a893a227434e9dc97309b85cd10c3be54817c Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 15:54:12 -0700 Subject: [PATCH 40/63] Pass the Parse Schema into untransform --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- src/Adapters/Storage/Mongo/MongoTransform.js | 10 +++++----- src/Controllers/DatabaseController.js | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index b4e1f84eb1..65eca277c6 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -199,10 +199,10 @@ export class MongoStorageAdapter { // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. // Accepts the schemaController for legacy reasons. - find(className, query, { skip, limit, sort }, schemaController) { + find(className, query, { skip, limit, sort }, schemaController, schema) { return this.adaptiveCollection(className) .then(collection => collection.find(query, { skip, limit, sort })) - .then(objects => objects.map(object => transform.mongoObjectToParseObject(schemaController, className, object))); + .then(objects => objects.map(object => transform.mongoObjectToParseObject(schemaController, className, object, schema))); } get transform() { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 3bb008f78a..7708ed3812 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -755,7 +755,7 @@ const nestedMongoObjectToNestedParseObject = mongoObject => { // Converts from a mongo-format object to a REST-format object. // Does not strip out anything based on a lack of authentication. -const mongoObjectToParseObject = (schema, className, mongoObject) => { +const mongoObjectToParseObject = (schemaController, className, mongoObject, schema) => { switch(typeof mongoObject) { case 'string': case 'number': @@ -831,8 +831,8 @@ const mongoObjectToParseObject = (schema, className, mongoObject) => { if (key.indexOf('_p_') == 0) { var newKey = key.substring(3); var expected; - if (schema && schema.getExpectedType) { - expected = schema.getExpectedType(className, newKey); + if (schemaController && schemaController.getExpectedType) { + expected = schemaController.getExpectedType(className, newKey); } if (!expected) { log.info('transform.js', @@ -861,7 +861,7 @@ const mongoObjectToParseObject = (schema, className, mongoObject) => { } else if (key[0] == '_' && key != '__type') { throw ('bad key in untransform: ' + key); } else { - var expectedType = schema.getExpectedType(className, key); + var expectedType = schemaController.getExpectedType(className, key); var value = mongoObject[key]; if (expectedType && expectedType.type === 'File' && FileCoder.isValidDatabaseObject(value)) { restObject[key] = FileCoder.databaseToJSON(value); @@ -876,7 +876,7 @@ const mongoObjectToParseObject = (schema, className, mongoObject) => { } } - return { ...restObject, ...schema.getRelationFields(className) }; + return { ...restObject, ...schemaController.getRelationFields(className) }; default: throw 'unknown js type'; } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 67240a9128..56edb8e02f 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -701,7 +701,7 @@ DatabaseController.prototype.find = function(className, query, { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); } else { - return this.adapter.find(className, mongoWhere, mongoOptions, schemaController) + return this.adapter.find(className, mongoWhere, mongoOptions, schemaController, schema) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); From 00de555ecb0aa4e1cc95e020c96ae901fdcb39e6 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 16:21:35 -0700 Subject: [PATCH 41/63] remove one use of schemaController --- spec/MongoTransform.spec.js | 8 ++++++-- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index ca142134f0..7d0aa45ee8 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -132,7 +132,9 @@ describe('mongoObjectToParseObject', () => { it('pointer', (done) => { var input = {_p_userPointer: '_User$123'}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, + }); expect(typeof output.userPointer).toEqual('object'); expect(output.userPointer).toEqual( {__type: 'Pointer', className: '_User', objectId: '123'} @@ -142,7 +144,9 @@ describe('mongoObjectToParseObject', () => { it('null pointer', (done) => { var input = {_p_userPointer: null}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, + }); expect(output.userPointer).toBeUndefined(); done(); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 7708ed3812..2af26933c4 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -834,7 +834,7 @@ const mongoObjectToParseObject = (schemaController, className, mongoObject, sche if (schemaController && schemaController.getExpectedType) { expected = schemaController.getExpectedType(className, newKey); } - if (!expected) { + if (!schema.fields[newKey]) { log.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey); From a55b2b62093960ec77a1b79e94dd845197c33b3e Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 16:24:07 -0700 Subject: [PATCH 42/63] remove another use of schemaController --- src/Adapters/Storage/Mongo/MongoTransform.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 2af26933c4..0e289c0686 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -835,12 +835,10 @@ const mongoObjectToParseObject = (schemaController, className, mongoObject, sche expected = schemaController.getExpectedType(className, newKey); } if (!schema.fields[newKey]) { - log.info('transform.js', - 'Found a pointer column not in the schema, dropping it.', - className, newKey); + log.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey); break; } - if (expected && expected.type !== 'Pointer') { + if (schema.fields[newKey].type !== 'Pointer') { log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key); break; } From d944255e4e6e7138662aea534dea077bd24c4c91 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 16:27:45 -0700 Subject: [PATCH 43/63] remove another use of schemaController --- src/Adapters/Storage/Mongo/MongoTransform.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0e289c0686..f56a230080 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -846,8 +846,7 @@ const mongoObjectToParseObject = (schemaController, className, mongoObject, sche break; } var objData = mongoObject[key].split('$'); - var newClass = (expected ? expected.targetClass : objData[0]); - if (objData[0] !== newClass) { + if (objData[0] !== schema.fields[newKey].targetClass) { throw 'pointer to incorrect className'; } restObject[newKey] = { From f4b1f7b9513350d3e366cefba2a1f9060910558a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 16:43:51 -0700 Subject: [PATCH 44/63] Remove all dependencies on schemaController --- spec/MongoTransform.spec.js | 23 +++++++++++++++----- src/Adapters/Storage/Mongo/MongoTransform.js | 9 ++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 7d0aa45ee8..9a47def9cb 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -153,7 +153,9 @@ describe('mongoObjectToParseObject', () => { it('file', (done) => { var input = {picture: 'pic.jpg'}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { picture: { type: 'File' }}, + }); expect(typeof output.picture).toEqual('object'); expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'}); done(); @@ -161,7 +163,9 @@ describe('mongoObjectToParseObject', () => { it('geopoint', (done) => { var input = {location: [180, -180]}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { location: { type: 'GeoPoint' }}, + }); expect(typeof output.location).toEqual('object'); expect(output.location).toEqual( {__type: 'GeoPoint', longitude: 180, latitude: -180} @@ -171,7 +175,9 @@ describe('mongoObjectToParseObject', () => { it('nested array', (done) => { var input = {arr: [{_testKey: 'testValue' }]}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { arr: { type: 'Array' } }, + }); expect(Array.isArray(output.arr)).toEqual(true); expect(output.arr).toEqual([{ _testKey: 'testValue'}]); done(); @@ -189,7 +195,9 @@ describe('mongoObjectToParseObject', () => { }, regularKey: "some data", }]} - let output = transform.mongoObjectToParseObject(dummySchema, null, input); + let output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { array: { type: 'Array' }}, + }); expect(dd(output, input)).toEqual(undefined); done(); }); @@ -271,7 +279,12 @@ describe('transform schema key changes', () => { long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER), double: new mongodb.Double(Number.MAX_VALUE) } - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + fields: { + long: { type: 'Number' }, + double: { type: 'Number' }, + }, + }); expect(output.long).toBe(Number.MAX_SAFE_INTEGER); expect(output.double).toBe(Number.MAX_VALUE); done(); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index f56a230080..a5c7f41eaa 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -830,10 +830,6 @@ const mongoObjectToParseObject = (schemaController, className, mongoObject, sche if (key.indexOf('_p_') == 0) { var newKey = key.substring(3); - var expected; - if (schemaController && schemaController.getExpectedType) { - expected = schemaController.getExpectedType(className, newKey); - } if (!schema.fields[newKey]) { log.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey); break; @@ -858,13 +854,12 @@ const mongoObjectToParseObject = (schemaController, className, mongoObject, sche } else if (key[0] == '_' && key != '__type') { throw ('bad key in untransform: ' + key); } else { - var expectedType = schemaController.getExpectedType(className, key); var value = mongoObject[key]; - if (expectedType && expectedType.type === 'File' && FileCoder.isValidDatabaseObject(value)) { + if (schema.fields[key] && schema.fields[key].type === 'File' && FileCoder.isValidDatabaseObject(value)) { restObject[key] = FileCoder.databaseToJSON(value); break; } - if (expectedType && expectedType.type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) { + if (schema.fields[key] && schema.fields[key].type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) { restObject[key] = GeoPointCoder.databaseToJSON(value); break; } From e440046be457dedfb7327c745b2f4cbded858755 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 17:01:47 -0700 Subject: [PATCH 45/63] Remove getRelationFields --- spec/MongoTransform.spec.js | 17 +++++++---------- src/Adapters/Storage/Mongo/MongoTransform.js | 11 ++++++++++- src/Controllers/SchemaController.js | 17 ----------------- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 9a47def9cb..d8136483be 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -17,9 +17,6 @@ var dummySchema = { } return; }, - getRelationFields: function() { - return {} - } }; @@ -39,7 +36,7 @@ describe('parseObjectToMongoObjectForCreate', () => { createdAt: "2015-10-06T21:24:50.332Z", updatedAt: "2015-10-06T21:24:50.332Z" }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); expect(output._created_at instanceof Date).toBe(true); expect(output._updated_at instanceof Date).toBe(true); done(); @@ -62,14 +59,14 @@ describe('parseObjectToMongoObjectForCreate', () => { //have __op delete in a new object. Figure out what this should actually be testing. notWorking('a delete op', (done) => { var input = {deleteMe: {__op: 'Delete'}}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); jequal(output, {}); done(); }); it('basic ACL', (done) => { var input = {ACL: {'0123': {'read': true, 'write': true}}}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); // This just checks that it doesn't crash, but it should check format. done(); }); @@ -124,7 +121,7 @@ describe('transformWhere', () => { describe('mongoObjectToParseObject', () => { it('built-in timestamps', (done) => { var input = {createdAt: new Date(), updatedAt: new Date()}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { fields: {} }); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); @@ -236,7 +233,7 @@ describe('transform schema key changes', () => { "Kevin": { "write": true } } }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); expect(typeof output._rperm).toEqual('object'); expect(typeof output._wperm).toEqual('object'); expect(output.ACL).toBeUndefined(); @@ -253,7 +250,7 @@ describe('transform schema key changes', () => { } }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); expect(typeof output._acl).toEqual('object'); expect(output._acl["Kevin"].w).toBeTruthy(); expect(output._acl["Kevin"].r).toBeUndefined(); @@ -265,7 +262,7 @@ describe('transform schema key changes', () => { _rperm: ["*"], _wperm: ["Kevin"] }; - var output = transform.mongoObjectToParseObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input, { fields: {} }); expect(typeof output.ACL).toEqual('object'); expect(output._rperm).toBeUndefined(); expect(output._wperm).toBeUndefined(); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index a5c7f41eaa..e335f3a02a 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -868,7 +868,16 @@ const mongoObjectToParseObject = (schemaController, className, mongoObject, sche } } - return { ...restObject, ...schemaController.getRelationFields(className) }; + const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation'); + let relationFields = {}; + relationFieldNames.forEach(relationFieldName => { + relationFields[relationFieldName] = { + __type: 'Relation', + className: schema.fields[relationFieldName].targetClass, + } + }); + + return { ...restObject, ...relationFields }; default: throw 'unknown js type'; } diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b3fdc7bb77..f855021974 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -686,23 +686,6 @@ class SchemaController { hasClass(className) { return this.reloadData().then(() => !!(this.data[className])); } - - getRelationFields(className) { - if (this.data && this.data[className]) { - let classData = this.data[className]; - return Object.keys(classData).filter((field) => { - return classData[field].type === 'Relation'; - }).reduce((memo, field) => { - let type = classData[field]; - memo[field] = { - __type: 'Relation', - className: type.targetClass - }; - return memo; - }, {}); - } - return {}; - } } // Returns a promise for a new Schema. From 7dca7e20b0781bd74525849e1e1eb38c56c7189d Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 17:18:46 -0700 Subject: [PATCH 46/63] Remove schemaController parameter --- spec/MongoTransform.spec.js | 18 +++++++++--------- .../Storage/Mongo/MongoStorageAdapter.js | 2 +- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index d8136483be..f667a2eee4 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -121,7 +121,7 @@ describe('transformWhere', () => { describe('mongoObjectToParseObject', () => { it('built-in timestamps', (done) => { var input = {createdAt: new Date(), updatedAt: new Date()}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { fields: {} }); + var output = transform.mongoObjectToParseObject(null, input, { fields: {} }); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); @@ -129,7 +129,7 @@ describe('mongoObjectToParseObject', () => { it('pointer', (done) => { var input = {_p_userPointer: '_User$123'}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + var output = transform.mongoObjectToParseObject(null, input, { fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, }); expect(typeof output.userPointer).toEqual('object'); @@ -141,7 +141,7 @@ describe('mongoObjectToParseObject', () => { it('null pointer', (done) => { var input = {_p_userPointer: null}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + var output = transform.mongoObjectToParseObject(null, input, { fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, }); expect(output.userPointer).toBeUndefined(); @@ -150,7 +150,7 @@ describe('mongoObjectToParseObject', () => { it('file', (done) => { var input = {picture: 'pic.jpg'}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + var output = transform.mongoObjectToParseObject(null, input, { fields: { picture: { type: 'File' }}, }); expect(typeof output.picture).toEqual('object'); @@ -160,7 +160,7 @@ describe('mongoObjectToParseObject', () => { it('geopoint', (done) => { var input = {location: [180, -180]}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + var output = transform.mongoObjectToParseObject(null, input, { fields: { location: { type: 'GeoPoint' }}, }); expect(typeof output.location).toEqual('object'); @@ -172,7 +172,7 @@ describe('mongoObjectToParseObject', () => { it('nested array', (done) => { var input = {arr: [{_testKey: 'testValue' }]}; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + var output = transform.mongoObjectToParseObject(null, input, { fields: { arr: { type: 'Array' } }, }); expect(Array.isArray(output.arr)).toEqual(true); @@ -192,7 +192,7 @@ describe('mongoObjectToParseObject', () => { }, regularKey: "some data", }]} - let output = transform.mongoObjectToParseObject(dummySchema, null, input, { + let output = transform.mongoObjectToParseObject(null, input, { fields: { array: { type: 'Array' }}, }); expect(dd(output, input)).toEqual(undefined); @@ -262,7 +262,7 @@ describe('transform schema key changes', () => { _rperm: ["*"], _wperm: ["Kevin"] }; - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { fields: {} }); + var output = transform.mongoObjectToParseObject(null, input, { fields: {} }); expect(typeof output.ACL).toEqual('object'); expect(output._rperm).toBeUndefined(); expect(output._wperm).toBeUndefined(); @@ -276,7 +276,7 @@ describe('transform schema key changes', () => { long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER), double: new mongodb.Double(Number.MAX_VALUE) } - var output = transform.mongoObjectToParseObject(dummySchema, null, input, { + var output = transform.mongoObjectToParseObject(null, input, { fields: { long: { type: 'Number' }, double: { type: 'Number' }, diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 65eca277c6..0be09a2a7d 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -202,7 +202,7 @@ export class MongoStorageAdapter { find(className, query, { skip, limit, sort }, schemaController, schema) { return this.adaptiveCollection(className) .then(collection => collection.find(query, { skip, limit, sort })) - .then(objects => objects.map(object => transform.mongoObjectToParseObject(schemaController, className, object, schema))); + .then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema))); } get transform() { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index e335f3a02a..5b7cc1e3d2 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -755,7 +755,7 @@ const nestedMongoObjectToNestedParseObject = mongoObject => { // Converts from a mongo-format object to a REST-format object. // Does not strip out anything based on a lack of authentication. -const mongoObjectToParseObject = (schemaController, className, mongoObject, schema) => { +const mongoObjectToParseObject = (className, mongoObject, schema) => { switch(typeof mongoObject) { case 'string': case 'number': From 405247082052f4dfbd5bdec82233804931774a7a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 17:29:26 -0700 Subject: [PATCH 47/63] remove schemaController paramater --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 3 +-- src/Controllers/DatabaseController.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 0be09a2a7d..681729b394 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -198,8 +198,7 @@ export class MongoStorageAdapter { } // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. - // Accepts the schemaController for legacy reasons. - find(className, query, { skip, limit, sort }, schemaController, schema) { + find(className, query, { skip, limit, sort }, schema) { return this.adaptiveCollection(className) .then(collection => collection.find(query, { skip, limit, sort })) .then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema))); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 56edb8e02f..8b88410b2c 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -701,7 +701,7 @@ DatabaseController.prototype.find = function(className, query, { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); } else { - return this.adapter.find(className, mongoWhere, mongoOptions, schemaController, schema) + return this.adapter.find(className, mongoWhere, mongoOptions, schema) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); From 1ae1d42c32bb99faa339bd53dbfca8289a5e282e Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 17:39:07 -0700 Subject: [PATCH 48/63] transformWhere in MongoAdapter --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 3 ++- src/Controllers/DatabaseController.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 681729b394..5768262a09 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -199,8 +199,9 @@ export class MongoStorageAdapter { // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. find(className, query, { skip, limit, sort }, schema) { + let mongoWhere = this.transform.transformWhere(className, query, schema); return this.adaptiveCollection(className) - .then(collection => collection.find(query, { skip, limit, sort })) + .then(collection => collection.find(mongoWhere, { skip, limit, sort })) .then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema))); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 8b88410b2c..db2276ff30 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -696,12 +696,12 @@ DatabaseController.prototype.find = function(className, query, { query = addReadACL(query, aclGroup); } validateQuery(query); - let mongoWhere = this.transform.transformWhere(className, query, schema); if (count) { + let mongoWhere = this.transform.transformWhere(className, query, schema); delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); } else { - return this.adapter.find(className, mongoWhere, mongoOptions, schema) + return this.adapter.find(className, query, mongoOptions, schema) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); From 14938bbe7a607e65da052d00e42c1e484cb7a358 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 19:00:58 -0700 Subject: [PATCH 49/63] create + use adapter count instead of collection count --- spec/InstallationsRouter.spec.js | 8 +++----- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 6 ++++++ src/Controllers/DatabaseController.js | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 82416aa42f..2d7224a0d6 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -123,11 +123,9 @@ describe('InstallationsRouter', () => { var router = new InstallationsRouter(); rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) - .then(() => { - return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); - }).then(() => { - return router.handleFind(request); - }).then((res) => { + .then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest)) + .then(() => router.handleFind(request)) + .then((res) => { var response = res.response; expect(response.results.length).toEqual(2); expect(response.count).toEqual(2); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 5768262a09..2bec9895eb 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -205,6 +205,12 @@ export class MongoStorageAdapter { .then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema))); } + // Executs a count. + count(className, query, mongoOptions) { + return this.adaptiveCollection(className) + .then(collection => collection.count(query, mongoOptions)); + } + get transform() { return transform; } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index db2276ff30..c3f92b1f7e 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -699,7 +699,7 @@ DatabaseController.prototype.find = function(className, query, { if (count) { let mongoWhere = this.transform.transformWhere(className, query, schema); delete mongoOptions.limit; - return collection.count(mongoWhere, mongoOptions); + return this.adapter.count(className, mongoWhere, mongoOptions); } else { return this.adapter.find(className, query, mongoOptions, schema) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); From cf0a4b246f5cf3e6fa4c515c557e1d0b43f77fc3 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:04:10 -0700 Subject: [PATCH 50/63] remove adaptive collection call --- src/Controllers/DatabaseController.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index c3f92b1f7e..e547c88395 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -679,8 +679,7 @@ DatabaseController.prototype.find = function(className, query, { return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) .then(() => this.reduceRelationKeys(className, query)) .then(() => this.reduceInRelation(className, query, schemaController)) - .then(() => this.adapter.adaptiveCollection(className)) - .then(collection => { + .then(() => { if (!isMaster) { query = this.addPointerPermissions(schemaController, className, op, query, aclGroup); } From c9be5a3aac541dc45196e0003bae818f1a3bb545 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:07:16 -0700 Subject: [PATCH 51/63] Destructure mongo options --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 2bec9895eb..fc3763c597 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -206,9 +206,9 @@ export class MongoStorageAdapter { } // Executs a count. - count(className, query, mongoOptions) { + count(className, query, { limit, skip, sort }) { return this.adaptiveCollection(className) - .then(collection => collection.count(query, mongoOptions)); + .then(collection => collection.count(query, { limit, skip, sort })); } get transform() { From aa072dabfff0bbee4929f3713d349370e3feeb51 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:09:59 -0700 Subject: [PATCH 52/63] Remove limit from count --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index fc3763c597..cf944ecdb3 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -206,9 +206,9 @@ export class MongoStorageAdapter { } // Executs a count. - count(className, query, { limit, skip, sort }) { + count(className, query, { skip, sort }) { return this.adaptiveCollection(className) - .then(collection => collection.count(query, { limit, skip, sort })); + .then(collection => collection.count(query, { skip, sort })); } get transform() { From e444ca8425c6fc85b980f75692aede3637051f28 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:12:03 -0700 Subject: [PATCH 53/63] Can't sort a count --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index cf944ecdb3..2cbe4fed24 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -206,9 +206,9 @@ export class MongoStorageAdapter { } // Executs a count. - count(className, query, { skip, sort }) { + count(className, query, { skip }) { return this.adaptiveCollection(className) - .then(collection => collection.count(query, { skip, sort })); + .then(collection => collection.count(query, { skip })); } get transform() { From 135b0e0254bd7323bc1d27331fadaa86f46cd951 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:15:51 -0700 Subject: [PATCH 54/63] Remove options from count --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- src/Controllers/DatabaseController.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 2cbe4fed24..52573f8b22 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -206,9 +206,9 @@ export class MongoStorageAdapter { } // Executs a count. - count(className, query, { skip }) { + count(className, query) { return this.adaptiveCollection(className) - .then(collection => collection.count(query, { skip })); + .then(collection => collection.count(query)); } get transform() { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e547c88395..2c6e82eb90 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -697,8 +697,7 @@ DatabaseController.prototype.find = function(className, query, { validateQuery(query); if (count) { let mongoWhere = this.transform.transformWhere(className, query, schema); - delete mongoOptions.limit; - return this.adapter.count(className, mongoWhere, mongoOptions); + return this.adapter.count(className, mongoWhere); } else { return this.adapter.find(className, query, mongoOptions, schema) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); From a763f7c2fc31f887a1336a30e3216344d7fed1d1 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:19:03 -0700 Subject: [PATCH 55/63] move transformWhere into mongo adapter --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 4 ++-- src/Controllers/DatabaseController.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 52573f8b22..cf3a99b10c 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -206,9 +206,9 @@ export class MongoStorageAdapter { } // Executs a count. - count(className, query) { + count(className, query, schema) { return this.adaptiveCollection(className) - .then(collection => collection.count(query)); + .then(collection => collection.count(transform.transformWhere(className, query, schema))); } get transform() { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 2c6e82eb90..4de6a21183 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -696,8 +696,7 @@ DatabaseController.prototype.find = function(className, query, { } validateQuery(query); if (count) { - let mongoWhere = this.transform.transformWhere(className, query, schema); - return this.adapter.count(className, mongoWhere); + return this.adapter.count(className, query, schema); } else { return this.adapter.find(className, query, mongoOptions, schema) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); From d428041a8304b76059994cd2f1983761022683c5 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:22:04 -0700 Subject: [PATCH 56/63] Consistent parameter order --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 2 +- src/Controllers/DatabaseController.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index cf3a99b10c..9e0637dc9a 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -198,7 +198,7 @@ export class MongoStorageAdapter { } // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. - find(className, query, { skip, limit, sort }, schema) { + find(className, query, schema, { skip, limit, sort }) { let mongoWhere = this.transform.transformWhere(className, query, schema); return this.adaptiveCollection(className) .then(collection => collection.find(mongoWhere, { skip, limit, sort })) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 4de6a21183..582021051b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -698,7 +698,7 @@ DatabaseController.prototype.find = function(className, query, { if (count) { return this.adapter.count(className, query, schema); } else { - return this.adapter.find(className, query, mongoOptions, schema) + return this.adapter.find(className, query, schema, mongoOptions) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); From 05ae010b91e04988fe87522115c2158238e85b67 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 20:28:12 -0700 Subject: [PATCH 57/63] Kill mongoOptions --- src/Controllers/DatabaseController.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 582021051b..ae80da3d8a 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -631,13 +631,6 @@ DatabaseController.prototype.find = function(className, query, { sort, count, } = {}) { - let mongoOptions = {}; - if (skip) { - mongoOptions.skip = skip; - } - if (limit) { - mongoOptions.limit = limit; - } let isMaster = acl === undefined; let aclGroup = acl || []; let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'; @@ -653,8 +646,8 @@ DatabaseController.prototype.find = function(className, query, { throw error; }) .then(schema => { + const transformedSort = {}; if (sort) { - mongoOptions.sort = {}; for (let fieldName in sort) { // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, // so duplicate that behaviour here. @@ -673,7 +666,7 @@ DatabaseController.prototype.find = function(className, query, { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`); } const mongoKey = this.transform.transformKey(className, fieldName, schema); - mongoOptions.sort[mongoKey] = sort[fieldName]; + transformedSort[mongoKey] = sort[fieldName]; } } return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) @@ -698,7 +691,7 @@ DatabaseController.prototype.find = function(className, query, { if (count) { return this.adapter.count(className, query, schema); } else { - return this.adapter.find(className, query, schema, mongoOptions) + return this.adapter.find(className, query, schema, { skip, limit, sort: transformedSort }) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); From 3ed3c7b62ff65b2ae2455a714b231c9cfac13d89 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 23 May 2016 21:16:03 -0700 Subject: [PATCH 58/63] Move more mongo specific stuff into mongo adapter --- .../Storage/Mongo/MongoStorageAdapter.js | 3 +- src/Controllers/DatabaseController.js | 44 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 9e0637dc9a..c9bb6d6706 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -200,8 +200,9 @@ export class MongoStorageAdapter { // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. find(className, query, schema, { skip, limit, sort }) { let mongoWhere = this.transform.transformWhere(className, query, schema); + let mongoSort = _.mapKeys(sort, (value, fieldName) => transform.transformKey(className, fieldName, schema)); return this.adaptiveCollection(className) - .then(collection => collection.find(mongoWhere, { skip, limit, sort })) + .then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort })) .then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema))); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index ae80da3d8a..05fab5bbc8 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -628,7 +628,7 @@ DatabaseController.prototype.find = function(className, query, { skip, limit, acl, - sort, + sort = {}, count, } = {}) { let isMaster = acl === undefined; @@ -646,29 +646,25 @@ DatabaseController.prototype.find = function(className, query, { throw error; }) .then(schema => { - const transformedSort = {}; - if (sort) { - for (let fieldName in sort) { - // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, - // so duplicate that behaviour here. - if (fieldName === '_created_at') { - fieldName = 'createdAt'; - sort['createdAt'] = sort['_created_at']; - } else if (fieldName === '_updated_at') { - fieldName = 'updatedAt'; - sort['updatedAt'] = sort['_updated_at']; - } - - if (!SchemaController.fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); - } - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`); - } - const mongoKey = this.transform.transformKey(className, fieldName, schema); - transformedSort[mongoKey] = sort[fieldName]; - } + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behaviour here. If both are specified, the corrent behaviour to match Parse.com is to + // use the one that appears first in the sort list. + if (sort && sort._created_at) { + sort.createdAt = sort._created_at; + delete sort._created_at; } + if (sort && sort._updated_at) { + sort.updatedAt = sort._updated_at; + delete sort._updated_at; + } + Object.keys(sort).forEach(fieldName => { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`); + } + if (!SchemaController.fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } + }); return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) .then(() => this.reduceRelationKeys(className, query)) .then(() => this.reduceInRelation(className, query, schemaController)) @@ -691,7 +687,7 @@ DatabaseController.prototype.find = function(className, query, { if (count) { return this.adapter.count(className, query, schema); } else { - return this.adapter.find(className, query, schema, { skip, limit, sort: transformedSort }) + return this.adapter.find(className, query, schema, { skip, limit, sort }) .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); From 4864518315aa924eac0fc4fc3875b0129e50d99b Mon Sep 17 00:00:00 2001 From: Jeremy Pease Date: Tue, 24 May 2016 16:52:45 -0400 Subject: [PATCH 59/63] Update schema mismatch error to include type string (#1898) --- src/Controllers/SchemaController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b3fdc7bb77..b239a3b255 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -492,7 +492,7 @@ class SchemaController { } else { throw new Parse.Error( Parse.Error.INCORRECT_TYPE, - `schema mismatch for ${className}.${fieldName}; expected ${expected} but got ${type}` + `schema mismatch for ${className}.${fieldName}; expected ${expected.type || expected} but got ${type}` ); } } From 0896f338243cb6895738623c6a5b8832eacc754f Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Tue, 24 May 2016 16:30:43 -0700 Subject: [PATCH 60/63] Remove unnecessary null check --- src/Controllers/DatabaseController.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 05fab5bbc8..76fc5589cc 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -649,11 +649,11 @@ DatabaseController.prototype.find = function(className, query, { // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, // so duplicate that behaviour here. If both are specified, the corrent behaviour to match Parse.com is to // use the one that appears first in the sort list. - if (sort && sort._created_at) { + if (sort._created_at) { sort.createdAt = sort._created_at; delete sort._created_at; } - if (sort && sort._updated_at) { + if (sort._updated_at) { sort.updatedAt = sort._updated_at; delete sort._updated_at; } From c9a138d9ca537d8b31e1e66ba7ed3fa6eaa11080 Mon Sep 17 00:00:00 2001 From: Drew Date: Tue, 24 May 2016 17:21:20 -0700 Subject: [PATCH 61/63] Break schemaController dependency. (#1901) * Break dependency on MongoCollection for updateMany * Move transformWhere usage into MongoTransform * Pass parse schema into transformUpdate * break dependency on schemaController * remove schema parameter * move key name validation up one level * Move validation out of mongo adapter * Move validation into Parse Server and transformUpdate in Mongo Adapter * Update mongo adapter * Use adapter API * use and fix mongo adapter api * Remove/rename stuff * Kill transform in DBController * better imports for transform * Tidy ConfigRouter * Remove schemaController in more places * Remove comment --- spec/MongoTransform.spec.js | 39 ++++------- .../Storage/Mongo/MongoStorageAdapter.js | 68 +++++++++++++------ src/Adapters/Storage/Mongo/MongoTransform.js | 53 +++------------ src/Controllers/DatabaseController.js | 41 +++++------ src/Routers/GlobalConfigRouter.js | 4 +- 5 files changed, 89 insertions(+), 116 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index f667a2eee4..cb5baf7758 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -5,26 +5,11 @@ let transform = require('../src/Adapters/Storage/Mongo/MongoTransform'); let dd = require('deep-diff'); let mongodb = require('mongodb'); -var dummySchema = { - data: {}, - getExpectedType: function(className, key) { - if (key == 'userPointer') { - return { type: 'Pointer', targetClass: '_User' }; - } else if (key == 'picture') { - return { type: 'File' }; - } else if (key == 'location') { - return { type: 'GeoPoint' }; - } - return; - }, -}; - - describe('parseObjectToMongoObjectForCreate', () => { it('a basic number', (done) => { var input = {five: 5}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {five: {type: 'Number'}} }); jequal(input, output); @@ -36,7 +21,7 @@ describe('parseObjectToMongoObjectForCreate', () => { createdAt: "2015-10-06T21:24:50.332Z", updatedAt: "2015-10-06T21:24:50.332Z" }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); expect(output._created_at instanceof Date).toBe(true); expect(output._updated_at instanceof Date).toBe(true); done(); @@ -48,7 +33,7 @@ describe('parseObjectToMongoObjectForCreate', () => { objectId: 'myId', className: 'Blah', }; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {pointers: [pointer]},{ + var out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{ fields: {pointers: {type: 'Array'}} }); jequal([pointer], out.pointers); @@ -59,14 +44,14 @@ describe('parseObjectToMongoObjectForCreate', () => { //have __op delete in a new object. Figure out what this should actually be testing. notWorking('a delete op', (done) => { var input = {deleteMe: {__op: 'Delete'}}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); jequal(output, {}); done(); }); it('basic ACL', (done) => { var input = {ACL: {'0123': {'read': true, 'write': true}}}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); // This just checks that it doesn't crash, but it should check format. done(); }); @@ -74,7 +59,7 @@ describe('parseObjectToMongoObjectForCreate', () => { describe('GeoPoints', () => { it('plain', (done) => { var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {location: geoPoint},{ + var out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{ fields: {location: {type: 'GeoPoint'}} }); expect(out.location).toEqual([180, -180]); @@ -83,7 +68,7 @@ describe('parseObjectToMongoObjectForCreate', () => { it('in array', (done) => { var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {locations: [geoPoint, geoPoint]},{ + var out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{ fields: {locations: {type: 'Array'}} }); expect(out.locations).toEqual([geoPoint, geoPoint]); @@ -92,7 +77,7 @@ describe('parseObjectToMongoObjectForCreate', () => { it('in sub-object', (done) => { var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, { locations: { start: geoPoint }},{ + var out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{ fields: {locations: {type: 'Object'}} }); expect(out).toEqual({ locations: { start: geoPoint } }); @@ -206,7 +191,7 @@ describe('transform schema key changes', () => { var input = { somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'} }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {somePointer: {type: 'Pointer'}} }); expect(typeof output._p_somePointer).toEqual('string'); @@ -218,7 +203,7 @@ describe('transform schema key changes', () => { var input = { userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'} }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {userPointer: {type: 'Pointer'}} }); expect(typeof output._p_userPointer).toEqual('string'); @@ -233,7 +218,7 @@ describe('transform schema key changes', () => { "Kevin": { "write": true } } }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); expect(typeof output._rperm).toEqual('object'); expect(typeof output._wperm).toEqual('object'); expect(output.ACL).toBeUndefined(); @@ -250,7 +235,7 @@ describe('transform schema key changes', () => { } }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { fields: {} }); + var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); expect(typeof output._acl).toEqual('object'); expect(output._acl["Kevin"].w).toBeTruthy(); expect(output._acl["Kevin"].r).toBeUndefined(); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index c9bb6d6706..e62e9e30ed 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,8 +1,17 @@ -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'; +import MongoCollection from './MongoCollection'; +import MongoSchemaCollection from './MongoSchemaCollection'; +import { + parse as parseUrl, + format as formatUrl, +} from '../../../vendor/mongodbUrl'; +import { + parseObjectToMongoObjectForCreate, + mongoObjectToParseObject, + transformKey, + transformWhere, + transformUpdate, +} from './MongoTransform'; +import _ from 'lodash'; let mongodb = require('mongodb'); let MongoClient = mongodb.MongoClient; @@ -159,12 +168,11 @@ export class MongoStorageAdapter { .then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className)); } - // TODO: As yet not particularly well specified. Creates an object. Shouldn't need the - // schemaController, but MongoTransform still needs it :( maybe shouldn't even need the schema, + // TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema, // and should infer from the type. Or maybe does need the schema for validations. Or maybe needs // the schem only for the legacy mongo format. We'll figure that out later. - createObject(className, object, schemaController, parseFormatSchema) { - const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema); + createObject(className, object, schema) { + const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema); return this.adaptiveCollection(className) .then(collection => collection.insertOne(mongoObject)) .catch(error => { @@ -176,15 +184,13 @@ export class MongoStorageAdapter { }); } - // Remove all objects that match the given parse query. Parse Query should be in Parse Format. + // Remove all objects that match the given Parse Query. // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. // If there is some other error, reject with INTERNAL_SERVER_ERROR. - - // Currently accepts the schema, that may not actually be necessary. deleteObjectsByQuery(className, query, schema) { return this.adaptiveCollection(className) .then(collection => { - let mongoWhere = transform.transformWhere(className, query, schema); + let mongoWhere = transformWhere(className, query, schema); return collection.deleteMany(mongoWhere) }) .then(({ result }) => { @@ -197,23 +203,43 @@ export class MongoStorageAdapter { }); } + // Apply the update to all objects that match the given Parse Query. + updateObjectsByQuery(className, query, schema, update) { + const mongoUpdate = transformUpdate(className, update, schema); + const mongoWhere = transformWhere(className, query, schema); + return this.adaptiveCollection(className) + .then(collection => collection.updateMany(mongoWhere, mongoUpdate)); + } + + // Hopefully we can get rid of this in favor of updateObjectsByQuery. + findOneAndUpdate(className, query, schema, update) { + const mongoUpdate = transformUpdate(className, update, schema); + const mongoWhere = transformWhere(className, query, schema); + return this.adaptiveCollection(className) + .then(collection => collection.findOneAndUpdate(mongoWhere, mongoUpdate)); + } + + // Hopefully we can get rid of this. It's only used for config and hooks. + upsertOneObject(className, query, schema, update) { + const mongoUpdate = transformUpdate(className, update, schema); + const mongoWhere = transformWhere(className, query, schema); + return this.adaptiveCollection(className) + .then(collection => collection.upsertOne(mongoWhere, mongoUpdate)); + } + // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. find(className, query, schema, { skip, limit, sort }) { - let mongoWhere = this.transform.transformWhere(className, query, schema); - let mongoSort = _.mapKeys(sort, (value, fieldName) => transform.transformKey(className, fieldName, schema)); + let mongoWhere = transformWhere(className, query, schema); + let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema)); return this.adaptiveCollection(className) .then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort })) - .then(objects => objects.map(object => transform.mongoObjectToParseObject(className, object, schema))); + .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))); } // Executs a count. count(className, query, schema) { return this.adaptiveCollection(className) - .then(collection => collection.count(transform.transformWhere(className, query, schema))); - } - - get transform() { - return transform; + .then(collection => collection.count(transformWhere(className, query, schema))); } } diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 5b7cc1e3d2..898c62e00f 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -19,7 +19,7 @@ const transformKey = (className, fieldName, schema) => { return fieldName; } -const transformKeyValueForUpdate = (schema, className, restKey, restValue) => { +const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSchema) => { // Check if the schema is known since it's a built-in field. var key = restKey; var timeField = false; @@ -38,12 +38,6 @@ const transformKeyValueForUpdate = (schema, className, restKey, restValue) => { key = '_updated_at'; timeField = true; break; - case '_email_verify_token': - key = "_email_verify_token"; - break; - case '_perishable_token': - key = "_perishable_token"; - break; case 'sessionToken': case '_session_token': key = '_session_token'; @@ -57,26 +51,9 @@ const transformKeyValueForUpdate = (schema, className, restKey, restValue) => { case '_wperm': return {key: key, value: restValue}; break; - case '$or': - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries'); - case '$and': - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $and in queries'); - default: - // Other auth data - var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); - if (authDataMatch) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'can only query on ' + key); - } } - // Handle special schema key changes - // TODO: it seems like this is likely to have edge cases where - // pointer types are missed - var expected = undefined; - if (schema && schema.getExpectedType) { - expected = schema.getExpectedType(className, key); - } - if ((expected && expected.type == 'Pointer') || (!expected && restValue && restValue.__type == 'Pointer')) { + if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) { key = '_p_' + key; } @@ -101,9 +78,6 @@ const transformKeyValueForUpdate = (schema, className, restKey, restValue) => { } // Handle normal objects by recursing - if (Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { - throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); - } value = _.mapValues(restValue, transformInteriorValue); return {key, value}; } @@ -223,13 +197,7 @@ function transformWhere(className, restWhere, schema) { return mongoWhere; } -const parseObjectKeyValueToMongoObjectKeyValue = ( - schema, - className, - restKey, - restValue, - parseFormatSchema -) => { +const parseObjectKeyValueToMongoObjectKeyValue = (className, restKey, restValue, schema) => { // Check if the schema is known since it's a built-in field. let transformedValue; let coercedToDate; @@ -267,7 +235,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( if (restValue && restValue.__type !== 'Bytes') { //Note: We may not know the type of a field here, as the user could be saving (null) to a field //That never existed before, meaning we can't infer the type. - if (parseFormatSchema.fields[restKey] && parseFormatSchema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') { + if (schema.fields[restKey] && schema.fields[restKey].type == 'Pointer' || restValue.__type == 'Pointer') { restKey = '_p_' + restKey; } } @@ -305,18 +273,17 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( // Main exposed method to create new objects. // restCreate is the "create" clause in REST API form. -function parseObjectToMongoObjectForCreate(schema, className, restCreate, parseFormatSchema) { +function parseObjectToMongoObjectForCreate(className, restCreate, schema) { if (className == '_User') { restCreate = transformAuthData(restCreate); } var mongoCreate = transformACL(restCreate); for (let restKey in restCreate) { let { key, value } = parseObjectKeyValueToMongoObjectKeyValue( - schema, className, restKey, restCreate[restKey], - parseFormatSchema + schema ); if (value !== undefined) { mongoCreate[key] = value; @@ -326,10 +293,7 @@ function parseObjectToMongoObjectForCreate(schema, className, restCreate, parseF } // Main exposed method to help update old objects. -function transformUpdate(schema, className, restUpdate) { - if (!restUpdate) { - throw 'got empty restUpdate'; - } +const transformUpdate = (className, restUpdate, parseFormatSchema) => { if (className == '_User') { restUpdate = transformAuthData(restUpdate); } @@ -348,9 +312,8 @@ function transformUpdate(schema, className, restUpdate) { mongoUpdate['$set']['_acl'] = acl._acl; } } - for (var restKey in restUpdate) { - var out = transformKeyValueForUpdate(schema, className, restKey, restUpdate[restKey]); + var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema); // If the output value is an object with any $ keys, it's an // operator that needs to be lifted onto the top level update diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 76fc5589cc..8fbf45dc4a 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -62,12 +62,6 @@ function DatabaseController(adapter, { skipValidation } = {}) { this.schemaPromise = null; this.skipValidation = !!skipValidation; this.connect(); - - Object.defineProperty(this, 'transform', { - get: function() { - return adapter.transform; - } - }) } DatabaseController.prototype.WithoutValidation = function() { @@ -171,6 +165,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { // 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. +const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token']; DatabaseController.prototype.update = function(className, query, update, { acl, many, @@ -188,8 +183,7 @@ DatabaseController.prototype.update = function(className, query, update, { .then(schemaController => { return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update')) .then(() => this.handleRelationUpdates(className, query.objectId, update)) - .then(() => this.adapter.adaptiveCollection(className)) - .then(collection => { + .then(() => { if (!isMaster) { query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup); } @@ -209,20 +203,27 @@ DatabaseController.prototype.update = function(className, query, update, { } throw error; }) - .then(parseFormatSchema => { - var mongoWhere = this.transform.transformWhere(className, query, parseFormatSchema); - mongoUpdate = this.transform.transformUpdate( - schemaController, - className, - update, - {validate: !this.skipValidation} - ); + .then(schema => { + Object.keys(update).forEach(fieldName => { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`); + } + fieldName = fieldName.split('.')[0]; + if (!SchemaController.fieldNameIsValid(fieldName) && !specialKeysForUpdate.includes(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`); + } + }); + for (let updateOperation in update) { + if (Object.keys(updateOperation).some(innerKey => innerKey.includes('$') || innerKey.includes('.'))) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + } if (many) { - return collection.updateMany(mongoWhere, mongoUpdate); + return this.adapter.updateObjectsByQuery(className, query, schema, update); } else if (upsert) { - return collection.upsertOne(mongoWhere, mongoUpdate); + return this.adapter.upsertOneObject(className, query, schema, update); } else { - return collection.findOneAndUpdate(mongoWhere, mongoUpdate); + return this.adapter.findOneAndUpdate(className, query, schema, update); } }); }) @@ -393,7 +394,7 @@ DatabaseController.prototype.create = function(className, object, { acl } = {}) .then(() => this.handleRelationUpdates(className, null, object)) .then(() => schemaController.enforceClassExists(className)) .then(() => schemaController.getOneSchema(className, true)) - .then(schema => this.adapter.createObject(className, object, schemaController, schema)) + .then(schema => this.adapter.createObject(className, object, schema)) .then(result => sanitizeDatabaseResult(originalObject, result.ops[0])); }) }; diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 5ab89b0b61..f876ab0bbf 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -24,9 +24,7 @@ export class GlobalConfigRouter extends PromiseRouter { return acc; }, {}); let database = req.config.database.WithoutValidation(); - return database.update('_GlobalConfig', {objectId: 1}, update, {upsert: true}).then(() => { - return Promise.resolve({ response: { result: true } }); - }); + return database.update('_GlobalConfig', {objectId: 1}, update, {upsert: true}).then(() => ({ response: { result: true } })); } mountRoutes() { From 98f8db36e8b1f7cf3e7f4abf6a1f85b561ea324e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ramos?= Date: Wed, 25 May 2016 16:33:57 -0700 Subject: [PATCH 62/63] Update issue template (#1899) Make it clearer what kind of information we are looking for. --- .github/ISSUE_TEMPLATE.md | 57 ++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 7c17eca414..b6904e95a8 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,10 @@ -Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report. The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly! +Please read the following instructions carefully. -Many members of the community use Stack Overflow and Server Fault to ask questions. Read through the existing questions or ask your own! +Check out https://github.com/ParsePlatform/parse-server/issues/1271 for an ideal bug report. +The closer your issue report is to that one, the more likely we are to be able to help, and the more likely we will be to fix the issue quickly! +Many members of the community use Stack Overflow and Server Fault to ask questions. +Read through the existing questions or ask your own! - Stack Overflow: http://stackoverflow.com/questions/tagged/parse.com - Server Fault: https://serverfault.com/tags/parse @@ -15,16 +18,50 @@ Make sure these boxes are checked before submitting your issue -- thanks for rep - [ ] You've searched through existing issues: https://github.com/ParsePlatform/Parse-Server/issues?utf8=%E2%9C%93&q=is%3Aissue Chances are that your issue has been reported or resolved before. -#### Environment Setup +- [ ] You have filled out every section below. Issues without sufficient information are more likely to be closed. -- Server: parse-server version, operating system, hardware, local or remote? -- Database: version, storage engine, hardware, local or remote? +-- -#### Steps to reproduce +### Issue Description -- Can this issue be reproduced using the Parse Server REST API? Include curl commands when applicable. -- What was the expected result? What is the actual outcome? +[DELETE EVERYTHING ABOVE THIS LINE BEFORE SUBMITTING YOUR ISSUE] -#### Logs/Trace +Describe your issue in as much detail as possible. -- You can turn on additional logging by configuring VERBOSE=1 in your environment. +[FILL THIS OUT] + +### Steps to reproduce + +Please include a detailed list of steps that reproduce the issue. Include curl commands when applicable. + +1. [FILL THIS OUT] +2. [FILL THIS OUT] +3. [FILL THIS OUT] + +#### Expected Results + +[FILL THIS OUT] + +#### Actual Outcome + +[FILL THIS OUT] + +### Environment Setup + +- **Server** + - parse-server version: [FILL THIS OUT] + - Operating System: [FILL THIS OUT] + - Hardware: [FILL THIS OUT] + - Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT] + +- **Database** + - MongoDB version: [FILL THIS OUT] + - Storage engine: [FILL THIS OUT] + - Hardware: [FILL THIS OUT] + - Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT] + +### Logs/Trace + +You can turn on additional logging by configuring VERBOSE=1 in your environment. + +[FILL THIS OUT] From 0850c184d36923f5260774a77d16fb2990f96a3a Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 25 May 2016 16:48:18 -0700 Subject: [PATCH 63/63] Fixes #1649 (#1650) * Regression test #1649 * Address comments * Comment * Change emails to help debug flaky test failures * More logging info to debug flaky tests --- spec/ValidationAndPasswordsReset.spec.js | 54 ++++++++++++++++++++---- src/Routers/UsersRouter.js | 45 ++++++++++++++------ 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index e953204931..cac3c56cfd 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -1,7 +1,9 @@ "use strict"; -var request = require('request'); -var Config = require("../src/Config"); +let MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); +let request = require('request'); +let Config = require("../src/Config"); + describe("Custom Pages Configuration", () => { it("should set the custom pages", (done) => { setServerConfiguration({ @@ -62,7 +64,7 @@ describe("Email Verification", () => { var user = new Parse.User(); user.setPassword("asdf"); user.setUsername("zxcv"); - user.setEmail('cool_guy@parse.com'); + user.setEmail('testIfEnabled@parse.com'); user.signUp(null, { success: function(user) { expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); @@ -150,7 +152,7 @@ describe("Email Verification", () => { expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); user.fetch() .then((user) => { - user.set("email", "cool_guy@parse.com"); + user.set("email", "testWhenUpdating@parse.com"); return user.save(); }).then((user) => { return user.fetch(); @@ -204,7 +206,7 @@ describe("Email Verification", () => { expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); user.fetch() .then((user) => { - user.set("email", "cool_guy@parse.com"); + user.set("email", "testValidLinkWhenUpdating@parse.com"); return user.save(); }).then((user) => { return user.fetch(); @@ -228,7 +230,7 @@ describe("Email Verification", () => { var calls = 0; var emailAdapter = { sendMail: function(options){ - expect(options.to).toBe('cool_guy@parse.com'); + expect(options.to).toBe('testSendSimpleAdapter@parse.com'); if (calls == 0) { expect(options.subject).toEqual('Please verify your e-mail for My Cool App'); expect(options.text.match(/verify_email/)).not.toBe(null); @@ -258,7 +260,7 @@ describe("Email Verification", () => { var user = new Parse.User(); user.setPassword("asdf"); user.setUsername("zxcv"); - user.set("email", "cool_guy@parse.com"); + user.set("email", "testSendSimpleAdapter@parse.com"); user.signUp(null, { success: function(user) { expect(calls).toBe(1); @@ -266,7 +268,7 @@ describe("Email Verification", () => { .then((user) => { return user.save(); }).then((user) => { - return Parse.User.requestPasswordReset("cool_guy@parse.com").catch((err) => { + return Parse.User.requestPasswordReset("testSendSimpleAdapter@parse.com").catch((err) => { fail('Should not fail requesting a password'); done(); }) @@ -282,6 +284,42 @@ describe("Email Verification", () => { }); }); + it('fails if you include an emailAdapter, set verifyUserEmails to false, dont set a publicServerURL, and try to send a password reset email (regression test for #1649)', done => { + setServerConfiguration({ + serverURL: 'http://localhost:8378/1', + appId: 'test', + appName: 'unused', + javascriptKey: 'test', + dotNetKey: 'windows', + clientKey: 'client', + restAPIKey: 'rest', + masterKey: 'test', + collectionPrefix: 'test_', + fileKey: 'test', + verifyUserEmails: false, + emailAdapter: MockEmailAdapterWithOptions({ + fromAddress: 'parse@example.com', + apiKey: 'k', + domain: 'd', + }), + }) + + let user = new Parse.User(); + user.setPassword("asdf"); + user.setUsername("zxcv"); + user.set("email", "testInvalidConfig@parse.com"); + user.signUp(null) + .then(user => Parse.User.requestPasswordReset("testInvalidConfig@parse.com")) + .then(result => { + console.log(result); + fail('sending password reset email should not have succeeded'); + done(); + }, error => { + expect(error.message).toEqual('An appName, publicServerURL, and emailAdapter are required for password reset functionality.') + done(); + }); + }); + it('does not send verification email if email verification is disabled', done => { var emailAdapter = { sendVerificationEmail: () => Promise.resolve(), diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index a5e6299c58..61757b077c 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -1,7 +1,7 @@ // These methods handle the User-related routes. import deepcopy from 'deepcopy'; - +import Config from '../Config'; import ClassesRouter from './ClassesRouter'; import PromiseRouter from '../PromiseRouter'; import rest from '../rest'; @@ -156,19 +156,36 @@ export class UsersRouter extends ClassesRouter { } handleResetRequest(req) { - let { email } = req.body; - if (!email) { - throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email"); - } - let userController = req.config.userController; - - return userController.sendPasswordResetEmail(email).then((token) => { - return Promise.resolve({ - response: {} - }); - }, (err) => { - throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `no user found with email ${email}`); - }); + try { + Config.validateEmailConfiguration({ + verifyUserEmails: true, //A bit of a hack, as this isn't the intended purpose of this parameter + appName: req.config.appName, + publicServerURL: req.config.publicServerURL, + }); + } catch (e) { + if (typeof e === 'string') { + // Maybe we need a Bad Configuration error, but the SDKs won't understand it. For now, Internal Server Error. + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset functionality.'); + } else { + throw e; + } + } + let { email } = req.body; + if (!email) { + throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email"); + } + let userController = req.config.userController; + return userController.sendPasswordResetEmail(email).then(token => { + return Promise.resolve({ + response: {} + }); + }, err => { + if (err.code === Parse.Error.OBJECT_NOT_FOUND) { + throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}.`); + } else { + throw err; + } + }); }