From 2520e7b6c620242deff651b0df50b9c2fbf610e3 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Sat, 27 Feb 2016 16:33:34 -0800 Subject: [PATCH 01/48] Initial commit of Google Cloud Storage File Adapter --- package.json | 1 + src/Adapters/Files/GCSAdapter.js | 96 ++++++++++++++++++++++++++++++++ src/index.js | 14 +++-- 3 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 src/Adapters/Files/GCSAdapter.js diff --git a/package.json b/package.json index 9837376d..22b11e1c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "commander": "^2.9.0", "deepcopy": "^0.6.1", "express": "^4.13.4", + "gcloud": "^0.28.0", "mime": "^1.3.4", "mongodb": "~2.1.0", "multer": "^1.1.0", diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js new file mode 100644 index 00000000..a5ee4096 --- /dev/null +++ b/src/Adapters/Files/GCSAdapter.js @@ -0,0 +1,96 @@ +// GCSAdapter +// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage +import * as gcloud from 'gcloud'; +import { FilesAdapter } from './FilesAdapter'; + +export class GCSAdapter extends FilesAdapter { + // GCS Project ID and the name of a corresponding Keyfile are required. + // See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication + // for more details. + constructor( + projectId, + keyFilename, + bucket, + { bucketPrefix = '', + directAccess = false } = {} + ) { + super(); + + this._bucket = bucket; + this._bucketPrefix = bucketPrefix; + this._directAccess = directAccess; + + let gcsOptions = { + projectId: projectId, + keyFilename: keyFilename + }; + + this._gcsClient = new gcloud.storage(gcsOptions); + } + + // For a given config object, filename, and data, store a file in GCS. + // Resolves the promise or fails with an error. + createFile(config, filename, data) { + return new Promise((resolve, reject) => { + let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); + // gcloud supports upload(file) not upload(bytes), so we need to stream. + var uploadStream = file.createWriteStream(options); + uploadStream.on('error', (err) => { + return reject(err); + }).on('finish', () => { + // Second call to set public read ACL after object is uploaded. + if (this._directAccess) { + file.makePublic((err, res) => { + if (err !== null) { + return reject(err); + } + resolve(); + }); + } + resolve(); + }); + uploadStream.write(data); + uploadStream.end(); + }); + } + + // Deletes a file with the given file name. + // Returns a promise that succeeds with the delete response, or fails with an error. + deleteFile(config, filename) { + return new Promise((resolve, reject) => { + let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); + file.delete((err, res) => { + if(err !== null) { + return reject(err); + } + resolve(res); + }); + }); + } + + // Search for and return a file if found by filename. + // Returns a promise that succeeds with the buffer result from GCS, or fails with an error. + getFileData(config, filename) { + return new Promise((resolve, reject) => { + let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); + file.download((err, data) => { + if (err !== null) { + return reject(err); + } + resolve(data); + }); + }); + } + + // Generates and returns the location of a file stored in GCS for the given request and filename. + // The location is the direct GCS link if the option is set, + // otherwise we serve the file through parse-server. + getFileLocation(config, filename) { + if (this._directAccess) { + return `https://${this._bucket}.storage.googleapis.com/${this._bucketPrefix + filename}`; + } + return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename)); + } +} + +export default GCSAdapter; diff --git a/src/index.js b/src/index.js index ad715902..6112a3ea 100644 --- a/src/index.js +++ b/src/index.js @@ -14,6 +14,7 @@ import cache from './cache'; import PromiseRouter from './PromiseRouter'; import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter'; import { S3Adapter } from './Adapters/Files/S3Adapter'; +import { GCSAdapter } from './Adapters/Files/GCSAdapter'; import { FilesController } from './Controllers/FilesController'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; @@ -91,11 +92,11 @@ function ParseServer({ serverURL = requiredParameter('You must provide a serverURL!'), maxUploadSize = '20mb' }) { - + // Initialize the node client SDK automatically Parse.initialize(appId, javascriptKey || 'unused', masterKey); Parse.serverURL = serverURL; - + if (databaseAdapter) { DatabaseAdapter.setAdapter(databaseAdapter); } @@ -103,7 +104,7 @@ function ParseServer({ if (databaseURI) { DatabaseAdapter.setAppDatabaseURI(appId, databaseURI); } - + if (cloud) { addParseCloud(); if (typeof cloud === 'function') { @@ -125,7 +126,7 @@ function ParseServer({ const pushController = new PushController(pushControllerAdapter); const loggerController = new LoggerController(loggerControllerAdapter); const hooksController = new HooksController(appId, collectionPrefix); - + cache.apps[appId] = { masterKey: masterKey, collectionPrefix: collectionPrefix, @@ -185,7 +186,7 @@ function ParseServer({ if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) { routers.push(require('./global_config')); } - + if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) { routers.push(new HooksRouter()); } @@ -229,5 +230,6 @@ function getClassName(parseClass) { module.exports = { ParseServer: ParseServer, - S3Adapter: S3Adapter + S3Adapter: S3Adapter, + GCSAdapter: GCSAdapter }; From deafb680aef5befa52ba636c989d556167ec9144 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Sat, 27 Feb 2016 16:47:26 -0800 Subject: [PATCH 02/48] Removed orphaned "options" --- src/Adapters/Files/GCSAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js index a5ee4096..23f9ef02 100644 --- a/src/Adapters/Files/GCSAdapter.js +++ b/src/Adapters/Files/GCSAdapter.js @@ -34,7 +34,7 @@ export class GCSAdapter extends FilesAdapter { return new Promise((resolve, reject) => { let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); // gcloud supports upload(file) not upload(bytes), so we need to stream. - var uploadStream = file.createWriteStream(options); + var uploadStream = file.createWriteStream(); uploadStream.on('error', (err) => { return reject(err); }).on('finish', () => { From 360cc3461d242c669d2ba6f315a4a5a930c2de9a Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Sat, 27 Feb 2016 21:27:32 -0800 Subject: [PATCH 03/48] Added tests! Note that tests won't run without a GCP Project, key, and GCS bucket --- spec/ParseFile+GCS.spec.js | 398 +++++++++++++++++++++++++++++++ src/Adapters/Files/GCSAdapter.js | 3 +- 2 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 spec/ParseFile+GCS.spec.js diff --git a/spec/ParseFile+GCS.spec.js b/spec/ParseFile+GCS.spec.js new file mode 100644 index 00000000..850704a3 --- /dev/null +++ b/spec/ParseFile+GCS.spec.js @@ -0,0 +1,398 @@ +// This is a port of the test suite: +// hungry/js/test/parse_file_test.js + +"use strict"; + +var request = require('request'); +var GCSAdapter = require('../src/index').GCSAdapter; + +var str = "Hello World!"; +var data = []; +for (var i = 0; i < str.length; i++) { + data.push(str.charCodeAt(i)); +} + +// Make sure that you fill these in, otherwise the tests won't run!!! +var GCP_PROJECT_ID = ""; +var GCP_KEYFILE_PATH = ""; +var GCS_BUCKET_NAME = ""; + +describe('Parse.File testing', () => { + describe('GCS directAccess: false', () => { + beforeEach(function(done){ + var port = 8378; + var GCSConfiguration = { + databaseURI: process.env.DATABASE_URI, + serverURL: 'http://localhost:' + port + '/1', + appId: 'test', + javascriptKey: 'test', + restAPIKey: 'rest', + masterKey: 'test', + fileKey: 'test', + filesAdapter: new GCSAdapter( + GCP_PROJECT_ID, + GCP_KEYFILE_PATH, + GCS_BUCKET_NAME, + { + bucketPrefix: 'private/', + directAccess: false + } + ) + }; + setServerConfiguration(GCSConfiguration); + done(); + }); + + it('works with Content-Type', done => { + var headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_file.txt$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + }); + }); + + it('works without Content-Type', done => { + var headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_file.txt$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + }); + }); + + it('supports REST end-to-end file create, read, delete, read', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/testfile.txt', + body: 'check one two', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_testfile.txt$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('check one two'); + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'test' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(200); + request.get({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: b.url + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(404); + done(); + }); + }); + }); + }); + }); + + it('blocks file deletions with missing or incorrect master-key header', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/thefile.jpg', + body: 'the file body' + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); + // missing X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b.error).toMatch(/unauthorized/); + // incorrect X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'tryagain' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b2 = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b2.error).toMatch(/unauthorized/); + done(); + }); + }); + }); + }); + + it('handles other filetypes', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/file.jpg', + body: 'argle bargle', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_file.jpg$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + }); + }); + }); + + describe('GCS directAccess: true', () => { + beforeEach(function(done){ + var port = 8378; + var GCSConfiguration = { + databaseURI: process.env.DATABASE_URI, + serverURL: 'http://localhost:' + port + '/1', + appId: 'test', + javascriptKey: 'test', + restAPIKey: 'rest', + masterKey: 'test', + fileKey: 'test', + filesAdapter: new GCSAdapter( + GCP_PROJECT_ID, + GCP_KEYFILE_PATH, + GCS_BUCKET_NAME, + { + bucketPrefix: 'public/', + directAccess: true + } + ) + }; + setServerConfiguration(GCSConfiguration); + done(); + }); + + it('works with Content-Type', done => { + var headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_file.txt$/); + var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*file.txt") + expect(b.url).toMatch(gcsRegex); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + }); + }); + + it('works without Content-Type', done => { + var headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_file.txt$/); + var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*file.txt") + expect(b.url).toMatch(gcsRegex); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + }); + }); + + it('supports REST end-to-end file create, read, delete, read', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + // Create the file + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/testfile.txt', + body: 'check one two', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_testfile.txt$/); + var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*testfile.txt") + expect(b.url).toMatch(gcsRegex); + // Read the file the first time + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('check one two'); + // Delete the file + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'test' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(200); + // Read the file the second time--expect it to be gone + // Note that we're reading from the public cloud storage URL + // This is different from the above test since it's assumed + // users are reading from the public URL + request.get({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: "https://" + GCS_BUCKET_NAME + ".storage.googleapis.com/public/.*testfile.txt" + }, (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(404); + done(); + }); + }); + }); + }); + }); + + it('blocks file deletions with missing or incorrect master-key header', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/thefile.jpg', + body: 'the file body' + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*thefile.jpg") + expect(b.url).toMatch(gcsRegex); + // missing X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b.error).toMatch(/unauthorized/); + // incorrect X-Parse-Master-Key header + request.del({ + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'tryagain' + }, + url: 'http://localhost:8378/1/files/' + b.name + }, (error, response, body) => { + expect(error).toBe(null); + var del_b2 = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b2.error).toMatch(/unauthorized/); + done(); + }); + }); + }); + }); + + it('handles other filetypes', done => { + var headers = { + 'Content-Type': 'image/jpeg', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + request.post({ + headers: headers, + url: 'http://localhost:8378/1/files/file.jpg', + body: 'argle bargle', + }, (error, response, body) => { + expect(error).toBe(null); + var b = JSON.parse(body); + expect(b.name).toMatch(/_file.jpg$/); + var gcsRegex = new RegExp("https:\/\/" + GCS_BUCKET_NAME + ".storage.googleapis.com\/public\/.*file.jpg") + expect(b.url).toMatch(gcsRegex); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + }); + }); + }); +}); diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js index 23f9ef02..b0f841e0 100644 --- a/src/Adapters/Files/GCSAdapter.js +++ b/src/Adapters/Files/GCSAdapter.js @@ -46,8 +46,9 @@ export class GCSAdapter extends FilesAdapter { } resolve(); }); + } else { + resolve(); } - resolve(); }); uploadStream.write(data); uploadStream.end(); From 3e41a21e27348ed737be3c46b06f17f44aedbc3d Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Sat, 27 Feb 2016 22:13:20 -0800 Subject: [PATCH 04/48] x-ing out the test suite for CI buildability. --- spec/ParseFile+GCS.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/ParseFile+GCS.spec.js b/spec/ParseFile+GCS.spec.js index 850704a3..ae6543e2 100644 --- a/spec/ParseFile+GCS.spec.js +++ b/spec/ParseFile+GCS.spec.js @@ -17,7 +17,9 @@ var GCP_PROJECT_ID = ""; var GCP_KEYFILE_PATH = ""; var GCS_BUCKET_NAME = ""; -describe('Parse.File testing', () => { +// Note the 'xdescribe', make sure to delete the 'x' once the above vars +// are filled in to run the test suite +xdescribe('Parse.File GCS testing', () => { describe('GCS directAccess: false', () => { beforeEach(function(done){ var port = 8378; From 5b8ad9d4c6c727475155a53928aaa403cc24e0d8 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Thu, 3 Mar 2016 22:50:19 -0800 Subject: [PATCH 05/48] No more .DS_Store --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index de88257b..8ea322c4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ lib/ # cache folder .cache + +# Mac DS_Store files +.DS_Store From 2c844a11b9e4adac8f9e02f08b0787bfc08e8bb8 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 17:01:14 -0500 Subject: [PATCH 06/48] Adds public_html and views for packaging --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index d2a0294c..29e873da 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "files": [ "bin/", "lib/", + "public_html/", + "views/", "LICENSE", "PATENTS", "README.md" From 069605e9c3ef9c457d044e53cf53273cd0300606 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 10:54:32 -0500 Subject: [PATCH 07/48] Improves loading of Push Adapter, fix loading of S3Adapter - Adds environment variables to configure S3Adapter --- src/Adapters/AdapterLoader.js | 7 ------- src/Adapters/Files/S3Adapter.js | 35 +++++++++++++++++---------------- src/index.js | 3 ++- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index 5b46f22d..fd6741c2 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -28,13 +28,6 @@ export function loadAdapter(adapter, defaultAdapter, options) { return loadAdapter(adapter.class, undefined, adapter.options); } else if (adapter.adapter) { return loadAdapter(adapter.adapter, undefined, adapter.options); - } else { - // Try to load the defaultAdapter with the options - // The default adapter should throw if the options are - // incompatible - try { - return loadAdapter(defaultAdapter, undefined, adapter); - } catch (e) {}; } // return the adapter as is as it's unusable otherwise return adapter; diff --git a/src/Adapters/Files/S3Adapter.js b/src/Adapters/Files/S3Adapter.js index e21ef8db..cbdf3f11 100644 --- a/src/Adapters/Files/S3Adapter.js +++ b/src/Adapters/Files/S3Adapter.js @@ -8,19 +8,20 @@ import requiredParameter from '../../requiredParameter'; const DEFAULT_S3_REGION = "us-east-1"; -function parseS3AdapterOptions(...options) { - if (options.length === 1 && typeof options[0] == "object") { - return options; +function requiredOrFromEnvironment(env, name) { + let environmentVariable = process.env[env]; + if (!environmentVariable) { + requiredParameter(`S3Adapter requires an ${name}`); } - - const additionalOptions = options[3] || {}; - - return { - accessKey: options[0], - secretKey: options[1], - bucket: options[2], - region: additionalOptions.region + return environmentVariable; +} + +function fromEnvironmentOrDefault(env, defaultValue) { + let environmentVariable = process.env[env]; + if (environmentVariable) { + return environmentVariable; } + return defaultValue; } export class S3Adapter extends FilesAdapter { @@ -28,12 +29,12 @@ export class S3Adapter extends FilesAdapter { // Providing AWS access and secret keys is mandatory // Region and bucket will use sane defaults if omitted constructor( - accessKey = requiredParameter('S3Adapter requires an accessKey'), - secretKey = requiredParameter('S3Adapter requires a secretKey'), - bucket, - { region = DEFAULT_S3_REGION, - bucketPrefix = '', - directAccess = false } = {}) { + accessKey = requiredOrFromEnvironment('S3_ACCESS_KEY', 'accessKey'), + secretKey = requiredOrFromEnvironment('S3_SECRET_KEY', 'secretKey'), + bucket = fromEnvironmentOrDefault('S3_BUCKET', undefined), + { region = fromEnvironmentOrDefault('S3_REGION', DEFAULT_S3_REGION), + bucketPrefix = fromEnvironmentOrDefault('S3_BUCKET_PREFIX', ''), + directAccess = fromEnvironmentOrDefault('S3_DIRECT_ACCESS', false) } = {}) { super(); this._region = region; diff --git a/src/index.js b/src/index.js index 076035f8..4f4b763a 100644 --- a/src/index.js +++ b/src/index.js @@ -133,7 +133,8 @@ function ParseServer({ const filesControllerAdapter = loadAdapter(filesAdapter, () => { return new GridStoreAdapter(databaseURI); }); - const pushControllerAdapter = loadAdapter(push, ParsePushAdapter); + // Pass the push options too as it works with the default + const pushControllerAdapter = loadAdapter(push && push.adapter, ParsePushAdapter, push); const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter); const emailControllerAdapter = loadAdapter(emailAdapter); // We pass the options and the base class for the adatper, From a44b1d9f76512e38adebbea95de15e7a4170e12d Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 12:04:41 -0500 Subject: [PATCH 08/48] Improves documentation, add loading tests --- README.md | 14 ++++++++++++++ spec/AdapterLoader.spec.js | 25 +++++++++++++++++++++++++ src/Adapters/AdapterLoader.js | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 84538c4f..b0a8d015 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,20 @@ PARSE_SERVER_MAX_UPLOAD_SIZE ``` +##### Configuring S3 Adapter + +You can use the following environment variable setup the S3 adapter + +```js +S3_ACCESS_KEY +S3_SECRET_KEY +S3_BUCKET +S3_REGION +S3_BUCKET_PREFIX +S3_DIRECT_ACCESS + +``` + ## Contributing We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md). diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index f32867e0..69381fc5 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -1,6 +1,8 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter; var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; +var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter"); +var S3Adapter = require("../src/Adapters/Files/S3Adapter").default; describe("AdapterLoader", ()=>{ @@ -84,4 +86,27 @@ describe("AdapterLoader", ()=>{ }).not.toThrow("foo is required for that adapter"); done(); }); + + it("should load push adapter from options", (done) => { + var options = { + ios: { + bundleId: 'bundle.id' + } + } + expect(() => { + var adapter = loadAdapter(undefined, ParsePushAdapter, options); + expect(adapter.constructor).toBe(ParsePushAdapter); + expect(adapter).not.toBe(undefined); + }).not.toThrow(); + done(); + }); + + it("should load S3Adapter from direct passing", (done) => { + var s3Adapter = new S3Adapter("key", "secret", "bucket") + expect(() => { + var adapter = loadAdapter(s3Adapter, FilesAdapter); + expect(adapter).toBe(s3Adapter); + }).not.toThrow(); + done(); + }) }); diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index fd6741c2..a9521f0b 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -29,7 +29,7 @@ export function loadAdapter(adapter, defaultAdapter, options) { } else if (adapter.adapter) { return loadAdapter(adapter.adapter, undefined, adapter.options); } - // return the adapter as is as it's unusable otherwise + // return the adapter as provided return adapter; } From e074a922fb765e786f06bcc7bb3302d835ddc2e9 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Thu, 3 Mar 2016 09:44:43 -0800 Subject: [PATCH 09/48] Fix leak warnings in tests, use mongodb-runner from node_modules --- package.json | 2 +- src/index.js | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 29e873da..1b6199cf 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "build": "./node_modules/.bin/babel src/ -d lib/", "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} ./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", - "posttest": "mongodb-runner stop", + "posttest": "./node_modules/.bin/mongodb-runner stop", "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test", "start": "node ./bin/parse-server", "prepublish": "npm run build" diff --git a/src/index.js b/src/index.js index 076035f8..49769b12 100644 --- a/src/index.js +++ b/src/index.js @@ -232,15 +232,18 @@ function ParseServer({ api.use(middlewares.handleParseErrors); - process.on('uncaughtException', (err) => { - if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error - console.log(`Unable to listen on port ${err.port}. The port is already in use.`); - process.exit(0); - } - else { - throw err; - } - }); + //This causes tests to spew some useless warnings, so disable in test + if (!process.env.TESTING) { + process.on('uncaughtException', (err) => { + if( err.code === "EADDRINUSE" ) { // user-friendly message for this common error + console.log(`Unable to listen on port ${err.port}. The port is already in use.`); + process.exit(0); + } + else { + throw err; + } + }); + } hooksController.load(); return api; From 172da3aaa31f4eaf50a618f9159a595ba54ed0ef Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Fri, 4 Mar 2016 18:43:19 -0800 Subject: [PATCH 10/48] Move HooksController to use MongoCollection instead of direct Mongo access. --- src/Adapters/Storage/Mongo/MongoCollection.js | 15 +- src/Controllers/HooksController.js | 170 ++++++++---------- 2 files changed, 86 insertions(+), 99 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 66113c3d..84f47899 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -1,4 +1,3 @@ - let mongodb = require('mongodb'); let Collection = mongodb.Collection; @@ -18,8 +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 @@ -59,6 +57,13 @@ export default class MongoCollection { }) } + // Atomically updates data in the database for a single (first) object that matched the query + // If there is nothing that matches the query - does insert + // Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5. + upsertOne(query, update) { + return this._mongoCollection.update(query, update, { upsert: true }); + } + // Atomically find and delete an object based on query. // The result is the promise with an object that was in the database before deleting. // Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done. @@ -70,6 +75,10 @@ export default class MongoCollection { }); } + remove(query) { + return this._mongoCollection.remove(query); + } + drop() { return this._mongoCollection.drop(); } diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index 1e47acd7..fbc7e920 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -8,104 +8,91 @@ import * as request from "request"; const DefaultHooksCollectionName = "_Hooks"; export class HooksController { - _applicationId: string; - _collectionPrefix: string; + _applicationId:string; + _collectionPrefix:string; _collection; - constructor(applicationId: string, collectionPrefix: string = '') { + constructor(applicationId:string, collectionPrefix:string = '') { this._applicationId = applicationId; this._collectionPrefix = collectionPrefix; } - - database() { - return DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix); + + load() { + return this._getHooks().then(hooks => { + hooks = hooks || []; + hooks.forEach((hook) => { + this.addHookToTriggers(hook); + }); + }); } - - collection() { + + getCollection() { if (this._collection) { return Promise.resolve(this._collection) } - return this.database().rawCollection(DefaultHooksCollectionName).then((collection) => { + + let database = DatabaseAdapter.getDatabaseConnection(this._applicationId, this._collectionPrefix); + return database.adaptiveCollection(DefaultHooksCollectionName).then(collection => { this._collection = collection; return collection; }); } - + getFunction(functionName) { - return this.getOne({functionName: functionName}) + return this._getHooks({ functionName: functionName }, 1).then(results => results[0]); } - + getFunctions() { - return this.get({functionName: { $exists: true }}) + return this._getHooks({ functionName: { $exists: true } }); } - + getTrigger(className, triggerName) { - return this.getOne({className: className, triggerName: triggerName }) + return this._getHooks({ className: className, triggerName: triggerName }, 1).then(results => results[0]); } - + getTriggers() { - return this.get({className: { $exists: true }, triggerName: { $exists: true }}) + return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } }); } - + deleteFunction(functionName) { triggers.removeFunction(functionName, this._applicationId); - return this.delete({functionName: functionName}); + return this._removeHooks({ functionName: functionName }); } - + deleteTrigger(className, triggerName) { triggers.removeTrigger(triggerName, className, this._applicationId); - return this.delete({className: className, triggerName: triggerName}); + return this._removeHooks({ className: className, triggerName: triggerName }); } - - delete(query) { - return this.collection().then((collection) => { - return collection.remove(query) - }).then( (res) => { + + _getHooks(query, limit) { + let options = limit ? { limit: limit } : undefined; + return this.getCollection().then(collection => collection.find(query, options)); + } + + _removeHooks(query) { + return this.getCollection().then(collection => { + return collection.remove(query); + }).then(() => { return {}; - }, 1); - } - - getOne(query) { - return this.collection() - .then(coll => coll.findOne(query, {_id: 0})) - .then(hook => { - return hook; }); } - - get(query) { - return this.collection() - .then(coll => coll.find(query, {_id: 0}).toArray()) - .then(hooks => { - return hooks; - }); - } - - getHooks() { - return this.collection() - .then(coll => coll.find({}, {_id: 0}).toArray()) - .then(hooks => { - return hooks; - }, () => ([])) - } - + saveHook(hook) { - var query; if (hook.functionName && hook.url) { - query = {functionName: hook.functionName } + query = { functionName: hook.functionName } } else if (hook.triggerName && hook.className && hook.url) { query = { className: hook.className, triggerName: hook.triggerName } } else { throw new Parse.Error(143, "invalid hook declaration"); } - return this.collection().then((collection) => { - return collection.update(query, hook, {upsert: true}) - }).then(function(res){ - return hook; - }) + return this.getCollection() + .then(collection => collection.upsertOne(query, hook)) + .then(() => { + return hook; + }); } - + addHookToTriggers(hook) { var wrappedFunction = wrapToHTTPRequest(hook); wrappedFunction.url = hook.url; @@ -114,13 +101,13 @@ export class HooksController { } else { triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId); } - } - + } + addHook(hook) { this.addHookToTriggers(hook); return this.saveHook(hook); } - + createOrUpdateHook(aHook) { var hook; if (aHook && aHook.functionName && aHook.url) { @@ -132,19 +119,19 @@ export class HooksController { hook.className = aHook.className; hook.url = aHook.url; hook.triggerName = aHook.triggerName; - + } else { throw new Parse.Error(143, "invalid hook declaration"); - } - + } + return this.addHook(hook); }; - + createHook(aHook) { if (aHook.functionName) { return this.getFunction(aHook.functionName).then((result) => { if (result) { - throw new Parse.Error(143,`function name: ${aHook.functionName} already exits`); + throw new Parse.Error(143, `function name: ${aHook.functionName} already exits`); } else { return this.createOrUpdateHook(aHook); } @@ -152,49 +139,39 @@ export class HooksController { } else if (aHook.className && aHook.triggerName) { return this.getTrigger(aHook.className, aHook.triggerName).then((result) => { if (result) { - throw new Parse.Error(143,`class ${aHook.className} already has trigger ${aHook.triggerName}`); + throw new Parse.Error(143, `class ${aHook.className} already has trigger ${aHook.triggerName}`); } return this.createOrUpdateHook(aHook); }); } - + throw new Parse.Error(143, "invalid hook declaration"); }; - + updateHook(aHook) { if (aHook.functionName) { return this.getFunction(aHook.functionName).then((result) => { if (result) { return this.createOrUpdateHook(aHook); } - throw new Parse.Error(143,`no function named: ${aHook.functionName} is defined`); + throw new Parse.Error(143, `no function named: ${aHook.functionName} is defined`); }); } else if (aHook.className && aHook.triggerName) { return this.getTrigger(aHook.className, aHook.triggerName).then((result) => { if (result) { return this.createOrUpdateHook(aHook); } - throw new Parse.Error(143,`class ${aHook.className} does not exist`); + throw new Parse.Error(143, `class ${aHook.className} does not exist`); }); } throw new Parse.Error(143, "invalid hook declaration"); }; - - load() { - return this.getHooks().then((hooks) => { - hooks = hooks || []; - hooks.forEach((hook) => { - this.addHookToTriggers(hook); - }); - }); - } - } function wrapToHTTPRequest(hook) { - return function(req, res) { - var jsonBody = {}; - for(var i in req) { + return (req, res) => { + let jsonBody = {}; + for (var i in req) { jsonBody[i] = req[i]; } if (req.object) { @@ -205,30 +182,31 @@ function wrapToHTTPRequest(hook) { jsonBody.original = req.original.toJSON(); jsonBody.original.className = req.original.className; } - var jsonRequest = {}; - jsonRequest.headers = { - 'Content-Type': 'application/json' - } - jsonRequest.body = JSON.stringify(jsonBody); - - request.post(hook.url, jsonRequest, function(err, httpResponse, body){ + let jsonRequest = { + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(jsonBody) + }; + + request.post(hook.url, jsonRequest, function (err, httpResponse, body) { var result; if (body) { if (typeof body == "string") { try { body = JSON.parse(body); - } catch(e) { - err = {error: "Malformed response", code: -1}; + } catch (e) { + err = { error: "Malformed response", code: -1 }; } } if (!err) { result = body.success; - err = body.error; + err = body.error; } } if (err) { return res.error(err); - } else { + } else { return res.success(result); } }); From c9f8453171860b5dd0dd3d5073ae42cb1d09ccc4 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 18:53:37 -0500 Subject: [PATCH 11/48] Fix reversed roles lookup --- spec/ParseRole.spec.js | 65 +++++++++++++++++++++++++++++++++++++----- src/Auth.js | 14 +++++---- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 8b4f989f..7f19cd79 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -1,4 +1,4 @@ - +"use strict"; // Roles are not accessible without the master key, so they are not intended // for use by clients. We can manually test them using the master key. @@ -64,26 +64,30 @@ describe('Parse Role testing', () => { var rolesNames = ["FooRole", "BarRole", "BazRole"]; - var createRole = function(name, parent, user) { + var createRole = function(name, sibling, user) { var role = new Parse.Role(name, new Parse.ACL()); if (user) { var users = role.relation('users'); users.add(user); } - if (parent) { - role.relation('roles').add(parent); + if (sibling) { + role.relation('roles').add(sibling); } return role.save({}, { useMasterKey: true }); } var roleIds = {}; createTestUser().then( (user) => { - - return createRole(rolesNames[0], null, null).then( (aRole) => { + // Put the user on the 1st role + return createRole(rolesNames[0], null, user).then( (aRole) => { roleIds[aRole.get("name")] = aRole.id; + // set the 1st role as a sibling of the second + // user will should have 2 role now return createRole(rolesNames[1], aRole, null); }).then( (anotherRole) => { roleIds[anotherRole.get("name")] = anotherRole.id; - return createRole(rolesNames[2], anotherRole, user); + // set this role as a sibling of the last + // the user should now have 3 roles + return createRole(rolesNames[2], anotherRole, null); }).then( (lastRole) => { roleIds[lastRole.get("name")] = lastRole.id; var auth = new Auth({ config: new Config("test"), isMaster: true, user: user }); @@ -118,6 +122,53 @@ describe('Parse Role testing', () => { }); }); }); + + it("Should properly resolve roles", (done) => { + let admin = new Parse.Role("Admin", new Parse.ACL()); + let moderator = new Parse.Role("Moderator", new Parse.ACL()); + let contentCreator = new Parse.Role('ContentManager', new Parse.ACL()); + + Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}).then(() => { + contentCreator.getRoles().add(moderator); + moderator.getRoles().add(admin); + return Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}); + }).then(() => { + var auth = new Auth({ config: new Config("test"), isMaster: true }); + // For each role, fetch their sibling, what they inherit + // return with result and roleId for later comparison + let promises = [admin, moderator, contentCreator].map((role) => { + return auth._getAllRoleNamesForId(role.id).then((result) => { + return Parse.Promise.as({ + id: role.id, + roleIds: result + }); + }) + }); + + return Parse.Promise.when(promises); + }).then((results) => { + + results.forEach((result) => { + let id = result.id; + let roleIds = result.roleIds; + if (id == admin.id) { + expect(roleIds.length).toBe(2); + expect(roleIds.indexOf(moderator.id)).not.toBe(-1); + expect(roleIds.indexOf(contentCreator.id)).not.toBe(-1); + } else if (id == moderator.id) { + expect(roleIds.length).toBe(1); + expect(roleIds.indexOf(contentCreator.id)).toBe(0); + } else if (id == contentCreator.id) { + expect(roleIds.length).toBe(0); + } + }); + done(); + }).fail((err) => { + console.error(err); + done(); + }) + + }); }); diff --git a/src/Auth.js b/src/Auth.js index 0b285789..9fae8b35 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -139,18 +139,18 @@ Auth.prototype._loadRoles = function() { }; // Given a role object id, get any other roles it is part of -// TODO: Make recursive to support role nesting beyond 1 level deep Auth.prototype._getAllRoleNamesForId = function(roleID) { + + // As per documentation, a Role inherits AnotherRole + // if this Role is in the roles pointer of this AnotherRole + // Let's find all the roles where this role is in a roles relation var rolePointer = { __type: 'Pointer', className: '_Role', objectId: roleID }; var restWhere = { - '$relatedTo': { - key: 'roles', - object: rolePointer - } + 'roles': rolePointer }; var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); @@ -161,6 +161,10 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) { } var roleIDs = results.map(r => r.objectId); + // we found a list of roles where the roleID + // is referenced in the roles relation, + // Get the roles where those found roles are also + // referenced the same way var parentRolesPromises = roleIDs.map( (roleId) => { return this._getAllRoleNamesForId(roleId); }); From 17bc79b372becbfab526aa719e44a7306dd8f32e Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Fri, 4 Mar 2016 19:40:22 -0500 Subject: [PATCH 12/48] Improves tests, ensure unicity of roleIds --- spec/ParseRole.spec.js | 29 ++++++++++++++++++----------- src/Auth.js | 9 ++------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 7f19cd79..636b8338 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -126,20 +126,23 @@ describe('Parse Role testing', () => { it("Should properly resolve roles", (done) => { let admin = new Parse.Role("Admin", new Parse.ACL()); let moderator = new Parse.Role("Moderator", new Parse.ACL()); - let contentCreator = new Parse.Role('ContentManager', new Parse.ACL()); - - Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}).then(() => { - contentCreator.getRoles().add(moderator); - moderator.getRoles().add(admin); - return Parse.Object.saveAll([admin, moderator, contentCreator], {useMasterKey: true}); + let superModerator = new Parse.Role("SuperModerator", new Parse.ACL()); + let contentManager = new Parse.Role('ContentManager', new Parse.ACL()); + let superContentManager = new Parse.Role('SuperContentManager', new Parse.ACL()); + Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}).then(() => { + contentManager.getRoles().add([moderator, superContentManager]); + moderator.getRoles().add([admin, superModerator]); + superContentManager.getRoles().add(superModerator); + return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}); }).then(() => { var auth = new Auth({ config: new Config("test"), isMaster: true }); // For each role, fetch their sibling, what they inherit // return with result and roleId for later comparison - let promises = [admin, moderator, contentCreator].map((role) => { + let promises = [admin, moderator, contentManager, superModerator].map((role) => { return auth._getAllRoleNamesForId(role.id).then((result) => { return Parse.Promise.as({ id: role.id, + name: role.get('name'), roleIds: result }); }) @@ -147,19 +150,23 @@ describe('Parse Role testing', () => { return Parse.Promise.when(promises); }).then((results) => { - results.forEach((result) => { let id = result.id; let roleIds = result.roleIds; if (id == admin.id) { expect(roleIds.length).toBe(2); expect(roleIds.indexOf(moderator.id)).not.toBe(-1); - expect(roleIds.indexOf(contentCreator.id)).not.toBe(-1); + expect(roleIds.indexOf(contentManager.id)).not.toBe(-1); } else if (id == moderator.id) { expect(roleIds.length).toBe(1); - expect(roleIds.indexOf(contentCreator.id)).toBe(0); - } else if (id == contentCreator.id) { + expect(roleIds.indexOf(contentManager.id)).toBe(0); + } else if (id == contentManager.id) { expect(roleIds.length).toBe(0); + } else if (id == superModerator.id) { + expect(roleIds.length).toBe(3); + expect(roleIds.indexOf(moderator.id)).not.toBe(-1); + expect(roleIds.indexOf(contentManager.id)).not.toBe(-1); + expect(roleIds.indexOf(superContentManager.id)).not.toBe(-1); } }); done(); diff --git a/src/Auth.js b/src/Auth.js index 9fae8b35..b45f93f3 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -173,14 +173,9 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) { }).then(function(results){ // Flatten let roleIDs = results.reduce( (memo, result) => { - if (typeof result == "object") { - memo = memo.concat(result); - } else { - memo.push(result); - } - return memo; + return memo.concat(result); }, []); - return Promise.resolve(roleIDs); + return Promise.resolve([...new Set(roleIDs)]); }); }; From 7cc059973b30df8c449b51a7bfbe348469aa751b Mon Sep 17 00:00:00 2001 From: Aneesh Devasthale Date: Sat, 5 Mar 2016 10:47:27 +0530 Subject: [PATCH 13/48] Modified the npm dev script to support Windows Windows does not support shebangs/hashbangs. Added the node command to run the bin/dev script. Extension of #831 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1b6199cf..ee0e0b4e 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "nodemon": "^1.8.1" }, "scripts": { - "dev": "npm run build && bin/dev", + "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", "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", From 4f643d970a947bc1446d4161ad1b90dfe2d4dd36 Mon Sep 17 00:00:00 2001 From: Long Nguyen Date: Sun, 6 Mar 2016 01:32:50 +0700 Subject: [PATCH 14/48] Fix create wrong _Session for Facebook login --- spec/RestCreate.spec.js | 9 +++++++++ src/RestWrite.js | 6 +++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index cddfd598..f9b94b37 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -175,17 +175,26 @@ describe('rest create', () => { } } }; + var newUserSignedUpByFacebookObjectId; rest.create(config, auth.nobody(config), '_User', data) .then((r) => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); + newUserSignedUpByFacebookObjectId = r.response.objectId; return rest.create(config, auth.nobody(config), '_User', data); }).then((r) => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); + expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId); + return rest.find(config, auth.master(config), + '_Session', {sessionToken: r.response.sessionToken}); + }).then((response) => { + expect(response.results.length).toEqual(1); + var output = response.results[0]; + expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId); done(); }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index a907a61c..d42f2f45 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -178,7 +178,11 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function() { this.data.updatedAt = this.updatedAt; if (!this.query) { this.data.createdAt = this.updatedAt; - this.data.objectId = cryptoUtils.newObjectId(); + + // Only assign new objectId if we are creating new object + if (!this.data.objectId) { + this.data.objectId = cryptoUtils.newObjectId(); + } } } return Promise.resolve(); From 91eeca3500edab78301004c6481747d209b6c134 Mon Sep 17 00:00:00 2001 From: Igor Shubovych Date: Sun, 6 Mar 2016 00:15:32 +0200 Subject: [PATCH 15/48] Fix Markdown format: make checkboxes visible https://youtu.be/ziSkbxWwDIQ?t=26 --- .github/ISSUE_TEMPLATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0fb61964..10ef133f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,10 +1,10 @@ Make sure these boxes are checked before submitting your issue -- thanks for reporting issues back to Parse Server! --[ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites). +- [ ] You've met the [prerequisites](https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#prerequisites). --[ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server. +- [ ] You're running the [latest version](https://github.com/ParsePlatform/parse-server/releases) of Parse Server. --[ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before. +- [ ] You've searched through [existing issues](https://github.com/ParsePlatform/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before. #### Environment Setup From 4ee9cb754c1693192a3cdebcacfcd8475b2668f3 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 00:34:44 -0800 Subject: [PATCH 16/48] Fix for related query on non-existing column --- spec/ParseRole.spec.js | 20 ++++++++++++++++++++ src/Controllers/DatabaseController.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 8b4f989f..5a9091d2 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -119,5 +119,25 @@ describe('Parse Role testing', () => { }); }); + it('can create role and query empty users', (done)=> { + var roleACL = new Parse.ACL(); + roleACL.setPublicReadAccess(true); + var role = new Parse.Role('subscribers', roleACL); + role.save({}, {useMasterKey : true}) + .then((x)=>{ + var query = role.relation('users').query(); + query.find({useMasterKey : true}) + .then((users)=>{ + done(); + }, (e)=>{ + fail('should not have errors'); + done(); + }); + }, (e) => { + console.log(e); + fail('should not have errored'); + }); + }); + }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 683b9be0..98243acb 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -89,7 +89,7 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { DatabaseController.prototype.redirectClassNameForKey = function(className, key) { return this.loadSchema().then((schema) => { var t = schema.getExpectedType(className, key); - var match = t.match(/^relation<(.*)>$/); + var match = t ? t.match(/^relation<(.*)>$/) : false; if (match) { return match[1]; } else { From 2d4c08c5a3d0e8ee9e44ecb76b52dc2f771dcab5 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 01:03:51 -0800 Subject: [PATCH 17/48] Test empty authData block on login for #413 --- spec/ParseAPI.spec.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 42ac3491..ef05201c 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -995,4 +995,32 @@ describe('miscellaneous', function() { }); }); + it('android login providing empty authData block works', (done) => { + let headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest' + }; + let data = { + username: 'pulse1989', + password: 'password1234', + authData: {} + }; + let requestOptions = { + headers: headers, + url: 'http://localhost:8378/1/users', + body: JSON.stringify(data) + }; + request.post(requestOptions, (error, response, body) => { + expect(error).toBe(null); + requestOptions.url = 'http://localhost:8378/1/login'; + request.get(requestOptions, (error, response, body) => { + expect(error).toBe(null); + let b = JSON.parse(body); + expect(typeof b['sessionToken']).toEqual('string'); + done(); + }); + }); + }); + }); From 908a4eb6436e5b616c11cce43a605f30cf2c0b28 Mon Sep 17 00:00:00 2001 From: Marco129 Date: Sun, 6 Mar 2016 17:22:36 +0800 Subject: [PATCH 18/48] Fix delete relation field when _Join collection not exist --- spec/Schema.spec.js | 32 ++++++++++++++++++++++++++++++++ src/Schema.js | 6 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 62ce711e..6ab08d6a 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -577,6 +577,38 @@ describe('Schema', () => { }); }); + it('can delete relation field when related _Join collection not exist', done => { + config.database.loadSchema() + .then(schema => { + schema.addClassIfNotExists('NewClass', { + relationField: {type: 'Relation', targetClass: '_User'} + }) + .then(mongoObj => { + expect(mongoObj).toEqual({ + _id: 'NewClass', + objectId: 'string', + updatedAt: 'string', + createdAt: 'string', + relationField: 'relation<_User>', + }); + }) + .then(() => config.database.collectionExists('_Join:relationField:NewClass')) + .then(exist => { + expect(exist).toEqual(false); + }) + .then(() => schema.deleteField('relationField', 'NewClass', config.database)) + .then(() => schema.reloadData()) + .then(() => { + expect(schema['data']['NewClass']).toEqual({ + objectId: 'string', + updatedAt: 'string', + createdAt: 'string' + }); + done(); + }); + }); + }); + it('can delete string fields and resave as number field', done => { Parse.Object.disableSingleInstance(); var obj1 = hasAllPODobject(); diff --git a/src/Schema.js b/src/Schema.js index 73eaa325..0ed55527 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -409,7 +409,11 @@ class Schema { if (this.data[className][fieldName].startsWith('relation<')) { //For relations, drop the _Join table - return database.dropCollection(`_Join:${fieldName}:${className}`); + return database.collectionExists(`_Join:${fieldName}:${className}`).then(exist => { + if (exist) { + return database.dropCollection(`_Join:${fieldName}:${className}`); + } + }); } // for non-relations, remove all the data. From 3266d59fccdea86eceb57c8bb2f5426d828e8018 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 01:56:10 -0800 Subject: [PATCH 19/48] beforeSave changes should propagate to the response --- spec/ParseAPI.spec.js | 17 +++++++++++++++++ src/RestWrite.js | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 42ac3491..2412ac5e 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -967,6 +967,23 @@ 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/src/RestWrite.js b/src/RestWrite.js index d42f2f45..72eda1fc 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -164,6 +164,7 @@ RestWrite.prototype.runBeforeTrigger = function() { }).then((response) => { if (response && response.object) { 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 @@ -806,6 +807,9 @@ RestWrite.prototype.runDatabaseOperation = function() { objectId: this.data.objectId, createdAt: this.data.createdAt }; + if (this.storage['changedByTrigger']) { + Object.assign(resp, this.data); + } if (this.storage['token']) { resp.sessionToken = this.storage['token']; } From 1450795516dd5948a4770398de642db9bb61ad52 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 03:32:49 -0800 Subject: [PATCH 20/48] Remove limit when counting results. --- src/RestQuery.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RestQuery.js b/src/RestQuery.js index 9a4764a9..1e0f344e 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -396,6 +396,7 @@ RestQuery.prototype.runCount = function() { } this.findOptions.count = true; delete this.findOptions.skip; + delete this.findOptions.limit; return this.config.database.find( this.className, this.restWhere, this.findOptions).then((c) => { this.response.count = c; From f23aed522a5857dca3e4a26c65817b06ce6d31b2 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 6 Mar 2016 17:20:11 -0500 Subject: [PATCH 21/48] Allow crossdomain on filesRouter --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 6244fc38..e4554d1a 100644 --- a/src/index.js +++ b/src/index.js @@ -182,7 +182,7 @@ function ParseServer({ var api = express(); //api.use("/apps", express.static(__dirname + "/public")); // File handling needs to be before default middlewares are applied - api.use('/', new FilesRouter().getExpressRouter({ + api.use('/', middlewares.allowCrossDomain, new FilesRouter().getExpressRouter({ maxUploadSize: maxUploadSize })); From 3a06117fa13a09aabaf01129efb4eb5f680d4023 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Sun, 6 Mar 2016 18:26:10 -0800 Subject: [PATCH 22/48] Adding a role scenario test for issue 827 --- spec/ParseRole.spec.js | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 1b7fbcca..f48fbf7f 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -197,5 +197,84 @@ describe('Parse Role testing', () => { }); }); + // Based on various scenarios described in issues #827 and #683, + it('should properly handle role permissions on objects', (done) => { + var user, user2, user3; + var role, role2, role3; + var obj, obj2; + + var prACL = new Parse.ACL(); + prACL.setPublicReadAccess(true); + var adminACL, superACL, customerACL; + + createTestUser().then((x) => { + user = x; + user2 = new Parse.User(); + return user2.save({ username: 'user2', password: 'omgbbq' }); + }).then((x) => { + user3 = new Parse.User(); + return user3.save({ username: 'user3', password: 'omgbbq' }); + }).then((x) => { + role = new Parse.Role('Admin', prACL); + role.getUsers().add(user); + return role.save({}, { useMasterKey: true }); + }).then(() => { + adminACL = new Parse.ACL(); + adminACL.setRoleReadAccess("Admin", true); + adminACL.setRoleWriteAccess("Admin", true); + + role2 = new Parse.Role('Super', prACL); + role2.getUsers().add(user2); + return role2.save({}, { useMasterKey: true }); + }).then(() => { + superACL = new Parse.ACL(); + superACL.setRoleReadAccess("Super", true); + superACL.setRoleWriteAccess("Super", true); + + role.getRoles().add(role2); + return role.save({}, { useMasterKey: true }); + }).then(() => { + role3 = new Parse.Role('Customer', prACL); + role3.getUsers().add(user3); + role3.getRoles().add(role); + return role3.save({}, { useMasterKey: true }); + }).then(() => { + customerACL = new Parse.ACL(); + customerACL.setRoleReadAccess("Customer", true); + customerACL.setRoleWriteAccess("Customer", true); + + var query = new Parse.Query('_Role'); + return query.find({ useMasterKey: true }); + }).then((x) => { + expect(x.length).toEqual(3); + + obj = new Parse.Object('TestObjectRoles'); + obj.set('ACL', customerACL); + return obj.save(null, { useMasterKey: true }); + }).then(() => { + // Above, the Admin role was added to the Customer role. + // An object secured by the Customer ACL should be able to be edited by the Admin user. + obj.set('changedByAdmin', true); + return obj.save(null, { sessionToken: user.getSessionToken() }); + }).then(() => { + obj2 = new Parse.Object('TestObjectRoles'); + obj2.set('ACL', adminACL); + return obj2.save(null, { useMasterKey: true }); + }, (e) => { + fail('Admin user should have been able to save.'); + done(); + }).then(() => { + // An object secured by the Admin ACL should not be able to be edited by a Customer role user. + obj2.set('changedByCustomer', true); + return obj2.save(null, { sessionToken: user3.getSessionToken() }); + }).then(() => { + fail('Customer user should not have been able to save.'); + done(); + }, (e) => { + expect(e.code).toEqual(101); + done(); + }) + }); + }); From 91ff816839daf9099d117c95252f0da27963b27c Mon Sep 17 00:00:00 2001 From: Marco129 Date: Mon, 7 Mar 2016 14:14:28 +0800 Subject: [PATCH 23/48] Improve delete flow for non-existence _Join collection --- src/Schema.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Schema.js b/src/Schema.js index 0ed55527..545b4953 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -409,10 +409,14 @@ class Schema { if (this.data[className][fieldName].startsWith('relation<')) { //For relations, drop the _Join table - return database.collectionExists(`_Join:${fieldName}:${className}`).then(exist => { - if (exist) { - return database.dropCollection(`_Join:${fieldName}:${className}`); + return database.dropCollection(`_Join:${fieldName}:${className}`) + .then(() => { + return Promise.resolve(); + }, error => { + if (error.message == 'ns not found') { + return Promise.resolve(); } + return Promise.reject(error); }); } From ce35b81cc6d6ab0b774f59b71ea0c235ebd73441 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Mon, 7 Mar 2016 00:30:21 -0800 Subject: [PATCH 24/48] New things for GCS Adapter --- spec/FilesControllerTestFactory.js | 25 ++++++++++++------------ src/Adapters/AdapterLoader.js | 7 +++---- src/Adapters/Files/GCSAdapter.js | 27 ++++++++++++++++++++------ src/Controllers/AdaptableController.js | 18 ++++++++--------- src/Controllers/FilesController.js | 8 +++++--- 5 files changed, 50 insertions(+), 35 deletions(-) diff --git a/spec/FilesControllerTestFactory.js b/spec/FilesControllerTestFactory.js index 217a383a..b467d031 100644 --- a/spec/FilesControllerTestFactory.js +++ b/spec/FilesControllerTestFactory.js @@ -1,35 +1,34 @@ - var FilesController = require('../src/Controllers/FilesController').FilesController; var Config = require("../src/Config"); var testAdapter = function(name, adapter) { // Small additional tests to improve overall coverage - + var config = new Config(Parse.applicationId); var filesController = new FilesController(adapter); describe("FilesController with "+name,()=>{ - + it("should properly expand objects", (done) => { - + var result = filesController.expandFilesInObject(config, function(){}); - + expect(result).toBeUndefined(); - + var fullFile = { type: '__type', url: "http://an.url" } - + var anObject = { aFile: fullFile } filesController.expandFilesInObject(config, anObject); expect(anObject.aFile.url).toEqual("http://an.url"); - + done(); - }) - + }) + it("should properly create, read, delete files", (done) => { var filename; filesController.createFile(config, "file.txt", "hello world").then( (result) => { @@ -51,14 +50,14 @@ var testAdapter = function(name, adapter) { console.error(err); done(); }).then((result) => { - + filesController.getFileData(config, filename).then((res) => { fail("the file should be deleted"); done(); }, (err) => { - done(); + done(); }); - + }, (err) => { fail("The adapter should delete the file"); console.error(err); diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index a9521f0b..654948e9 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -1,6 +1,5 @@ export function loadAdapter(adapter, defaultAdapter, options) { - - if (!adapter) + if (!adapter) { if (!defaultAdapter) { return options; @@ -20,7 +19,7 @@ export function loadAdapter(adapter, defaultAdapter, options) { if (adapter.default) { adapter = adapter.default; } - + return loadAdapter(adapter, undefined, options); } else if (adapter.module) { return loadAdapter(adapter.module, undefined, adapter.options); @@ -30,7 +29,7 @@ export function loadAdapter(adapter, defaultAdapter, options) { return loadAdapter(adapter.adapter, undefined, adapter.options); } // return the adapter as provided - return adapter; + return adapter; } export default loadAdapter; diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js index e749502d..065592eb 100644 --- a/src/Adapters/Files/GCSAdapter.js +++ b/src/Adapters/Files/GCSAdapter.js @@ -4,18 +4,33 @@ import { storage } from 'gcloud'; import { FilesAdapter } from './FilesAdapter'; import requiredParameter from '../../requiredParameter'; +function requiredOrFromEnvironment(env, name) { + let environmentVariable = process.env[env]; + if (!environmentVariable) { + requiredParameter(`GCSAdapter requires an ${name}`); + } + return environmentVariable; +} + +function fromEnvironmentOrDefault(env, defaultValue) { + let environmentVariable = process.env[env]; + if (environmentVariable) { + return environmentVariable; + } + return defaultValue; +} + export class GCSAdapter extends FilesAdapter { // GCS Project ID and the name of a corresponding Keyfile are required. // Unlike the S3 adapter, you must create a new Cloud Storage bucket, as this is not created automatically. // See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication // for more details. constructor( - projectId = requiredParameter('GCSAdapter requires a GCP Project ID'), - keyFilename = requiredParameter('GCSAdapter requires a GCP keyfile'), - bucket = requiredParameter('GCSAdapter requires a GCS bucket name'), - { bucketPrefix = '', - directAccess = false } = {} - ) { + projectId = requiredOrFromEnvironment('GCP_PROJECT_ID', 'projectId'), + keyFilename = requiredOrFromEnvironment('GCP_KEYFILE_PATH', 'keyfile path'), + bucket = requiredOrFromEnvironment('GCS_BUCKET_NAME', 'bucket name'), + { bucketPrefix = fromEnvironmentOrDefault('GCS_BUCKET_PREFIX', ''), + directAccess = fromEnvironmentOrDefault('GCS_DIRECT_ACCESS', false) } = {}) { super(); this._bucket = bucket; diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index ab7d7156..7ff8ce29 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -2,7 +2,7 @@ AdaptableController.js AdaptableController is the base class for all controllers -that support adapter, +that support adapter, The super class takes care of creating the right instance for the adapter based on the parameters passed @@ -28,30 +28,30 @@ export class AdaptableController { this.validateAdapter(adapter); this[_adapter] = adapter; } - + get adapter() { return this[_adapter]; } - + get config() { return new Config(this.appId); } - + expectedAdapterType() { throw new Error("Subclasses should implement expectedAdapterType()"); } - + validateAdapter(adapter) { if (!adapter) { throw new Error(this.constructor.name+" requires an adapter"); } - + let Type = this.expectedAdapterType(); // Allow skipping for testing - if (!Type) { + if (!Type) { return; } - + // Makes sure the prototype matches let mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( (obj, key) => { const adapterType = typeof adapter[key]; @@ -64,7 +64,7 @@ export class AdaptableController { } return obj; }, {}); - + if (Object.keys(mismatches).length > 0) { throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches); } diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 712e326c..9abd87ff 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -13,11 +13,11 @@ export class FilesController extends AdaptableController { } createFile(config, filename, data, contentType) { - + let extname = path.extname(filename); - + const hasExtension = extname.length > 0; - + if (!hasExtension && contentType && mime.extension(contentType)) { filename = filename + '.' + mime.extension(contentType); } else if (hasExtension && !contentType) { @@ -27,6 +27,8 @@ export class FilesController extends AdaptableController { filename = randomHexString(32) + '_' + filename; var location = this.adapter.getFileLocation(config, filename); + console.log(this.adapter); + console.log(location); return this.adapter.createFile(config, filename, data, contentType).then(() => { return Promise.resolve({ url: location, From 0f00d659cb95cbcf4baa683e3c4205f42632dae6 Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Mon, 7 Mar 2016 00:34:41 -0800 Subject: [PATCH 25/48] Removed extraneous console.log() --- src/Adapters/Files/GCSAdapter.js | 2 -- src/Controllers/FilesController.js | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js index 065592eb..8bd19447 100644 --- a/src/Adapters/Files/GCSAdapter.js +++ b/src/Adapters/Files/GCSAdapter.js @@ -82,7 +82,6 @@ export class GCSAdapter extends FilesAdapter { return new Promise((resolve, reject) => { let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename); file.delete((err, res) => { - console.log("delete: ", filename, err, res); if(err !== null) { return reject(err); } @@ -100,7 +99,6 @@ export class GCSAdapter extends FilesAdapter { file.exists((err, exists) => { if (exists) { file.download((err, data) => { - console.log("get: ", filename, err, data); if (err !== null) { return reject(err); } diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 9abd87ff..335dfdf2 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -27,8 +27,6 @@ export class FilesController extends AdaptableController { filename = randomHexString(32) + '_' + filename; var location = this.adapter.getFileLocation(config, filename); - console.log(this.adapter); - console.log(location); return this.adapter.createFile(config, filename, data, contentType).then(() => { return Promise.resolve({ url: location, From 2c5144028bf5c641224a0fbd5accc63569dc2e6b Mon Sep 17 00:00:00 2001 From: Mike McDonald Date: Mon, 7 Mar 2016 00:47:08 -0800 Subject: [PATCH 26/48] Added tests to adapter loader, cleaned up README, renamed to GCS_BUCKET from GCS_BUCKET_NAME --- README.md | 22 ++++++++++++++++-- spec/AdapterLoader.spec.js | 40 ++++++++++++++++++++------------ spec/FilesController.spec.js | 8 +++---- src/Adapters/Files/GCSAdapter.js | 2 +- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b0a8d015..c4a86d10 100644 --- a/README.md +++ b/README.md @@ -135,9 +135,14 @@ PARSE_SERVER_MAX_UPLOAD_SIZE ``` -##### Configuring S3 Adapter +##### Configuring File Adapters +Parse Server allows developers to choose from several options when hosting files: the `GridStoreAdapter`, which backed by MongoDB; the `S3Adapter`, which is backed by [Amazon S3](https://aws.amazon.com/s3/); or the `GCSAdapter`, which is backed by [Google Cloud Storage](https://cloud.google.com/storage/). -You can use the following environment variable setup the S3 adapter +`GridStoreAdapter` is used by default and requires no setup, but if you're interested in using S3 or GCS, additional configuration information is available below. + +###### Configuring `S3Adapter` + +You can use the following environment variable setup to enable the S3 adapter: ```js S3_ACCESS_KEY @@ -149,6 +154,19 @@ S3_DIRECT_ACCESS ``` +###### Configuring `GCSAdapter` + +You can use the following environment variable setup to enable the GCS adapter: + +```js +GCP_PROJECT_ID +GCP_KEYFILE_PATH +GCS_BUCKET +GCS_BUCKET_PREFIX +GCS_DIRECT_ACCESS + +``` + ## Contributing We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Server guide](CONTRIBUTING.md). diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index 69381fc5..56bf0d44 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -3,44 +3,45 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter; var FilesAdapter = require("../src/Adapters/Files/FilesAdapter").default; var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter"); var S3Adapter = require("../src/Adapters/Files/S3Adapter").default; +var GCSAdapter = require("../src/Adapters/Files/GCSAdapter").default; describe("AdapterLoader", ()=>{ - + it("should instantiate an adapter from string in object", (done) => { var adapterPath = require('path').resolve("./spec/MockAdapter"); var adapter = loadAdapter({ adapter: adapterPath, options: { - key: "value", + key: "value", foo: "bar" } }); - + expect(adapter instanceof Object).toBe(true); expect(adapter.options.key).toBe("value"); expect(adapter.options.foo).toBe("bar"); done(); }); - + it("should instantiate an adapter from string", (done) => { var adapterPath = require('path').resolve("./spec/MockAdapter"); var adapter = loadAdapter(adapterPath); - + expect(adapter instanceof Object).toBe(true); done(); }); - + it("should instantiate an adapter from string that is module", (done) => { var adapterPath = require('path').resolve("./src/Adapters/Files/FilesAdapter"); var adapter = loadAdapter({ adapter: adapterPath }); - + expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - + it("should instantiate an adapter from function/Class", (done) => { var adapter = loadAdapter({ adapter: FilesAdapter @@ -48,27 +49,27 @@ describe("AdapterLoader", ()=>{ expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - + it("should instantiate the default adapter from Class", (done) => { var adapter = loadAdapter(null, FilesAdapter); expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - + it("should use the default adapter", (done) => { var defaultAdapter = new FilesAdapter(); var adapter = loadAdapter(null, defaultAdapter); expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - + it("should use the provided adapter", (done) => { var originalAdapter = new FilesAdapter(); var adapter = loadAdapter(originalAdapter); expect(adapter).toBe(originalAdapter); done(); }); - + it("should fail loading an improperly configured adapter", (done) => { var Adapter = function(options) { if (!options.foo) { @@ -79,14 +80,14 @@ describe("AdapterLoader", ()=>{ param: "key", doSomething: function() {} }; - + expect(() => { var adapter = loadAdapter(adapterOptions, Adapter); expect(adapter).toEqual(adapterOptions); }).not.toThrow("foo is required for that adapter"); done(); }); - + it("should load push adapter from options", (done) => { var options = { ios: { @@ -100,7 +101,7 @@ describe("AdapterLoader", ()=>{ }).not.toThrow(); done(); }); - + it("should load S3Adapter from direct passing", (done) => { var s3Adapter = new S3Adapter("key", "secret", "bucket") expect(() => { @@ -109,4 +110,13 @@ describe("AdapterLoader", ()=>{ }).not.toThrow(); done(); }) + + it("should load GCSAdapter from direct passing", (done) => { + var gcsAdapter = new GCSAdapter("projectId", "path/to/keyfile", "bucket") + expect(() => { + var adapter = loadAdapter(gcsAdapter, FilesAdapter); + expect(adapter).toBe(gcsAdapter); + }).not.toThrow(); + done(); + }) }); diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index 183dcb27..3b2108e7 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -32,21 +32,21 @@ describe("FilesController",()=>{ console.log("set S3_ACCESS_KEY and S3_SECRET_KEY to test S3Adapter") } - if (process.env.GCP_PROJECT_ID && process.env.GCP_KEYFILE_PATH && process.env.GCS_BUCKET_NAME) { + if (process.env.GCP_PROJECT_ID && process.env.GCP_KEYFILE_PATH && process.env.GCS_BUCKET) { // Test the GCS Adapter - var gcsAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET_NAME); + var gcsAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET); FCTestFactory.testAdapter("GCSAdapter", gcsAdapter); // Test GCS with direct access - var gcsDirectAccessAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET_NAME, { + var gcsDirectAccessAdapter = new GCSAdapter(process.env.GCP_PROJECT_ID, process.env.GCP_KEYFILE_PATH, process.env.GCS_BUCKET, { directAccess: true }); FCTestFactory.testAdapter("GCSAdapterDirect", gcsDirectAccessAdapter); } else if (!process.env.TRAVIS) { - console.log("set GCP_PROJECT_ID, GCP_KEYFILE_PATH, and GCS_BUCKET_NAME to test GCSAdapter") + console.log("set GCP_PROJECT_ID, GCP_KEYFILE_PATH, and GCS_BUCKET to test GCSAdapter") } }); diff --git a/src/Adapters/Files/GCSAdapter.js b/src/Adapters/Files/GCSAdapter.js index 8bd19447..8fb34af8 100644 --- a/src/Adapters/Files/GCSAdapter.js +++ b/src/Adapters/Files/GCSAdapter.js @@ -28,7 +28,7 @@ export class GCSAdapter extends FilesAdapter { constructor( projectId = requiredOrFromEnvironment('GCP_PROJECT_ID', 'projectId'), keyFilename = requiredOrFromEnvironment('GCP_KEYFILE_PATH', 'keyfile path'), - bucket = requiredOrFromEnvironment('GCS_BUCKET_NAME', 'bucket name'), + bucket = requiredOrFromEnvironment('GCS_BUCKET', 'bucket name'), { bucketPrefix = fromEnvironmentOrDefault('GCS_BUCKET_PREFIX', ''), directAccess = fromEnvironmentOrDefault('GCS_DIRECT_ACCESS', false) } = {}) { super(); From 5cdcadea36a4d326c29118dcc6a80f1355770838 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 08:26:35 -0500 Subject: [PATCH 27/48] Fixes bug when querying equalTo on objectId and relation - Adds $eq operator in transform - Makes $eq operator on objectId when adding $in operator --- spec/ParseRelation.spec.js | 40 +++++++++++++++++++++++++++ src/Controllers/DatabaseController.js | 17 ++++++++---- src/transform.js | 1 + 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index e1416ecb..403628ed 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -329,6 +329,46 @@ describe('Parse.Relation testing', () => { }); }); + it("query on pointer and relation fields with equal bis", (done) => { + var ChildObject = Parse.Object.extend("ChildObject"); + var childObjects = []; + for (var i = 0; i < 10; i++) { + childObjects.push(new ChildObject({x: i})); + } + + Parse.Object.saveAll(childObjects).then(() => { + var ParentObject = Parse.Object.extend("ParentObject"); + var parent = new ParentObject(); + parent.set("x", 4); + var relation = parent.relation("toChilds"); + relation.add(childObjects[0]); + relation.add(childObjects[1]); + relation.add(childObjects[2]); + + var parent2 = new ParentObject(); + parent2.set("x", 3); + parent2.relation("toChilds").add(childObjects[2]); + + var parents = []; + parents.push(parent); + parents.push(parent2); + parents.push(new ParentObject()); + + return Parse.Object.saveAll(parents).then(() => { + var query = new Parse.Query(ParentObject); + query.equalTo("objectId", parent2.id); + // childObjects[2] is in 2 relations + // before the fix, that woul yield 2 results + query.equalTo("toChilds", childObjects[2]); + + return query.find().then((list) => { + equal(list.length, 1, "There should be 1 result"); + done(); + }); + }); + }); + }); + it("or queries on pointer and relation fields", (done) => { var ChildObject = Parse.Object.extend("ChildObject"); var childObjects = []; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 98243acb..042a086d 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -455,13 +455,18 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) { DatabaseController.prototype.addInObjectIdsIds = function(ids, query) { if (typeof query.objectId == 'string') { - query.objectId = {'$in': [query.objectId]}; + // Add equality op as we are sure + // we had a constraint on that one + query.objectId = {'$eq': query.objectId}; } - query.objectId = query.objectId || {}; - let queryIn = [].concat(query.objectId['$in'] || [], ids || []); - // make a set and spread to remove duplicates - query.objectId = {'$in': [...new Set(queryIn)]}; - return query; + query.objectId = query.objectId || {}; + let queryIn = [].concat(query.objectId['$in'] || [], ids || []); + // make a set and spread to remove duplicates + // replace the $in operator as other constraints + // may be set + query.objectId['$in'] = [...new Set(queryIn)]; + + return query; } // Runs a query on the database. diff --git a/src/transform.js b/src/transform.js index 8d75b58e..738f2453 100644 --- a/src/transform.js +++ b/src/transform.js @@ -412,6 +412,7 @@ function transformConstraint(constraint, inArray) { case '$gte': case '$exists': case '$ne': + case '$eq': answer[key] = transformAtom(constraint[key], true, {inArray: inArray}); break; From 963811d0225cdb8ef1d027b382711ae9dca7875c Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 7 Mar 2016 13:44:45 -0800 Subject: [PATCH 28/48] Handle legacy _client_permissions key in _SCHEMA. Fixes #888. --- spec/Schema.spec.js | 29 +++++++++++++++++++++++++++++ src/Routers/SchemasRouter.js | 30 +++++------------------------- src/Schema.js | 22 ++++++++++++++++++++++ 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 6ab08d6a..2912067f 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -703,4 +703,33 @@ describe('Schema', () => { }); done(); }); + + it('handles legacy _client_permissions keys without crashing', done => { + Schema.mongoSchemaToSchemaAPIResponse({ + "_id":"_Installation", + "_client_permissions":{ + "get":true, + "find":true, + "update":true, + "create":true, + "delete":true, + }, + "_metadata":{ + "class_permissions":{ + "get":{"*":true}, + "find":{"*":true}, + "update":{"*":true}, + "create":{"*":true}, + "delete":{"*":true}, + "addField":{"*":true}, + } + }, + "installationId":"string", + "deviceToken":"string", + "deviceType":"string", + "channels":"array", + "user":"*_User", + }); + done(); + }); }); diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 59fef02d..0d1a4cf9 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -4,7 +4,7 @@ var express = require('express'), Parse = require('parse/node').Parse, Schema = require('../Schema'); -import PromiseRouter from '../PromiseRouter'; +import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; function classNameMismatchResponse(bodyClass, pathClass) { @@ -14,30 +14,10 @@ function classNameMismatchResponse(bodyClass, pathClass) { ); } -function mongoSchemaAPIResponseFields(schema) { - var fieldNames = Object.keys(schema).filter(key => key !== '_id' && key !== '_metadata'); - var response = fieldNames.reduce((obj, fieldName) => { - obj[fieldName] = Schema.mongoFieldTypeToSchemaAPIType(schema[fieldName]) - return obj; - }, {}); - response.ACL = {type: 'ACL'}; - response.createdAt = {type: 'Date'}; - response.updatedAt = {type: 'Date'}; - response.objectId = {type: 'String'}; - return response; -} - -function mongoSchemaToSchemaAPIResponse(schema) { - return { - className: schema._id, - fields: mongoSchemaAPIResponseFields(schema), - }; -} - function getAllSchemas(req) { return req.config.database.adaptiveCollection('_SCHEMA') .then(collection => collection.find({})) - .then(schemas => schemas.map(mongoSchemaToSchemaAPIResponse)) + .then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse)) .then(schemas => ({ response: { results: schemas }})); } @@ -51,7 +31,7 @@ function getOneSchema(req) { } return results[0]; }) - .then(schema => ({ response: mongoSchemaToSchemaAPIResponse(schema) })); + .then(schema => ({ response: Schema.mongoSchemaToSchemaAPIResponse(schema) })); } function createSchema(req) { @@ -68,7 +48,7 @@ function createSchema(req) { return req.config.database.loadSchema() .then(schema => schema.addClassIfNotExists(className, req.body.fields)) - .then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) })); + .then(result => ({ response: Schema.mongoSchemaToSchemaAPIResponse(result) })); } function modifySchema(req) { @@ -118,7 +98,7 @@ function modifySchema(req) { if (err) { reject(err); } - resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)}); + resolve({ response: Schema.mongoSchemaToSchemaAPIResponse(mongoObject.result)}); }) })); }); diff --git a/src/Schema.js b/src/Schema.js index 545b4953..3e25b09d 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -760,6 +760,27 @@ function getObjectType(obj) { return 'object'; } +const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions']; +function mongoSchemaAPIResponseFields(schema) { + var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1); + var response = fieldNames.reduce((obj, fieldName) => { + obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName]) + return obj; + }, {}); + response.ACL = {type: 'ACL'}; + response.createdAt = {type: 'Date'}; + response.updatedAt = {type: 'Date'}; + response.objectId = {type: 'String'}; + return response; +} + +function mongoSchemaToSchemaAPIResponse(schema) { + return { + className: schema._id, + fields: mongoSchemaAPIResponseFields(schema), + }; +} + module.exports = { load: load, classNameIsValid: classNameIsValid, @@ -768,4 +789,5 @@ module.exports = { schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, buildMergedSchemaObject: buildMergedSchemaObject, mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType, + mongoSchemaToSchemaAPIResponse, }; From cea4b2bd6add0994983e7ba672c63c018e2933ed Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 14:06:46 -0800 Subject: [PATCH 29/48] Migrate and fix GlobalConfig database storage. --- src/Routers/GlobalConfigRouter.js | 38 ++++++++++++------------------- 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 53abdac5..156cecf6 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -1,38 +1,28 @@ // global_config.js -var Parse = require('parse/node').Parse; - import PromiseRouter from '../PromiseRouter'; import * as middleware from "../middlewares"; export class GlobalConfigRouter extends PromiseRouter { getGlobalConfig(req) { - return req.config.database.rawCollection('_GlobalConfig') - .then(coll => coll.findOne({'_id': 1})) - .then(globalConfig => ({response: { params: globalConfig.params }})) - .catch(() => ({ - status: 404, - response: { - code: Parse.Error.INVALID_KEY_NAME, - error: 'config does not exist', + return req.config.database.adaptiveCollection('_GlobalConfig') + .then(coll => coll.find({ '_id': 1 }, { limit: 1 })) + .then(results => { + if (results.length != 1) { + // If there is no config in the database - return empty config. + return { response: { params: {} } }; } - })); + let globalConfig = results[0]; + return { response: { params: globalConfig.params } }; + }); } + updateGlobalConfig(req) { - return req.config.database.rawCollection('_GlobalConfig') - .then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body })) - .then(response => { - return { response: { result: true } } - }) - .catch(() => ({ - status: 404, - response: { - code: Parse.Error.INVALID_KEY_NAME, - error: 'config cannot be updated', - } - })); + return req.config.database.adaptiveCollection('_GlobalConfig') + .then(coll => coll.upsertOne({ _id: 1 }, { $set: req.body })) + .then(() => ({ response: { result: true } })); } - + mountRoutes() { this.route('GET', '/config', req => { return this.getGlobalConfig(req) }); this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, req => { return this.updateGlobalConfig(req) }); From fa6954169e309a257933cf2bb27bdc42c27f1bc8 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 14:11:43 -0800 Subject: [PATCH 30/48] Migrate ParseGlobalConfig.spec to new database storage API. --- spec/ParseGlobalConfig.spec.js | 54 +++++++++---------- src/Adapters/Storage/Mongo/MongoCollection.js | 4 ++ 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index 399c9ee6..176c7860 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -5,21 +5,21 @@ var Parse = require('parse/node').Parse; let Config = require('../src/Config'); describe('a GlobalConfig', () => { - beforeEach(function(done) { + beforeEach(function (done) { let config = new Config('test'); - config.database.rawCollection('_GlobalConfig') - .then(coll => coll.updateOne({ '_id': 1}, { $set: { params: { companies: ['US', 'DK'] } } }, { upsert: true })) + config.database.adaptiveCollection('_GlobalConfig') + .then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } })) .then(done()); }); it('can be retrieved', (done) => { request.get({ - url: 'http://localhost:8378/1/config', - json: true, + url : 'http://localhost:8378/1/config', + json : true, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, + 'X-Parse-Master-Key' : 'test' + } }, (error, response, body) => { expect(response.statusCode).toEqual(200); expect(body.params.companies).toEqual(['US', 'DK']); @@ -29,13 +29,13 @@ describe('a GlobalConfig', () => { it('can be updated when a master key exists', (done) => { request.put({ - url: 'http://localhost:8378/1/config', - json: true, - body: { params: { companies: ['US', 'DK', 'SE'] } }, + url : 'http://localhost:8378/1/config', + json : true, + body : { params: { companies: ['US', 'DK', 'SE'] } }, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' - }, + 'X-Parse-Master-Key' : 'test' + } }, (error, response, body) => { expect(response.statusCode).toEqual(200); expect(body.result).toEqual(true); @@ -45,35 +45,35 @@ describe('a GlobalConfig', () => { it('fail to update if master key is missing', (done) => { request.put({ - url: 'http://localhost:8378/1/config', - json: true, - body: { params: { companies: [] } }, + url : 'http://localhost:8378/1/config', + json : true, + body : { params: { companies: [] } }, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' - }, + 'X-Parse-REST-API-Key' : 'rest' + } }, (error, response, body) => { expect(response.statusCode).toEqual(403); expect(body.error).toEqual('unauthorized: master key is required'); done(); }); - }); + }); it('failed getting config when it is missing', (done) => { let config = new Config('test'); - config.database.rawCollection('_GlobalConfig') - .then(coll => coll.deleteOne({ '_id': 1}, {}, {})) - .then(_ => { + config.database.adaptiveCollection('_GlobalConfig') + .then(coll => coll.deleteOne({ '_id': 1 })) + .then(() => { request.get({ - url: 'http://localhost:8378/1/config', - json: true, + url : 'http://localhost:8378/1/config', + json : true, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, + 'X-Parse-Master-Key' : 'test' + } }, (error, response, body) => { - expect(response.statusCode).toEqual(404); - expect(body.code).toEqual(Parse.Error.INVALID_KEY_NAME); + expect(response.statusCode).toEqual(200); + expect(body.params).toEqual({}); done(); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 84f47899..f19705c4 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -75,6 +75,10 @@ export default class MongoCollection { }); } + deleteOne(query) { + return this._mongoCollection.deleteOne(query); + } + remove(query) { return this._mongoCollection.remove(query); } From f2ead46580ed25637469a41b202e72a7cf6f3db4 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 14:16:26 -0800 Subject: [PATCH 31/48] Remove .rawCollection method from DatabaseController. --- src/Adapters/Storage/Mongo/MongoCollection.js | 4 ++++ src/Controllers/DatabaseController.js | 6 +----- src/Controllers/PushController.js | 16 ++++++---------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index f19705c4..78c90fef 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -64,6 +64,10 @@ export default class MongoCollection { return this._mongoCollection.update(query, update, { upsert: true }); } + updateMany(query, update) { + return this._mongoCollection.updateMany(query, update); + } + // Atomically find and delete an object based on query. // The result is the promise with an object that was in the database before deleting. // Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done. diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 042a086d..83c2703a 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -35,7 +35,7 @@ DatabaseController.prototype.collection = function(className) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className); } - return this.rawCollection(className); + return this.adapter.collection(this.collectionPrefix + className); }; DatabaseController.prototype.adaptiveCollection = function(className) { @@ -46,10 +46,6 @@ DatabaseController.prototype.collectionExists = function(className) { return this.adapter.collectionExists(this.collectionPrefix + className); }; -DatabaseController.prototype.rawCollection = function(className) { - return this.adapter.collection(this.collectionPrefix + className); -}; - DatabaseController.prototype.dropCollection = function(className) { return this.adapter.dropCollection(this.collectionPrefix + className); }; diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 55cb6095..fb03ab52 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -63,23 +63,19 @@ export class PushController extends AdaptableController { let badgeUpdate = Promise.resolve(); if (body.badge) { - var op = {}; + let op = {}; if (body.badge == "Increment") { - op = {'$inc': {'badge': 1}} + op = { $inc: { badge: 1 } } } else if (Number(body.badge)) { - op = {'$set': {'badge': body.badge } } + op = { $set: { badge: body.badge } } } else { throw "Invalid value for badge, expected number or 'Increment'"; } let updateWhere = deepcopy(where); + updateWhere.deviceType = 'ios'; // Only on iOS! - // Only on iOS! - updateWhere.deviceType = 'ios'; - - // TODO: @nlutsenko replace with better thing - badgeUpdate = config.database.rawCollection("_Installation").then((coll) => { - return coll.update(updateWhere, op, { multi: true }); - }); + badgeUpdate = config.database.adaptiveCollection("_Installation") + .then(coll => coll.updateMany(updateWhere, op)); } return badgeUpdate.then(() => { From 47061d8e98c8dfa69125dc4cf5dd02ade29d61ed Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 14:23:05 -0800 Subject: [PATCH 32/48] Migrate PushRouter to shared master-key middleware. --- spec/PushRouter.spec.js | 34 ------------------------- src/Routers/PushRouter.js | 52 ++++++++++++++------------------------- 2 files changed, 18 insertions(+), 68 deletions(-) diff --git a/spec/PushRouter.spec.js b/spec/PushRouter.spec.js index e7273dd5..d71f9f5c 100644 --- a/spec/PushRouter.spec.js +++ b/spec/PushRouter.spec.js @@ -2,40 +2,6 @@ var PushRouter = require('../src/Routers/PushRouter').PushRouter; var request = require('request'); describe('PushRouter', () => { - it('can check valid master key of request', (done) => { - // Make mock request - var request = { - info: { - masterKey: 'masterKey' - }, - config: { - masterKey: 'masterKey' - } - } - - expect(() => { - PushRouter.validateMasterKey(request); - }).not.toThrow(); - done(); - }); - - it('can check invalid master key of request', (done) => { - // Make mock request - var request = { - info: { - masterKey: 'masterKey' - }, - config: { - masterKey: 'masterKeyAgain' - } - } - - expect(() => { - PushRouter.validateMasterKey(request); - }).toThrow(); - done(); - }); - it('can get query condition when channels is set', (done) => { // Make mock request var request = { diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index f75d9998..c3af0d28 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,57 +1,42 @@ -import PushController from '../Controllers/PushController' import PromiseRouter from '../PromiseRouter'; +import * as middleware from "../middlewares"; +import { Parse } from "parse/node"; export class PushRouter extends PromiseRouter { mountRoutes() { - this.route("POST", "/push", req => { return this.handlePOST(req); }); - } - - /** - * Check whether the api call has master key or not. - * @param {Object} request A request object - */ - static validateMasterKey(req) { - if (req.info.masterKey !== req.config.masterKey) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Master key is invalid, you should only use master key to send push'); - } + this.route("POST", "/push", middleware.promiseEnforceMasterKeyAccess, PushRouter.handlePOST); } - handlePOST(req) { - // TODO: move to middlewares when support for Promise middlewares - PushRouter.validateMasterKey(req); - + static handlePOST(req) { const pushController = req.config.pushController; if (!pushController) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Push controller is not set'); + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Push controller is not set'); } - var where = PushRouter.getQueryCondition(req); - + let where = PushRouter.getQueryCondition(req); pushController.sendPush(req.body, where, req.config, req.auth); return Promise.resolve({ - response: { - 'result': true - } + response: { + 'result': true + } }); } - - /** + + /** * Get query condition from the request body. - * @param {Object} request A request object + * @param {Object} req A request object * @returns {Object} The query condition, the where field in a query api call */ static getQueryCondition(req) { - var body = req.body || {}; - var hasWhere = typeof body.where !== 'undefined'; - var hasChannels = typeof body.channels !== 'undefined'; + let body = req.body || {}; + let hasWhere = typeof body.where !== 'undefined'; + let hasChannels = typeof body.channels !== 'undefined'; - var where; + let where; if (hasWhere && hasChannels) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query can not be set at the same time.'); + 'Channels and query can not be set at the same time.'); } else if (hasWhere) { where = body.where; } else if (hasChannels) { @@ -62,11 +47,10 @@ export class PushRouter extends PromiseRouter { } } else { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query should be set at least one.'); + 'Channels and query should be set at least one.'); } return where; } - } export default PushRouter; From de0f71cc9e4f29a668b9d73aed4a1b459831196e Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 14:25:04 -0800 Subject: [PATCH 33/48] Remove useless masterKey validation in PushController.sendPush. --- spec/PushController.spec.js | 25 ------------------------- src/Controllers/PushController.js | 14 +------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 1821d1a3..64a7959a 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -3,31 +3,6 @@ var PushController = require('../src/Controllers/PushController').PushController var Config = require('../src/Config'); describe('PushController', () => { - it('can check valid master key of request', (done) => { - // Make mock request - var auth = { - isMaster: true - } - - expect(() => { - PushController.validateMasterKey(auth); - }).not.toThrow(); - done(); - }); - - it('can check invalid master key of request', (done) => { - // Make mock request - var auth = { - isMaster: false - } - - expect(() => { - PushController.validateMasterKey(auth); - }).toThrow(); - done(); - }); - - it('can validate device type when no device type is set', (done) => { // Make query condition var where = { diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index fb03ab52..116c78ed 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -36,17 +36,6 @@ export class PushController extends AdaptableController { } } } - - /** - * Check whether the api call has master key or not. - * @param {Object} request A request object - */ - static validateMasterKey(auth = {}) { - if (!auth.isMaster) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Master key is invalid, you should only use master key to send push'); - } - } sendPush(body = {}, where = {}, config, auth) { var pushAdapter = this.adapter; @@ -54,7 +43,6 @@ export class PushController extends AdaptableController { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Push adapter is not available'); } - PushController.validateMasterKey(auth); PushController.validatePushType(where, pushAdapter.getValidPushTypes()); // Replace the expiration_time with a valid Unix epoch milliseconds time body['expiration_time'] = PushController.getExpirationTime(body); @@ -140,6 +128,6 @@ export class PushController extends AdaptableController { expectedAdapterType() { return PushAdapter; } -}; +} export default PushController; From 654a540b6acf2e17806027699003f91ed4313581 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 15:42:42 -0800 Subject: [PATCH 34/48] Fix race condition in GlobalConfig test. --- spec/ParseGlobalConfig.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index 176c7860..4b684553 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -5,11 +5,11 @@ var Parse = require('parse/node').Parse; let Config = require('../src/Config'); describe('a GlobalConfig', () => { - beforeEach(function (done) { + beforeEach(done => { let config = new Config('test'); config.database.adaptiveCollection('_GlobalConfig') .then(coll => coll.upsertOne({ '_id': 1 }, { $set: { params: { companies: ['US', 'DK'] } } })) - .then(done()); + .then(() => { done(); }); }); it('can be retrieved', (done) => { From bf96f0d28ab8d24bacaede2375816e1f708788d5 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 17:19:55 -0500 Subject: [PATCH 35/48] Fixes problems related to increment badge - name conventions are aweful in PushController - properly looks at the badge into body.data instead of body - We may want to refactor that as it's confusing to use a full body --- spec/Parse.Push.spec.js | 59 +++++++++++++++++++++++++++++++ spec/PushController.spec.js | 14 ++++---- src/Controllers/PushController.js | 37 ++++++++++--------- 3 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 spec/Parse.Push.spec.js diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js new file mode 100644 index 00000000..a2b71d5f --- /dev/null +++ b/spec/Parse.Push.spec.js @@ -0,0 +1,59 @@ +describe('Parse.Push', () => { + it('should properly send push', (done) => { + var pushAdapter = { + send: function(body, installations) { + var badge = body.data.badge; + installations.forEach((installation) => { + if (installation.deviceType == "ios") { + expect(installation.badge).toEqual(badge); + expect(installation.originalBadge+1).toEqual(installation.badge); + } else { + expect(installation.badge).toBeUndefined(); + } + }); + return Promise.resolve({ + body: body, + installations: installations + }); + }, + getValidPushTypes: function() { + return ["ios", "android"]; + } + } + setServerConfiguration({ + appId: Parse.applicationId, + masterKey: Parse.masterKey, + serverURL: Parse.serverURL, + push: { + adapter: pushAdapter + } + }); + var installations = []; + while(installations.length != 10) { + var installation = new Parse.Object("_Installation"); + installation.set("installationId", "installation_"+installations.length); + installation.set("deviceToken","device_token_"+installations.length) + installation.set("badge", installations.length); + installation.set("originalBadge", installations.length); + installation.set("deviceType", "ios"); + installations.push(installation); + } + Parse.Object.saveAll(installations).then(() => { + return Parse.Push.send({ + where: { + deviceType: 'ios' + }, + data: { + badge: 'Increment', + alert: 'Hello world!' + } + }, {useMasterKey: true}); + }) + .then(() => { + done(); + }, (err) => { + console.error(err); + done(); + }); + }); +}); \ No newline at end of file diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 64a7959a..e2ced56f 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -107,10 +107,10 @@ describe('PushController', () => { it('properly increment badges', (done) => { - var payload = { + var payload = {data:{ alert: "Hello World!", badge: "Increment", - } + }} var installations = []; while(installations.length != 10) { var installation = new Parse.Object("_Installation"); @@ -132,7 +132,7 @@ describe('PushController', () => { var pushAdapter = { send: function(body, installations) { - var badge = body.badge; + var badge = body.data.badge; installations.forEach((installation) => { if (installation.deviceType == "ios") { expect(installation.badge).toEqual(badge); @@ -171,10 +171,10 @@ describe('PushController', () => { it('properly set badges to 1', (done) => { - var payload = { + var payload = {data: { alert: "Hello World!", badge: 1, - } + }} var installations = []; while(installations.length != 10) { var installation = new Parse.Object("_Installation"); @@ -188,7 +188,7 @@ describe('PushController', () => { var pushAdapter = { send: function(body, installations) { - var badge = body.badge; + var badge = body.data.badge; installations.forEach((installation) => { expect(installation.badge).toEqual(badge); expect(1).toEqual(installation.badge); @@ -219,6 +219,6 @@ describe('PushController', () => { done(); }); - }) + }); }); diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 116c78ed..8dbc6a59 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -48,55 +48,60 @@ export class PushController extends AdaptableController { body['expiration_time'] = PushController.getExpirationTime(body); // TODO: If the req can pass the checking, we return immediately instead of waiting // pushes to be sent. We probably change this behaviour in the future. - let badgeUpdate = Promise.resolve(); + let badgeUpdate = () => { + return Promise.resolve(); + } - if (body.badge) { + if (body.data && body.data.badge) { + let badge = body.data.badge; let op = {}; - if (body.badge == "Increment") { + if (badge == "Increment") { op = { $inc: { badge: 1 } } - } else if (Number(body.badge)) { - op = { $set: { badge: body.badge } } + } else if (Number(badge)) { + op = { $set: { badge: badge } } } else { throw "Invalid value for badge, expected number or 'Increment'"; } let updateWhere = deepcopy(where); updateWhere.deviceType = 'ios'; // Only on iOS! - badgeUpdate = config.database.adaptiveCollection("_Installation") + badgeUpdate = () => { + return config.database.adaptiveCollection("_Installation") .then(coll => coll.updateMany(updateWhere, op)); + } } - return badgeUpdate.then(() => { - return rest.find(config, auth, '_Installation', where) + return badgeUpdate().then(() => { + return rest.find(config, auth, '_Installation', where); }).then((response) => { - if (body.badge && body.badge == "Increment") { + if (body.data && body.data.badge && body.data.badge == "Increment") { // Collect the badges to reduce the # of calls let badgeInstallationsMap = response.results.reduce((map, installation) => { let badge = installation.badge; if (installation.deviceType != "ios") { badge = UNSUPPORTED_BADGE_KEY; } - map[badge] = map[badge] || []; - map[badge].push(installation); + map[badge+''] = map[badge+''] || []; + map[badge+''].push(installation); return map; }, {}); - + // Map the on the badges count and return the send result let promises = Object.keys(badgeInstallationsMap).map((badge) => { let payload = deepcopy(body); if (badge == UNSUPPORTED_BADGE_KEY) { - delete payload.badge; + delete payload.data.badge; } else { - payload.badge = parseInt(badge); + payload.data.badge = parseInt(badge); } - return pushAdapter.send(payload, badgeInstallationsMap[badge]); + return pushAdapter.send(payload, badgeInstallationsMap[badge]); }); return Promise.all(promises); } return pushAdapter.send(body, response.results); }); } - + /** * Get expiration time from the request body. * @param {Object} request A request object From 438cf58d4c436f8480fa7ac74b2d7edfc05d2f66 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 22:45:46 -0800 Subject: [PATCH 36/48] Fix early server response in Schema validation. --- src/Schema.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Schema.js b/src/Schema.js index 3e25b09d..13ccb7cd 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -448,9 +448,12 @@ class Schema { geocount++; } if (geocount > 1) { - throw new Parse.Error( - Parse.Error.INCORRECT_TYPE, - 'there can only be one geopoint field in a class'); + // Make sure all field validation operations run before we return. + // If not - we are continuing to run logic, but already provided response from the server. + return promise.then(() => { + return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE, + 'there can only be one geopoint field in a class')); + }); } if (!expected) { continue; From 0abd5a59312ec127c6a3fe5c7a6283fff3822870 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 22:45:54 -0800 Subject: [PATCH 37/48] Re-enable GeoPoint test. --- spec/ParseGeoPoint.spec.js | 58 ++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/spec/ParseGeoPoint.spec.js b/spec/ParseGeoPoint.spec.js index 66613603..7d6a8291 100644 --- a/spec/ParseGeoPoint.spec.js +++ b/spec/ParseGeoPoint.spec.js @@ -39,37 +39,33 @@ describe('Parse.GeoPoint testing', () => { }); }); -// -// This test is disabled, since it's extremely flaky on Travis-CI. -// Tracking issue: https://github.com/ParsePlatform/parse-server/issues/572 -// -// it('geo line', (done) => { -// var line = []; -// for (var i = 0; i < 10; ++i) { -// var obj = new TestObject(); -// var point = new Parse.GeoPoint(i * 4.0 - 12.0, i * 3.2 - 11.0); -// obj.set('location', point); -// obj.set('construct', 'line'); -// obj.set('seq', i); -// line.push(obj); -// } -// Parse.Object.saveAll(line, { -// success: function() { -// var query = new Parse.Query(TestObject); -// var point = new Parse.GeoPoint(24, 19); -// query.equalTo('construct', 'line'); -// query.withinMiles('location', point, 10000); -// query.find({ -// success: function(results) { -// equal(results.length, 10); -// equal(results[0].get('seq'), 9); -// equal(results[3].get('seq'), 6); -// done(); -// } -// }); -// } -// }); -// }); + it('geo line', (done) => { + var line = []; + for (var i = 0; i < 10; ++i) { + var obj = new TestObject(); + var point = new Parse.GeoPoint(i * 4.0 - 12.0, i * 3.2 - 11.0); + obj.set('location', point); + obj.set('construct', 'line'); + obj.set('seq', i); + line.push(obj); + } + Parse.Object.saveAll(line, { + success: function() { + var query = new Parse.Query(TestObject); + var point = new Parse.GeoPoint(24, 19); + query.equalTo('construct', 'line'); + query.withinMiles('location', point, 10000); + query.find({ + success: function(results) { + equal(results.length, 10); + equal(results[0].get('seq'), 9); + equal(results[3].get('seq'), 6); + done(); + } + }); + } + }); + }); it('geo max distance large', (done) => { var objects = []; From a163327ac9ae5765698b032d1bf8e8271baf14c2 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 19:26:40 -0800 Subject: [PATCH 38/48] Remove usages of non-adaptive collection inside DatabaseController. --- src/Adapters/Storage/Mongo/MongoCollection.js | 8 +- src/Controllers/DatabaseController.js | 132 +++++++++--------- src/Controllers/HooksController.js | 2 +- 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 78c90fef..6e1a73c1 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -57,6 +57,10 @@ export default class MongoCollection { }) } + insertOne(object) { + return this._mongoCollection.insertOne(object); + } + // Atomically updates data in the database for a single (first) object that matched the query // If there is nothing that matches the query - does insert // Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5. @@ -83,8 +87,8 @@ export default class MongoCollection { return this._mongoCollection.deleteOne(query); } - remove(query) { - return this._mongoCollection.remove(query); + deleteMany(query) { + return this._mongoCollection.deleteMany(query); } drop() { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 83c2703a..e2761bd5 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -54,6 +54,14 @@ function returnsTrue() { return true; } +DatabaseController.prototype.validateClassName = function(className) { + if (!Schema.classNameIsValid(className)) { + const error = new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className); + return Promise.reject(error); + } + return Promise.resolve(); +}; + // Returns a promise for a schema object. // If we are provided a acceptor, then we run it on the schema. // If the schema isn't accepted, we reload it at most once. @@ -230,30 +238,28 @@ DatabaseController.prototype.handleRelationUpdates = function(className, // Adds a relation. // Returns a promise that resolves successfully iff the add was successful. -DatabaseController.prototype.addRelation = function(key, fromClassName, - fromId, toId) { - var doc = { +DatabaseController.prototype.addRelation = function(key, fromClassName, fromId, toId) { + let doc = { relatedId: toId, - owningId: fromId + owningId : fromId }; - var className = '_Join:' + key + ':' + fromClassName; - return this.collection(className).then((coll) => { - return coll.update(doc, doc, {upsert: true}); + let className = `_Join:${key}:${fromClassName}`; + return this.adaptiveCollection(className).then((coll) => { + return coll.upsertOne(doc, doc); }); }; // Removes a relation. // Returns a promise that resolves successfully iff the remove was // successful. -DatabaseController.prototype.removeRelation = function(key, fromClassName, - fromId, toId) { +DatabaseController.prototype.removeRelation = function(key, fromClassName, fromId, toId) { var doc = { relatedId: toId, owningId: fromId }; - var className = '_Join:' + key + ':' + fromClassName; - return this.collection(className).then((coll) => { - return coll.remove(doc); + let className = `_Join:${key}:${fromClassName}`; + return this.adaptiveCollection(className).then(coll => { + return coll.deleteOne(doc); }); }; @@ -269,40 +275,36 @@ DatabaseController.prototype.destroy = function(className, query, options = {}) var aclGroup = options.acl || []; var schema; - return this.loadSchema().then((s) => { - schema = s; - if (!isMaster) { - return schema.validatePermission(className, aclGroup, 'delete'); - } - return Promise.resolve(); - }).then(() => { - - return this.collection(className); - }).then((coll) => { - var mongoWhere = transform.transformWhere(schema, className, query); - - if (options.acl) { - var writePerms = [ - {_wperm: {'$exists': false}} - ]; - for (var entry of options.acl) { - writePerms.push({_wperm: {'$in': [entry]}}); + return this.loadSchema() + .then(s => { + schema = s; + if (!isMaster) { + return schema.validatePermission(className, aclGroup, 'delete'); } - mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]}; - } + return Promise.resolve(); + }) + .then(() => this.adaptiveCollection(className)) + .then(collection => { + let mongoWhere = transform.transformWhere(schema, className, query); - return coll.remove(mongoWhere); - }).then((resp) => { - //Check _Session to avoid changing password failed without any session. - if (resp.result.n === 0 && className !== "_Session") { - return Promise.reject( - new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.')); - - } - }, (error) => { - throw error; - }); + if (options.acl) { + var writePerms = [ + { _wperm: { '$exists': false } } + ]; + for (var entry of options.acl) { + writePerms.push({ _wperm: { '$in': [entry] } }); + } + mongoWhere = { '$and': [mongoWhere, { '$or': writePerms }] }; + } + return collection.deleteMany(mongoWhere); + }) + .then(resp => { + //Check _Session to avoid changing password failed without any session. + // TODO: @nlutsenko Stop relying on `result.n` + if (resp.result.n === 0 && className !== "_Session") { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + }); }; // Inserts an object into the database. @@ -312,21 +314,21 @@ DatabaseController.prototype.create = function(className, object, options) { var isMaster = !('acl' in options); var aclGroup = options.acl || []; - return this.loadSchema().then((s) => { - schema = s; - if (!isMaster) { - return schema.validatePermission(className, aclGroup, 'create'); - } - return Promise.resolve(); - }).then(() => { - - return this.handleRelationUpdates(className, null, object); - }).then(() => { - return this.collection(className); - }).then((coll) => { - var mongoObject = transform.transformCreate(schema, className, object); - return coll.insert([mongoObject]); - }); + return this.validateClassName(className) + .then(() => this.loadSchema()) + .then(s => { + schema = s; + if (!isMaster) { + return schema.validatePermission(className, aclGroup, 'create'); + } + return Promise.resolve(); + }) + .then(() => this.handleRelationUpdates(className, null, object)) + .then(() => this.adaptiveCollection(className)) + .then(coll => { + var mongoObject = transform.transformCreate(schema, className, object); + return coll.insertOne(mongoObject); + }); }; // Runs a mongo query on the database. @@ -386,14 +388,14 @@ DatabaseController.prototype.owningIds = function(className, key, relatedIds) { // equal-to-pointer constraints on relation fields. // Returns a promise that resolves when query is mutated DatabaseController.prototype.reduceInRelation = function(className, query, schema) { - + // Search for an in-relation or equal-to-relation // Make it sequential for now, not sure of paralleization side effects if (query['$or']) { let ors = query['$or']; return Promise.all(ors.map((aQuery, index) => { return this.reduceInRelation(className, aQuery, schema).then((aQuery) => { - query['$or'][index] = aQuery; + query['$or'][index] = aQuery; }) })); } @@ -413,14 +415,14 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem relatedIds = [query[key].objectId]; } return this.owningIds(className, key, relatedIds).then((ids) => { - delete query[key]; + delete query[key]; this.addInObjectIdsIds(ids, query); return Promise.resolve(query); }); } return Promise.resolve(query); }) - + return Promise.all(promises).then(() => { return Promise.resolve(query); }) @@ -429,13 +431,13 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem // Modifies query so that it no longer has $relatedTo // Returns a promise that resolves when query is mutated DatabaseController.prototype.reduceRelationKeys = function(className, query) { - + if (query['$or']) { return Promise.all(query['$or'].map((aQuery) => { return this.reduceRelationKeys(className, aQuery); })); } - + var relatedTo = query['$relatedTo']; if (relatedTo) { return this.relatedIds( diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index fbc7e920..cbf26f8a 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -71,7 +71,7 @@ export class HooksController { _removeHooks(query) { return this.getCollection().then(collection => { - return collection.remove(query); + return collection.deleteMany(query); }).then(() => { return {}; }); From fb5b8fb58fff99d09e101c2e55880cc87a6938ad Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 20:56:26 -0800 Subject: [PATCH 39/48] Migrate Schema.js to adaptive mongo collection. --- src/Adapters/Storage/Mongo/MongoCollection.js | 6 +++- src/Controllers/DatabaseController.js | 18 +++------- src/Schema.js | 33 +++++++++---------- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 6e1a73c1..1e501824 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -54,7 +54,7 @@ export default class MongoCollection { return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => { // Value is the object where mongo returns multiple fields. return document.value; - }) + }); } insertOne(object) { @@ -68,6 +68,10 @@ export default class MongoCollection { return this._mongoCollection.update(query, update, { upsert: true }); } + updateOne(query, update) { + return this._mongoCollection.updateOne(query, update); + } + updateMany(query, update) { return this._mongoCollection.updateMany(query, update); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e2761bd5..f261db56 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -28,16 +28,6 @@ DatabaseController.prototype.connect = function() { return this.adapter.connect(); }; -// Returns a promise for a Mongo collection. -// Generally just for internal use. -DatabaseController.prototype.collection = function(className) { - if (!Schema.classNameIsValid(className)) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, - 'invalid className: ' + className); - } - return this.adapter.collection(this.collectionPrefix + className); -}; - DatabaseController.prototype.adaptiveCollection = function(className) { return this.adapter.adaptiveCollection(this.collectionPrefix + className); }; @@ -68,9 +58,9 @@ DatabaseController.prototype.validateClassName = function(className) { DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { if (!this.schemaPromise) { - this.schemaPromise = this.collection('_SCHEMA').then((coll) => { + this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => { delete this.schemaPromise; - return Schema.load(coll); + return Schema.load(collection); }); return this.schemaPromise; } @@ -79,9 +69,9 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { if (acceptor(schema)) { return schema; } - this.schemaPromise = this.collection('_SCHEMA').then((coll) => { + this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => { delete this.schemaPromise; - return Schema.load(coll); + return Schema.load(collection); }); return this.schemaPromise; }); diff --git a/src/Schema.js b/src/Schema.js index 13ccb7cd..a3bf8245 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -168,12 +168,12 @@ function schemaAPITypeToMongoFieldType(type) { // '_metadata' is ignored for now // Everything else is expected to be a userspace field. class Schema { - collection; + _collection; data; perms; constructor(collection) { - this.collection = collection; + this._collection = collection; // this.data[className][fieldName] tells you the type of that field this.data = {}; @@ -184,8 +184,8 @@ class Schema { reloadData() { this.data = {}; this.perms = {}; - return this.collection.find({}, {}).toArray().then(mongoSchema => { - for (let obj of mongoSchema) { + return this._collection.find({}).then(results => { + for (let obj of results) { let className = null; let classData = {}; let permsData = null; @@ -231,7 +231,7 @@ class Schema { return Promise.reject(mongoObject); } - return this.collection.insertOne(mongoObject.result) + return this._collection.insertOne(mongoObject.result) .then(result => result.ops[0]) .catch(error => { if (error.code === 11000) { //Mongo's duplicate key error @@ -268,7 +268,7 @@ class Schema { 'schema is frozen, cannot add: ' + className); } // We don't have this class. Update the schema - return this.collection.insert([{_id: className}]).then(() => { + return this._collection.insertOne({ _id: className }).then(() => { // The schema update succeeded. Reload the schema return this.reloadData(); }, () => { @@ -280,10 +280,9 @@ class Schema { }).then(() => { // Ensure that the schema now validates return this.validateClassName(className, true); - }, (error) => { + }, () => { // The schema still doesn't validate. Give up - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema class name does not revalidate'); + throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); }); } @@ -296,7 +295,7 @@ class Schema { } }; update = {'$set': update}; - return this.collection.findAndModify(query, {}, update, {}).then(() => { + return this._collection.updateOne(query, update).then(() => { // The update succeeded. Reload the schema return this.reloadData(); }); @@ -354,12 +353,12 @@ class Schema { // We don't have this field. Update the schema. // Note that we use the $exists guard and $set to avoid race // conditions in the database. This is important! - var query = {_id: className}; - query[key] = {'$exists': false}; + var query = { _id: className }; + query[key] = { '$exists': false }; var update = {}; update[key] = type; update = {'$set': update}; - return this.collection.findAndModify(query, {}, update, {}).then(() => { + return this._collection.upsertOne(query, update).then(() => { // The update succeeded. Reload the schema return this.reloadData(); }, () => { @@ -422,14 +421,14 @@ class Schema { // for non-relations, remove all the data. // This is necessary to ensure that the data is still gone if they add the same field. - return database.collection(className) + return database.adaptiveCollection(className) .then(collection => { - var mongoFieldName = this.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName; - return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true }); + let mongoFieldName = this.data[className][fieldName].startsWith('*') ? `_p_${fieldName}` : fieldName; + return collection.updateMany({}, { "$unset": { [mongoFieldName]: null } }); }); }) // Save the _SCHEMA object - .then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }})); + .then(() => this._collection.updateOne({ _id: className }, { $unset: { [fieldName]: null } })); }); } From 49eb9df1ef585819f24b9cdfc96edcf194eec84f Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Mon, 7 Mar 2016 20:56:51 -0800 Subject: [PATCH 40/48] Remove private Schema API usage from SchemasRouter. --- src/Routers/SchemasRouter.js | 41 +++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 0d1a4cf9..74b15285 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -18,7 +18,7 @@ function getAllSchemas(req) { return req.config.database.adaptiveCollection('_SCHEMA') .then(collection => collection.find({})) .then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse)) - .then(schemas => ({ response: { results: schemas }})); + .then(schemas => ({ response: { results: schemas } })); } function getOneSchema(req) { @@ -65,7 +65,7 @@ function modifySchema(req) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`); } - let existingFields = Object.assign(schema.data[className], {_id: className}); + let existingFields = Object.assign(schema.data[className], { _id: className }); Object.keys(submittedFields).forEach(name => { let field = submittedFields[name]; if (existingFields[name] && field.__op !== 'Delete') { @@ -83,24 +83,27 @@ function modifySchema(req) { } // Finally we have checked to make sure the request is valid and we can start deleting fields. - // Do all deletions first, then a single save to _SCHEMA collection to handle all additions. - let deletionPromises = []; - Object.keys(submittedFields).forEach(submittedFieldName => { - if (submittedFields[submittedFieldName].__op === 'Delete') { - let promise = schema.deleteField(submittedFieldName, className, req.config.database); - deletionPromises.push(promise); + // Do all deletions first, then add fields to avoid duplicate geopoint error. + let deletePromises = []; + let insertedFields = []; + Object.keys(submittedFields).forEach(fieldName => { + if (submittedFields[fieldName].__op === 'Delete') { + const promise = schema.deleteField(fieldName, className, req.config.database); + deletePromises.push(promise); + } else { + insertedFields.push(fieldName); } }); - - return Promise.all(deletionPromises) - .then(() => new Promise((resolve, reject) => { - schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => { - if (err) { - reject(err); - } - resolve({ response: Schema.mongoSchemaToSchemaAPIResponse(mongoObject.result)}); - }) - })); + return Promise.all(deletePromises) // Delete Everything + .then(() => schema.reloadData()) // Reload our Schema, so we have all the new values + .then(() => { + let promises = insertedFields.map(fieldName => { + const mongoType = mongoObject.result[fieldName]; + return schema.validateField(className, fieldName, mongoType); + }); + return Promise.all(promises); + }) + .then(() => ({ response: Schema.mongoSchemaToSchemaAPIResponse(mongoObject.result) })); }); } @@ -140,7 +143,7 @@ function deleteSchema(req) { // We've dropped the collection now, so delete the item from _SCHEMA // and clear the _Join collections return req.config.database.adaptiveCollection('_SCHEMA') - .then(coll => coll.findOneAndDelete({_id: req.params.className})) + .then(coll => coll.findOneAndDelete({ _id: req.params.className })) .then(document => { if (document === null) { //tried to delete non-existent class From 440099267d07efa326dba2aaec7821df86025877 Mon Sep 17 00:00:00 2001 From: Carmen Date: Tue, 8 Mar 2016 20:23:55 +0800 Subject: [PATCH 41/48] #911 support params option in Parse.Cloud.httpRequest --- spec/HTTPRequest.spec.js | 37 ++++++++++++++++++++++++++++++++++- src/cloud-code/httpRequest.js | 9 ++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index ad4e289f..abc5decf 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -24,7 +24,11 @@ app.get("/301", function(req, res){ app.post('/echo', function(req, res){ res.json(req.body); -}) +}); + +app.get('/qs', function(req, res){ + res.json(req.query); +}); app.listen(13371); @@ -193,4 +197,35 @@ describe("httpRequest", () => { } }); }) + + it("should params object to query string", (done) => { + httpRequest({ + url: httpRequestServer+"/qs", + params: { + foo: "bar" + } + }).then(function(httpResponse){ + expect(httpResponse.status).toBe(200); + expect(httpResponse.data).toEqual({foo: "bar"}); + done(); + }, function(){ + fail("should not fail"); + done(); + }) + }); + + it("should params string to query string", (done) => { + httpRequest({ + url: httpRequestServer+"/qs", + params: "foo=bar&foo2=bar2" + }).then(function(httpResponse){ + expect(httpResponse.status).toBe(200); + expect(httpResponse.data).toEqual({foo: "bar", foo2: 'bar2'}); + done(); + }, function(){ + fail("should not fail"); + done(); + }) + }); + }); diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 4e8ff654..35af79ca 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -1,4 +1,5 @@ var request = require("request"), + querystring = require('querystring'), Parse = require('parse/node').Parse; var encodeBody = function(body, headers = {}) { @@ -34,7 +35,13 @@ module.exports = function(options) { options.body = encodeBody(options.body, options.headers); // set follow redirects to false by default options.followRedirect = options.followRedirects == true; - + // support params options + if (typeof options.params === 'object') { + options.qs = options.params; + } else if (typeof options.params === 'string') { + options.qs = querystring.parse(options.params); + } + request(options, (error, response, body) => { if (error) { if (callbacks.error) { From 2f63c5528da25c5e91ee671da79bec81e3b9ab3a Mon Sep 17 00:00:00 2001 From: Marco129 Date: Wed, 9 Mar 2016 02:15:27 +0800 Subject: [PATCH 42/48] Minimize db query --- src/RestQuery.js | 4 +--- src/RestWrite.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/RestQuery.js b/src/RestQuery.js index 1e0f344e..a6770f47 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -168,9 +168,7 @@ RestQuery.prototype.validateClientClassCreation = function() { let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product']; if (this.config.allowClientClassCreation === false && !this.auth.isMaster && sysClass.indexOf(this.className) === -1) { - return this.config.database.loadSchema().then((schema) => { - return schema.hasClass(this.className) - }).then((hasClass) => { + return this.config.database.collectionExists(this.className).then((hasClass) => { if (hasClass === true) { return Promise.resolve(); } diff --git a/src/RestWrite.js b/src/RestWrite.js index 72eda1fc..9e07c93a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -112,9 +112,7 @@ RestWrite.prototype.validateClientClassCreation = function() { let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product']; if (this.config.allowClientClassCreation === false && !this.auth.isMaster && sysClass.indexOf(this.className) === -1) { - return this.config.database.loadSchema().then((schema) => { - return schema.hasClass(this.className) - }).then((hasClass) => { + return this.config.database.collectionExists(this.className).then((hasClass) => { if (hasClass === true) { return Promise.resolve(); } From b9f08d9694cf9ee2f648de999a9c9a44dbfea4b4 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 8 Mar 2016 16:08:41 -0800 Subject: [PATCH 43/48] Do not mutate parameter object in DatabaseController. --- src/Controllers/DatabaseController.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index f261db56..80eea63b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -6,6 +6,7 @@ var Parse = require('parse/node').Parse; var Schema = require('./../Schema'); var transform = require('./../transform'); +const deepcopy = require('deepcopy'); // options can contain: // collectionPrefix: the string to put in front of every collection name. @@ -130,6 +131,9 @@ DatabaseController.prototype.untransformObject = function( // one of the provided strings must provide the caller with // write permissions. DatabaseController.prototype.update = function(className, query, update, options) { + // Make a copy of the object, so we don't mutate the incoming data. + update = deepcopy(update); + var acceptor = function(schema) { return schema.hasKeys(className, Object.keys(query)); }; @@ -300,6 +304,9 @@ DatabaseController.prototype.destroy = function(className, query, options = {}) // Inserts an object into the database. // Returns a promise that resolves successfully iff the object saved. DatabaseController.prototype.create = function(className, object, options) { + // Make a copy of the object, so we don't mutate the incoming data. + object = deepcopy(object); + var schema; var isMaster = !('acl' in options); var aclGroup = options.acl || []; From 0f07c5204eb48119527d718a594b91926d1500e6 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 8 Mar 2016 16:15:49 -0800 Subject: [PATCH 44/48] Add test validating that we have ACL propagate to before/after save hooks. --- spec/ParseAPI.spec.js | 99 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index b4f7f8e0..bd29bcc7 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -4,6 +4,7 @@ var DatabaseAdapter = require('../src/DatabaseAdapter'); var request = require('request'); +const Parse = require("parse/node"); describe('miscellaneous', function() { it('create a GameScore object', function(done) { @@ -372,8 +373,8 @@ describe('miscellaneous', function() { done(); }); }); - - it('test cloud function shoud echo keys', function(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); @@ -399,7 +400,7 @@ describe('miscellaneous', function() { expect(results.length).toEqual(1); expect(results[0]['foo']).toEqual('bar'); done(); - }).fail( err => { + }).fail(err => { fail(err); done(); }) @@ -415,9 +416,9 @@ describe('miscellaneous', function() { // 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 => { + }); + + it('object is set on create and update', done => { let triggerTime = 0; // Register a mock beforeSave hook Parse.Cloud.beforeSave('GameScore', (req, res) => { @@ -683,7 +684,7 @@ describe('miscellaneous', function() { // Make sure the checking has been triggered expect(triggerTime).toBe(2); // Clear mock afterSave - Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); + Parse.Cloud._removeHook("Triggers", "afterSave", "GameScore"); done(); }, function(error) { console.error(error); @@ -732,6 +733,90 @@ describe('miscellaneous', function() { }); }); + it('beforeSave receives ACL', done => { + let triggerTime = 0; + // Register a mock beforeSave hook + Parse.Cloud.beforeSave('GameScore', function(req, res) { + let object = req.object; + if (triggerTime == 0) { + let acl = object.getACL(); + expect(acl.getPublicReadAccess()).toBeTruthy(); + expect(acl.getPublicWriteAccess()).toBeTruthy(); + } else if (triggerTime == 1) { + let acl = object.getACL(); + expect(acl.getPublicReadAccess()).toBeFalsy(); + expect(acl.getPublicWriteAccess()).toBeTruthy(); + } else { + res.error(); + } + triggerTime++; + res.success(); + }); + + let obj = new Parse.Object('GameScore'); + let acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(true); + obj.setACL(acl); + obj.save().then(() => { + acl.setPublicReadAccess(false); + obj.setACL(acl); + return obj.save(); + }).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); + fail(error); + done(); + }); + }); + + it('afterSave receives ACL', done => { + let triggerTime = 0; + // Register a mock beforeSave hook + Parse.Cloud.afterSave('GameScore', function(req, res) { + let object = req.object; + if (triggerTime == 0) { + let acl = object.getACL(); + expect(acl.getPublicReadAccess()).toBeTruthy(); + expect(acl.getPublicWriteAccess()).toBeTruthy(); + } else if (triggerTime == 1) { + let acl = object.getACL(); + expect(acl.getPublicReadAccess()).toBeFalsy(); + expect(acl.getPublicWriteAccess()).toBeTruthy(); + } else { + res.error(); + } + triggerTime++; + res.success(); + }); + + let obj = new Parse.Object('GameScore'); + let acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(true); + obj.setACL(acl); + obj.save().then(() => { + acl.setPublicReadAccess(false); + obj.setACL(acl); + return obj.save(); + }).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); + fail(error); + done(); + }); + }); + it('test cloud function error handling', (done) => { // Register a function which will fail Parse.Cloud.define('willFail', (req, res) => { From e93873f7b10265ba9cf71fd7b673897cc5aec377 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Tue, 8 Mar 2016 17:08:07 -0800 Subject: [PATCH 45/48] Do not require where clause in condition on queries. --- spec/ParseQuery.spec.js | 46 ++++++++++++++++++++++++++++++++--------- src/RestQuery.js | 2 -- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 9171d12c..2c5cb5d4 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -2,6 +2,9 @@ // hungry/js/test/parse_query_test.js // // Some new tests are added. +'use strict'; + +const Parse = require('parse/node'); describe('Parse.Query testing', () => { it("basic query", function(done) { @@ -1574,6 +1577,29 @@ describe('Parse.Query testing', () => { }); }); + it("dontSelect query without conditions", function(done) { + const RestaurantObject = Parse.Object.extend("Restaurant"); + const PersonObject = Parse.Object.extend("Person"); + const objects = [ + new RestaurantObject({ location: "Djibouti" }), + new RestaurantObject({ location: "Ouagadougou" }), + new PersonObject({ name: "Bob", hometown: "Djibouti" }), + new PersonObject({ name: "Tom", hometown: "Yoloblahblahblah" }), + new PersonObject({ name: "Billy", hometown: "Ouagadougou" }) + ]; + + Parse.Object.saveAll(objects, function() { + const query = new Parse.Query(RestaurantObject); + const mainQuery = new Parse.Query(PersonObject); + mainQuery.doesNotMatchKeyInQuery("hometown", "location", query); + mainQuery.find().then(results => { + equal(results.length, 1); + equal(results[0].get('name'), 'Tom'); + done(); + }); + }); + }); + it("object with length", function(done) { var TestObject = Parse.Object.extend("TestObject"); var obj = new TestObject(); @@ -2088,7 +2114,7 @@ describe('Parse.Query testing', () => { console.log(error); }); }); - + // #371 it('should properly interpret a query', (done) => { var query = new Parse.Query("C1"); @@ -2104,7 +2130,7 @@ describe('Parse.Query testing', () => { done(); }) }); - + it('should properly interpret a query', (done) => { var user = new Parse.User(); user.set("username", "foo"); @@ -2112,22 +2138,22 @@ describe('Parse.Query testing', () => { return user.save().then( (user) => { var objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id); var blockedUserQuery = user.relation("blockedUsers").query(); - + var aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse"); aResponseQuery.equalTo("userA", user); aResponseQuery.equalTo("userAResponse", 1); - + var bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse"); bResponseQuery.equalTo("userB", user); bResponseQuery.equalTo("userBResponse", 1); - + var matchOr = Parse.Query.or(aResponseQuery, bResponseQuery); var matchRelationshipA = new Parse.Query("_User"); matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr); var matchRelationshipB = new Parse.Query("_User"); matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr); - - + + var orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB); var query = new Parse.Query("_User"); query.doesNotMatchQuery("objectId", orQuery); @@ -2140,8 +2166,8 @@ describe('Parse.Query testing', () => { fail("should not fail"); done(); }); - - + + }); - + }); diff --git a/src/RestQuery.js b/src/RestQuery.js index a6770f47..c14ac415 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -324,12 +324,10 @@ RestQuery.prototype.replaceDontSelect = function() { !dontSelectValue.key || typeof dontSelectValue.query !== 'object' || !dontSelectValue.query.className || - !dontSelectValue.query.where || Object.keys(dontSelectValue).length !== 2) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'improper usage of $dontSelect'); } - var subquery = new RestQuery( this.config, this.auth, dontSelectValue.query.className, dontSelectValue.query.where); From d594f935674e241ce124a7015bd1dbc27e51acc2 Mon Sep 17 00:00:00 2001 From: Fosco Marotto Date: Wed, 9 Mar 2016 08:29:49 -0800 Subject: [PATCH 46/48] Updating to 2.1.5 --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb02c932..d06701d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ ## Parse Server Changelog +### 2.1.5 (3/9/2016) + +* New: FileAdapter for Google Cloud Storage [\#708](https://github.com/ParsePlatform/parse-server/pull/708) (mcdonamp) +* Improvement: Minimize extra schema queries in some scenarios. [\#919](https://github.com/ParsePlatform/parse-server/pull/919) (Marco129) +* Improvement: Move DatabaseController and Schema fully to adaptive mongo collection. [\#909](https://github.com/ParsePlatform/parse-server/pull/909) (nlutsenko) +* Improvement: Cleanup PushController/PushRouter, remove raw mongo collection access. [\#903](https://github.com/ParsePlatform/parse-server/pull/903) (nlutsenko) +* Improvement: Increment badge the right way [\#902](https://github.com/ParsePlatform/parse-server/pull/902) (flovilmart) +* Improvement: Migrate ParseGlobalConfig to new database storage API. [\#901](https://github.com/ParsePlatform/parse-server/pull/901) (nlutsenko) +* Improvement: Improve delete flow for non-existent \_Join collection [\#881](https://github.com/ParsePlatform/parse-server/pull/881) (Marco129) +* Improvement: Adding a role scenario test for issue 827 [\#878](https://github.com/ParsePlatform/parse-server/pull/878) (gfosco) +* Improvement: Test empty authData block on login for \#413 [\#863](https://github.com/ParsePlatform/parse-server/pull/863) (gfosco) +* Improvement: Modified the npm dev script to support Windows [\#846](https://github.com/ParsePlatform/parse-server/pull/846) (aneeshd16) +* Improvement: Move HooksController to use MongoCollection instead of direct Mongo access. [\#844](https://github.com/ParsePlatform/parse-server/pull/844) (nlutsenko) +* Improvement: Adds public\_html and views for packaging [\#839](https://github.com/ParsePlatform/parse-server/pull/839) (flovilmart) +* Improvement: Better support for windows builds [\#831](https://github.com/ParsePlatform/parse-server/pull/831) (flovilmart) +* Improvement: Convert Schema.js to ES6 class. [\#826](https://github.com/ParsePlatform/parse-server/pull/826) (nlutsenko) +* Improvement: Remove duplicated instructions [\#816](https://github.com/ParsePlatform/parse-server/pull/816) (hramos) +* Improvement: Completely migrate SchemasRouter to new MongoCollection API. [\#794](https://github.com/ParsePlatform/parse-server/pull/794) (nlutsenko) +* Fix: Do not require where clause in $dontSelect condition on queries. [\#925](https://github.com/ParsePlatform/parse-server/pull/925) (nlutsenko) +* Fix: Make sure that ACLs propagate to before/after save hooks. [\#924](https://github.com/ParsePlatform/parse-server/pull/924) (nlutsenko) +* Fix: Support params option in Parse.Cloud.httpRequest. [\#912](https://github.com/ParsePlatform/parse-server/pull/912) (carmenlau) +* Fix: Fix flaky Parse.GeoPoint test. [\#908](https://github.com/ParsePlatform/parse-server/pull/908) (nlutsenko) +* Fix: Handle legacy \_client\_permissions key in \_SCHEMA. [\#900](https://github.com/ParsePlatform/parse-server/pull/900) (drew-gross) +* Fix: Fixes bug when querying equalTo on objectId and relation [\#887](https://github.com/ParsePlatform/parse-server/pull/887) (flovilmart) +* Fix: Allow crossdomain on filesRouter [\#876](https://github.com/ParsePlatform/parse-server/pull/876) (flovilmart) +* Fix: Remove limit when counting results. [\#867](https://github.com/ParsePlatform/parse-server/pull/867) (gfosco) +* Fix: beforeSave changes should propagate to the response [\#865](https://github.com/ParsePlatform/parse-server/pull/865) (gfosco) +* Fix: Delete relation field when \_Join collection not exist [\#864](https://github.com/ParsePlatform/parse-server/pull/864) (Marco129) +* Fix: Related query on non-existing column [\#861](https://github.com/ParsePlatform/parse-server/pull/861) (gfosco) +* Fix: Update markdown in .github/ISSUE\_TEMPLATE.md [\#859](https://github.com/ParsePlatform/parse-server/pull/859) (igorshubovych) +* Fix: Issue with creating wrong \_Session for Facebook login [\#857](https://github.com/ParsePlatform/parse-server/pull/857) (tobernguyen) +* Fix: Leak warnings in tests, use mongodb-runner from node\_modules [\#843](https://github.com/ParsePlatform/parse-server/pull/843) (drew-gross) +* Fix: Reversed roles lookup [\#841](https://github.com/ParsePlatform/parse-server/pull/841) (flovilmart) +* Fix: Improves loading of Push Adapter, fix loading of S3Adapter [\#833](https://github.com/ParsePlatform/parse-server/pull/833) (flovilmart) +* Fix: Add field to system schema [\#828](https://github.com/ParsePlatform/parse-server/pull/828) (Marco129) + ### 2.1.4 (3/3/2016) * New: serverInfo endpoint that returns server version and info about the server's features diff --git a/package.json b/package.json index 89ceb754..60bf17f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "2.1.4", + "version": "2.1.5", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 2730398b92fa928cfca2f5d97440cf862b03dd09 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Wed, 9 Mar 2016 15:20:59 -0800 Subject: [PATCH 47/48] Add new MongoSchemaCollection class that manages schemas for all collections. --- .../Storage/Mongo/MongoSchemaCollection.js | 58 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 9 +++ src/Controllers/DatabaseController.js | 4 ++ 3 files changed, 71 insertions(+) create mode 100644 src/Adapters/Storage/Mongo/MongoSchemaCollection.js diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js new file mode 100644 index 00000000..992068b5 --- /dev/null +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -0,0 +1,58 @@ + +import MongoCollection from './MongoCollection'; + +function _mongoSchemaQueryFromNameQuery(name: string, query) { + return _mongoSchemaObjectFromNameFields(name, query); +} + +function _mongoSchemaObjectFromNameFields(name: string, fields) { + let object = { _id: name }; + if (fields) { + Object.keys(fields).forEach(key => { + object[key] = fields[key]; + }); + } + return object; +} + +export default class MongoSchemaCollection { + _collection: MongoCollection; + + constructor(collection: MongoCollection) { + this._collection = collection; + } + + getAllSchemas() { + return this._collection._rawFind({}); + } + + findSchema(name: string) { + return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => { + return results[0]; + }); + } + + // Atomically find and delete an object based on query. + // The result is the promise with an object that was in the database before deleting. + // Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done. + findAndDeleteSchema(name: string) { + // arguments: query, sort + return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []).then(document => { + // Value is the object where mongo returns multiple fields. + return document.value; + }); + } + + addSchema(name: string, fields) { + let mongoObject = _mongoSchemaObjectFromNameFields(name, fields); + return this._collection.insertOne(mongoObject); + } + + updateSchema(name: string, update) { + return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update); + } + + upsertSchema(name: string, query: string, update) { + return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update); + } +} diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 201388b2..e3d59493 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,9 +1,12 @@ import MongoCollection from './MongoCollection'; +import MongoSchemaCollection from './MongoSchemaCollection'; let mongodb = require('mongodb'); let MongoClient = mongodb.MongoClient; +const MongoSchemaCollectionName = '_SCHEMA'; + export class MongoStorageAdapter { // Private _uri: string; @@ -38,6 +41,12 @@ export class MongoStorageAdapter { .then(rawCollection => new MongoCollection(rawCollection)); } + schemaCollection(collectionPrefix: string) { + return this.connect() + .then(() => this.adaptiveCollection(collectionPrefix + MongoSchemaCollectionName)) + .then(collection => new MongoSchemaCollection(collection)); + } + collectionExists(name: string) { return this.connect().then(() => { return this.database.listCollections({ name: name }).toArray(); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 80eea63b..8d4fc4ff 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -33,6 +33,10 @@ DatabaseController.prototype.adaptiveCollection = function(className) { return this.adapter.adaptiveCollection(this.collectionPrefix + className); }; +DatabaseController.prototype.schemaCollection = function() { + return this.adapter.schemaCollection(this.collectionPrefix); +}; + DatabaseController.prototype.collectionExists = function(className) { return this.adapter.collectionExists(this.collectionPrefix + className); }; From d86f0a8c69c766e5416400cb0161bf75a631749f Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Wed, 9 Mar 2016 15:21:29 -0800 Subject: [PATCH 48/48] Use schema collection instead of adaptive collection for all schema operations. --- src/Adapters/Storage/Mongo/MongoCollection.js | 11 ---------- src/Controllers/DatabaseController.js | 4 ++-- src/Routers/SchemasRouter.js | 21 +++++++++---------- src/Schema.js | 15 +++++++------ 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 1e501824..12c9df66 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -76,17 +76,6 @@ export default class MongoCollection { return this._mongoCollection.updateMany(query, update); } - // Atomically find and delete an object based on query. - // The result is the promise with an object that was in the database before deleting. - // Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done. - findOneAndDelete(query) { - // arguments: query, sort - return this._mongoCollection.findAndRemove(query, []).then(document => { - // Value is the object where mongo returns multiple fields. - return document.value; - }); - } - deleteOne(query) { return this._mongoCollection.deleteOne(query); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 8d4fc4ff..d5752088 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -63,7 +63,7 @@ DatabaseController.prototype.validateClassName = function(className) { DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { if (!this.schemaPromise) { - this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => { + this.schemaPromise = this.schemaCollection().then(collection => { delete this.schemaPromise; return Schema.load(collection); }); @@ -74,7 +74,7 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) { if (acceptor(schema)) { return schema; } - this.schemaPromise = this.adaptiveCollection('_SCHEMA').then(collection => { + this.schemaPromise = this.schemaCollection().then(collection => { delete this.schemaPromise; return Schema.load(collection); }); diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 74b15285..a0a90ef2 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -15,23 +15,22 @@ function classNameMismatchResponse(bodyClass, pathClass) { } function getAllSchemas(req) { - return req.config.database.adaptiveCollection('_SCHEMA') - .then(collection => collection.find({})) + return req.config.database.schemaCollection() + .then(collection => collection.getAllSchemas()) .then(schemas => schemas.map(Schema.mongoSchemaToSchemaAPIResponse)) .then(schemas => ({ response: { results: schemas } })); } function getOneSchema(req) { const className = req.params.className; - return req.config.database.adaptiveCollection('_SCHEMA') - .then(collection => collection.find({ '_id': className }, { limit: 1 })) - .then(results => { - if (results.length != 1) { + return req.config.database.schemaCollection() + .then(collection => collection.findSchema(className)) + .then(mongoSchema => { + if (!mongoSchema) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); } - return results[0]; - }) - .then(schema => ({ response: Schema.mongoSchemaToSchemaAPIResponse(schema) })); + return { response: Schema.mongoSchemaToSchemaAPIResponse(mongoSchema) }; + }); } function createSchema(req) { @@ -142,8 +141,8 @@ function deleteSchema(req) { .then(() => { // We've dropped the collection now, so delete the item from _SCHEMA // and clear the _Join collections - return req.config.database.adaptiveCollection('_SCHEMA') - .then(coll => coll.findOneAndDelete({ _id: req.params.className })) + return req.config.database.schemaCollection() + .then(coll => coll.findAndDeleteSchema(req.params.className)) .then(document => { if (document === null) { //tried to delete non-existent class diff --git a/src/Schema.js b/src/Schema.js index a3bf8245..2a048a54 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -184,7 +184,7 @@ class Schema { reloadData() { this.data = {}; this.perms = {}; - return this._collection.find({}).then(results => { + return this._collection.getAllSchemas().then(results => { for (let obj of results) { let className = null; let classData = {}; @@ -231,7 +231,7 @@ class Schema { return Promise.reject(mongoObject); } - return this._collection.insertOne(mongoObject.result) + return this._collection.addSchema(className, mongoObject.result) .then(result => result.ops[0]) .catch(error => { if (error.code === 11000) { //Mongo's duplicate key error @@ -268,7 +268,7 @@ class Schema { 'schema is frozen, cannot add: ' + className); } // We don't have this class. Update the schema - return this._collection.insertOne({ _id: className }).then(() => { + return this._collection.addSchema(className).then(() => { // The schema update succeeded. Reload the schema return this.reloadData(); }, () => { @@ -288,14 +288,13 @@ class Schema { // Sets the Class-level permissions for a given className, which must exist. setPermissions(className, perms) { - var query = {_id: className}; var update = { _metadata: { class_permissions: perms } }; update = {'$set': update}; - return this._collection.updateOne(query, update).then(() => { + return this._collection.updateSchema(className, update).then(() => { // The update succeeded. Reload the schema return this.reloadData(); }); @@ -353,12 +352,12 @@ class Schema { // We don't have this field. Update the schema. // Note that we use the $exists guard and $set to avoid race // conditions in the database. This is important! - var query = { _id: className }; + let query = {}; query[key] = { '$exists': false }; var update = {}; update[key] = type; update = {'$set': update}; - return this._collection.upsertOne(query, update).then(() => { + return this._collection.upsertSchema(className, query, update).then(() => { // The update succeeded. Reload the schema return this.reloadData(); }, () => { @@ -428,7 +427,7 @@ class Schema { }); }) // Save the _SCHEMA object - .then(() => this._collection.updateOne({ _id: className }, { $unset: { [fieldName]: null } })); + .then(() => this._collection.updateSchema(className, { $unset: { [fieldName]: null } })); }); }