From 36bee12c24facd02834026eaef9f7ea540e7cb2d Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 15 Jul 2020 16:13:14 +0100 Subject: [PATCH 01/33] fix: upgrade uuid from 8.1.0 to 8.2.0 (#6800) Snyk has created this PR to upgrade uuid from 8.1.0 to 8.2.0. See this package in NPM: https://www.npmjs.com/package/uuid See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index cc58dd1a64..a0d267ed9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12128,9 +12128,9 @@ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, "uuid": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", - "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", + "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" }, "v8-compile-cache": { "version": "2.1.0", diff --git a/package.json b/package.json index 55115e3baa..abb1a3c9de 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "semver": "7.3.2", "subscriptions-transport-ws": "0.9.16", "tv4": "1.3.0", - "uuid": "8.1.0", + "uuid": "8.2.0", "winston": "3.2.1", "winston-daily-rotate-file": "4.5.0", "ws": "7.3.0" From cbf9da517b2029eafd16a9c0c46e675319d86c7d Mon Sep 17 00:00:00 2001 From: SebC Date: Wed, 15 Jul 2020 18:56:08 +0200 Subject: [PATCH 02/33] Add production Google Auth Adapter instead of using the development url (#6734) * Add the production Google Auth Adapter instead of using the development url * Update tests to the new google auth * lint --- spec/AuthenticationAdapters.spec.js | 216 ++++++++++++++++++---------- src/Adapters/Auth/google.js | 173 ++++++++++++++++------ 2 files changed, 269 insertions(+), 120 deletions(-) diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 1dfd190e7c..53b701f544 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -19,7 +19,7 @@ const responses = { microsoft: { id: 'userId', mail: 'userMail' }, }; -describe('AuthenticationProviders', function() { +describe('AuthenticationProviders', function () { [ 'apple', 'gcenter', @@ -42,8 +42,8 @@ describe('AuthenticationProviders', function() { 'weibo', 'phantauth', 'microsoft', - ].map(function(providerName) { - it('Should validate structure of ' + providerName, done => { + ].map(function (providerName) { + it('Should validate structure of ' + providerName, (done) => { const provider = require('../lib/Adapters/Auth/' + providerName); jequal(typeof provider.validateAuthData, 'function'); jequal(typeof provider.validateAppId, 'function'); @@ -66,12 +66,12 @@ describe('AuthenticationProviders', function() { }); it(`should provide the right responses for adapter ${providerName}`, async () => { - const noResponse = ['twitter', 'apple', 'gcenter']; + const noResponse = ['twitter', 'apple', 'gcenter', 'google']; if (noResponse.includes(providerName)) { return; } spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake( - options => { + (options) => { if ( options === 'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials' @@ -101,7 +101,7 @@ describe('AuthenticationProviders', function() { }); }); - const getMockMyOauthProvider = function() { + const getMockMyOauthProvider = function () { return { authData: { id: '12345', @@ -114,7 +114,7 @@ describe('AuthenticationProviders', function() { synchronizedAuthToken: null, synchronizedExpiration: null, - authenticate: function(options) { + authenticate: function (options) { if (this.shouldError) { options.error(this, 'An error occurred'); } else if (this.shouldCancel) { @@ -123,7 +123,7 @@ describe('AuthenticationProviders', function() { options.success(this, this.authData); } }, - restoreAuthentication: function(authData) { + restoreAuthentication: function (authData) { if (!authData) { this.synchronizedUserId = null; this.synchronizedAuthToken = null; @@ -135,10 +135,10 @@ describe('AuthenticationProviders', function() { this.synchronizedExpiration = authData.expiration_date; return true; }, - getAuthType: function() { + getAuthType: function () { return 'myoauth'; }, - deauthenticate: function() { + deauthenticate: function () { this.loggedOut = true; this.restoreAuthentication(null); }, @@ -146,16 +146,16 @@ describe('AuthenticationProviders', function() { }; Parse.User.extend({ - extended: function() { + extended: function () { return true; }, }); - const createOAuthUser = function(callback) { + const createOAuthUser = function (callback) { return createOAuthUserWithSessionToken(undefined, callback); }; - const createOAuthUserWithSessionToken = function(token, callback) { + const createOAuthUserWithSessionToken = function (token, callback) { const jsonBody = { authData: { myoauth: getMockMyOauthProvider().authData, @@ -175,7 +175,7 @@ describe('AuthenticationProviders', function() { body: jsonBody, }; return request(options) - .then(response => { + .then((response) => { if (callback) { callback(null, response, response.data); } @@ -184,7 +184,7 @@ describe('AuthenticationProviders', function() { body: response.data, }; }) - .catch(error => { + .catch((error) => { if (callback) { callback(error); } @@ -192,7 +192,7 @@ describe('AuthenticationProviders', function() { }); }; - it('should create user with REST API', done => { + it('should create user with REST API', (done) => { createOAuthUser((error, response, body) => { expect(error).toBe(null); const b = body; @@ -203,7 +203,7 @@ describe('AuthenticationProviders', function() { const q = new Parse.Query('_Session'); q.equalTo('sessionToken', sessionToken); q.first({ useMasterKey: true }) - .then(res => { + .then((res) => { if (!res) { fail('should not fail fetching the session'); done(); @@ -219,7 +219,7 @@ describe('AuthenticationProviders', function() { }); }); - it('should only create a single user with REST API', done => { + it('should only create a single user with REST API', (done) => { let objectId; createOAuthUser((error, response, body) => { expect(error).toBe(null); @@ -239,9 +239,9 @@ describe('AuthenticationProviders', function() { }); }); - it("should fail to link if session token don't match user", done => { + it("should fail to link if session token don't match user", (done) => { Parse.User.signUp('myUser', 'password') - .then(user => { + .then((user) => { return createOAuthUserWithSessionToken(user.getSessionToken()); }) .then(() => { @@ -250,7 +250,7 @@ describe('AuthenticationProviders', function() { .then(() => { return Parse.User.signUp('myUser2', 'password'); }) - .then(user => { + .then((user) => { return createOAuthUserWithSessionToken(user.getSessionToken()); }) .then(fail, ({ data }) => { @@ -330,16 +330,16 @@ describe('AuthenticationProviders', function() { expect(typeof authAdapter.validateAppId).toBe('function'); } - it('properly loads custom adapter', done => { + it('properly loads custom adapter', (done) => { const validAuthData = { id: 'hello', token: 'world', }; const adapter = { - validateAppId: function() { + validateAppId: function () { return Promise.resolve(); }, - validateAuthData: function(authData) { + validateAuthData: function (authData) { if ( authData.id == validAuthData.id && authData.token == validAuthData.token @@ -370,14 +370,14 @@ describe('AuthenticationProviders', function() { expect(appIdSpy).not.toHaveBeenCalled(); done(); }, - err => { + (err) => { jfail(err); done(); } ); }); - it('properly loads custom adapter module object', done => { + it('properly loads custom adapter module object', (done) => { const authenticationHandler = authenticationLoader({ customAuthentication: path.resolve('./spec/support/CustomAuth.js'), }); @@ -394,14 +394,14 @@ describe('AuthenticationProviders', function() { () => { done(); }, - err => { + (err) => { jfail(err); done(); } ); }); - it('properly loads custom adapter module object (again)', done => { + it('properly loads custom adapter module object (again)', (done) => { const authenticationHandler = authenticationLoader({ customAuthentication: { module: path.resolve('./spec/support/CustomAuthFunction.js'), @@ -421,7 +421,7 @@ describe('AuthenticationProviders', function() { () => { done(); }, - err => { + (err) => { jfail(err); done(); } @@ -530,7 +530,7 @@ describe('AuthenticationProviders', function() { expect(providerOptions.appSecret).toEqual('secret'); }); - it('should fail if Facebook appIds is not configured properly', done => { + it('should fail if Facebook appIds is not configured properly', (done) => { const options = { facebookaccountkit: { appIds: [], @@ -540,13 +540,13 @@ describe('AuthenticationProviders', function() { 'facebookaccountkit', options ); - adapter.validateAppId(appIds).then(done.fail, err => { + adapter.validateAppId(appIds).then(done.fail, (err) => { expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); done(); }); }); - it('should fail to validate Facebook accountkit auth with bad token', done => { + it('should fail to validate Facebook accountkit auth with bad token', (done) => { const options = { facebookaccountkit: { appIds: ['a', 'b'], @@ -560,14 +560,14 @@ describe('AuthenticationProviders', function() { 'facebookaccountkit', options ); - adapter.validateAuthData(authData).then(done.fail, err => { + adapter.validateAuthData(authData).then(done.fail, (err) => { expect(err.code).toBe(190); expect(err.type).toBe('OAuthException'); done(); }); }); - it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', done => { + it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', (done) => { const options = { facebookaccountkit: { appIds: ['a', 'b'], @@ -582,11 +582,13 @@ describe('AuthenticationProviders', function() { 'facebookaccountkit', options ); - adapter.validateAuthData(authData, providerOptions).then(done.fail, err => { - expect(err.code).toBe(190); - expect(err.type).toBe('OAuthException'); - done(); - }); + adapter + .validateAuthData(authData, providerOptions) + .then(done.fail, (err) => { + expect(err.code).toBe(190); + expect(err.type).toBe('OAuthException'); + done(); + }); }); }); @@ -627,66 +629,124 @@ describe('instagram auth adapter', () => { describe('google auth adapter', () => { const google = require('../lib/Adapters/Auth/google'); - const httpsRequest = require('../lib/Adapters/Auth/httpsRequest'); + const jwt = require('jsonwebtoken'); - it('should use id_token for validation is passed', async () => { - spyOn(httpsRequest, 'get').and.callFake(() => { - return Promise.resolve({ sub: 'userId' }); - }); - await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + it('should throw error with missing id_token', async () => { + try { + await google.validateAuthData({}, {}); + fail(); + } catch (e) { + expect(e.message).toBe('id token is invalid for this user.'); + } }); - it('should use id_token for validation is passed and responds with user_id', async () => { - spyOn(httpsRequest, 'get').and.callFake(() => { - return Promise.resolve({ user_id: 'userId' }); - }); - await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + it('should not decode invalid id_token', async () => { + try { + await google.validateAuthData( + { id: 'the_user_id', id_token: 'the_token' }, + {} + ); + fail(); + } catch (e) { + expect(e.message).toBe('provided token does not decode as JWT'); + } }); - it('should use access_token for validation is passed and responds with user_id', async () => { - spyOn(httpsRequest, 'get').and.callFake(() => { - return Promise.resolve({ user_id: 'userId' }); - }); - await google.validateAuthData( - { id: 'userId', access_token: 'the_token' }, - {} + // it('should throw error if public key used to encode token is not available', async () => { + // const fakeDecodedToken = { header: { kid: '789', alg: 'RS256' } }; + // try { + // spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); + + // await google.validateAuthData({ id: 'the_user_id', id_token: 'the_token' }, {}); + // fail(); + // } catch (e) { + // expect(e.message).toBe( + // `Unable to find matching key for Key ID: ${fakeDecodedToken.header.kid}` + // ); + // } + // }); + + it('(using client id as string) should verify id_token', async () => { + const fakeClaim = { + iss: 'https://accounts.google.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + const result = await google.validateAuthData( + { id: 'the_user_id', id_token: 'the_token' }, + { clientId: 'secret' } ); + expect(result).toEqual(fakeClaim); }); - it('should use access_token for validation is passed with sub', async () => { - spyOn(httpsRequest, 'get').and.callFake(() => { - return Promise.resolve({ sub: 'userId' }); - }); - await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); + it('(using client id as string) should throw error with with invalid jwt issuer', async () => { + const fakeClaim = { + iss: 'https://not.google.com', + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + + try { + await google.validateAuthData( + { id: 'the_user_id', id_token: 'the_token' }, + { clientId: 'secret' } + ); + fail(); + } catch (e) { + expect(e.message).toBe( + 'id token not issued by correct provider - expected: https://accounts.google.com | from: https://not.google.com' + ); + } }); - it('should fail when the id_token is invalid', async () => { - spyOn(httpsRequest, 'get').and.callFake(() => { - return Promise.resolve({ sub: 'badId' }); - }); + xit('(using client id as string) should throw error with invalid jwt client_id', async () => { + const fakeClaim = { + iss: 'https://accounts.google.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + try { await google.validateAuthData( - { id: 'userId', id_token: 'the_token' }, - {} + { id: 'INSERT ID HERE', token: 'INSERT APPLE TOKEN HERE' }, + { clientId: 'secret' } ); fail(); } catch (e) { - expect(e.message).toBe('Google auth is invalid for this user.'); + expect(e.message).toBe('jwt audience invalid. expected: secret'); } }); - it('should fail when the access_token is invalid', async () => { - spyOn(httpsRequest, 'get').and.callFake(() => { - return Promise.resolve({ sub: 'badId' }); - }); + xit('should throw error with invalid user id', async () => { + const fakeClaim = { + iss: 'https://accounts.google.com', + aud: 'secret', + exp: Date.now(), + sub: 'the_user_id', + }; + const fakeDecodedToken = { header: { kid: '123', alg: 'RS256' } }; + spyOn(jwt, 'decode').and.callFake(() => fakeDecodedToken); + spyOn(jwt, 'verify').and.callFake(() => fakeClaim); + try { await google.validateAuthData( - { id: 'userId', access_token: 'the_token' }, - {} + { id: 'invalid user', token: 'INSERT APPLE TOKEN HERE' }, + { clientId: 'INSERT CLIENT ID HERE' } ); fail(); } catch (e) { - expect(e.message).toBe('Google auth is invalid for this user.'); + expect(e.message).toBe('auth data is invalid for this user.'); } }); }); @@ -1593,13 +1653,13 @@ describe('microsoft graph auth adapter', () => { }); }); - it('should fail to validate Microsoft Graph auth with bad token', done => { + it('should fail to validate Microsoft Graph auth with bad token', (done) => { const authData = { id: 'fake-id', mail: 'fake@mail.com', access_token: 'very.long.bad.token', }; - microsoft.validateAuthData(authData).then(done.fail, err => { + microsoft.validateAuthData(authData).then(done.fail, (err) => { expect(err.code).toBe(101); expect(err.message).toBe( 'Microsoft Graph auth is invalid for this user.' diff --git a/src/Adapters/Auth/google.js b/src/Adapters/Auth/google.js index 9dacabdd62..267aebb6df 100644 --- a/src/Adapters/Auth/google.js +++ b/src/Adapters/Auth/google.js @@ -1,47 +1,90 @@ +"use strict"; + // Helper functions for accessing the google API. var Parse = require('parse/node').Parse; -const httpsRequest = require('./httpsRequest'); - -function validateIdToken(id, token) { - return googleRequest('tokeninfo?id_token=' + token).then(response => { - if (response && (response.sub == id || response.user_id == id)) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Google auth is invalid for this user.' - ); + +const https = require('https'); +const jwt = require('jsonwebtoken'); + +const TOKEN_ISSUER = 'https://accounts.google.com'; + +let cache = {}; + + +// Retrieve Google Signin Keys (with cache control) +function getGoogleKeyByKeyId(keyId) { + if (cache[keyId] && cache.expiresAt > new Date()) { + return cache[keyId]; + } + + return new Promise((resolve, reject) => { + https.get(`https://www.googleapis.com/oauth2/v3/certs`, res => { + let data = ''; + res.on('data', chunk => { + data += chunk.toString('utf8'); + }); + res.on('end', () => { + const {keys} = JSON.parse(data); + const pems = keys.reduce((pems, {n: modulus, e: exposant, kid}) => Object.assign(pems, {[kid]: rsaPublicKeyToPEM(modulus, exposant)}), {}); + + if (res.headers['cache-control']) { + var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/); + + if (expire) { + cache = Object.assign({}, pems, {expiresAt: new Date((new Date()).getTime() + Number(expire[1]) * 1000)}); + } + } + + resolve(pems[keyId]); + }); + }).on('error', reject); }); } -function validateAuthToken(id, token) { - return googleRequest('tokeninfo?access_token=' + token).then(response => { - if (response && (response.sub == id || response.user_id == id)) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Google auth is invalid for this user.' - ); - }); +function getHeaderFromToken(token) { + const decodedToken = jwt.decode(token, {complete: true}); + + if (!decodedToken) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `provided token does not decode as JWT`); + } + + return decodedToken.header; } -// Returns a promise that fulfills if this user id is valid. -function validateAuthData(authData) { - if (authData.id_token) { - return validateIdToken(authData.id, authData.id_token); - } else { - return validateAuthToken(authData.id, authData.access_token).then( - () => { - // Validation with auth token worked - return; - }, - () => { - // Try with the id_token param - return validateIdToken(authData.id, authData.access_token); - } - ); +async function verifyIdToken({id_token: token, id}, {clientId}) { + if (!token) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token is invalid for this user.`); + } + + const { kid: keyId, alg: algorithm } = getHeaderFromToken(token); + let jwtClaims; + const googleKey = await getGoogleKeyByKeyId(keyId); + + try { + jwtClaims = jwt.verify(token, googleKey, { algorithms: algorithm, audience: clientId }); + } catch (exception) { + const message = exception.message; + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`); + } + + if (jwtClaims.iss !== TOKEN_ISSUER) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token not issued by correct provider - expected: ${TOKEN_ISSUER} | from: ${jwtClaims.iss}`); + } + + if (jwtClaims.sub !== id) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `auth data is invalid for this user.`); } + + if (clientId && jwtClaims.aud !== clientId) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token not authorized for this clientId.`); + } + + return jwtClaims; +} + +// Returns a promise that fulfills if this user id is valid. +function validateAuthData(authData, options) { + return verifyIdToken(authData, options); } // Returns a promise that fulfills if this app id is valid. @@ -49,12 +92,58 @@ function validateAppId() { return Promise.resolve(); } -// A promisey wrapper for api requests -function googleRequest(path) { - return httpsRequest.get('https://www.googleapis.com/oauth2/v3/' + path); -} - module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData, + validateAuthData: validateAuthData }; + + +// Helpers functions to convert the RSA certs to PEM (from jwks-rsa) +function rsaPublicKeyToPEM(modulusB64, exponentB64) { + const modulus = new Buffer(modulusB64, 'base64'); + const exponent = new Buffer(exponentB64, 'base64'); + const modulusHex = prepadSigned(modulus.toString('hex')); + const exponentHex = prepadSigned(exponent.toString('hex')); + const modlen = modulusHex.length / 2; + const explen = exponentHex.length / 2; + + const encodedModlen = encodeLengthHex(modlen); + const encodedExplen = encodeLengthHex(explen); + const encodedPubkey = '30' + + encodeLengthHex(modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2) + + '02' + encodedModlen + modulusHex + + '02' + encodedExplen + exponentHex; + + const der = new Buffer(encodedPubkey, 'hex') + .toString('base64'); + + let pem = '-----BEGIN RSA PUBLIC KEY-----\n'; + pem += `${der.match(/.{1,64}/g).join('\n')}`; + pem += '\n-----END RSA PUBLIC KEY-----\n'; + return pem; +} + +function prepadSigned(hexStr) { + const msb = hexStr[0]; + if (msb < '0' || msb > '7') { + return `00${hexStr}`; + } + return hexStr; +} + +function toHex(number) { + const nstr = number.toString(16); + if (nstr.length % 2) { + return `0${nstr}`; + } + return nstr; +} + +function encodeLengthHex(n) { + if (n <= 127) { + return toHex(n); + } + const nHex = toHex(n); + const lengthOfLengthByte = 128 + nHex.length / 2; + return toHex(lengthOfLengthByte) + nHex; +} From 3bd5684f67a16ec96907b50ab5fc9daa9e4fa8e0 Mon Sep 17 00:00:00 2001 From: Manuel Date: Wed, 15 Jul 2020 20:10:33 +0200 Subject: [PATCH 03/33] Add idempotency (#6748) * added idempotency router and middleware * added idempotency rules for routes classes, functions, jobs, installaions, users * fixed typo * ignore requests without header * removed unused var * enabled feature only for MongoDB * changed code comment * fixed inconsistend storage adapter specification * Trigger notification * Travis CI trigger * Travis CI trigger * Travis CI trigger * rebuilt option definitions * fixed incorrect import path * added new request ID header to allowed headers * fixed typescript typos * add new system class to spec helper * fixed typescript typos * re-added postgres conn parameter * removed postgres conn parameter * fixed incorrect schema for index creation * temporarily disabling index creation to fix postgres issue * temporarily disabling index creation to fix postgres issue * temporarily disabling index creation to fix postgres issue * temporarily disabling index creation to fix postgres issue * temporarily disabling index creation to fix postgres issue * temporarily disabling index creation to fix postgres issue * temporarily disabling index creation to fix postgres issue * trying to fix postgres issue * fixed incorrect auth when writing to _Idempotency * trying to fix postgres issue * Travis CI trigger * added test cases * removed number grouping * fixed test description * trying to fix postgres issue * added Github readme docs * added change log * refactored tests; fixed some typos * fixed test case * fixed default TTL value * Travis CI Trigger * Travis CI Trigger * Travis CI Trigger * added test case to increase coverage * Trigger Travis CI * changed configuration syntax to use regex; added test cases * removed unused vars * removed IdempotencyRouter * Trigger Travis CI * updated docs * updated docs * updated docs * updated docs * update docs * Trigger Travis CI * fixed coverage * removed code comments --- CHANGELOG.md | 1 + README.md | 33 + resources/buildConfigDefinitions.js | 10 + spec/Idempotency.spec.js | 247 ++++ spec/ParseQuery.Aggregate.spec.js | 2 +- spec/helper.js | 1 + .../Storage/Mongo/MongoStorageAdapter.js | 6 +- .../Postgres/PostgresStorageAdapter.js | 5 +- src/Adapters/Storage/StorageAdapter.js | 2 +- src/Config.js | 23 +- src/Controllers/DatabaseController.js | 49 + src/Controllers/SchemaController.js | 14 + src/Options/Definitions.js | 1025 ++++++++--------- src/Options/docs.js | 8 + src/Options/index.js | 13 + src/Routers/ClassesRouter.js | 5 +- src/Routers/FunctionsRouter.js | 4 +- src/Routers/InstallationsRouter.js | 5 +- src/Routers/UsersRouter.js | 5 +- src/middlewares.js | 50 +- src/rest.js | 1 + 21 files changed, 976 insertions(+), 533 deletions(-) create mode 100644 spec/Idempotency.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index d719f9fca2..3ee55a1936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/4.2.0...master) +- NEW (EXPERIMENTAL): Idempotency enforcement for client requests. This deduplicates requests where the client intends to send one request to Parse Server but due to network issues the server receives the request multiple times. **Caution, this is an experimental feature that may not be appropriate for production.** [#6744](https://github.com/parse-community/parse-server/issues/6744). Thanks to [Manuel Trezza](https://github.com/mtrezza). ### 4.2.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.1.0...4.2.0) diff --git a/README.md b/README.md index 51338bcc31..7dde76a41f 100644 --- a/README.md +++ b/README.md @@ -388,6 +388,39 @@ Parse Server allows developers to choose from several options when hosting files `GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters). +### Idempodency Enforcement + +**Caution, this is an experimental feature that may not be appropriate for production.** + +This feature deduplicates identical requests that are received by Parse Server mutliple times, typically due to network issues or network adapter access restrictions on mobile operating systems. + +Identical requests are identified by their request header `X-Parse-Request-Id`. Therefore a client request has to include this header for deduplication to be applied. Requests that do not contain this header cannot be deduplicated and are processed normally by Parse Server. This means rolling out this feature to clients is seamless as Parse Server still processes request without this header when this feature is enbabled. + +> This feature needs to be enabled on the client side to send the header and on the server to process the header. Refer to the specific Parse SDK docs to see whether the feature is supported yet. + +Deduplication is only done for object creation and update (`POST` and `PUT` requests). Deduplication is not done for object finding and deletion (`GET` and `DELETE` requests), as these operations are already idempotent by definition. + +#### Configuration example +``` +let api = new ParseServer({ + idempotencyOptions: { + paths: [".*"], // enforce for all requests + ttl: 120 // keep request IDs for 120s + } +} +``` +#### Parameters + +| Parameter | Optional | Type | Default value | Example values | Environment variable | Description | +|-----------|----------|--------|---------------|-----------|-----------|-------------| +| `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | +| `idempotencyOptions.paths`| yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | +| `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | + +#### Notes + +- This feature is currently only available for MongoDB and not for Postgres. + ### Logging Parse Server will, by default, log: diff --git a/resources/buildConfigDefinitions.js b/resources/buildConfigDefinitions.js index 8215792a82..a640e1c3c7 100644 --- a/resources/buildConfigDefinitions.js +++ b/resources/buildConfigDefinitions.js @@ -52,6 +52,9 @@ function getENVPrefix(iface) { if (iface.id.name === 'LiveQueryOptions') { return 'PARSE_SERVER_LIVEQUERY_'; } + if (iface.id.name === 'IdempotencyOptions') { + return 'PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_'; + } } function processProperty(property, iface) { @@ -170,6 +173,13 @@ function parseDefaultValue(elt, value, t) { }); literalValue = t.objectExpression(props); } + if (type == 'IdempotencyOptions') { + const object = parsers.objectParser(value); + const props = Object.keys(object).map((key) => { + return t.objectProperty(key, object[value]); + }); + literalValue = t.objectExpression(props); + } if (type == 'ProtectedFields') { const prop = t.objectProperty( t.stringLiteral('_User'), t.objectPattern([ diff --git a/spec/Idempotency.spec.js b/spec/Idempotency.spec.js new file mode 100644 index 0000000000..2d0e99aa19 --- /dev/null +++ b/spec/Idempotency.spec.js @@ -0,0 +1,247 @@ +'use strict'; +const Config = require('../lib/Config'); +const Definitions = require('../lib/Options/Definitions'); +const request = require('../lib/request'); +const rest = require('../lib/rest'); +const auth = require('../lib/Auth'); +const uuid = require('uuid'); + +describe_only_db('mongo')('Idempotency', () => { + // Parameters + /** Enable TTL expiration simulated by removing entry instead of waiting for MongoDB TTL monitor which + runs only every 60s, so it can take up to 119s until entry removal - ain't nobody got time for that */ + const SIMULATE_TTL = true; + // Helpers + async function deleteRequestEntry(reqId) { + const config = Config.get(Parse.applicationId); + const res = await rest.find( + config, + auth.master(config), + '_Idempotency', + { reqId: reqId }, + { limit: 1 } + ); + await rest.del( + config, + auth.master(config), + '_Idempotency', + res.results[0].objectId); + } + async function setup(options) { + await reconfigureServer({ + appId: Parse.applicationId, + masterKey: Parse.masterKey, + serverURL: Parse.serverURL, + idempotencyOptions: options, + }); + } + // Setups + beforeEach(async () => { + if (SIMULATE_TTL) { jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; } + await setup({ + paths: [ + "functions/.*", + "jobs/.*", + "classes/.*", + "users", + "installations" + ], + ttl: 30, + }); + }); + // Tests + it('should enforce idempotency for cloud code function', async () => { + let counter = 0; + Parse.Cloud.define('myFunction', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(30); + await request(params); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual("Duplicate request"); + }); + expect(counter).toBe(1); + }); + + it('should delete request entry after TTL', async () => { + let counter = 0; + Parse.Cloud.define('myFunction', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + await expectAsync(request(params)).toBeResolved(); + if (SIMULATE_TTL) { + await deleteRequestEntry('abc-123'); + } else { + await new Promise(resolve => setTimeout(resolve, 130000)); + } + await expectAsync(request(params)).toBeResolved(); + expect(counter).toBe(2); + }); + + it('should enforce idempotency for cloud code jobs', async () => { + let counter = 0; + Parse.Cloud.job('myJob', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/jobs/myJob', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual("Duplicate request"); + }); + expect(counter).toBe(1); + }); + + it('should enforce idempotency for class object creation', async () => { + let counter = 0; + Parse.Cloud.afterSave('MyClass', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/classes/MyClass', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual("Duplicate request"); + }); + expect(counter).toBe(1); + }); + + it('should enforce idempotency for user object creation', async () => { + let counter = 0; + Parse.Cloud.afterSave('_User', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/users', + body: { + username: "user", + password: "pass" + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual("Duplicate request"); + }); + expect(counter).toBe(1); + }); + + it('should enforce idempotency for installation object creation', async () => { + let counter = 0; + Parse.Cloud.afterSave('_Installation', () => { + counter++; + }); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/installations', + body: { + installationId: "1", + deviceType: "ios" + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + await expectAsync(request(params)).toBeResolved(); + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual("Duplicate request"); + }); + expect(counter).toBe(1); + }); + + it('should not interfere with calls of different request ID', async () => { + let counter = 0; + Parse.Cloud.afterSave('MyClass', () => { + counter++; + }); + const promises = [...Array(100).keys()].map(() => { + const params = { + method: 'POST', + url: 'http://localhost:8378/1/classes/MyClass', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': uuid.v4() + } + }; + return request(params); + }); + await expectAsync(Promise.all(promises)).toBeResolved(); + expect(counter).toBe(100); + }); + + it('should re-throw any other error unchanged when writing request entry fails for any other reason', async () => { + spyOn(rest, 'create').and.rejectWith(new Parse.Error(0, "some other error")); + Parse.Cloud.define('myFunction', () => {}); + const params = { + method: 'POST', + url: 'http://localhost:8378/1/functions/myFunction', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + 'X-Parse-Request-Id': 'abc-123' + } + }; + await request(params).then(fail, e => { + expect(e.status).toEqual(400); + expect(e.data.error).toEqual("some other error"); + }); + }); + + it('should use default configuration when none is set', async () => { + await setup({}); + expect(Config.get(Parse.applicationId).idempotencyOptions.ttl).toBe(Definitions.IdempotencyOptions.ttl.default); + expect(Config.get(Parse.applicationId).idempotencyOptions.paths).toBe(Definitions.IdempotencyOptions.paths.default); + }); + + it('should throw on invalid configuration', async () => { + await expectAsync(setup({ paths: 1 })).toBeRejected(); + await expectAsync(setup({ ttl: 'a' })).toBeRejected(); + await expectAsync(setup({ ttl: 0 })).toBeRejected(); + await expectAsync(setup({ ttl: -1 })).toBeRejected(); + }); +}); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index b7dd291d1c..d9a684d0a9 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -1440,7 +1440,7 @@ describe('Parse.Query Aggregate testing', () => { ['location'], 'geoIndex', false, - '2dsphere' + { indexType: '2dsphere' }, ); // Create objects const GeoObject = Parse.Object.extend('GeoObject'); diff --git a/spec/helper.js b/spec/helper.js index 16d25ba1b8..84e704f7e3 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -230,6 +230,7 @@ afterEach(function(done) { '_Session', '_Product', '_Audience', + '_Idempotency' ].indexOf(className) >= 0 ); } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 7b8d2a3ef3..9c08d3a073 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -692,7 +692,7 @@ export class MongoStorageAdapter implements StorageAdapter { fieldNames: string[], indexName: ?string, caseInsensitive: boolean = false, - indexType: any = 1 + options?: Object = {}, ): Promise { schema = convertParseSchemaToMongoSchema(schema); const indexCreationRequest = {}; @@ -700,11 +700,12 @@ export class MongoStorageAdapter implements StorageAdapter { transformKey(className, fieldName, schema) ); mongoFieldNames.forEach((fieldName) => { - indexCreationRequest[fieldName] = indexType; + indexCreationRequest[fieldName] = options.indexType !== undefined ? options.indexType : 1; }); const defaultOptions: Object = { background: true, sparse: true }; const indexNameOptions: Object = indexName ? { name: indexName } : {}; + const ttlOptions: Object = options.ttl !== undefined ? { expireAfterSeconds: options.ttl } : {}; const caseInsensitiveOptions: Object = caseInsensitive ? { collation: MongoCollection.caseInsensitiveCollation() } : {}; @@ -712,6 +713,7 @@ export class MongoStorageAdapter implements StorageAdapter { ...defaultOptions, ...caseInsensitiveOptions, ...indexNameOptions, + ...ttlOptions, }; return this._adaptiveCollection(className) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 6b58120778..ce7b746f4f 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1209,6 +1209,7 @@ export class PostgresStorageAdapter implements StorageAdapter { '_GlobalConfig', '_GraphQLConfig', '_Audience', + '_Idempotency', ...results.map((result) => result.className), ...joins, ]; @@ -2576,9 +2577,9 @@ export class PostgresStorageAdapter implements StorageAdapter { fieldNames: string[], indexName: ?string, caseInsensitive: boolean = false, - conn: ?any = null + options?: Object = {}, ): Promise { - conn = conn != null ? conn : this._client; + const conn = options.conn !== undefined ? options.conn : this._client; const defaultIndexName = `parse_default_${fieldNames.sort().join('_')}`; const indexNameOptions: Object = indexName != null ? { name: indexName } : { name: defaultIndexName }; diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index 0256841b23..7031c21d7f 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -93,7 +93,7 @@ export interface StorageAdapter { fieldNames: string[], indexName?: string, caseSensitive?: boolean, - indexType?: any + options?: Object, ): Promise; ensureUniqueness( className: string, diff --git a/src/Config.js b/src/Config.js index 2077626ff8..214e22ca57 100644 --- a/src/Config.js +++ b/src/Config.js @@ -6,6 +6,7 @@ import AppCache from './cache'; import SchemaCache from './Controllers/SchemaCache'; import DatabaseController from './Controllers/DatabaseController'; import net from 'net'; +import { IdempotencyOptions } from './Options/Definitions'; function removeTrailingSlash(str) { if (!str) { @@ -73,6 +74,7 @@ export class Config { masterKey, readOnlyMasterKey, allowHeaders, + idempotencyOptions, }) { if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); @@ -104,14 +106,27 @@ export class Config { throw 'publicServerURL should be a valid HTTPS URL starting with https://'; } } - this.validateSessionConfiguration(sessionLength, expireInactiveSessions); - this.validateMasterKeyIps(masterKeyIps); - this.validateMaxLimit(maxLimit); - this.validateAllowHeaders(allowHeaders); + this.validateIdempotencyOptions(idempotencyOptions); + } + + static validateIdempotencyOptions(idempotencyOptions) { + if (!idempotencyOptions) { return; } + if (idempotencyOptions.ttl === undefined) { + idempotencyOptions.ttl = IdempotencyOptions.ttl.default; + } else if (!isNaN(idempotencyOptions.ttl) && idempotencyOptions.ttl <= 0) { + throw 'idempotency TTL value must be greater than 0 seconds'; + } else if (isNaN(idempotencyOptions.ttl)) { + throw 'idempotency TTL value must be a number'; + } + if (!idempotencyOptions.paths) { + idempotencyOptions.paths = IdempotencyOptions.paths.default; + } else if (!(idempotencyOptions.paths instanceof Array)) { + throw 'idempotency paths must be of an array of strings'; + } } static validateAccountLockoutPolicy(accountLockout) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index f71350eef3..657b40db8a 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -244,6 +244,7 @@ const filterSensitiveData = ( }; import type { LoadSchemaOptions } from './types'; +import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; // Runs an update on the database. // Returns a promise for an object with the new values for field @@ -1736,6 +1737,12 @@ class DatabaseController { ...SchemaController.defaultColumns._Role, }, }; + const requiredIdempotencyFields = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._Idempotency, + }, + }; const userClassPromise = this.loadSchema().then(schema => schema.enforceClassExists('_User') @@ -1743,6 +1750,9 @@ class DatabaseController { const roleClassPromise = this.loadSchema().then(schema => schema.enforceClassExists('_Role') ); + const idempotencyClassPromise = this.adapter instanceof MongoStorageAdapter + ? this.loadSchema().then((schema) => schema.enforceClassExists('_Idempotency')) + : Promise.resolve(); const usernameUniqueness = userClassPromise .then(() => @@ -1807,6 +1817,43 @@ class DatabaseController { throw error; }); + const idempotencyRequestIdIndex = this.adapter instanceof MongoStorageAdapter + ? idempotencyClassPromise + .then(() => + this.adapter.ensureUniqueness( + '_Idempotency', + requiredIdempotencyFields, + ['reqId'] + )) + .catch((error) => { + logger.warn( + 'Unable to ensure uniqueness for idempotency request ID: ', + error + ); + throw error; + }) + : Promise.resolve(); + + const idempotencyExpireIndex = this.adapter instanceof MongoStorageAdapter + ? idempotencyClassPromise + .then(() => + this.adapter.ensureIndex( + '_Idempotency', + requiredIdempotencyFields, + ['expire'], + 'ttl', + false, + { ttl: 0 }, + )) + .catch((error) => { + logger.warn( + 'Unable to create TTL index for idempotency expire date: ', + error + ); + throw error; + }) + : Promise.resolve(); + const indexPromise = this.adapter.updateSchemaWithIndexes(); // Create tables for volatile classes @@ -1819,6 +1866,8 @@ class DatabaseController { emailUniqueness, emailCaseInsensitiveIndex, roleUniqueness, + idempotencyRequestIdIndex, + idempotencyExpireIndex, adapterInit, indexPromise, ]); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index c5b04dcfc6..be8a3102b4 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -144,6 +144,10 @@ const defaultColumns: { [string]: SchemaFields } = Object.freeze({ lastUsed: { type: 'Date' }, timesUsed: { type: 'Number' }, }, + _Idempotency: { + reqId: { type: 'String' }, + expire: { type: 'Date' }, + } }); const requiredColumns = Object.freeze({ @@ -161,6 +165,7 @@ const systemClasses = Object.freeze([ '_JobStatus', '_JobSchedule', '_Audience', + '_Idempotency' ]); const volatileClasses = Object.freeze([ @@ -171,6 +176,7 @@ const volatileClasses = Object.freeze([ '_GraphQLConfig', '_JobSchedule', '_Audience', + '_Idempotency' ]); // Anything that start with role @@ -660,6 +666,13 @@ const _AudienceSchema = convertSchemaToAdapterSchema( classLevelPermissions: {}, }) ); +const _IdempotencySchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_Idempotency', + fields: defaultColumns._Idempotency, + classLevelPermissions: {}, + }) +); const VolatileClassesSchemas = [ _HooksSchema, _JobStatusSchema, @@ -668,6 +681,7 @@ const VolatileClassesSchemas = [ _GlobalConfigSchema, _GraphQLConfigSchema, _AudienceSchema, + _IdempotencySchema ]; const dbTypeMatchesObjectType = ( diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 682784b1f4..3879198d68 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -3,527 +3,522 @@ This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js */ -var parsers = require('./parsers'); +var parsers = require("./parsers"); module.exports.ParseServerOptions = { - accountLockout: { - env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', - help: 'account lockout policy for failed login attempts', - action: parsers.objectParser, - }, - allowClientClassCreation: { - env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', - help: 'Enable (or disable) client class creation, defaults to true', - action: parsers.booleanParser, - default: true, - }, - allowCustomObjectId: { - env: 'PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID', - help: 'Enable (or disable) custom objectId', - action: parsers.booleanParser, - default: false, - }, - allowHeaders: { - env: 'PARSE_SERVER_ALLOW_HEADERS', - help: 'Add headers to Access-Control-Allow-Headers', - action: parsers.arrayParser, - }, - allowOrigin: { - env: 'PARSE_SERVER_ALLOW_ORIGIN', - help: 'Sets the origin to Access-Control-Allow-Origin', - }, - analyticsAdapter: { - env: 'PARSE_SERVER_ANALYTICS_ADAPTER', - help: 'Adapter module for the analytics', - action: parsers.moduleOrObjectParser, - }, - appId: { - env: 'PARSE_SERVER_APPLICATION_ID', - help: 'Your Parse Application ID', - required: true, - }, - appName: { - env: 'PARSE_SERVER_APP_NAME', - help: 'Sets the app name', - }, - auth: { - env: 'PARSE_SERVER_AUTH_PROVIDERS', - help: - 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', - action: parsers.objectParser, - }, - cacheAdapter: { - env: 'PARSE_SERVER_CACHE_ADAPTER', - help: 'Adapter module for the cache', - action: parsers.moduleOrObjectParser, - }, - cacheMaxSize: { - env: 'PARSE_SERVER_CACHE_MAX_SIZE', - help: 'Sets the maximum size for the in memory cache, defaults to 10000', - action: parsers.numberParser('cacheMaxSize'), - default: 10000, - }, - cacheTTL: { - env: 'PARSE_SERVER_CACHE_TTL', - help: - 'Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)', - action: parsers.numberParser('cacheTTL'), - default: 5000, - }, - clientKey: { - env: 'PARSE_SERVER_CLIENT_KEY', - help: 'Key for iOS, MacOS, tvOS clients', - }, - cloud: { - env: 'PARSE_SERVER_CLOUD', - help: 'Full path to your cloud code main.js', - }, - cluster: { - env: 'PARSE_SERVER_CLUSTER', - help: - 'Run with cluster, optionally set the number of processes default to os.cpus().length', - action: parsers.numberOrBooleanParser, - }, - collectionPrefix: { - env: 'PARSE_SERVER_COLLECTION_PREFIX', - help: 'A collection prefix for the classes', - default: '', - }, - customPages: { - env: 'PARSE_SERVER_CUSTOM_PAGES', - help: 'custom pages for password validation and reset', - action: parsers.objectParser, - default: {}, - }, - databaseAdapter: { - env: 'PARSE_SERVER_DATABASE_ADAPTER', - help: 'Adapter module for the database', - action: parsers.moduleOrObjectParser, - }, - databaseOptions: { - env: 'PARSE_SERVER_DATABASE_OPTIONS', - help: 'Options to pass to the mongodb client', - action: parsers.objectParser, - }, - databaseURI: { - env: 'PARSE_SERVER_DATABASE_URI', - help: - 'The full URI to your database. Supported databases are mongodb or postgres.', - required: true, - default: 'mongodb://localhost:27017/parse', - }, - directAccess: { - env: 'PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS', - help: - 'Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.', - action: parsers.booleanParser, - default: false, - }, - dotNetKey: { - env: 'PARSE_SERVER_DOT_NET_KEY', - help: 'Key for Unity and .Net SDK', - }, - emailAdapter: { - env: 'PARSE_SERVER_EMAIL_ADAPTER', - help: 'Adapter module for email sending', - action: parsers.moduleOrObjectParser, - }, - emailVerifyTokenValidityDuration: { - env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', - help: 'Email verification token validity duration, in seconds', - action: parsers.numberParser('emailVerifyTokenValidityDuration'), - }, - enableAnonymousUsers: { - env: 'PARSE_SERVER_ENABLE_ANON_USERS', - help: 'Enable (or disable) anonymous users, defaults to true', - action: parsers.booleanParser, - default: true, - }, - enableExpressErrorHandler: { - env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', - help: 'Enables the default express error handler for all errors', - action: parsers.booleanParser, - default: false, - }, - enableSingleSchemaCache: { - env: 'PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE', - help: - 'Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.', - action: parsers.booleanParser, - default: false, - }, - expireInactiveSessions: { - env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', - help: - 'Sets wether we should expire the inactive sessions, defaults to true', - action: parsers.booleanParser, - default: true, - }, - fileKey: { - env: 'PARSE_SERVER_FILE_KEY', - help: 'Key for your files', - }, - filesAdapter: { - env: 'PARSE_SERVER_FILES_ADAPTER', - help: 'Adapter module for the files sub-system', - action: parsers.moduleOrObjectParser, - }, - graphQLPath: { - env: 'PARSE_SERVER_GRAPHQL_PATH', - help: 'Mount path for the GraphQL endpoint, defaults to /graphql', - default: '/graphql', - }, - graphQLSchema: { - env: 'PARSE_SERVER_GRAPH_QLSCHEMA', - help: 'Full path to your GraphQL custom schema.graphql file', - }, - host: { - env: 'PARSE_SERVER_HOST', - help: 'The host to serve ParseServer on, defaults to 0.0.0.0', - default: '0.0.0.0', - }, - javascriptKey: { - env: 'PARSE_SERVER_JAVASCRIPT_KEY', - help: 'Key for the Javascript SDK', - }, - jsonLogs: { - env: 'JSON_LOGS', - help: 'Log as structured JSON objects', - action: parsers.booleanParser, - }, - liveQuery: { - env: 'PARSE_SERVER_LIVE_QUERY', - help: "parse-server's LiveQuery configuration object", - action: parsers.objectParser, - }, - liveQueryServerOptions: { - env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', - help: - 'Live query server configuration options (will start the liveQuery server)', - action: parsers.objectParser, - }, - loggerAdapter: { - env: 'PARSE_SERVER_LOGGER_ADAPTER', - help: 'Adapter module for the logging sub-system', - action: parsers.moduleOrObjectParser, - }, - logLevel: { - env: 'PARSE_SERVER_LOG_LEVEL', - help: 'Sets the level for logs', - }, - logsFolder: { - env: 'PARSE_SERVER_LOGS_FOLDER', - help: - "Folder for the logs (defaults to './logs'); set to null to disable file based logging", - default: './logs', - }, - masterKey: { - env: 'PARSE_SERVER_MASTER_KEY', - help: 'Your Parse Master Key', - required: true, - }, - masterKeyIps: { - env: 'PARSE_SERVER_MASTER_KEY_IPS', - help: - 'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)', - action: parsers.arrayParser, - default: [], - }, - maxLimit: { - env: 'PARSE_SERVER_MAX_LIMIT', - help: 'Max value for limit option on queries, defaults to unlimited', - action: parsers.numberParser('maxLimit'), - }, - maxLogFiles: { - env: 'PARSE_SERVER_MAX_LOG_FILES', - help: - "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", - action: parsers.objectParser, - }, - maxUploadSize: { - env: 'PARSE_SERVER_MAX_UPLOAD_SIZE', - help: 'Max file size for uploads, defaults to 20mb', - default: '20mb', - }, - middleware: { - env: 'PARSE_SERVER_MIDDLEWARE', - help: 'middleware for express server, can be string or function', - }, - mountGraphQL: { - env: 'PARSE_SERVER_MOUNT_GRAPHQL', - help: 'Mounts the GraphQL endpoint', - action: parsers.booleanParser, - default: false, - }, - mountPath: { - env: 'PARSE_SERVER_MOUNT_PATH', - help: 'Mount path for the server, defaults to /parse', - default: '/parse', - }, - mountPlayground: { - env: 'PARSE_SERVER_MOUNT_PLAYGROUND', - help: 'Mounts the GraphQL Playground - never use this option in production', - action: parsers.booleanParser, - default: false, - }, - objectIdSize: { - env: 'PARSE_SERVER_OBJECT_ID_SIZE', - help: "Sets the number of characters in generated object id's, default 10", - action: parsers.numberParser('objectIdSize'), - default: 10, - }, - passwordPolicy: { - env: 'PARSE_SERVER_PASSWORD_POLICY', - help: 'Password policy for enforcing password related rules', - action: parsers.objectParser, - }, - playgroundPath: { - env: 'PARSE_SERVER_PLAYGROUND_PATH', - help: 'Mount path for the GraphQL Playground, defaults to /playground', - default: '/playground', - }, - port: { - env: 'PORT', - help: 'The port to run the ParseServer, defaults to 1337.', - action: parsers.numberParser('port'), - default: 1337, - }, - preserveFileName: { - env: 'PARSE_SERVER_PRESERVE_FILE_NAME', - help: 'Enable (or disable) the addition of a unique hash to the file names', - action: parsers.booleanParser, - default: false, - }, - preventLoginWithUnverifiedEmail: { - env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', - help: - 'Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false', - action: parsers.booleanParser, - default: false, - }, - protectedFields: { - env: 'PARSE_SERVER_PROTECTED_FIELDS', - help: - 'Protected fields that should be treated with extra security when fetching details.', - action: parsers.objectParser, - default: { - _User: { - '*': ['email'], - }, - }, - }, - publicServerURL: { - env: 'PARSE_PUBLIC_SERVER_URL', - help: 'Public URL to your parse server with http:// or https://.', - }, - push: { - env: 'PARSE_SERVER_PUSH', - help: - 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', - action: parsers.objectParser, - }, - readOnlyMasterKey: { - env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', - help: - 'Read-only key, which has the same capabilities as MasterKey without writes', - }, - restAPIKey: { - env: 'PARSE_SERVER_REST_API_KEY', - help: 'Key for REST calls', - }, - revokeSessionOnPasswordReset: { - env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', - help: - "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", - action: parsers.booleanParser, - default: true, - }, - scheduledPush: { - env: 'PARSE_SERVER_SCHEDULED_PUSH', - help: 'Configuration for push scheduling, defaults to false.', - action: parsers.booleanParser, - default: false, - }, - schemaCacheTTL: { - env: 'PARSE_SERVER_SCHEMA_CACHE_TTL', - help: - 'The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.', - action: parsers.numberParser('schemaCacheTTL'), - default: 5000, - }, - serverCloseComplete: { - env: 'PARSE_SERVER_SERVER_CLOSE_COMPLETE', - help: 'Callback when server has closed', - }, - serverStartComplete: { - env: 'PARSE_SERVER_SERVER_START_COMPLETE', - help: 'Callback when server has started', - }, - serverURL: { - env: 'PARSE_SERVER_URL', - help: 'URL to your parse server with http:// or https://.', - required: true, - }, - sessionLength: { - env: 'PARSE_SERVER_SESSION_LENGTH', - help: 'Session duration, in seconds, defaults to 1 year', - action: parsers.numberParser('sessionLength'), - default: 31536000, - }, - silent: { - env: 'SILENT', - help: 'Disables console output', - action: parsers.booleanParser, - }, - startLiveQueryServer: { - env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', - help: 'Starts the liveQuery server', - action: parsers.booleanParser, - }, - userSensitiveFields: { - env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', - help: - 'Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields', - action: parsers.arrayParser, - }, - verbose: { - env: 'VERBOSE', - help: 'Set the logging to verbose', - action: parsers.booleanParser, - }, - verifyUserEmails: { - env: 'PARSE_SERVER_VERIFY_USER_EMAILS', - help: 'Enable (or disable) user email validation, defaults to false', - action: parsers.booleanParser, - default: false, - }, - webhookKey: { - env: 'PARSE_SERVER_WEBHOOK_KEY', - help: 'Key sent with outgoing webhook calls', - }, + "accountLockout": { + "env": "PARSE_SERVER_ACCOUNT_LOCKOUT", + "help": "account lockout policy for failed login attempts", + "action": parsers.objectParser + }, + "allowClientClassCreation": { + "env": "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", + "help": "Enable (or disable) client class creation, defaults to true", + "action": parsers.booleanParser, + "default": true + }, + "allowCustomObjectId": { + "env": "PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID", + "help": "Enable (or disable) custom objectId", + "action": parsers.booleanParser, + "default": false + }, + "allowHeaders": { + "env": "PARSE_SERVER_ALLOW_HEADERS", + "help": "Add headers to Access-Control-Allow-Headers", + "action": parsers.arrayParser + }, + "allowOrigin": { + "env": "PARSE_SERVER_ALLOW_ORIGIN", + "help": "Sets the origin to Access-Control-Allow-Origin" + }, + "analyticsAdapter": { + "env": "PARSE_SERVER_ANALYTICS_ADAPTER", + "help": "Adapter module for the analytics", + "action": parsers.moduleOrObjectParser + }, + "appId": { + "env": "PARSE_SERVER_APPLICATION_ID", + "help": "Your Parse Application ID", + "required": true + }, + "appName": { + "env": "PARSE_SERVER_APP_NAME", + "help": "Sets the app name" + }, + "auth": { + "env": "PARSE_SERVER_AUTH_PROVIDERS", + "help": "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication", + "action": parsers.objectParser + }, + "cacheAdapter": { + "env": "PARSE_SERVER_CACHE_ADAPTER", + "help": "Adapter module for the cache", + "action": parsers.moduleOrObjectParser + }, + "cacheMaxSize": { + "env": "PARSE_SERVER_CACHE_MAX_SIZE", + "help": "Sets the maximum size for the in memory cache, defaults to 10000", + "action": parsers.numberParser("cacheMaxSize"), + "default": 10000 + }, + "cacheTTL": { + "env": "PARSE_SERVER_CACHE_TTL", + "help": "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)", + "action": parsers.numberParser("cacheTTL"), + "default": 5000 + }, + "clientKey": { + "env": "PARSE_SERVER_CLIENT_KEY", + "help": "Key for iOS, MacOS, tvOS clients" + }, + "cloud": { + "env": "PARSE_SERVER_CLOUD", + "help": "Full path to your cloud code main.js" + }, + "cluster": { + "env": "PARSE_SERVER_CLUSTER", + "help": "Run with cluster, optionally set the number of processes default to os.cpus().length", + "action": parsers.numberOrBooleanParser + }, + "collectionPrefix": { + "env": "PARSE_SERVER_COLLECTION_PREFIX", + "help": "A collection prefix for the classes", + "default": "" + }, + "customPages": { + "env": "PARSE_SERVER_CUSTOM_PAGES", + "help": "custom pages for password validation and reset", + "action": parsers.objectParser, + "default": {} + }, + "databaseAdapter": { + "env": "PARSE_SERVER_DATABASE_ADAPTER", + "help": "Adapter module for the database", + "action": parsers.moduleOrObjectParser + }, + "databaseOptions": { + "env": "PARSE_SERVER_DATABASE_OPTIONS", + "help": "Options to pass to the mongodb client", + "action": parsers.objectParser + }, + "databaseURI": { + "env": "PARSE_SERVER_DATABASE_URI", + "help": "The full URI to your database. Supported databases are mongodb or postgres.", + "required": true, + "default": "mongodb://localhost:27017/parse" + }, + "directAccess": { + "env": "PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS", + "help": "Replace HTTP Interface when using JS SDK in current node runtime, defaults to false. Caution, this is an experimental feature that may not be appropriate for production.", + "action": parsers.booleanParser, + "default": false + }, + "dotNetKey": { + "env": "PARSE_SERVER_DOT_NET_KEY", + "help": "Key for Unity and .Net SDK" + }, + "emailAdapter": { + "env": "PARSE_SERVER_EMAIL_ADAPTER", + "help": "Adapter module for email sending", + "action": parsers.moduleOrObjectParser + }, + "emailVerifyTokenValidityDuration": { + "env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", + "help": "Email verification token validity duration, in seconds", + "action": parsers.numberParser("emailVerifyTokenValidityDuration") + }, + "enableAnonymousUsers": { + "env": "PARSE_SERVER_ENABLE_ANON_USERS", + "help": "Enable (or disable) anonymous users, defaults to true", + "action": parsers.booleanParser, + "default": true + }, + "enableExpressErrorHandler": { + "env": "PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER", + "help": "Enables the default express error handler for all errors", + "action": parsers.booleanParser, + "default": false + }, + "enableSingleSchemaCache": { + "env": "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE", + "help": "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.", + "action": parsers.booleanParser, + "default": false + }, + "expireInactiveSessions": { + "env": "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS", + "help": "Sets wether we should expire the inactive sessions, defaults to true", + "action": parsers.booleanParser, + "default": true + }, + "fileKey": { + "env": "PARSE_SERVER_FILE_KEY", + "help": "Key for your files" + }, + "filesAdapter": { + "env": "PARSE_SERVER_FILES_ADAPTER", + "help": "Adapter module for the files sub-system", + "action": parsers.moduleOrObjectParser + }, + "graphQLPath": { + "env": "PARSE_SERVER_GRAPHQL_PATH", + "help": "Mount path for the GraphQL endpoint, defaults to /graphql", + "default": "/graphql" + }, + "graphQLSchema": { + "env": "PARSE_SERVER_GRAPH_QLSCHEMA", + "help": "Full path to your GraphQL custom schema.graphql file" + }, + "host": { + "env": "PARSE_SERVER_HOST", + "help": "The host to serve ParseServer on, defaults to 0.0.0.0", + "default": "0.0.0.0" + }, + "idempotencyOptions": { + "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS", + "help": "Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production.", + "action": parsers.objectParser, + "default": {} + }, + "javascriptKey": { + "env": "PARSE_SERVER_JAVASCRIPT_KEY", + "help": "Key for the Javascript SDK" + }, + "jsonLogs": { + "env": "JSON_LOGS", + "help": "Log as structured JSON objects", + "action": parsers.booleanParser + }, + "liveQuery": { + "env": "PARSE_SERVER_LIVE_QUERY", + "help": "parse-server's LiveQuery configuration object", + "action": parsers.objectParser + }, + "liveQueryServerOptions": { + "env": "PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS", + "help": "Live query server configuration options (will start the liveQuery server)", + "action": parsers.objectParser + }, + "loggerAdapter": { + "env": "PARSE_SERVER_LOGGER_ADAPTER", + "help": "Adapter module for the logging sub-system", + "action": parsers.moduleOrObjectParser + }, + "logLevel": { + "env": "PARSE_SERVER_LOG_LEVEL", + "help": "Sets the level for logs" + }, + "logsFolder": { + "env": "PARSE_SERVER_LOGS_FOLDER", + "help": "Folder for the logs (defaults to './logs'); set to null to disable file based logging", + "default": "./logs" + }, + "masterKey": { + "env": "PARSE_SERVER_MASTER_KEY", + "help": "Your Parse Master Key", + "required": true + }, + "masterKeyIps": { + "env": "PARSE_SERVER_MASTER_KEY_IPS", + "help": "Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)", + "action": parsers.arrayParser, + "default": [] + }, + "maxLimit": { + "env": "PARSE_SERVER_MAX_LIMIT", + "help": "Max value for limit option on queries, defaults to unlimited", + "action": parsers.numberParser("maxLimit") + }, + "maxLogFiles": { + "env": "PARSE_SERVER_MAX_LOG_FILES", + "help": "Maximum number of logs to keep. If not set, no logs will be removed. This can be a number of files or number of days. If using days, add 'd' as the suffix. (default: null)", + "action": parsers.objectParser + }, + "maxUploadSize": { + "env": "PARSE_SERVER_MAX_UPLOAD_SIZE", + "help": "Max file size for uploads, defaults to 20mb", + "default": "20mb" + }, + "middleware": { + "env": "PARSE_SERVER_MIDDLEWARE", + "help": "middleware for express server, can be string or function" + }, + "mountGraphQL": { + "env": "PARSE_SERVER_MOUNT_GRAPHQL", + "help": "Mounts the GraphQL endpoint", + "action": parsers.booleanParser, + "default": false + }, + "mountPath": { + "env": "PARSE_SERVER_MOUNT_PATH", + "help": "Mount path for the server, defaults to /parse", + "default": "/parse" + }, + "mountPlayground": { + "env": "PARSE_SERVER_MOUNT_PLAYGROUND", + "help": "Mounts the GraphQL Playground - never use this option in production", + "action": parsers.booleanParser, + "default": false + }, + "objectIdSize": { + "env": "PARSE_SERVER_OBJECT_ID_SIZE", + "help": "Sets the number of characters in generated object id's, default 10", + "action": parsers.numberParser("objectIdSize"), + "default": 10 + }, + "passwordPolicy": { + "env": "PARSE_SERVER_PASSWORD_POLICY", + "help": "Password policy for enforcing password related rules", + "action": parsers.objectParser + }, + "playgroundPath": { + "env": "PARSE_SERVER_PLAYGROUND_PATH", + "help": "Mount path for the GraphQL Playground, defaults to /playground", + "default": "/playground" + }, + "port": { + "env": "PORT", + "help": "The port to run the ParseServer, defaults to 1337.", + "action": parsers.numberParser("port"), + "default": 1337 + }, + "preserveFileName": { + "env": "PARSE_SERVER_PRESERVE_FILE_NAME", + "help": "Enable (or disable) the addition of a unique hash to the file names", + "action": parsers.booleanParser, + "default": false + }, + "preventLoginWithUnverifiedEmail": { + "env": "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL", + "help": "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false", + "action": parsers.booleanParser, + "default": false + }, + "protectedFields": { + "env": "PARSE_SERVER_PROTECTED_FIELDS", + "help": "Protected fields that should be treated with extra security when fetching details.", + "action": parsers.objectParser, + "default": { + "_User": { + "*": ["email"] + } + } + }, + "publicServerURL": { + "env": "PARSE_PUBLIC_SERVER_URL", + "help": "Public URL to your parse server with http:// or https://." + }, + "push": { + "env": "PARSE_SERVER_PUSH", + "help": "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications", + "action": parsers.objectParser + }, + "readOnlyMasterKey": { + "env": "PARSE_SERVER_READ_ONLY_MASTER_KEY", + "help": "Read-only key, which has the same capabilities as MasterKey without writes" + }, + "restAPIKey": { + "env": "PARSE_SERVER_REST_API_KEY", + "help": "Key for REST calls" + }, + "revokeSessionOnPasswordReset": { + "env": "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET", + "help": "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.", + "action": parsers.booleanParser, + "default": true + }, + "scheduledPush": { + "env": "PARSE_SERVER_SCHEDULED_PUSH", + "help": "Configuration for push scheduling, defaults to false.", + "action": parsers.booleanParser, + "default": false + }, + "schemaCacheTTL": { + "env": "PARSE_SERVER_SCHEMA_CACHE_TTL", + "help": "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.", + "action": parsers.numberParser("schemaCacheTTL"), + "default": 5000 + }, + "serverCloseComplete": { + "env": "PARSE_SERVER_SERVER_CLOSE_COMPLETE", + "help": "Callback when server has closed" + }, + "serverStartComplete": { + "env": "PARSE_SERVER_SERVER_START_COMPLETE", + "help": "Callback when server has started" + }, + "serverURL": { + "env": "PARSE_SERVER_URL", + "help": "URL to your parse server with http:// or https://.", + "required": true + }, + "sessionLength": { + "env": "PARSE_SERVER_SESSION_LENGTH", + "help": "Session duration, in seconds, defaults to 1 year", + "action": parsers.numberParser("sessionLength"), + "default": 31536000 + }, + "silent": { + "env": "SILENT", + "help": "Disables console output", + "action": parsers.booleanParser + }, + "startLiveQueryServer": { + "env": "PARSE_SERVER_START_LIVE_QUERY_SERVER", + "help": "Starts the liveQuery server", + "action": parsers.booleanParser + }, + "userSensitiveFields": { + "env": "PARSE_SERVER_USER_SENSITIVE_FIELDS", + "help": "Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields", + "action": parsers.arrayParser + }, + "verbose": { + "env": "VERBOSE", + "help": "Set the logging to verbose", + "action": parsers.booleanParser + }, + "verifyUserEmails": { + "env": "PARSE_SERVER_VERIFY_USER_EMAILS", + "help": "Enable (or disable) user email validation, defaults to false", + "action": parsers.booleanParser, + "default": false + }, + "webhookKey": { + "env": "PARSE_SERVER_WEBHOOK_KEY", + "help": "Key sent with outgoing webhook calls" + } }; module.exports.CustomPagesOptions = { - choosePassword: { - env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', - help: 'choose password page path', - }, - invalidLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', - help: 'invalid link page path', - }, - invalidVerificationLink: { - env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK', - help: 'invalid verification link page path', - }, - linkSendFail: { - env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL', - help: 'verification link send fail page path', - }, - linkSendSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS', - help: 'verification link send success page path', - }, - parseFrameURL: { - env: 'PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL', - help: 'for masking user-facing pages', - }, - passwordResetSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', - help: 'password reset success page path', - }, - verifyEmailSuccess: { - env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', - help: 'verify email success page path', - }, + "choosePassword": { + "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", + "help": "choose password page path" + }, + "invalidLink": { + "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", + "help": "invalid link page path" + }, + "invalidVerificationLink": { + "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_VERIFICATION_LINK", + "help": "invalid verification link page path" + }, + "linkSendFail": { + "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_FAIL", + "help": "verification link send fail page path" + }, + "linkSendSuccess": { + "env": "PARSE_SERVER_CUSTOM_PAGES_LINK_SEND_SUCCESS", + "help": "verification link send success page path" + }, + "parseFrameURL": { + "env": "PARSE_SERVER_CUSTOM_PAGES_PARSE_FRAME_URL", + "help": "for masking user-facing pages" + }, + "passwordResetSuccess": { + "env": "PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS", + "help": "password reset success page path" + }, + "verifyEmailSuccess": { + "env": "PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS", + "help": "verify email success page path" + } }; module.exports.LiveQueryOptions = { - classNames: { - env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', - help: "parse-server's LiveQuery classNames", - action: parsers.arrayParser, - }, - pubSubAdapter: { - env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', - help: 'LiveQuery pubsub adapter', - action: parsers.moduleOrObjectParser, - }, - redisOptions: { - env: 'PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS', - help: "parse-server's LiveQuery redisOptions", - action: parsers.objectParser, - }, - redisURL: { - env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', - help: "parse-server's LiveQuery redisURL", - }, - wssAdapter: { - env: 'PARSE_SERVER_LIVEQUERY_WSS_ADAPTER', - help: 'Adapter module for the WebSocketServer', - action: parsers.moduleOrObjectParser, - }, + "classNames": { + "env": "PARSE_SERVER_LIVEQUERY_CLASSNAMES", + "help": "parse-server's LiveQuery classNames", + "action": parsers.arrayParser + }, + "pubSubAdapter": { + "env": "PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER", + "help": "LiveQuery pubsub adapter", + "action": parsers.moduleOrObjectParser + }, + "redisOptions": { + "env": "PARSE_SERVER_LIVEQUERY_REDIS_OPTIONS", + "help": "parse-server's LiveQuery redisOptions", + "action": parsers.objectParser + }, + "redisURL": { + "env": "PARSE_SERVER_LIVEQUERY_REDIS_URL", + "help": "parse-server's LiveQuery redisURL" + }, + "wssAdapter": { + "env": "PARSE_SERVER_LIVEQUERY_WSS_ADAPTER", + "help": "Adapter module for the WebSocketServer", + "action": parsers.moduleOrObjectParser + } }; module.exports.LiveQueryServerOptions = { - appId: { - env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', - help: - 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', - }, - cacheTimeout: { - env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', - help: - "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).", - action: parsers.numberParser('cacheTimeout'), - }, - keyPairs: { - env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', - help: - 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', - action: parsers.objectParser, - }, - logLevel: { - env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', - help: - 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', - }, - masterKey: { - env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', - help: - 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', - }, - port: { - env: 'PARSE_LIVE_QUERY_SERVER_PORT', - help: 'The port to run the LiveQuery server, defaults to 1337.', - action: parsers.numberParser('port'), - default: 1337, - }, - pubSubAdapter: { - env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', - help: 'LiveQuery pubsub adapter', - action: parsers.moduleOrObjectParser, - }, - redisOptions: { - env: 'PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS', - help: "parse-server's LiveQuery redisOptions", - action: parsers.objectParser, - }, - redisURL: { - env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', - help: "parse-server's LiveQuery redisURL", - }, - serverURL: { - env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', - help: - 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', - }, - websocketTimeout: { - env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', - help: - 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', - action: parsers.numberParser('websocketTimeout'), - }, - wssAdapter: { - env: 'PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER', - help: 'Adapter module for the WebSocketServer', - action: parsers.moduleOrObjectParser, - }, + "appId": { + "env": "PARSE_LIVE_QUERY_SERVER_APP_ID", + "help": "This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId." + }, + "cacheTimeout": { + "env": "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT", + "help": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).", + "action": parsers.numberParser("cacheTimeout") + }, + "keyPairs": { + "env": "PARSE_LIVE_QUERY_SERVER_KEY_PAIRS", + "help": "A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.", + "action": parsers.objectParser + }, + "logLevel": { + "env": "PARSE_LIVE_QUERY_SERVER_LOG_LEVEL", + "help": "This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO." + }, + "masterKey": { + "env": "PARSE_LIVE_QUERY_SERVER_MASTER_KEY", + "help": "This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey." + }, + "port": { + "env": "PARSE_LIVE_QUERY_SERVER_PORT", + "help": "The port to run the LiveQuery server, defaults to 1337.", + "action": parsers.numberParser("port"), + "default": 1337 + }, + "pubSubAdapter": { + "env": "PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER", + "help": "LiveQuery pubsub adapter", + "action": parsers.moduleOrObjectParser + }, + "redisOptions": { + "env": "PARSE_LIVE_QUERY_SERVER_REDIS_OPTIONS", + "help": "parse-server's LiveQuery redisOptions", + "action": parsers.objectParser + }, + "redisURL": { + "env": "PARSE_LIVE_QUERY_SERVER_REDIS_URL", + "help": "parse-server's LiveQuery redisURL" + }, + "serverURL": { + "env": "PARSE_LIVE_QUERY_SERVER_SERVER_URL", + "help": "This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL." + }, + "websocketTimeout": { + "env": "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT", + "help": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).", + "action": parsers.numberParser("websocketTimeout") + }, + "wssAdapter": { + "env": "PARSE_LIVE_QUERY_SERVER_WSS_ADAPTER", + "help": "Adapter module for the WebSocketServer", + "action": parsers.moduleOrObjectParser + } +}; +module.exports.IdempotencyOptions = { + "paths": { + "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS", + "help": "An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths.", + "action": parsers.arrayParser, + "default": [] + }, + "ttl": { + "env": "PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL", + "help": "The duration in seconds after which a request record is discarded from the database, defaults to 300s.", + "action": parsers.numberParser("ttl"), + "default": 300 + } }; diff --git a/src/Options/docs.js b/src/Options/docs.js index 0e24594bcb..78821a0543 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -33,6 +33,7 @@ * @property {String} graphQLPath Mount path for the GraphQL endpoint, defaults to /graphql * @property {String} graphQLSchema Full path to your GraphQL custom schema.graphql file * @property {String} host The host to serve ParseServer on, defaults to 0.0.0.0 + * @property {IdempotencyOptions} idempotencyOptions Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production. * @property {String} javascriptKey Key for the Javascript SDK * @property {Boolean} jsonLogs Log as structured JSON objects * @property {LiveQueryOptions} liveQuery parse-server's LiveQuery configuration object @@ -111,3 +112,10 @@ * @property {Number} websocketTimeout Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s). * @property {Adapter} wssAdapter Adapter module for the WebSocketServer */ + +/** + * @interface IdempotencyOptions + * @property {String[]} paths An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths. + * @property {Number} ttl The duration in seconds after which a request record is discarded from the database, defaults to 300s. + */ + diff --git a/src/Options/index.js b/src/Options/index.js index d4c09dd790..0e97d84b5a 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -188,6 +188,10 @@ export interface ParseServerOptions { startLiveQueryServer: ?boolean; /* Live query server configuration options (will start the liveQuery server) */ liveQueryServerOptions: ?LiveQueryServerOptions; + /* Options for request idempotency to deduplicate identical requests that may be caused by network issues. Caution, this is an experimental feature that may not be appropriate for production. + :ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS + :DEFAULT: false */ + idempotencyOptions: ?IdempotencyOptions; /* Full path to your GraphQL custom schema.graphql file */ graphQLSchema: ?string; /* Mounts the GraphQL endpoint @@ -272,3 +276,12 @@ export interface LiveQueryServerOptions { /* Adapter module for the WebSocketServer */ wssAdapter: ?Adapter; } + +export interface IdempotencyOptions { + /* An array of paths for which the feature should be enabled. The mount path must not be included, for example instead of `/parse/functions/myFunction` specifiy `functions/myFunction`. The entries are interpreted as regular expression, for example `functions/.*` matches all functions, `jobs/.*` matches all jobs, `classes/.*` matches all classes, `.*` matches all paths. + :DEFAULT: [] */ + paths: ?(string[]); + /* The duration in seconds after which a request record is discarded from the database, defaults to 300s. + :DEFAULT: 300 */ + ttl: ?number; +} diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index b5bba161fa..23e3016b39 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -2,6 +2,7 @@ import PromiseRouter from '../PromiseRouter'; import rest from '../rest'; import _ from 'lodash'; import Parse from 'parse/node'; +import { promiseEnsureIdempotency } from '../middlewares'; const ALLOWED_GET_QUERY_KEYS = [ 'keys', @@ -247,10 +248,10 @@ export class ClassesRouter extends PromiseRouter { this.route('GET', '/classes/:className/:objectId', req => { return this.handleGet(req); }); - this.route('POST', '/classes/:className', req => { + this.route('POST', '/classes/:className', promiseEnsureIdempotency, req => { return this.handleCreate(req); }); - this.route('PUT', '/classes/:className/:objectId', req => { + this.route('PUT', '/classes/:className/:objectId', promiseEnsureIdempotency, req => { return this.handleUpdate(req); }); this.route('DELETE', '/classes/:className/:objectId', req => { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index c31eeb031f..a90e5c7491 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -4,7 +4,7 @@ var Parse = require('parse/node').Parse, triggers = require('../triggers'); import PromiseRouter from '../PromiseRouter'; -import { promiseEnforceMasterKeyAccess } from '../middlewares'; +import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; import { jobStatusHandler } from '../StatusHandler'; import _ from 'lodash'; import { logger } from '../logger'; @@ -34,11 +34,13 @@ export class FunctionsRouter extends PromiseRouter { this.route( 'POST', '/functions/:functionName', + promiseEnsureIdempotency, FunctionsRouter.handleCloudFunction ); this.route( 'POST', '/jobs/:jobName', + promiseEnsureIdempotency, promiseEnforceMasterKeyAccess, function (req) { return FunctionsRouter.handleCloudJob(req); diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 28876b7f31..f87f54c605 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -2,6 +2,7 @@ import ClassesRouter from './ClassesRouter'; import rest from '../rest'; +import { promiseEnsureIdempotency } from '../middlewares'; export class InstallationsRouter extends ClassesRouter { className() { @@ -36,10 +37,10 @@ export class InstallationsRouter extends ClassesRouter { this.route('GET', '/installations/:objectId', req => { return this.handleGet(req); }); - this.route('POST', '/installations', req => { + this.route('POST', '/installations', promiseEnsureIdempotency, req => { return this.handleCreate(req); }); - this.route('PUT', '/installations/:objectId', req => { + this.route('PUT', '/installations/:objectId', promiseEnsureIdempotency, req => { return this.handleUpdate(req); }); this.route('DELETE', '/installations/:objectId', req => { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 05d0081792..9d015f9659 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -8,6 +8,7 @@ import rest from '../rest'; import Auth from '../Auth'; import passwordCrypto from '../password'; import { maybeRunTrigger, Types as TriggerTypes } from '../triggers'; +import { promiseEnsureIdempotency } from '../middlewares'; export class UsersRouter extends ClassesRouter { className() { @@ -445,7 +446,7 @@ export class UsersRouter extends ClassesRouter { this.route('GET', '/users', req => { return this.handleFind(req); }); - this.route('POST', '/users', req => { + this.route('POST', '/users', promiseEnsureIdempotency, req => { return this.handleCreate(req); }); this.route('GET', '/users/me', req => { @@ -454,7 +455,7 @@ export class UsersRouter extends ClassesRouter { this.route('GET', '/users/:objectId', req => { return this.handleGet(req); }); - this.route('PUT', '/users/:objectId', req => { + this.route('PUT', '/users/:objectId', promiseEnsureIdempotency, req => { return this.handleUpdate(req); }); this.route('DELETE', '/users/:objectId', req => { diff --git a/src/middlewares.js b/src/middlewares.js index a73f4c8105..526372ba39 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -4,9 +4,11 @@ import auth from './Auth'; import Config from './Config'; import ClientSDK from './ClientSDK'; import defaultLogger from './logger'; +import rest from './rest'; +import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter'; export const DEFAULT_ALLOWED_HEADERS = - 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, Content-Type, Pragma, Cache-Control'; + 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, X-Parse-Request-Id, Content-Type, Pragma, Cache-Control'; const getMountForRequest = function (req) { const mountPathLength = req.originalUrl.length - req.url.length; @@ -406,6 +408,52 @@ export function promiseEnforceMasterKeyAccess(request) { return Promise.resolve(); } +/** + * Deduplicates a request to ensure idempotency. Duplicates are determined by the request ID + * in the request header. If a request has no request ID, it is executed anyway. + * @param {*} req The request to evaluate. + * @returns Promise<{}> + */ +export function promiseEnsureIdempotency(req) { + // Enable feature only for MongoDB + if (!(req.config.database.adapter instanceof MongoStorageAdapter)) { return Promise.resolve(); } + // Get parameters + const config = req.config; + const requestId = ((req || {}).headers || {})["x-parse-request-id"]; + const { paths, ttl } = config.idempotencyOptions; + if (!requestId || !config.idempotencyOptions) { return Promise.resolve(); } + // Request path may contain trailing slashes, depending on the original request, so remove + // leading and trailing slashes to make it easier to specify paths in the configuration + const reqPath = req.path.replace(/^\/|\/$/, ''); + // Determine whether idempotency is enabled for current request path + let match = false; + for (const path of paths) { + // Assume one wants a path to always match from the beginning to prevent any mistakes + const regex = new RegExp(path.charAt(0) === '^' ? path : '^' + path); + if (reqPath.match(regex)) { + match = true; + break; + } + } + if (!match) { return Promise.resolve(); } + // Try to store request + const expiryDate = new Date(new Date().setSeconds(new Date().getSeconds() + ttl)); + return rest.create( + config, + auth.master(config), + '_Idempotency', + { reqId: requestId, expire: Parse._encode(expiryDate) } + ).catch (e => { + if (e.code == Parse.Error.DUPLICATE_VALUE) { + throw new Parse.Error( + Parse.Error.DUPLICATE_REQUEST, + 'Duplicate request' + ); + } + throw e; + }); +} + function invalidRequest(req, res) { res.status(403); res.end('{"error":"unauthorized"}'); diff --git a/src/rest.js b/src/rest.js index 812a0da7f7..e3ffe2a38e 100644 --- a/src/rest.js +++ b/src/rest.js @@ -284,6 +284,7 @@ const classesWithMasterOnlyAccess = [ '_Hooks', '_GlobalConfig', '_JobSchedule', + '_Idempotency', ]; // Disallowing access to the _Role collection except by master key function enforceRoleSecurity(method, className, auth) { From 93a88c5cdeaf2e05ea11ce852ae04b507fa0f487 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 16 Jul 2020 22:13:29 +0200 Subject: [PATCH 04/33] Add version to fix CDN (#6804) --- src/GraphQL/ParseGraphQLServer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index a041721887..5426cf5fee 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -109,6 +109,7 @@ class ParseGraphQLServer { res.write( renderPlaygroundPage({ endpoint: this.config.graphQLPath, + version: '1.7.25', subscriptionEndpoint: this.config.subscriptionsPath, headers: { 'X-Parse-Application-Id': this.parseServer.config.appId, From 44015c3e351fc7cf0ac3e99bc64738d35ddc3cba Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 17 Jul 2020 11:36:38 +1000 Subject: [PATCH 05/33] Before Connect + Before Subscribe help required (#6793) * Before Connect + Before Subscribe #1 * Cleanup and Documentation * Add E2E tests * Bump parse to 2.15.0 Co-authored-by: Diamond Lewis --- package-lock.json | 30 +-- package.json | 3 +- spec/ParseLiveQuery.spec.js | 95 +++++++- spec/ParseLiveQueryServer.spec.js | 302 +++++++++++++++----------- src/LiveQuery/Client.js | 10 +- src/LiveQuery/ParseLiveQueryServer.js | 167 ++++++++------ src/cloud-code/Parse.Cloud.js | 64 ++++++ src/triggers.js | 83 ++++++- 8 files changed, 527 insertions(+), 227 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0d267ed9f..448648ea22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10028,31 +10028,31 @@ } }, "parse": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-2.14.0.tgz", - "integrity": "sha512-S4bbF80Aom/xDk4YNkzZG1xBHYbiFQGueJWyO4DpYlajfkEs3gp0oszFDnGadTARyCgoQGxNE4Qkege/QqNETA==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-2.15.0.tgz", + "integrity": "sha512-Aupg+qd6I4X5uTacpsxROg5GlhkVn2+qOHtyOhlGj/Woi75c5cPD8kn7qhhLKcVVpe2L+HoJ+yGkMdI8IjKBKA==", "requires": { - "@babel/runtime": "7.10.2", - "@babel/runtime-corejs3": "7.10.2", + "@babel/runtime": "7.10.3", + "@babel/runtime-corejs3": "7.10.3", "crypto-js": "4.0.0", "react-native-crypto-js": "1.0.0", - "uuid": "3.3.3", + "uuid": "3.4.0", "ws": "7.3.0", "xmlhttprequest": "1.8.0" }, "dependencies": { "@babel/runtime": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz", - "integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==", + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", + "integrity": "sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==", "requires": { "regenerator-runtime": "^0.13.4" } }, "@babel/runtime-corejs3": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.2.tgz", - "integrity": "sha512-+a2M/u7r15o3dV1NEizr9bRi+KUVnrs/qYxF0Z06DAPx/4VCWaz1WA7EcbE+uqGgt39lp5akWGmHsTseIkHkHg==", + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz", + "integrity": "sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw==", "requires": { "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" @@ -10064,9 +10064,9 @@ "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, diff --git a/package.json b/package.json index abb1a3c9de..b0e152428c 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "lru-cache": "5.1.1", "mime": "2.4.6", "mongodb": "3.5.9", - "parse": "2.14.0", + "parse": "2.15.0", "pg-promise": "10.5.7", "pluralize": "8.0.0", "redis": "3.0.2", @@ -104,6 +104,7 @@ "posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner stop", "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 nyc jasmine", "start": "node ./bin/parse-server", + "prettier": "prettier --write {src,spec}/**/*.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'" }, diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index fa83588b9d..80eebaa733 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1,6 +1,6 @@ 'use strict'; -describe('ParseLiveQuery', function() { +describe('ParseLiveQuery', function () { it('can subscribe to query', async done => { await reconfigureServer({ liveQuery: { @@ -24,6 +24,97 @@ describe('ParseLiveQuery', function() { await object.save(); }); + it('can handle beforeConnect / beforeSubscribe hooks', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeSubscribe('TestObject', req => { + expect(req.op).toBe('subscribe'); + expect(req.requestId).toBe(1); + expect(req.query).toBeDefined(); + expect(req.user).toBeUndefined(); + }); + + Parse.Cloud.beforeConnect(req => { + expect(req.event).toBe('connect'); + expect(req.clients).toBe(0); + expect(req.subscriptions).toBe(0); + expect(req.useMasterKey).toBe(false); + expect(req.installationId).toBeDefined(); + expect(req.user).toBeUndefined(); + expect(req.sessionToken).toBeUndefined(); + expect(req.client).toBeDefined(); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', async object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can handle beforeConnect error', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeConnect(() => { + throw new Error('You shall not pass!'); + }); + Parse.LiveQuery.on('error', error => { + expect(error).toBe('You shall not pass!'); + done(); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + await query.subscribe(); + }); + + it('can handle beforeSubscribe error', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeSubscribe(TestObject, () => { + throw new Error('You shall not subscribe!'); + }); + Parse.LiveQuery.on('error', error => { + expect(error).toBe('You shall not subscribe!'); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('error', error => { + expect(error).toBe('You shall not subscribe!'); + done(); + }); + }); + it('handle invalid websocket payload length', async done => { await reconfigureServer({ liveQuery: { @@ -61,7 +152,7 @@ describe('ParseLiveQuery', function() { }, 1000); }); - afterEach(async function(done) { + afterEach(async function (done) { const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); client.close(); // Wait for live query client to disconnect diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 63ac0a0505..6c1b831a5d 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -11,8 +11,8 @@ const queryHashValue = 'hash'; const testUserId = 'userId'; const testClassName = 'TestObject'; -describe('ParseLiveQueryServer', function() { - beforeEach(function(done) { +describe('ParseLiveQueryServer', function () { + beforeEach(function (done) { // Mock ParseWebSocketServer const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); jasmine.mockLibrary( @@ -21,7 +21,7 @@ describe('ParseLiveQueryServer', function() { mockParseWebSocketServer ); // Mock Client - const mockClient = function(id, socket, hasMasterKey) { + const mockClient = function (id, socket, hasMasterKey) { this.pushConnect = jasmine.createSpy('pushConnect'); this.pushSubscribe = jasmine.createSpy('pushSubscribe'); this.pushUnsubscribe = jasmine.createSpy('pushUnsubscribe'); @@ -38,7 +38,7 @@ describe('ParseLiveQueryServer', function() { mockClient.pushError = jasmine.createSpy('pushError'); jasmine.mockLibrary('../lib/LiveQuery/Client', 'Client', mockClient); // Mock Subscription - const mockSubscriotion = function() { + const mockSubscriotion = function () { this.addClientSubscription = jasmine.createSpy('addClientSubscription'); this.deleteClientSubscription = jasmine.createSpy( 'deleteClientSubscription' @@ -69,13 +69,13 @@ describe('ParseLiveQueryServer', function() { ); // Mock ParsePubSub const mockParsePubSub = { - createPublisher: function() { + createPublisher: function () { return { publish: jasmine.createSpy('publish'), on: jasmine.createSpy('on'), }; }, - createSubscriber: function() { + createSubscriber: function () { return { subscribe: jasmine.createSpy('subscribe'), on: jasmine.createSpy('on'), @@ -114,7 +114,7 @@ describe('ParseLiveQueryServer', function() { done(); }); - it('can be initialized', function() { + it('can be initialized', function () { const httpServer = {}; const parseLiveQueryServer = new ParseLiveQueryServer(httpServer); @@ -123,7 +123,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.subscriptions.size).toBe(0); }); - it('can be initialized from ParseServer', function() { + it('can be initialized from ParseServer', function () { const httpServer = {}; const parseLiveQueryServer = ParseServer.createLiveQueryServer( httpServer, @@ -135,7 +135,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.subscriptions.size).toBe(0); }); - it('can be initialized from ParseServer without httpServer', function(done) { + it('can be initialized from ParseServer without httpServer', function (done) { const parseLiveQueryServer = ParseServer.createLiveQueryServer(undefined, { port: 22345, }); @@ -147,7 +147,7 @@ describe('ParseLiveQueryServer', function() { }); describe_only_db('mongo')('initialization', () => { - it('can be initialized through ParseServer without liveQueryServerOptions', function(done) { + it('can be initialized through ParseServer without liveQueryServerOptions', function (done) { const parseServer = ParseServer.start({ appId: 'hello', masterKey: 'world', @@ -166,7 +166,7 @@ describe('ParseLiveQueryServer', function() { }); }); - it('can be initialized through ParseServer with liveQueryServerOptions', function(done) { + it('can be initialized through ParseServer with liveQueryServerOptions', function (done) { const parseServer = ParseServer.start({ appId: 'hello', masterKey: 'world', @@ -192,7 +192,7 @@ describe('ParseLiveQueryServer', function() { }); }); - it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + it('properly passes the CLP to afterSave/afterDelete hook', function (done) { function setPermissionsOnClass(className, permissions, doPut) { const request = require('request'); let op = request.post; @@ -285,7 +285,7 @@ describe('ParseLiveQueryServer', function() { .catch(done.fail); }); - it('can handle connect command', function() { + it('can handle connect command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1, @@ -293,7 +293,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._validateKeys = jasmine .createSpy('validateKeys') .and.returnValue(true); - parseLiveQueryServer._handleConnect(parseWebSocket, { + await parseLiveQueryServer._handleConnect(parseWebSocket, { sessionToken: 'token', }); @@ -307,16 +307,62 @@ describe('ParseLiveQueryServer', function() { expect(client.pushConnect).toHaveBeenCalled(); }); - it('can handle subscribe command without clientId', function() { + it('basic beforeConnect rejection', async () => { + Parse.Cloud.beforeConnect(function () { + throw new Error('You shall not pass!'); + }); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const parseWebSocket = { + clientId: -1, + }; + await parseLiveQueryServer._handleConnect(parseWebSocket, { + sessionToken: 'token', + }); + expect(parseLiveQueryServer.clients.size).toBe(0); + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); + + it('basic beforeSubscribe rejection', async () => { + Parse.Cloud.beforeSubscribe('test', function () { + throw new Error('You shall not pass!'); + }); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const parseWebSocket = { + clientId: -1, + }; + await parseLiveQueryServer._handleConnect(parseWebSocket, { + sessionToken: 'token', + }); + const query = { + className: 'test', + where: { + key: 'value', + }, + fields: ['test'], + }; + const requestId = 2; + const request = { + query: query, + requestId: requestId, + sessionToken: 'sessionToken', + }; + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + expect(parseLiveQueryServer.clients.size).toBe(1); + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); + + it('can handle subscribe command without clientId', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; - parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); + await parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle subscribe command with new query', function() { + it('can handle subscribe command with new query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; @@ -338,7 +384,7 @@ describe('ParseLiveQueryServer', function() { requestId: requestId, sessionToken: 'sessionToken', }; - parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make sure we add the subscription to the server const subscriptions = parseLiveQueryServer.subscriptions; @@ -363,7 +409,7 @@ describe('ParseLiveQueryServer', function() { expect(client.pushSubscribe).toHaveBeenCalledWith(requestId); }); - it('can handle subscribe command with existing query', function() { + it('can handle subscribe command with existing query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add two mock clients const clientId = 1; @@ -382,7 +428,7 @@ describe('ParseLiveQueryServer', function() { }, fields: ['test'], }; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -401,7 +447,7 @@ describe('ParseLiveQueryServer', function() { fields: ['testAgain'], }; const requestIdAgain = 1; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientIdAgain, requestIdAgain, @@ -427,7 +473,7 @@ describe('ParseLiveQueryServer', function() { expect(args[1].fields).toBe(queryAgain.fields); }); - it('can handle unsubscribe command without clientId', function() { + it('can handle unsubscribe command without clientId', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {}); @@ -436,7 +482,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle unsubscribe command without not existed client', function() { + it('can handle unsubscribe command without not existed client', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: 1, @@ -447,7 +493,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle unsubscribe command without not existed query', function() { + it('can handle unsubscribe command without not existed query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; @@ -462,7 +508,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle unsubscribe command', function() { + it('can handle unsubscribe command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; @@ -472,7 +518,7 @@ describe('ParseLiveQueryServer', function() { clientId: 1, }; const requestId = 2; - const subscription = addMockSubscription( + const subscription = await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -481,7 +527,7 @@ describe('ParseLiveQueryServer', function() { // Mock client.getSubscriptionInfo const subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent() .args[1]; - client.getSubscriptionInfo = function() { + client.getSubscriptionInfo = function () { return subscriptionInfo; }; // Handle unsubscribe command @@ -502,7 +548,7 @@ describe('ParseLiveQueryServer', function() { expect(subscriptions.size).toBe(0); }); - it('can set connect command message handler for a parseWebSocket', function() { + it('can set connect command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe'); @@ -525,7 +571,7 @@ describe('ParseLiveQueryServer', function() { expect(args[0]).toBe(parseWebSocket); }); - it('can set subscribe command message handler for a parseWebSocket', function() { + it('can set subscribe command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleSubscribe = jasmine.createSpy( @@ -551,7 +597,7 @@ describe('ParseLiveQueryServer', function() { expect(JSON.stringify(args[1])).toBe(subscribeRequest); }); - it('can set unsubscribe command message handler for a parseWebSocket', function() { + it('can set unsubscribe command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy( @@ -577,7 +623,7 @@ describe('ParseLiveQueryServer', function() { expect(JSON.stringify(args[1])).toBe(unsubscribeRequest); }); - it('can set update command message handler for a parseWebSocket', function() { + it('can set update command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough(); @@ -612,7 +658,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer._handleSubscribe).toHaveBeenCalled(); }); - it('can set missing command message handler for a parseWebSocket', function() { + it('can set missing command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); @@ -628,7 +674,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can set unknown command message handler for a parseWebSocket', function() { + it('can set unknown command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); @@ -644,7 +690,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() { + it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -657,7 +703,7 @@ describe('ParseLiveQueryServer', function() { parseWebSocket.emit('disconnect'); }); - it('can forward event to cloud code', function() { + it('can forward event to cloud code', function () { const cloudCodeHandler = { handler: () => {}, }; @@ -680,7 +726,7 @@ describe('ParseLiveQueryServer', function() { // TODO: Test server can set disconnect command message handler for a parseWebSocket - it('has no subscription and can handle object delete command', function() { + it('has no subscription and can handle object delete command', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); @@ -696,7 +742,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._onAfterDelete(message, {}); }); - it('can handle object delete command which does not match any subscription', function() { + it('can handle object delete command which does not match any subscription', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); @@ -714,13 +760,13 @@ describe('ParseLiveQueryServer', function() { addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); const client = parseLiveQueryServer.clients.get(clientId); // Mock _matchesSubscription to return not matching - parseLiveQueryServer._matchesSubscription = function() { + parseLiveQueryServer._matchesSubscription = function () { return false; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return true; }; parseLiveQueryServer._onAfterDelete(message); @@ -729,7 +775,7 @@ describe('ParseLiveQueryServer', function() { expect(client.pushDelete).not.toHaveBeenCalled(); }); - it('can handle object delete command which matches some subscriptions', function(done) { + it('can handle object delete command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); @@ -746,26 +792,26 @@ describe('ParseLiveQueryServer', function() { addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); const client = parseLiveQueryServer.clients.get(clientId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function() { + parseLiveQueryServer._matchesSubscription = function () { return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterDelete(message); // Make sure we send command to client, since _matchesACL is async, we have to // wait and check - setTimeout(function() { + setTimeout(function () { expect(client.pushDelete).toHaveBeenCalled(); done(); }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('has no subscription and can handle object save command', function() { + it('has no subscription and can handle object save command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); @@ -773,7 +819,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._onAfterSave(message); }); - it('can handle object save command which does not match any subscription', function(done) { + it('can handle object save command which does not match any subscription', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); @@ -782,19 +828,19 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return not matching - parseLiveQueryServer._matchesSubscription = function() { + parseLiveQueryServer._matchesSubscription = function () { return false; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; // Trigger onAfterSave parseLiveQueryServer._onAfterSave(message); // Make sure we do not send command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -804,7 +850,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object enter command which matches some subscriptions', function(done) { + it('can handle object enter command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); @@ -813,25 +859,25 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching // In order to mimic a enter, we need original match return false // and the current match return true let counter = 0; - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; return counter % 2 === 0; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send enter command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -841,7 +887,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object update command which matches some subscriptions', function(done) { + it('can handle object update command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); @@ -850,21 +896,21 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).toHaveBeenCalled(); @@ -874,7 +920,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object leave command which matches some subscriptions', function(done) { + it('can handle object leave command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); @@ -883,25 +929,25 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching // In order to mimic a leave, we need original match return true // and the current match return false let counter = 0; - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; return counter % 2 !== 0; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send leave command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -911,7 +957,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle update command with original object', function(done) { + it('can handle update command with original object', async done => { jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); const Client = require('../lib/LiveQuery/Client').Client; const parseLiveQueryServer = new ParseLiveQueryServer({}); @@ -930,27 +976,27 @@ describe('ParseLiveQueryServer', function() { // Add mock subscription const requestId = 2; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, parseWebSocket ); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushUpdate).toHaveBeenCalled(); const args = parseWebSocket.send.calls.mostRecent().args; const toSend = JSON.parse(args[0]); @@ -961,7 +1007,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object create command which matches some subscriptions', function(done) { + it('can handle object create command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); @@ -970,21 +1016,21 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -994,7 +1040,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle create command with fields', function(done) { + it('can handle create command with fields', async done => { jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); const Client = require('../lib/LiveQuery/Client').Client; const parseLiveQueryServer = new ParseLiveQueryServer({}); @@ -1019,7 +1065,7 @@ describe('ParseLiveQueryServer', function() { }, fields: ['test'], }; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -1027,20 +1073,20 @@ describe('ParseLiveQueryServer', function() { query ); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).toHaveBeenCalled(); const args = parseWebSocket.send.calls.mostRecent().args; const toSend = JSON.parse(args[0]); @@ -1050,7 +1096,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can match subscription for null or undefined parse object', function() { + it('can match subscription for null or undefined parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { @@ -1067,7 +1113,7 @@ describe('ParseLiveQueryServer', function() { expect(subscription.match).not.toHaveBeenCalled(); }); - it('can match subscription', function() { + it('can match subscription', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { @@ -1082,7 +1128,7 @@ describe('ParseLiveQueryServer', function() { expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query); }); - it('can inflate parse object', function() { + it('can inflate parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request const objectJSON = { @@ -1177,20 +1223,20 @@ describe('ParseLiveQueryServer', function() { expect(originalObject.updatedAt).not.toBeUndefined(); }); - it('can match undefined ACL', function(done) { + it('can match undefined ACL', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const client = {}; const requestId = 0; parseLiveQueryServer ._matchesACL(undefined, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with none exist requestId', function(done) { + it('can match ACL with none exist requestId', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); const client = { @@ -1202,13 +1248,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('can match ACL with public read access', function(done) { + it('can match ACL with public read access', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); @@ -1223,13 +1269,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with valid subscription sessionToken', function(done) { + it('can match ACL with valid subscription sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1244,13 +1290,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with valid client sessionToken', function(done) { + it('can match ACL with valid client sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1267,13 +1313,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with invalid subscription and client sessionToken', function(done) { + it('can match ACL with invalid subscription and client sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1290,13 +1336,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('can match ACL with subscription sessionToken checking error', function(done) { + it('can match ACL with subscription sessionToken checking error', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1313,13 +1359,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('can match ACL with client sessionToken checking error', function(done) { + it('can match ACL with client sessionToken checking error', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1336,13 +1382,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it("won't match ACL that doesn't have public read or any roles", function(done) { + it("won't match ACL that doesn't have public read or any roles", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1357,13 +1403,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it("won't match non-public ACL with role when there is no user", function(done) { + it("won't match non-public ACL with role when there is no user", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1377,14 +1423,14 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }) .catch(done.fail); }); - it("won't match ACL with role based read access set to false", function(done) { + it("won't match ACL with role based read access set to false", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1398,7 +1444,7 @@ describe('ParseLiveQueryServer', function() { }; const requestId = 0; - spyOn(Parse, 'Query').and.callFake(function() { + spyOn(Parse, 'Query').and.callFake(function () { let shouldReturn = false; return { equalTo() { @@ -1427,20 +1473,20 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('will match ACL with role based read access set to true', function(done) { + it('will match ACL with role based read access set to true', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1454,7 +1500,7 @@ describe('ParseLiveQueryServer', function() { }; const requestId = 0; - spyOn(Parse, 'Query').and.callFake(function() { + spyOn(Parse, 'Query').and.callFake(function () { let shouldReturn = false; return { equalTo() { @@ -1493,7 +1539,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); @@ -1625,7 +1671,7 @@ describe('ParseLiveQueryServer', function() { }); }); - it('can validate key when valid key is provided', function() { + it('can validate key when valid key is provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1643,7 +1689,7 @@ describe('ParseLiveQueryServer', function() { ).toBeTruthy(); }); - it('can validate key when invalid key is provided', function() { + it('can validate key when invalid key is provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1661,7 +1707,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it('can validate key when key is not provided', function() { + it('can validate key when key is not provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1677,7 +1723,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it('can validate key when validKerPairs is empty', function() { + it('can validate key when validKerPairs is empty', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = {}; @@ -1686,7 +1732,7 @@ describe('ParseLiveQueryServer', function() { ).toBeTruthy(); }); - it('can validate client has master key when valid', function() { + it('can validate client has master key when valid', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1704,7 +1750,7 @@ describe('ParseLiveQueryServer', function() { ).toBeTruthy(); }); - it("can validate client doesn't have master key when invalid", function() { + it("can validate client doesn't have master key when invalid", function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1722,7 +1768,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it("can validate client doesn't have master key when not provided", function() { + it("can validate client doesn't have master key when not provided", function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1737,7 +1783,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it("can validate client doesn't have master key when validKeyPairs is empty", function() { + it("can validate client doesn't have master key when validKeyPairs is empty", function () { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = { masterKey: 'test', @@ -1748,7 +1794,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it('will match non-public ACL when client has master key', function(done) { + it('will match non-public ACL when client has master key', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1762,13 +1808,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it("won't match non-public ACL when client has no master key", function(done) { + it("won't match non-public ACL when client has no master key", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1782,7 +1828,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); @@ -1822,7 +1868,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.authCache.get('invalid')).not.toBe(undefined); }); - afterEach(function() { + afterEach(function () { jasmine.restoreLibrary( '../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer' @@ -1842,7 +1888,7 @@ describe('ParseLiveQueryServer', function() { return client; } - function addMockSubscription( + async function addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -1870,13 +1916,13 @@ describe('ParseLiveQueryServer', function() { requestId: requestId, sessionToken: 'sessionToken', }; - parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make mock subscription const subscription = parseLiveQueryServer.subscriptions .get(query.className) .get(queryHashValue); - subscription.hasSubscribingClient = function() { + subscription.hasSubscribingClient = function () { return false; }; subscription.className = query.className; @@ -1915,7 +1961,7 @@ describe('ParseLiveQueryServer', function() { }); describe('LiveQueryController', () => { - it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + it('properly passes the CLP to afterSave/afterDelete hook', function (done) { function setPermissionsOnClass(className, permissions, doPut) { const request = require('request'); let op = request.post; diff --git a/src/LiveQuery/Client.js b/src/LiveQuery/Client.js index 26cd999834..253234b9bb 100644 --- a/src/LiveQuery/Client.js +++ b/src/LiveQuery/Client.js @@ -62,15 +62,17 @@ class Client { parseWebSocket: any, code: number, error: string, - reconnect: boolean = true + reconnect: boolean = true, + requestId: number | void = null ): void { Client.pushResponse( parseWebSocket, JSON.stringify({ op: 'error', - error: error, - code: code, - reconnect: reconnect, + error, + code, + reconnect, + requestId, }) ); } diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index a19ff3962c..5d28367961 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -10,7 +10,11 @@ import { ParsePubSub } from './ParsePubSub'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { runLiveQueryEventHandlers } from '../triggers'; +import { + runLiveQueryEventHandlers, + maybeRunConnectTrigger, + maybeRunSubscribeTrigger, +} from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; import LRU from 'lru-cache'; @@ -574,7 +578,7 @@ class ParseLiveQueryServer { return false; } - _handleConnect(parseWebsocket: any, request: any): any { + async _handleConnect(parseWebsocket: any, request: any): any { if (!this._validateKeys(request, this.keyPairs)) { Client.pushError(parseWebsocket, 4, 'Key in request is not valid'); logger.error('Key in request is not valid'); @@ -589,19 +593,34 @@ class ParseLiveQueryServer { request.sessionToken, request.installationId ); - parseWebsocket.clientId = clientId; - this.clients.set(parseWebsocket.clientId, client); - logger.info(`Create new client: ${parseWebsocket.clientId}`); - client.pushConnect(); - runLiveQueryEventHandlers({ - client, - event: 'connect', - clients: this.clients.size, - subscriptions: this.subscriptions.size, - sessionToken: request.sessionToken, - useMasterKey: client.hasMasterKey, - installationId: request.installationId, - }); + try { + const req = { + client, + event: 'connect', + clients: this.clients.size, + subscriptions: this.subscriptions.size, + sessionToken: request.sessionToken, + useMasterKey: client.hasMasterKey, + installationId: request.installationId, + }; + await maybeRunConnectTrigger('beforeConnect', req); + parseWebsocket.clientId = clientId; + this.clients.set(parseWebsocket.clientId, client); + logger.info(`Create new client: ${parseWebsocket.clientId}`); + client.pushConnect(); + runLiveQueryEventHandlers(req); + } catch (error) { + Client.pushError( + parseWebsocket, + error.code || 101, + error.message || error, + false + ); + logger.error( + `Failed running beforeConnect for session ${request.sessionToken} with:\n Error: ` + + JSON.stringify(error) + ); + } } _hasMasterKey(request: any, validKeyPairs: any): boolean { @@ -636,7 +655,7 @@ class ParseLiveQueryServer { return isValid; } - _handleSubscribe(parseWebsocket: any, request: any): any { + async _handleSubscribe(parseWebsocket: any, request: any): any { // If we can not find this client, return error to client if (!Object.prototype.hasOwnProperty.call(parseWebsocket, 'clientId')) { Client.pushError( @@ -650,61 +669,77 @@ class ParseLiveQueryServer { return; } const client = this.clients.get(parseWebsocket.clientId); - - // Get subscription from subscriptions, create one if necessary - const subscriptionHash = queryHash(request.query); - // Add className to subscriptions if necessary const className = request.query.className; - if (!this.subscriptions.has(className)) { - this.subscriptions.set(className, new Map()); - } - const classSubscriptions = this.subscriptions.get(className); - let subscription; - if (classSubscriptions.has(subscriptionHash)) { - subscription = classSubscriptions.get(subscriptionHash); - } else { - subscription = new Subscription( - className, - request.query.where, - subscriptionHash - ); - classSubscriptions.set(subscriptionHash, subscription); - } + try { + await maybeRunSubscribeTrigger('beforeSubscribe', className, request); - // Add subscriptionInfo to client - const subscriptionInfo = { - subscription: subscription, - }; - // Add selected fields, sessionToken and installationId for this subscription if necessary - if (request.query.fields) { - subscriptionInfo.fields = request.query.fields; - } - if (request.sessionToken) { - subscriptionInfo.sessionToken = request.sessionToken; - } - client.addSubscriptionInfo(request.requestId, subscriptionInfo); + // Get subscription from subscriptions, create one if necessary + const subscriptionHash = queryHash(request.query); + // Add className to subscriptions if necessary - // Add clientId to subscription - subscription.addClientSubscription( - parseWebsocket.clientId, - request.requestId - ); + if (!this.subscriptions.has(className)) { + this.subscriptions.set(className, new Map()); + } + const classSubscriptions = this.subscriptions.get(className); + let subscription; + if (classSubscriptions.has(subscriptionHash)) { + subscription = classSubscriptions.get(subscriptionHash); + } else { + subscription = new Subscription( + className, + request.query.where, + subscriptionHash + ); + classSubscriptions.set(subscriptionHash, subscription); + } - client.pushSubscribe(request.requestId); + // Add subscriptionInfo to client + const subscriptionInfo = { + subscription: subscription, + }; + // Add selected fields, sessionToken and installationId for this subscription if necessary + if (request.query.fields) { + subscriptionInfo.fields = request.query.fields; + } + if (request.sessionToken) { + subscriptionInfo.sessionToken = request.sessionToken; + } + client.addSubscriptionInfo(request.requestId, subscriptionInfo); - logger.verbose( - `Create client ${parseWebsocket.clientId} new subscription: ${request.requestId}` - ); - logger.verbose('Current client number: %d', this.clients.size); - runLiveQueryEventHandlers({ - client, - event: 'subscribe', - clients: this.clients.size, - subscriptions: this.subscriptions.size, - sessionToken: request.sessionToken, - useMasterKey: client.hasMasterKey, - installationId: client.installationId, - }); + // Add clientId to subscription + subscription.addClientSubscription( + parseWebsocket.clientId, + request.requestId + ); + + client.pushSubscribe(request.requestId); + + logger.verbose( + `Create client ${parseWebsocket.clientId} new subscription: ${request.requestId}` + ); + logger.verbose('Current client number: %d', this.clients.size); + runLiveQueryEventHandlers({ + client, + event: 'subscribe', + clients: this.clients.size, + subscriptions: this.subscriptions.size, + sessionToken: request.sessionToken, + useMasterKey: client.hasMasterKey, + installationId: client.installationId, + }); + } catch (e) { + Client.pushError( + parseWebsocket, + e.code || 101, + e.message || e, + false, + request.requestId + ); + logger.error( + `Failed running beforeSubscribe on ${className} for session ${request.sessionToken} with:\n Error: ` + + JSON.stringify(e) + ); + } } _handleUpdateSubscription(parseWebsocket: any, request: any): any { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 861eb68b2b..088c4dc3c1 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -453,6 +453,60 @@ ParseCloud.afterDeleteFile = function (handler) { ); }; +/** + * Registers a before live query server connect function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.beforeConnect(async (request) => { + * // code here + * }) + *``` + * + * @method beforeConnect + * @name Parse.Cloud.beforeConnect + * @param {Function} func The function to before connection is made. This function can be async and should take just one parameter, {@link Parse.Cloud.ConnectTriggerRequest}. + */ +ParseCloud.beforeConnect = function (handler) { + triggers.addConnectTrigger( + triggers.Types.beforeConnect, + handler, + Parse.applicationId + ); +}; + +/** + * Registers a before live query subscription function. + * + * **Available in Cloud Code only.** + * + * If you want to use beforeSubscribe for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1. + * ``` + * Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => { + * // code here + * }) + * + * Parse.Cloud.beforeSubscribe(Parse.User, (request) => { + * // code here + * }) + *``` + * + * @method beforeSubscribe + * @name Parse.Cloud.beforeSubscribe + * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before subscription function for. This can instead be a String that is the className of the subclass. + * @param {Function} func The function to run before a subscription. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. + */ +ParseCloud.beforeSubscribe = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger( + triggers.Types.beforeSubscribe, + className, + handler, + Parse.applicationId + ); +}; + ParseCloud.onLiveQueryEvent = function (handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; @@ -499,6 +553,16 @@ module.exports = ParseCloud; * @property {Object} log The current logger inside Parse Server. */ +/** + * @interface Parse.Cloud.ConnectTriggerRequest + * @property {String} installationId If set, the installationId triggering the request. + * @property {Boolean} useMasterKey If true, means the master key was used. + * @property {Parse.User} user If set, the user that made the request. + * @property {Integer} clients The number of clients connected. + * @property {Integer} subscriptions The number of subscriptions connected. + * @property {String} sessionToken If set, the session of the user that made the request. + */ + /** * @interface Parse.Cloud.BeforeFindRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/triggers.js b/src/triggers.js index 998e6a3cf6..96dcb65e47 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -16,16 +16,19 @@ export const Types = { afterSaveFile: 'afterSaveFile', beforeDeleteFile: 'beforeDeleteFile', afterDeleteFile: 'afterDeleteFile', + beforeConnect: 'beforeConnect', + beforeSubscribe: 'beforeSubscribe', }; const FileClassName = '@File'; +const ConnectClassName = '@Connect'; -const baseStore = function() { +const baseStore = function () { const Validators = {}; const Functions = {}; const Jobs = {}; const LiveQuery = []; - const Triggers = Object.keys(Types).reduce(function(base, key) { + const Triggers = Object.keys(Types).reduce(function (base, key) { base[key] = {}; return base; }, {}); @@ -132,6 +135,10 @@ export function addFileTrigger(type, handler, applicationId) { add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId); } +export function addConnectTrigger(type, handler, applicationId) { + add(Category.Triggers, `${type}.${ConnectClassName}`, handler, applicationId); +} + export function addLiveQueryEventHandler(handler, applicationId) { applicationId = applicationId || Parse.applicationId; _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); @@ -233,10 +240,12 @@ export function getRequestObject( request.original = originalParseObject; } - if (triggerType === Types.beforeSave || + if ( + triggerType === Types.beforeSave || triggerType === Types.afterSave || triggerType === Types.beforeDelete || - triggerType === Types.afterDelete) { + triggerType === Types.afterDelete + ) { // Set a copy of the context on the request object. request.context = Object.assign({}, context); } @@ -300,7 +309,7 @@ export function getRequestQueryObject( // Any changes made to the object in a beforeSave will be included. export function getResponseObject(request, resolve, reject) { return { - success: function(response) { + success: function (response) { if (request.triggerName === Types.afterFind) { if (!response) { response = request.objects; @@ -335,7 +344,7 @@ export function getResponseObject(request, resolve, reject) { } return resolve(response); }, - error: function(error) { + error: function (error) { if (error instanceof Parse.Error) { reject(error); } else if (error instanceof Error) { @@ -585,7 +594,7 @@ export function maybeRunTrigger( if (!parseObject) { return Promise.resolve({}); } - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { var trigger = getTrigger( parseObject.className, triggerType, @@ -721,7 +730,12 @@ export function getRequestFileObject(triggerType, auth, fileObject, config) { return request; } -export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) { +export async function maybeRunFileTrigger( + triggerType, + fileObject, + config, + auth +) { const fileTrigger = getFileTrigger(triggerType, config.applicationId); if (typeof fileTrigger === 'function') { try { @@ -737,8 +751,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) 'Parse.File', { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, result, - auth, - ) + auth + ); return result || fileObject; } catch (error) { logTriggerErrorBeforeHook( @@ -746,10 +760,57 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) 'Parse.File', { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, auth, - error, + error ); throw error; } } return fileObject; } + +export async function maybeRunConnectTrigger(triggerType, request) { + const trigger = getTrigger( + ConnectClassName, + triggerType, + Parse.applicationId + ); + if (!trigger) { + return; + } + request.user = await userForSessionToken(request.sessionToken); + return trigger(request); +} + +export async function maybeRunSubscribeTrigger( + triggerType, + className, + request +) { + const trigger = getTrigger(className, triggerType, Parse.applicationId); + if (!trigger) { + return; + } + const parseQuery = new Parse.Query(className); + parseQuery.withJSON(request.query); + request.query = parseQuery; + request.user = await userForSessionToken(request.sessionToken); + return trigger(request); +} + +async function userForSessionToken(sessionToken) { + if (!sessionToken) { + return; + } + const q = new Parse.Query('_Session'); + q.equalTo('sessionToken', sessionToken); + const session = await q.first({ useMasterKey: true }); + if (!session) { + return; + } + const user = session.get('user'); + if (!user) { + return; + } + await user.fetch({ useMasterKey: true }); + return user; +} From 4f7fd1732b88a8ec8b69524b7d4c4013c0c31cb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 22:10:35 -0500 Subject: [PATCH 06/33] chore(deps): bump lodash from 4.17.16 to 4.17.19 (#6807) Bumps [lodash](https://github.com/lodash/lodash) from 4.17.16 to 4.17.19. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.16...4.17.19) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 448648ea22..377eb58194 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8479,9 +8479,9 @@ } }, "lodash": { - "version": "4.17.16", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.16.tgz", - "integrity": "sha512-mzxOTaU4AsJhnIujhngm+OnA6JX4fTI8D5H26wwGd+BJ57bW70oyRwTqo6EFJm1jTZ7hCo7yVzH1vB8TMFd2ww==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.assignin": { "version": "4.2.0", diff --git a/package.json b/package.json index b0e152428c..8cafce1847 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "jsonwebtoken": "8.5.1", "jwks-rsa": "1.8.1", "ldapjs": "2.0.0", - "lodash": "4.17.16", + "lodash": "4.17.19", "lru-cache": "5.1.1", "mime": "2.4.6", "mongodb": "3.5.9", From 2e708cc77cb29afc2616da3b8b120428cb71a7b7 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Fri, 17 Jul 2020 04:46:27 +0100 Subject: [PATCH 07/33] fix: upgrade @graphql-tools/utils from 6.0.10 to 6.0.11 (#6809) Snyk has created this PR to upgrade @graphql-tools/utils from 6.0.10 to 6.0.11. See this package in NPM: https://www.npmjs.com/package/@graphql-tools/utils See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 58 +++++++++++++++++++++++++++++++++++++++++++---- package.json | 2 +- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 377eb58194..edf0793f2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,11 @@ "xss": "^1.0.6" } }, + "@ardatan/aggregate-error": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.1.tgz", + "integrity": "sha512-UQ9BequOTIavs0pTHLMwQwKQF8tTV1oezY/H2O9chA+JNPFZSua55xpU5dPSjAU9/jLJ1VwU+HJuTVN8u7S6Fg==" + }, "@babel/cli": { "version": "7.10.0", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.10.0.tgz", @@ -2725,6 +2730,15 @@ "tslib": "~2.0.0" }, "dependencies": { + "@graphql-tools/utils": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", + "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "requires": { + "aggregate-error": "3.0.1", + "camel-case": "4.1.1" + } + }, "tslib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", @@ -2742,6 +2756,15 @@ "tslib": "~2.0.0" }, "dependencies": { + "@graphql-tools/utils": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", + "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "requires": { + "aggregate-error": "3.0.1", + "camel-case": "4.1.1" + } + }, "tslib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", @@ -2758,6 +2781,15 @@ "tslib": "~2.0.0" }, "dependencies": { + "@graphql-tools/utils": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", + "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "requires": { + "aggregate-error": "3.0.1", + "camel-case": "4.1.1" + } + }, "tslib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", @@ -2778,6 +2810,15 @@ "tslib": "~2.0.0" }, "dependencies": { + "@graphql-tools/utils": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", + "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "requires": { + "aggregate-error": "3.0.1", + "camel-case": "4.1.1" + } + }, "tslib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", @@ -2786,11 +2827,11 @@ } }, "@graphql-tools/utils": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", - "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", + "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", "requires": { - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" } }, @@ -2806,6 +2847,15 @@ "tslib": "~2.0.0" }, "dependencies": { + "@graphql-tools/utils": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", + "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "requires": { + "aggregate-error": "3.0.1", + "camel-case": "4.1.1" + } + }, "tslib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", diff --git a/package.json b/package.json index 8cafce1847..0053ea0a3b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@apollographql/graphql-playground-html": "1.6.26", "@graphql-tools/stitch": "6.0.10", - "@graphql-tools/utils": "6.0.10", + "@graphql-tools/utils": "6.0.11", "@parse/fs-files-adapter": "1.0.1", "@parse/push-adapter": "3.2.0", "@parse/s3-files-adapter": "1.4.0", From 85ec22ce3bca380ae262bc39ee5c67f6816dbf5b Mon Sep 17 00:00:00 2001 From: Omair Vaiyani Date: Fri, 17 Jul 2020 04:56:47 +0100 Subject: [PATCH 08/33] fix(direct-access): save context not present if direct access enabled (#6764) * fix(direct-access): save context not present if direct access enabled [Open discussion](https://github.com/parse-community/parse-server/issues/6459) for feature with other issues * only send context when present * use object spread * revert and add test * rename test Co-authored-by: dplewis --- spec/ParseServerRESTController.spec.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index b3b8ea36f3..3e02cde71c 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -161,9 +161,9 @@ describe('ParseServerRESTController', () => { expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toBe( databaseAdapter.createObject.calls.argsFor(1)[3] ); - expect(results.map(result => result.get('key')).sort()).toEqual( - ['value1', 'value2'] - ); + expect( + results.map(result => result.get('key')).sort() + ).toEqual(['value1', 'value2']); done(); }); }); @@ -517,6 +517,22 @@ describe('ParseServerRESTController', () => { }); }); + it('should handle a POST request with context', async () => { + Parse.Cloud.beforeSave('MyObject', req => { + expect(req.context.a).toEqual('a'); + }); + Parse.Cloud.afterSave('MyObject', req => { + expect(req.context.a).toEqual('a'); + }); + + await RESTController.request( + 'POST', + '/classes/MyObject', + { key: 'value' }, + { context: { a: 'a' } } + ); + }); + it('ensures sessionTokens are properly handled', done => { let userId; Parse.User.signUp('user', 'pass') From 5e67e7da43960d8c3c281b4baabb4b35b257c424 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Fri, 17 Jul 2020 06:02:34 +0100 Subject: [PATCH 09/33] fix: upgrade @graphql-tools/stitch from 6.0.10 to 6.0.11 (#6808) Snyk has created this PR to upgrade @graphql-tools/stitch from 6.0.10 to 6.0.11. See this package in NPM: https://www.npmjs.com/package/@graphql-tools/stitch See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Diamond Lewis --- package-lock.json | 100 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/package-lock.json b/package-lock.json index edf0793f2f..c186e9f836 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2720,22 +2720,22 @@ } }, "@graphql-tools/delegate": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.10.tgz", - "integrity": "sha512-FBHrmpSI9QpNbvqc5D4wdQW0WrNVUA2ylFhzsNRk9yvlKzcVKqiTrOpb++j7TLB+tG06dpSkfAssPcgZvU60fw==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.11.tgz", + "integrity": "sha512-c5mVcjCPUqWabe3oPpuXs1IxXj58xsJhuG48vJJjDrTRuRXNZCJb5aa2+VLJEQkkW4tq/qmLcU8zeOfo2wdGng==", "requires": { - "@graphql-tools/schema": "6.0.10", - "@graphql-tools/utils": "6.0.10", - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", + "@graphql-tools/schema": "6.0.11", + "@graphql-tools/utils": "6.0.11", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", - "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", + "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", "requires": { - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" } }, @@ -2747,21 +2747,21 @@ } }, "@graphql-tools/merge": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.0.10.tgz", - "integrity": "sha512-fnz9h5vdA8LXc9TvmhnRXykwFZWZ4FdBeo4g3R1KqcQCp65ByCMcBuCJtYf4VxPrcgTLGlWtVOHrItCi0kdioA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.0.11.tgz", + "integrity": "sha512-jNXl5pOdjfTRm+JKMpD47hsafM44Ojt7oi25Cflydw9VaWlQ5twFUSXk2rydP0mx1Twdxozk9ZCj4qiTdzw9ag==", "requires": { - "@graphql-tools/schema": "6.0.10", - "@graphql-tools/utils": "6.0.10", + "@graphql-tools/schema": "6.0.11", + "@graphql-tools/utils": "6.0.11", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", - "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", + "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", "requires": { - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" } }, @@ -2773,20 +2773,20 @@ } }, "@graphql-tools/schema": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-6.0.10.tgz", - "integrity": "sha512-g8iy36dgf/Cpyz7bHSE2axkE8PdM5VYdS2tntmytLvPaN3Krb8IxBpZBJhmiICwyAAkruQE7OjDfYr8vP8jY4A==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-6.0.11.tgz", + "integrity": "sha512-Zl9LTwOnkMaNtgs1+LJEYtklywtn602kRbxkRFeA7nFGaDmFPFHZnfQqcLsfhaPA8S0jNCQnbucHERCz8pRUYA==", "requires": { - "@graphql-tools/utils": "6.0.10", + "@graphql-tools/utils": "6.0.11", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", - "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", + "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", "requires": { - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" } }, @@ -2798,24 +2798,24 @@ } }, "@graphql-tools/stitch": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/stitch/-/stitch-6.0.10.tgz", - "integrity": "sha512-45xgk/ggXEkj6Ys4Hf1sV0ngzzvPhcGvA23/NG6E5LSkt4GM0TjtRpqwWMMoKJps9+1JX9/RSbHBAchC+zZj3w==", - "requires": { - "@graphql-tools/delegate": "6.0.10", - "@graphql-tools/merge": "6.0.10", - "@graphql-tools/schema": "6.0.10", - "@graphql-tools/utils": "6.0.10", - "@graphql-tools/wrap": "6.0.10", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/stitch/-/stitch-6.0.11.tgz", + "integrity": "sha512-pXELKOJV56C6VkILeyavtGWqzCi0e2gGtOhPiWKcV4MW5RwFN0QsPF4xSLZ7vpX8PVXovyuf/xdI62IvKXoywg==", + "requires": { + "@graphql-tools/delegate": "6.0.11", + "@graphql-tools/merge": "6.0.11", + "@graphql-tools/schema": "6.0.11", + "@graphql-tools/utils": "6.0.11", + "@graphql-tools/wrap": "6.0.11", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", - "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", + "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", "requires": { - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" } }, @@ -2836,23 +2836,23 @@ } }, "@graphql-tools/wrap": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.0.10.tgz", - "integrity": "sha512-260f+eks3pSltokwueFJXQSwf7QdsjccphXINBIa0hwPyF8mPanyJlqd5GxkkG+C2K/oOXm8qaxc6pp7lpaomQ==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.0.11.tgz", + "integrity": "sha512-zy6ftDahsgrsaTPXPJgNNCt+0BTtuq37bZ5K9Ayf58wuxxW06fNLUp76wCUJWzb7nsML5aECQF9+STw1iQJ5qg==", "requires": { - "@graphql-tools/delegate": "6.0.10", - "@graphql-tools/schema": "6.0.10", - "@graphql-tools/utils": "6.0.10", + "@graphql-tools/delegate": "6.0.11", + "@graphql-tools/schema": "6.0.11", + "@graphql-tools/utils": "6.0.11", "aggregate-error": "3.0.1", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.10.tgz", - "integrity": "sha512-1s3vBnYUIDLBGEaV1VF3lv1Xq54lT8Oz7tNNypv7K7cv3auKX7idRtjP8RM6hKpGod46JNZgu3NNOshMUEyEyA==", + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", + "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", "requires": { - "aggregate-error": "3.0.1", + "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" } }, diff --git a/package.json b/package.json index 0053ea0a3b..eb572d3c75 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "license": "BSD-3-Clause", "dependencies": { "@apollographql/graphql-playground-html": "1.6.26", - "@graphql-tools/stitch": "6.0.10", + "@graphql-tools/stitch": "6.0.11", "@graphql-tools/utils": "6.0.11", "@parse/fs-files-adapter": "1.0.1", "@parse/push-adapter": "3.2.0", From ea1ec9b325d5a2739c240c37ef94accb95fe22c1 Mon Sep 17 00:00:00 2001 From: Tom Fox <13188249+TomWFox@users.noreply.github.com> Date: Fri, 17 Jul 2020 10:47:07 +0100 Subject: [PATCH 10/33] Update bug report template (#6805) * Update bug report template * fixes * nit * address Manuel's review * Delete old issue template * Delete old unused migration image * Improve SO prompt --- .github/ISSUE_TEMPLATE.md | 45 ------------------ .github/ISSUE_TEMPLATE/---feature-request.md | 3 ++ .github/ISSUE_TEMPLATE/---getting-help.md | 7 ++- .../ISSUE_TEMPLATE/---parse-server-3-0-0.md | 3 ++ .../ISSUE_TEMPLATE/---push-notifications.md | 4 +- .github/ISSUE_TEMPLATE/---report-an-issue.md | 17 ++++--- .github/MigrationPhases.png | Bin 13828 -> 0 bytes 7 files changed, 25 insertions(+), 54 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .github/MigrationPhases.png diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index bb6766d8aa..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,45 +0,0 @@ - - -### Issue Description - - - -### Steps to reproduce - - - -#### Expected Results - - - -#### Actual Outcome - - - -### Environment Setup - -- **Server** - - parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT] - - Operating System: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT] - -- **Database** - - MongoDB version: [FILL THIS OUT] - - Storage engine: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT] - -### Logs/Trace - -Include all relevant logs. You can turn on additional logging by configuring VERBOSE=1 in your environment. diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---feature-request.md index a19d228731..9ce4b51612 100644 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ b/.github/ISSUE_TEMPLATE/---feature-request.md @@ -1,6 +1,9 @@ --- name: "\U0001F4A1 Feature request" about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/---getting-help.md b/.github/ISSUE_TEMPLATE/---getting-help.md index 331bb3021e..4b75b325de 100644 --- a/.github/ISSUE_TEMPLATE/---getting-help.md +++ b/.github/ISSUE_TEMPLATE/---getting-help.md @@ -1,5 +1,10 @@ --- -name: "🙋‍Getting Help" +name: "\U0001F64B‍Getting Help" about: Join https://community.parseplatform.org +title: '' +labels: '' +assignees: '' --- + + diff --git a/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md b/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md index 49e9f447ff..72373ca25e 100644 --- a/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md +++ b/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md @@ -1,6 +1,9 @@ --- name: "\U0001F525 parse-server 3.0.0" about: Report an issue while migrating to parse-server 3.0.0 +title: '' +labels: '' +assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/---push-notifications.md b/.github/ISSUE_TEMPLATE/---push-notifications.md index 43998b70f8..9654d7c23f 100644 --- a/.github/ISSUE_TEMPLATE/---push-notifications.md +++ b/.github/ISSUE_TEMPLATE/---push-notifications.md @@ -1,6 +1,9 @@ --- name: "\U0001F4F2 Push Notifications" about: Issues with setting up or delivering push notifications +title: '' +labels: '' +assignees: '' --- @@ -46,4 +49,3 @@ Please provide a copy of your `push` configuration here, obfuscating any sensiti ### Logs/Trace - diff --git a/.github/ISSUE_TEMPLATE/---report-an-issue.md b/.github/ISSUE_TEMPLATE/---report-an-issue.md index 78583e73d3..b8c8fed9b0 100644 --- a/.github/ISSUE_TEMPLATE/---report-an-issue.md +++ b/.github/ISSUE_TEMPLATE/---report-an-issue.md @@ -1,21 +1,24 @@ --- name: "\U0001F41B Report an issue" about: Report an issue on parse-server +title: '' +labels: '' +assignees: '' --- - ### Issue Description diff --git a/.github/MigrationPhases.png b/.github/MigrationPhases.png deleted file mode 100644 index dfaca26604482e6752dc80c61f558335c2cba3c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13828 zcmbuF1y>wRw5}(C1c$*1?(Xg~B)CIxcMa|uG&sTCgKKbicXxLW?r{4%=MUU6P$cZDtmgS(3GCqPBs3gBAO!UP z{y%_H({Vr`_yG%1Q3VBaTPIsbb6Y!NNl{T^I|o}+3u_Y)$Zfei(M&=4h_L_tAl(T~ z69SzdxDHJ-{bvCLKiwrJK@1lNR#K?SA+6Zd1fzES&7~uTulN*GM~r11u781Ntf;Wi z=Z}=|{FCCejqScCqv<_`?SN~d_2OFaf=7=KbWAKxikg!O;xTMu=cJEGNRJ&RrTy>$ zf3g$qlv_K6oS2dl9;Cnh8Z816T>StI+AI(NPUw2))NDokf(;7&;OCSXkp|_54<+2( zgjS3XqWcW0`Q{Lc4=RNL1+()|V1oGIK!|VL_aG3NcltXBI4H;o3wnkY&UN5c5(Htw zfXciW^^k=*U_jz5^3K}=3mzsXlViitAW$Kfl==*;P`xgke zKkTnWl{^3!m9GEI1=0!am>=gE>DuV|(|^L$zwgUf6dgzL{2j8^9^ch)5b=624mSQys4HgoOA4E+ zXkL2vC#`%awJB={walN9s~r{3y=k|E%)ZcHDL0{t-bBA(K2QrI@(+S&@9BrvpLU1^y_q-_m)~x@%KQV9?*KG3 z2)v&l$xsQ^S%U%ElDEWX^FyBi9THJ;)HHx*WkH1=jqnz4jloc$PN+}pejhyap-uIX z?luvBY$CmZKca$>E@M6-_#u28{6ItR|ECH~DjSirOSLF?tqS{i3yl9$l^f1%i&Q&^ zun`GG1PreV`s$?b- zJW7$LBDgVnG3bLDyR6gUtKv^FB)be(pZXMNhlP<8rJZ<>e}2yXZO+e2w3p{@PNEs{ zL%4KO^M-iYKl58u4}Uvx|8Ne|La`;LVhR4c(c!bCsS z&f~+k2Q)7VAE7tyw~ud}KA`uX!oF*-( zTCyi_UMPNUx;kW4j$eXbv{eM3*oUSdb5^@6K49YcP0jkJYjv*{# zCI|KuaYkZW@(Z$txXL(~!8{CxFwDLXH2I<2vV0ZARBCMMXzGjE*;$!6{<-}kxVf;| zl3CmvB$&z1yr0UlXTQDS)-(=Qgq>^wG5$Iw$X6NK=5c2!EwwB|%&%+1>+UVHX3h(ZoY*|T8Pa;P8{!?pMEYPHRYXp* zaK(P^-a}Kw6Z4bNNy%A_GUYr+Debk*Z!q%(UBGW?odor44SgYBQa{OuB@JH?0$&2 zL`X!Y^P@2QUOiqDEQ=`9EsH_vDbp+~CR;9pE~A#hnI_1Ym+YQwlKMT_ITa}lFKvQV z&vM=Tt<&Fn%YdY6(R9k>a%y^*b;zOIq;xMkp#pQIWU^%H_&3Mc@&xZh^ZZjHQdfTu z<%(&cY&3)Ai6vcCd3D>5kmaT&xHFgKTe%+j>vYOYRlc(dfjPlW!HxE*_KBZI;JvEh zoVl%Ar|YBQ87}#Y)VH;%ImO+^iOZb0dgZ_w+CrDYhVlAw?E~BcEk^bvg(OTy8%A5x z#j(NUYO8($wdNE0g}RE2ijCFPwfrBlKSa;>&n?dRRxdo5`3?E!J+-fTJwJO?{Nq1* zTpAgZK6matVY2nSu)cs=4`3@dH62~$s@WKr4cVy~5I-8ZlD&Q%Qadu+9=I&M^*pxS zpSa$*c<1k=B&OTc?>rG65VqL7?2H!C6&dm8^{?Kn+MM1z2cN|R#vI2`N zohU8I$EB?$UXn4=RWkEsGBaE0A1%Xt!kGUsBqXOMHw!r%{}@~?cZAt*8mCKfHEW-H z>APlPn$#oKw6o0Vhm3dUSv=I{*Fst7@l2&_j!W?Jd5b?WbNyc3H`B0dc z+zoH2@>J03-TF9BAKw}FZ0ByjaTr+PO3zB?Q@Ad8RIa^3ox^OQtet4j+vwf5#jAPz z8Lck+dwSw={zNCl`>KAsvlu*zw1&6FdA|I0k$m0BtY%~X$)WtJ`|6FMroL9CidKql z-BzdZ)?qJVcx{Dx1!E=tyW)47m9Ylf<+nOb1@9Khssi_rt|(_d!Smg-*6I;owV5A1 zYbGrnFXS&r>zP#o40VZh37&AC*Y|=eNsW~!X3<%AcPAH2EAQoU+J$A&yvh! zo1Oh)wTe^RPIl{EnW9}G%c-YnckFCwBgj1leOuTa`L7NK61%_0^9aS$qBQVV?9+W( zYd=}gn1A_!SxdX3wCo}OHT}@~{)f<8X-}mym8{%+`ZQ17CHtP!Bg4Z!N1YEkW{i*8yzWaUSD|!-YMPbJIyO!F9%m1> z6&(Z@E+;>l&VAa-2Wkr2Tum>I@O-$&*Ad&@FR`BI=T>TL@7E<89WTyXTZTPbJp=B) z-EZ&@`Z7ON9iqM|9vO`dB*-IX&c6M)_N-IX|=$AKhdNXU8?%xgo zWI}=1+0Zv_2tW*<{MMm_6^SlJeOHY$Ty13j)~y?Hsc~pqDieD8Lv5vO5HU zPR&6eWpP z;^}((&w=4F1-9d*1B>pB3>ypOe6N4lCr=R-48@~g(|q(cOYMAn_XC@zH7&%n9+sIG z148=A85(ApDen8;X6rC{h+=`@030YuR>T01+i6{WJ!xQidb++c2v!F~NQ>CxhL0S~ z90-OX`Kp8=sa&N4^a7+GWq{K*HP{7*EV7AcNc=y?gw-Tc4Z#6;5@_|G{%g~0KrD%$ z`M+P?;{J7N+_Cdhk9&JuV2)$ZRF#Q-Gyrz!ACybysxS#lq)NP1 zr5zZTj9&|JqE;%$XI^1p7Nzd<>*_E`c6WE7J@JJP?*fCr!q66_h(N=L!scjl)YJr8 zPO?rsqWAD;|M_x*(K{^K;fDD8-S~R@CwQ15EqKtkB!AgJw2Kg?v%rCZ?I<}%@wp|` z#>4c=UQ`ipttZF{XDU^DSePU*W_nPD&!T97*{Bsxh~~2vj&T3KMz;yI@p#SOl2Cl~;*9<(KdLOvRGFm?tf==!KG0UJ3E7AHc zq}yftXJYy7;N*U(&GD1sH~uBd`9iU_m}|87#WaHxtCmH2I%2Ek78}pJDJsQ&y6Dg3 zvi8+AzLJHpX%n+T!Gg&TSe^MUmbpoxU{tV?sFw--qP5MgnMSAu@JZm>5%th?$-)O|8*r9&IyY(?CC0!Yy|mQp_fWWwO$H#bE* zN3AJ$l1f!J4F?Yo&`+>tnlxc?u8{1evCLKzT(v73UmEETn&c!VT$m;!J~0QQ412Q3 z3XU1X08?l%x|cl>>YWe1nE^G)HZheTuj*P#R6f_FCj6st57c3|KnLAr2Cr2qrE5F& zzR4xXOj})Ic;b-GlIG95s9W+$UX&w@Qy-C(c(1*{vboNOr5m@;SDplBoak`^k7(gX zN5Lf(jD*);SiHaSseD+DK3HIRbTvI+28d+88_h8(JMa7-NDHN`6fd8ReZzU`S0j)ntpOdhYg#h^P{UC754p)-OWd;eJ<3D9(1i{>DPI zL}B@5Cwc#jFTJ3;I5>(#oXd9QhhBjwh47`n z|8#mlG=PRk$QA3U;`0?&g11+5_?LW7`x{XW1Hq9wKosz?yUR8H;~47v;@aM6?|no? za3DQ6FQ}a)pFe@P^PCt{?x=NNH@Td&9394TUU)|)ehnqNp=W=9#hJQOKjQv`z%b-) z9Cx(M((WE|^w@ar+1?egKzf%LLI-upGXu49%i0>QBLvcb?4+;`7XLf zyZ0zMUbB9}jXvbez&?0?2;XLRls4TJ2c2`uKeU|7F)Y2O;z>PlB-+wzW&q6R1&^HG zC3YMGOuCtmbyDs3xa+p3RGgir&0MHu!@HWo+dbTV^GrtKKrw`m4d#wMbGuqM9_C+m zOhnHH!fGc@#WP$|e3ukzq_zCO+lJ7ghmpwWmCX!D`@C(_KJghHsmE?w_O$6cvHQ(- z%~cbB>w6yiDFY`r5)PaYQ;zCy%0~RZW@aIf7#_T9WH9u(nQ3E}#gLbo%X2VeKn~N} z4Gpx0(_*_Ge=sqk_Q_hUWms9qqq=C}7xHfUyXK2FaLW;N-g3TYS{HIa%rmK%qwTo& zS?|>fkLIyau6epdSz`{!I=ShVmFaxmGa*7114u@%97f(|oY{2nyWGTqv_p3d7*yUo zcSjZB`@eGEmNTWuK9j_kw7f3d%`Q*`08#I)!S9y=>xs{Ay<_~f^k)-g;k9OpCI?#= zwm9sOWN=6x#a8dtuW?_Pfy>s5YI%|2zS!1(d2=daWM?G3kkcILk2gyF3Xu9wPE*g> zrigka6<#OIIYJ8~P)RQ%KjP6T*#*`yyX;>_f5a1QG3#N}sN>()@WPO=D`Pb2Y4wiB zI+ zVHL-C4KvP$VN4;5h}CbUHzI(T*Iji3<39n{qXRp|uQ&>^-{2^b_KH9scP0Vvi!KH^ z>YV!ce!&^PHt}_9*|~9%t$<=oCWozScT1jtBSibq&vVXN8E+YG{oT$NBD*X59zIwJY2Kk+5rT4rCgmLii(?Wr@0PY4VoAkhq7O$NbjRV`5Zq6+KH{ zP`aE-HsC?PtINvQpqS^q-`?z0)vsCRgY?N>yPSHl3JGPs5|E|Oy1bVkC>JTiomj70&PN7?wPq49OW}apCIjGYP zyp*x0?sF@z4Hl94KqJ5X3?M}G`i(GYU6`P{i0u6ZzikV=LM&DL5oKTd+GMU##qiOy z`W4&y1H&ip{IE=bgbw>6NaA3 z0l_eZcNiSb{VO0Z$25<e-z|Ng~^<4bbVUkPjKcOs0rz zUs1mqUt4%L-Ddf7M@ebgu1lti4~ueheij{Loqq(ooK8$2klmg9Ae>!GHJGC?*zwzP z=bEmz3W^{{p1wk^MI-HKe+~e0uuei37Tj%TGU;wqaND7*3DLaJv0@ z06h|$iPtt#W*)e#*l2HLy^&3fdqgTYvH;Ifi6ax?t)Lm5^j zJ3nl;HZZ8tEM)a622Nr{PMt{lGdfY)CF`M=EF)pJxos3o+ZSEF2HGb?7%)$mmF~p< zXkW25n02wy^?gKv%?avWJn9y-rIqtJVE#aYF>$Ru?^2X7_5F7kMQ(c@^iSBF*VHtv zS{~g%6_JxCdjTqhmJ>}~@^dX|8uZSVUo9zo`?>1FO6;3is*Om?7h zBYr7+c0-2VvT8~9!`9n}O(1l(o*E7m(;k2K}5zxf= z@h}$TYIWFrv7A8LKPhrjelfxkq1bgO+w>7)4!}h?A*gQhHC(N~R zrQ~OqI#RM+fPN7eV^`hL$~_}%mp@^nnT}D7EF%M%2N2>gO&#n(*jS<{NpqA^(~jY_ z8Ah`RX>+EMJGxT%`VRro`WaqV$VZY}Sbm8Qou*QG;_=udEb_<^zu+xiXqPNZ`4?)a z5LigVkf1*HYrsU)I(=}vTE>wf$B~CDHU@?>SHRW?#Ao8{gpLU+iPPA|j->=ISDp^I z`ntNwLrJ-CXhTB=6RKX&f7;EkAqlML)*l=g;Y(m=!eYUdhOr!)Ux|-JGIuwH9DT%# z!!im1?4&VbTjv*SO`(q(qRtVP>&Ic?T)c9%?J@U^zEObi_-7!MWy~jato!iC?le!? zlUgHzK!^RW_fnvL9Uo;|KoD#{aTE`O|E%jjbGG39{X46LFb^vWwTfLD4BAD}P1J0Q zr`h6>wz^ua2HKGkD}z%10dTrnSKbJYlG1qFWk0rN`MTHka-CMkivu1+qI7O#ul)Y? zI|lsRct&x79v&V*YEY*nlXPL>bXZJpRk^w2UWS&IPgi?c#DO@UzpHTx4qi3b3YDpp zTzvieHxG*US-A4?&?KZ1n{b|DU;`M%aDB@Wc1GqJrjZwCR$Nl+EfgY1? ztK#=LjeB`YM+7p-(|PPc8B#8cDyvLFA;i~GQp!m6AfcqJ%@cin zy&n>fN|%>zQB|I)wcR-S31t*T(6Tr^{dQ70P;bq{ZTR)&?koi8yu+b_rd25^a9T-; z&sy_Yd3hL}+STq=eNoY-Y}#E@Gp{{&@6#=xMC{8}$Zl;dJvKI#t1DHYf|%GiiV%4g zPn?FPruX^k_aF!++Q&Z7J9Vul14M!v}3?D3pX6b`B_TkVZUPY?oEg$~`*(}_-3*WcmcX5i*$>)zYbe@?TBQvw2R z0#MpxM^^sUg#Y~uAA)k>dOVxZ%-d91d5|M&QCBBRLu2N8#=E|_h=z(P0si{!+uDNO zyT|eDURhbUzklrL=u2Rr#LT^nBu|5h^l-a(Ap*jPp^>ONf5j~X9$`d2y|I^)!m`d~ zEHw+bdWbyL!t+BTS)kA3p1Ehs^uz=@ zNqoAOROy_jdlMvCfHEf>-0a4}N8TGPa0>_BT2P?cw!XQy*KE)umulMnU>L4zi->~4 z?Q**9x^c$i{Ul{&C2nl2@Z-Fk&5SiKPZSKHO>j9|F7FAsH<~EDyNi8B4s|}MA2B9$ zaWUjvOif$y_7;%K9DR6uZX1Zwi62>cx`88y<)D+Eoun|bP3Nm85pF9 zUIA4E6|&(~V+9;4Rej~4DfYE0-R{FSOZ63HVp;$mt6b~8UV&Ody2aFy1PH>>QLlPQ zGOEJGwY8;qMyI;zwU+1^_Sd6ArjF!It*GNp$VYb9hrtkkcP&m(}^U;J!qH_ zViiIycG;vvRZq9ZNp>9-6|)dzc0b6Vcz;?b1tNE$68qDqAx6FN zKh(G+@o8G4l=!Faw;~V-<#nS=c@6XgkQors*9u*RM!T*4=L@Fi)0%}@%P}B^!4TZY z_BNfN@qz_BEiKGbj_wx)|F*{)Gep>&PT`Bg!x*Oa#ax9VH1ub!2?bqUK7C;f$l%!6 z(AZd0b#+jNpu=wG;USohk0r{eC%6v|+KZ3RUf9o7OA99009u4N5ccE1<%Tz1=vQ`N zqL4#=g~3-86&c|a{QWCPKdMKHN)oueLqs-Kt-TTfOZztFp zQ}gG~b>F8MWh<;{dE4aMTX8fxEE4^(e^I)r>G24+~2{ zQ!`JY2qzd0@usQf#UuO?9gUQL;LMF*&dp6&E3JDU5(x1zFVgdQvk9Dx#N%2rDo;z8 zW_LVNx@)u-wMeCe<>y|!zdIWq9Nb)Kpp%R(srUg4cWLwEhna7EL(<|(kGgSm2Af%h z^@fk}NMZny_f2_51~(@sB1!KS7y(Ph!Qn49{NvHlf_Nk@F~(J$rnk1Xo1$XVSJK!a zen3gU5FVa}r~7DjX})tI^3RCB4C6vmQ^l&)H;<2#6B9X{k0pTKCXJ+USb2Fo-JUZM z6^O#QoS+{Firb}f+RTa*d0!9HIgZ)>kn!>sK!6_>i#S}E%CWSxbar($Ee!ALsO#h4 zsLKQjs~%HjQ4t<9akKamQq&B44|9afzWrvWMnKo#j(vF8FmS$sxIbZ7^o8T)c4WIZ z%*u*%8PNwIWkyAl43edj{?^ol%SQ9w9ZKwgKL<>$>q2H^yxRc%s4FVcrgUsbmMxvj z5=e7DQT?O6K3^#BHx{(YV}&gy9ul80N3Zwhbx(J8sGx|^X1if=Jt|4O`RVi1YO98> z@(d0E0V(*Ov{Z?-bm`OW36#hc&ecD=yG#24dr}&jpJ(*Ku=Y*bf*p<+Yk;x?)i*s| z>$Gxf>w~pwYMe4Y?y;JZQl0gZEv7ONIk_<_N?7jRRXrZpkGl(^zKZ3ZylZk>TP6}o zslpl8TOt;r_)a1UBQWd(k=H+8rQ)+{Tq=1v&AQ{3n;R>jX0Nxxk^VNR8>+95V(M7E zI}nn0-wLV*QvF$X;e2oJ_~792zQdOR3*qdv*Y|Pcx{6F>fXv$Xmh_oZvwAhG=3nCD;t% z0~HmYfM9hD5iOLOhNkN!1&7l}z0E_}$w@+8-OoLpUtfZp{Huux=4zv2tzjQMr?dj~ zY-VN!ED5I?FaS95b^Z}u+G}s4wJ%7qP zBjZBXnv}2QVT=pAv6)xXY+CA1bCX7wpx@H!swumY4nyF?K~V71*^;B-c)HM#1ON?D z?(Pna1i%c0^>zRI=l8volb2_y<7T?-N22^p#8=X{e?65=+7&f5qSn@jY!l@AF*E)! zrfjwJ`;syd?|{ILk0TQjGBPqECovr$<6^K`t8ZL%Y)FH9wQLz$Po}DWta1L9Ft=qw zg1`DA7xy}jvzq2!BzF|}T8ydUH0h#6W z1WNAR8Q<%1sUt92t*zQT__@CCuy+enc`&ap*FU3$-sGq;6fpYgM~93FqoQ2iUk>j1 z2^aq3l+Sll%GviPQaF*j!wGtC*I7)Vece&Ww{xYB78V+SD$aSx{ZX=6Q^-*%=`+mf z>xssS!sn@6z$=u+`xdKWWHbUGOA4>Eu^0bM$D1O5YuBSEjwWx$hXEGC3s(-!I!ytQ zO)PYV_;{6zHBX1V;cqy>np&98_gSc+fsXqx=U-$Jr0CG#>|PkZ{Ob!};<8O-(zw!| zcz1;l3LDu|Z?N$zBzBS&DO$k8$InOMAjb&?l+u|_jnToyg;{p?1WAmH;`d8MhkW^b z*w>GZ83CNZjP_0VL_`fWW|f~|o^d1l0oZJ8v`cebK3zC0sOceRpM6i#i+XzUSX`0b zyHZUpFkt>XN#}v1`SE8Y?*(LQjc<@&Kqp?xSxhV{ElpWGM2gWijg?~FkTBe_qHcM) zAS|qTcJ}5xZM&>X8Z0l66SEP=?G+Oz;!*4f$q#l_rpI&Np=@;hhk@=C(qUL8Qn zW@es1a}<;$PaTN6yN!s51%QRZeq7K{f#-R>%%hh(Nnc63{60)^UYq{+o1WeBC%{I% zObNtWE(p*^&udY6U)g8A&<8 zz3uIo5 zm;E%I%S%*%*sU!TBk+0jk7vymkErBL(|Mg!$cq~r1({lz6G!%d+krH7GGX+VI}(%&PX ze0+v#@_D@j9Kg;1(-gaa*?89Wc6WJFe}&;hCOY7;-Q5~TGX*jU(hG}1|k>QR%k2l>qh& z@ZVVU$)i|-+HD>){-5F&NxsaNJ-Q!FA4Nq?^W7`%&X>bDI@&%yG{V5Rw!<3}sHAZb z4UV^KsH=PIN<5uZPAnYVw76y!m6Z+HTlYtpg&>6+2>~SKd~^6MmfURO4gHz$@2$l= z>D%jTn-gyjVD29uDZB)2hvV}_0u1$;giz1Rmv0m3+|9;~+aLF{sAQ%1-LF`)CL#e9 za%UB#$6`fXZn@tD9*!G-d+_(ouV0-dB~Jj9d@ogv_`=pZG&H`ho0_~RB=GvQV#`D* z-~o;@TByLBW=F?8cD=RqF*SVy+&Sfc*JbTlTIw1bv$#E$4absA;k03o9-yJ8_kO*7 z+~3%3xwvz4Z)$xy)~2EgnVr2>RP1VRCzSf*F`eH#6qhwL*7irC2!O|5p>RXvjJj=4 z2}MQ3h&}YCut-7M7V`Pe)L*n!Ra=Y(ekbXs3fLPOiiyc7E1x8@1n}TTsg`C5`5G!I zbyw&NKtY*!KL>~ReH6^P@5e*%@>(r7vvYW|Q5l$>MF5bfFAUqz5E2_F@9y5z+{B~P zrfCOw;f4)eexH}B(b3kct7X98-=4V2q+E^0QcM7v-qgqq{b^`)!O%lXfUjk7I|guH z@{&A!29WL5CUi?n+|m+UQnG+}DOm_)w@hP4GFC=<1eZ0RNitUGH6HEDms+PIb~CdO zBHz`lO3?= zB_8ohUP_8tNQGqgEd(&oyZ}8Y6$6elMKq+}X0_3EcMuw%O7Qt~fJD?Vi7AUD&fR)F zoz-F2@9K*8j}id}d1-0f#ft0l&=3iEZ$okM?P^mgI#m=i!&z^r^xhtaZEJP5?|Tt2 zCupHXdL4rEBA9r1nraK_K%wdKA3_TqG$&tM#XlHJTP&2{3pUtwPp{#)s2o4d&iOkov-E`ow{+6{)dnq2PqPur&a?8{DKM)AO;>D7TPu)_@7MORJm2rHh1t2H{i^E8sV4<1oRmWL=n(pTmfkT zAi*N3Fz3kwDs;}&AsDj0{^RseX5Z>)KSl)ho7m@X;xIehSOf&xy1D`2zl@ooU^G}a zqkhDFyI;U-5iYL6?QMrNX#}{-$Z&)KdwNRB{IasoBuu$UO} zK#t{SXG4?3$0@Hi*gjlD2c?CEzGh?);1m2QxZ^y@Hk?So30}mpv$qElW6$oFAIO$4 zx>VH)6{5{(w%VA3kBmx0w6(RR1O(MJyA|p?C!JY0qYvq-R%PJWm3e!6rmk)UAfU=S zv~;AT)byk88Lsmofe&(->Akvd7jHox3fq+cYbJ!rL|U6Zdb&Eld$2;eX#z!0NCyBbtkLxLnV(IBf;(9{xS%h(60DGv}5rE3Of-LW{(Z0~h0;Sy-rP z(@BYspM0IB4l~4S7;A_~OLLSC2TZ$Pvb#)L5S%}G@%%h;z|YlI_YR9_Wefr=tSb8* z-HdS{GdOM61A*MbK{S>dXHfP{Ia05xxL=72E9>jS1e|ArRHSNY%@^lWI*0i}jqnGx zV)pq2HiHSlpf3SHcOBgE_|%mZ6;mFm@1cKA0AcoYGX<1%@5{bK=acpQ%X0(gwfKiP ziLV3%mI+c;EWGPFCvUe^%*1X_66D+cVk2ga=bEB*2j5hIcchEvwHgCV)Dm~VdkwYk za~G>AVFru4Ly|h$i1a95(rVV~+)Ry(qyZ8^Niwx%IyzKH(Ua3R9~{V1p+yT1s%&K) zF!V4sw$oi_2!iaLo`MMpW3qf(+S_@U6-))A)#)v*t*eWeoENf(#~Fba4ljRAX4Y2I zb=~*aw3?iyC4PpWr`_?Z3oBq&rAjY80?f!g6 zc8ZbMr9XRw2qE$9{pFUUkUeuWnStgjpn2v-cM33!eA}c#M{cfCJxyJHerkq!NsiP^L~(G*S5iYQ zg_hJVZAnQ?yg;DosS%4cfzVxL0k6*~#nQrp<7OvLf|OBp;yKC}E8zP)aTJX3AfUPx zY^Qt+&&}5SZ73Xkfe%=5# zq<@wN6C2wHu=OtH*N2CbySvK2e~Vzr=LQBp>2&CRRjhri)@LiqpWeu27kaxU0ZPeR{?_Q}EtK`oe zCoyIOgIV=P0^!J-d5p_6uqPtI5f1FQEi4vraZ>@03OIhi15ne_o(xTYgldb6eX7)~ zo0^-e{`0Nc>k+YjruRib!1L~SGRHdye77S?jz+I>wF=OQ0Dtri-J@m6u+pUGmM*9rbrL7K6 zO>yzrWKL~w?^~54k=Z?))Qv!bxRtG%6gla2)XIMXFcTs5r=A;Sx$M8-tO*JF(6w{g z9nmLK6qf-v_J$s!2gg)*Ec8EF9hf&qH~tFI?_ia?ej( zE_xYeQ#HA%Rpx3CX$y>=+T(Y0h+@7l**-9jzBq4q8WMPN288vH$Q;Pv57-cj%-b7! zr=GdL1QeD>ZcVxUL-Pgwm?bO$ej$R0dV4Bmy&B|E^OBJbuoofwO@7*pP5{57SlS8#Nq&?2S^?4b|364p_rw4I From f6ed5067b0c21492124b77480e532123a919185d Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Fri, 17 Jul 2020 10:15:29 -0500 Subject: [PATCH 11/33] Remove wontfix label from stalebot (#6810) Replacing the `wontfix` label with `stale` label. `wontfix` looks like we would never fix them. --- .github/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/stale.yml b/.github/stale.yml index e08c0e16f9..6807ed061f 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -17,7 +17,7 @@ exemptLabels: - security - up-for-grabs # Label to use when marking an issue as stale -staleLabel: wontfix +staleLabel: stale # Limit to only `issues` not `pulls` only: issues # Comment to post when marking an issue as stale. Set to `false` to disable From 78239ac9071167fdf243c55ae4bc9a2c0b0d89aa Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 17 Jul 2020 18:50:41 +0200 Subject: [PATCH 12/33] Merge pull request from GHSA-236h-rqv8-8q73 * Fix graphql viewer breach * fix * remove comment --- spec/ParseGraphQLServer.spec.js | 330 +++++++++++++++----------- src/GraphQL/loaders/usersMutations.js | 32 ++- src/GraphQL/loaders/usersQueries.js | 45 ++-- 3 files changed, 226 insertions(+), 181 deletions(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index b805cabe96..c1f23bc733 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -170,7 +170,7 @@ describe('ParseGraphQLServer', () => { new ParseGraphQLServer(parseServer, { graphQLPath: 'somepath', }).applyGraphQL({ - use: (path) => { + use: path => { useCount++; expect(path).toEqual('somepath'); }, @@ -208,7 +208,7 @@ describe('ParseGraphQLServer', () => { graphQLPath: 'graphQL', playgroundPath: 'somepath', }).applyPlayground({ - get: (path) => { + get: path => { useCount++; expect(path).toEqual('somepath'); }, @@ -436,9 +436,7 @@ describe('ParseGraphQLServer', () => { parseGraphQLServer.applyGraphQL(expressApp); parseGraphQLServer.applyPlayground(expressApp); parseGraphQLServer.createSubscriptions(httpServer); - await new Promise((resolve) => - httpServer.listen({ port: 13377 }, resolve) - ); + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve)); const subscriptionClient = new SubscriptionClient( 'ws://localhost:13377/subscriptions', @@ -506,7 +504,7 @@ describe('ParseGraphQLServer', () => { let checked = false; const apolloClient = new ApolloClient({ link: new ApolloLink((operation, forward) => { - return forward(operation).map((response) => { + return forward(operation).map(response => { const context = operation.getContext(); const { response: { headers }, @@ -541,7 +539,7 @@ describe('ParseGraphQLServer', () => { it('should handle Parse headers', async () => { let checked = false; const originalGetGraphQLOptions = parseGraphQLServer._getGraphQLOptions; - parseGraphQLServer._getGraphQLOptions = async (req) => { + parseGraphQLServer._getGraphQLOptions = async req => { expect(req.info).toBeDefined(); expect(req.config).toBeDefined(); expect(req.auth).toBeDefined(); @@ -643,7 +641,7 @@ describe('ParseGraphQLServer', () => { }) ).data['__type']; expect(fileType.kind).toEqual('OBJECT'); - expect(fileType.fields.map((field) => field.name).sort()).toEqual([ + expect(fileType.fields.map(field => field.name).sort()).toEqual([ 'name', 'url', ]); @@ -665,7 +663,7 @@ describe('ParseGraphQLServer', () => { }) ).data['__type']; expect(classType.kind).toEqual('INTERFACE'); - expect(classType.fields.map((field) => field.name).sort()).toEqual([ + expect(classType.fields.map(field => field.name).sort()).toEqual([ 'ACL', 'createdAt', 'objectId', @@ -690,7 +688,7 @@ describe('ParseGraphQLServer', () => { ).data['__type']; expect(readPreferenceType.kind).toEqual('ENUM'); expect( - readPreferenceType.enumValues.map((value) => value.name).sort() + readPreferenceType.enumValues.map(value => value.name).sort() ).toEqual([ 'NEAREST', 'PRIMARY', @@ -731,7 +729,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__schema'].types.map((type) => type.name); + ).data['__schema'].types.map(type => type.name); const expectedTypes = [ 'ParseObject', @@ -741,7 +739,7 @@ describe('ParseGraphQLServer', () => { 'Upload', ]; expect( - expectedTypes.every((type) => schemaTypes.indexOf(type) !== -1) + expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) ).toBeTruthy(JSON.stringify(schemaTypes.types)); }); }); @@ -768,7 +766,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__schema'].types.map((type) => type.name); + ).data['__schema'].types.map(type => type.name); expect(schemaTypes).toContain('Node'); }); @@ -786,7 +784,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__type'].fields.map((field) => field.name); + ).data['__type'].fields.map(field => field.name); expect(queryFields).toContain('node'); }); @@ -804,7 +802,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__type'].fields.map((field) => field.name); + ).data['__type'].fields.map(field => field.name); expect(userFields).toContain('id'); expect(userFields).toContain('objectId'); @@ -824,7 +822,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createFileInputFields).toEqual(['clientMutationId', 'upload']); @@ -844,7 +842,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createFilePayloadFields).toEqual([ @@ -869,7 +867,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(callFunctionInputFields).toEqual([ @@ -895,7 +893,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(callFunctionPayloadFields).toEqual([ @@ -918,7 +916,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(inputFields).toEqual(['clientMutationId', 'fields']); @@ -938,7 +936,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(payloadFields).toEqual(['clientMutationId', 'viewer']); @@ -958,7 +956,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(inputFields).toEqual([ @@ -982,7 +980,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(payloadFields).toEqual(['clientMutationId', 'viewer']); @@ -1002,7 +1000,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(inputFields).toEqual(['clientMutationId']); @@ -1022,7 +1020,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(payloadFields).toEqual(['clientMutationId', 'viewer']); @@ -1042,7 +1040,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(inputFields).toEqual([ @@ -1066,7 +1064,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(payloadFields).toEqual(['class', 'clientMutationId']); @@ -1086,7 +1084,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(inputFields).toEqual([ @@ -1110,7 +1108,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(payloadFields).toEqual(['class', 'clientMutationId']); @@ -1130,7 +1128,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(inputFields).toEqual(['clientMutationId', 'name']); @@ -1150,7 +1148,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(payloadFields).toEqual(['class', 'clientMutationId']); @@ -1175,7 +1173,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createObjectInputFields).toEqual([ @@ -1203,7 +1201,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createObjectPayloadFields).toEqual([ @@ -1231,7 +1229,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createObjectInputFields).toEqual([ @@ -1260,7 +1258,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createObjectPayloadFields).toEqual([ @@ -1288,7 +1286,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].inputFields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createObjectInputFields).toEqual(['clientMutationId', 'id']); @@ -1313,7 +1311,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type'].fields - .map((field) => field.name) + .map(field => field.name) .sort(); expect(createObjectPayloadFields).toEqual([ @@ -1339,7 +1337,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__schema'].types.map((type) => type.name); + ).data['__schema'].types.map(type => type.name); const expectedTypes = [ 'Role', @@ -1354,7 +1352,7 @@ describe('ParseGraphQLServer', () => { 'UpdateUserFieldsInput', ]; expect( - expectedTypes.every((type) => schemaTypes.indexOf(type) !== -1) + expectedTypes.every(type => schemaTypes.indexOf(type) !== -1) ).toBeTruthy(JSON.stringify(schemaTypes)); }); @@ -1373,7 +1371,7 @@ describe('ParseGraphQLServer', () => { `, }) ).data['__type']; - const possibleTypes = objectType.possibleTypes.map((o) => o.name); + const possibleTypes = objectType.possibleTypes.map(o => o.name); expect(possibleTypes).toContain('User'); expect(possibleTypes).toContain('Role'); expect(possibleTypes).toContain('Element'); @@ -1397,7 +1395,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__type'].fields.map((field) => field.name); + ).data['__type'].fields.map(field => field.name); expect(userFields.indexOf('foo') !== -1).toBeTruthy(); }); @@ -1414,7 +1412,7 @@ describe('ParseGraphQLServer', () => { } `, }) - ).data['__type'].fields.map((field) => field.name); + ).data['__type'].fields.map(field => field.name); expect(userFields.includes('password')).toBeFalsy(); }); }); @@ -1896,13 +1894,13 @@ describe('ParseGraphQLServer', () => { `, }); expect( - __type.inputFields.find((o) => o.name === 'price').type.kind + __type.inputFields.find(o => o.name === 'price').type.kind ).toEqual('SCALAR'); expect( - __type.inputFields.find((o) => o.name === 'engine').type.kind + __type.inputFields.find(o => o.name === 'engine').type.kind ).toEqual('NON_NULL'); expect( - __type.inputFields.find((o) => o.name === 'doors').type.kind + __type.inputFields.find(o => o.name === 'doors').type.kind ).toEqual('NON_NULL'); const { @@ -1922,13 +1920,13 @@ describe('ParseGraphQLServer', () => { `, }); expect( - __type2.fields.find((o) => o.name === 'price').type.kind + __type2.fields.find(o => o.name === 'price').type.kind ).toEqual('SCALAR'); expect( - __type2.fields.find((o) => o.name === 'engine').type.kind + __type2.fields.find(o => o.name === 'engine').type.kind ).toEqual('NON_NULL'); expect( - __type2.fields.find((o) => o.name === 'doors').type.kind + __type2.fields.find(o => o.name === 'doors').type.kind ).toEqual('NON_NULL'); }); @@ -2787,7 +2785,7 @@ describe('ParseGraphQLServer', () => { ).toEqual(2); expect( findSecondaryObjectsResult.data.secondaryObjects.edges - .map((value) => value.node.someField) + .map(value => value.node.someField) .sort() ).toEqual(['some value 22', 'some value 44']); expect( @@ -2954,7 +2952,7 @@ describe('ParseGraphQLServer', () => { ).toEqual('some value 22'); expect( createPrimaryObjectResult.data.createPrimaryObject.primaryObject.relationField.edges - .map((value) => value.node.someField) + .map(value => value.node.someField) .sort() ).toEqual(['some value 22', 'some value 44']); expect( @@ -3193,7 +3191,7 @@ describe('ParseGraphQLServer', () => { }, }, }); - const classes = Object.keys(result.data).map((fieldName) => ({ + const classes = Object.keys(result.data).map(fieldName => ({ clientMutationId: result.data[fieldName].clientMutationId, class: { name: result.data[fieldName].class.name, @@ -3358,9 +3356,9 @@ describe('ParseGraphQLServer', () => { }, }); findResult.data.classes = findResult.data.classes - .filter((schemaClass) => !schemaClass.name.startsWith('_')) + .filter(schemaClass => !schemaClass.name.startsWith('_')) .sort((a, b) => (a.name > b.name ? 1 : -1)); - findResult.data.classes.forEach((schemaClass) => { + findResult.data.classes.forEach(schemaClass => { schemaClass.schemaFields = schemaClass.schemaFields.sort((a, b) => a.name > b.name ? 1 : -1 ); @@ -4277,10 +4275,10 @@ describe('ParseGraphQLServer', () => { expect(result.manyRelations.length).toEqual(2); const customerSubObject = result.manyRelations.find( - (o) => o.objectId === obj1.id + o => o.objectId === obj1.id ); const someClassSubObject = result.manyRelations.find( - (o) => o.objectId === obj2.id + o => o.objectId === obj2.id ); expect(customerSubObject).toBeDefined(); @@ -4289,7 +4287,7 @@ describe('ParseGraphQLServer', () => { 'imCustomerOne' ); const formatedArrayField = customerSubObject.arrayField.map( - (elem) => elem.value + elem => elem.value ); expect(formatedArrayField).toEqual(arrayField); expect(someClassSubObject.someClassField).toEqual( @@ -4445,7 +4443,7 @@ describe('ParseGraphQLServer', () => { await Promise.all( objects .slice(0, 3) - .map((obj) => + .map(obj => expectAsync( getObject(obj.className, obj.id) ).toBeRejectedWith(jasmine.stringMatching('Object not found')) @@ -4456,7 +4454,7 @@ describe('ParseGraphQLServer', () => { .someField ).toEqual('someValue4'); await Promise.all( - objects.map(async (obj) => + objects.map(async obj => expect( ( await getObject(obj.className, obj.id, { @@ -4467,7 +4465,7 @@ describe('ParseGraphQLServer', () => { ) ); await Promise.all( - objects.map(async (obj) => + objects.map(async obj => expect( ( await getObject(obj.className, obj.id, { @@ -4478,7 +4476,7 @@ describe('ParseGraphQLServer', () => { ) ); await Promise.all( - objects.map(async (obj) => + objects.map(async obj => expect( ( await getObject(obj.className, obj.id, { @@ -4494,7 +4492,7 @@ describe('ParseGraphQLServer', () => { }) ).toBeRejectedWith(jasmine.stringMatching('Object not found')); await Promise.all( - [object1, object3, object4].map(async (obj) => + [object1, object3, object4].map(async obj => expect( ( await getObject(obj.className, obj.id, { @@ -4505,7 +4503,7 @@ describe('ParseGraphQLServer', () => { ) ); await Promise.all( - objects.slice(0, 3).map((obj) => + objects.slice(0, 3).map(obj => expectAsync( getObject(obj.className, obj.id, { 'X-Parse-Session-Token': user4.getSessionToken(), @@ -4521,7 +4519,7 @@ describe('ParseGraphQLServer', () => { ).data.get.someField ).toEqual('someValue4'); await Promise.all( - objects.slice(0, 2).map((obj) => + objects.slice(0, 2).map(obj => expectAsync( getObject(obj.className, obj.id, { 'X-Parse-Session-Token': user5.getSessionToken(), @@ -4646,7 +4644,7 @@ describe('ParseGraphQLServer', () => { ).toBeDefined(); }); - it('should respect protectedFields', async (done) => { + it('should respect protectedFields', async done => { await prepareData(); await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); @@ -4762,7 +4760,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if ( call.args[0].ns.collection.indexOf('GraphQLClass') >= 0 ) { @@ -4826,7 +4824,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; expect(call.args[0].options.readPreference.mode).toBe( @@ -4886,7 +4884,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; expect(call.args[0].options.readPreference.mode).toBe( @@ -4936,7 +4934,7 @@ describe('ParseGraphQLServer', () => { expect(result.data.customers.edges.length).toEqual(2); - result.data.customers.edges.forEach((resultObj) => { + result.data.customers.edges.forEach(resultObj => { const obj = resultObj.node.objectId === obj1.id ? obj1 : obj2; expect(resultObj.node.objectId).toEqual(obj.id); expect(resultObj.node.someField).toEqual(obj.get('someField')); @@ -4977,12 +4975,12 @@ describe('ParseGraphQLServer', () => { expect( (await findObjects('GraphQLClass')).data.find.edges.map( - (object) => object.node.someField + object => object.node.someField ) ).toEqual([]); expect( (await findObjects('PublicClass')).data.find.edges.map( - (object) => object.node.someField + object => object.node.someField ) ).toEqual(['someValue4']); expect( @@ -4991,7 +4989,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Master-Key': 'test', }) ).data.find.edges - .map((object) => object.node.someField) + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2', 'someValue3']); expect( @@ -4999,7 +4997,7 @@ describe('ParseGraphQLServer', () => { await findObjects('PublicClass', { 'X-Parse-Master-Key': 'test', }) - ).data.find.edges.map((object) => object.node.someField) + ).data.find.edges.map(object => object.node.someField) ).toEqual(['someValue4']); expect( ( @@ -5007,7 +5005,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Session-Token': user1.getSessionToken(), }) ).data.find.edges - .map((object) => object.node.someField) + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2', 'someValue3']); expect( @@ -5015,7 +5013,7 @@ describe('ParseGraphQLServer', () => { await findObjects('PublicClass', { 'X-Parse-Session-Token': user1.getSessionToken(), }) - ).data.find.edges.map((object) => object.node.someField) + ).data.find.edges.map(object => object.node.someField) ).toEqual(['someValue4']); expect( ( @@ -5023,7 +5021,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Session-Token': user2.getSessionToken(), }) ).data.find.edges - .map((object) => object.node.someField) + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2', 'someValue3']); expect( @@ -5032,7 +5030,7 @@ describe('ParseGraphQLServer', () => { 'X-Parse-Session-Token': user3.getSessionToken(), }) ).data.find.edges - .map((object) => object.node.someField) + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue3']); expect( @@ -5040,14 +5038,14 @@ describe('ParseGraphQLServer', () => { await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user4.getSessionToken(), }) - ).data.find.edges.map((object) => object.node.someField) + ).data.find.edges.map(object => object.node.someField) ).toEqual([]); expect( ( await findObjects('GraphQLClass', { 'X-Parse-Session-Token': user5.getSessionToken(), }) - ).data.find.edges.map((object) => object.node.someField) + ).data.find.edges.map(object => object.node.someField) ).toEqual(['someValue3']); }); @@ -5100,7 +5098,7 @@ describe('ParseGraphQLServer', () => { expect( result.data.graphQLClasses.edges - .map((object) => object.node.someField) + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue3']); }); @@ -5178,7 +5176,7 @@ describe('ParseGraphQLServer', () => { expect( result.data.graphQLClasses.edges - .map((object) => object.node.someField) + .map(object => object.node.someField) .sort() ).toEqual(['someValue1', 'someValue2']); }); @@ -5345,7 +5343,7 @@ describe('ParseGraphQLServer', () => { }); expect( - result.data.find.edges.map((obj) => obj.node.someField) + result.data.find.edges.map(obj => obj.node.someField) ).toEqual(['someValue14', 'someValue17']); }); @@ -5416,7 +5414,7 @@ describe('ParseGraphQLServer', () => { let result = await find(); expect( - result.data.someClasses.edges.map((edge) => edge.node.numberField) + result.data.someClasses.edges.map(edge => edge.node.numberField) ).toEqual(numberArray(0, 99)); expect(result.data.someClasses.count).toEqual(100); expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( @@ -5432,7 +5430,7 @@ describe('ParseGraphQLServer', () => { result = await find({ first: 10 }); expect( - result.data.someClasses.edges.map((edge) => edge.node.numberField) + result.data.someClasses.edges.map(edge => edge.node.numberField) ).toEqual(numberArray(0, 9)); expect(result.data.someClasses.count).toEqual(100); expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( @@ -5451,7 +5449,7 @@ describe('ParseGraphQLServer', () => { after: result.data.someClasses.pageInfo.endCursor, }); expect( - result.data.someClasses.edges.map((edge) => edge.node.numberField) + result.data.someClasses.edges.map(edge => edge.node.numberField) ).toEqual(numberArray(10, 19)); expect(result.data.someClasses.count).toEqual(100); expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( @@ -5467,7 +5465,7 @@ describe('ParseGraphQLServer', () => { result = await find({ last: 10 }); expect( - result.data.someClasses.edges.map((edge) => edge.node.numberField) + result.data.someClasses.edges.map(edge => edge.node.numberField) ).toEqual(numberArray(90, 99)); expect(result.data.someClasses.count).toEqual(100); expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( @@ -5486,7 +5484,7 @@ describe('ParseGraphQLServer', () => { before: result.data.someClasses.pageInfo.startCursor, }); expect( - result.data.someClasses.edges.map((edge) => edge.node.numberField) + result.data.someClasses.edges.map(edge => edge.node.numberField) ).toEqual(numberArray(80, 89)); expect(result.data.someClasses.count).toEqual(100); expect(result.data.someClasses.pageInfo.hasPreviousPage).toEqual( @@ -5820,7 +5818,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; expect(call.args[0].options.readPreference.mode).toBe( @@ -5877,7 +5875,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; expect(call.args[0].options.readPreference.mode).toBe( @@ -5937,7 +5935,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if (call.args[0].ns.collection.indexOf('GraphQLClass') >= 0) { foundGraphQLClassReadPreference = true; expect(call.args[0].options.readPreference.mode).toBe( @@ -6008,7 +6006,7 @@ describe('ParseGraphQLServer', () => { let foundUserClassReadPreference = false; databaseAdapter.database.serverConfig.cursor.calls .all() - .forEach((call) => { + .forEach(call => { if ( call.args[0].ns.collection.indexOf('GraphQLClass') >= 0 ) { @@ -6067,7 +6065,7 @@ describe('ParseGraphQLServer', () => { } expect( - result.data.graphQLClasses.edges.map((edge) => edge.node.objectId) + result.data.graphQLClasses.edges.map(edge => edge.node.objectId) ).toEqual([object3.id, object1.id, object2.id]); }); @@ -6120,7 +6118,7 @@ describe('ParseGraphQLServer', () => { expect( result.data.parentClass.graphQLClasses.edges.map( - (edge) => edge.node.objectId + edge => edge.node.objectId ) ).toEqual([object3.id, object1.id, object2.id]); } @@ -6384,7 +6382,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( updateObject(obj.className, obj.id, { @@ -6405,7 +6403,7 @@ describe('ParseGraphQLServer', () => { await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue1'); await Promise.all( - objects.map(async (obj) => { + objects.map(async obj => { expect( ( await updateObject( @@ -6421,7 +6419,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - objects.map(async (obj) => { + objects.map(async obj => { expect( ( await updateObject( @@ -6437,7 +6435,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - objects.map(async (obj) => { + objects.map(async obj => { expect( ( await updateObject( @@ -6453,7 +6451,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - [object1, object3, object4].map(async (obj) => { + [object1, object3, object4].map(async obj => { expect( ( await updateObject( @@ -6480,7 +6478,7 @@ describe('ParseGraphQLServer', () => { await object2.fetch({ useMasterKey: true }); expect(object2.get('someField')).toEqual(originalFieldValue); await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( updateObject( @@ -6507,7 +6505,7 @@ describe('ParseGraphQLServer', () => { await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue6'); await Promise.all( - objects.slice(0, 2).map(async (obj) => { + objects.slice(0, 2).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( updateObject( @@ -6583,7 +6581,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( updateObject(obj.className, obj.id, { @@ -6607,7 +6605,7 @@ describe('ParseGraphQLServer', () => { await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue1'); await Promise.all( - objects.map(async (obj) => { + objects.map(async obj => { expect( ( await updateObject( @@ -6626,7 +6624,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - objects.map(async (obj) => { + objects.map(async obj => { expect( ( await updateObject( @@ -6645,7 +6643,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - objects.map(async (obj) => { + objects.map(async obj => { expect( ( await updateObject( @@ -6664,7 +6662,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - [object1, object3, object4].map(async (obj) => { + [object1, object3, object4].map(async obj => { expect( ( await updateObject( @@ -6694,7 +6692,7 @@ describe('ParseGraphQLServer', () => { await object2.fetch({ useMasterKey: true }); expect(object2.get('someField')).toEqual(originalFieldValue); await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( updateObject( @@ -6724,7 +6722,7 @@ describe('ParseGraphQLServer', () => { await object4.fetch({ useMasterKey: true }); expect(object4.get('someField')).toEqual('changedValue6'); await Promise.all( - objects.slice(0, 2).map(async (obj) => { + objects.slice(0, 2).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( updateObject( @@ -6851,7 +6849,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( deleteObject(obj.className, obj.id) @@ -6861,7 +6859,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( deleteObject(obj.className, obj.id, { @@ -6952,7 +6950,7 @@ describe('ParseGraphQLServer', () => { } await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( deleteObject(obj.className, obj.id) @@ -6962,7 +6960,7 @@ describe('ParseGraphQLServer', () => { }) ); await Promise.all( - objects.slice(0, 3).map(async (obj) => { + objects.slice(0, 3).map(async obj => { const originalFieldValue = obj.get('someField'); await expectAsync( deleteObject(obj.className, obj.id, { @@ -7185,6 +7183,56 @@ describe('ParseGraphQLServer', () => { expect(resultFoo).toBeDefined(); expect(resultFoo.bar).toEqual('hello'); }); + it('should return logged user and do not by pass pointer security', async () => { + const masterKeyOnlyACL = new Parse.ACL(); + masterKeyOnlyACL.setPublicReadAccess(false); + masterKeyOnlyACL.setPublicWriteAccess(false); + const foo = new Parse.Object('Foo'); + foo.setACL(masterKeyOnlyACL); + foo.set('bar', 'hello'); + await foo.save(null, { useMasterKey: true }); + const userName = 'userx1', + password = 'user1', + email = 'emailUserx1@parse.com'; + + const user = new Parse.User(); + user.setUsername(userName); + user.setPassword(password); + user.setEmail(email); + user.set('userFoo', foo); + await user.signUp(); + + await parseGraphQLServer.parseGraphQLSchema.databaseController.schemaCache.clear(); + + const session = await Parse.Session.current(); + const result = await apolloClient.query({ + query: gql` + query GetCurrentUser { + viewer { + sessionToken + user { + id + objectId + userFoo { + bar + } + } + } + } + `, + context: { + headers: { + 'X-Parse-Session-Token': session.getSessionToken(), + }, + }, + }); + + const sessionToken = result.data.viewer.sessionToken; + const { objectId, userFoo: resultFoo } = result.data.viewer.user; + expect(objectId).toEqual(user.id); + expect(sessionToken).toBeDefined(); + expect(resultFoo).toEqual(null); + }); }); describe('Users Mutations', () => { @@ -7635,8 +7683,8 @@ describe('ParseGraphQLServer', () => { } }); - it('should accept different params', (done) => { - Parse.Cloud.define('hello', async (req) => { + it('should accept different params', done => { + Parse.Cloud.define('hello', async req => { expect(req.params.date instanceof Date).toBe(true); expect(req.params.date.getTime()).toBe(1463907600000); expect(req.params.dateList[0] instanceof Date).toBe(true); @@ -7772,7 +7820,7 @@ describe('ParseGraphQLServer', () => { ).data['__type']; expect(functionEnum.kind).toEqual('ENUM'); expect( - functionEnum.enumValues.map((value) => value.name).sort() + functionEnum.enumValues.map(value => value.name).sort() ).toEqual(['_underscored', 'a', 'b', 'contains1Number']); } catch (e) { handleError(e); @@ -7814,12 +7862,12 @@ describe('ParseGraphQLServer', () => { ).data['__type']; expect(functionEnum.kind).toEqual('ENUM'); expect( - functionEnum.enumValues.map((value) => value.name).sort() + functionEnum.enumValues.map(value => value.name).sort() ).toEqual(['a']); expect( parseGraphQLServer.parseGraphQLSchema.log.warn.calls .all() - .map((call) => call.args[0]) + .map(call => call.args[0]) .sort() ).toEqual([ 'Function 1NumberInTheBeggning could not be added to the auto schema because GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/.', @@ -8715,13 +8763,13 @@ describe('ParseGraphQLServer', () => { expect(result.name).toEqual('imACountry2'); expect(result.companies.edges.length).toEqual(3); expect( - result.companies.edges.some((o) => o.node.objectId === company.id) + result.companies.edges.some(o => o.node.objectId === company.id) ).toBeTruthy(); expect( - result.companies.edges.some((o) => o.node.name === 'imACompany2') + result.companies.edges.some(o => o.node.name === 'imACompany2') ).toBeTruthy(); expect( - result.companies.edges.some((o) => o.node.name === 'imACompany3') + result.companies.edges.some(o => o.node.name === 'imACompany3') ).toBeTruthy(); } ); @@ -8806,16 +8854,16 @@ describe('ParseGraphQLServer', () => { expect(result.companies.edges.length).toEqual(2); expect( result.companies.edges.some( - (c) => + c => c.node.name === 'imACompany2' && - c.node.teams.edges.some((t) => t.node.name === 'imATeam2') + c.node.teams.edges.some(t => t.node.name === 'imATeam2') ) ).toBeTruthy(); expect( result.companies.edges.some( - (c) => + c => c.node.name === 'imACompany3' && - c.node.teams.edges.some((t) => t.node.name === 'imATeam3') + c.node.teams.edges.some(t => t.node.name === 'imATeam3') ) ).toBeTruthy(); }); @@ -8884,17 +8932,13 @@ describe('ParseGraphQLServer', () => { expect(result.objectId).toEqual(country.id); expect(result.companies.edges.length).toEqual(2); expect( - result.companies.edges.some( - (o) => o.node.objectId === company2.id - ) + result.companies.edges.some(o => o.node.objectId === company2.id) ).toBeTruthy(); expect( - result.companies.edges.some((o) => o.node.name === 'imACompany3') + result.companies.edges.some(o => o.node.name === 'imACompany3') ).toBeTruthy(); expect( - result.companies.edges.some( - (o) => o.node.objectId === company1.id - ) + result.companies.edges.some(o => o.node.objectId === company1.id) ).toBeFalsy(); } ); @@ -8966,7 +9010,7 @@ describe('ParseGraphQLServer', () => { expect(result.name).toEqual('imACountry2'); expect(result.companies.edges.length).toEqual(1); expect( - result.companies.edges.some((o) => o.node.name === 'imACompany2') + result.companies.edges.some(o => o.node.name === 'imACompany2') ).toBeTruthy(); } ); @@ -9017,10 +9061,10 @@ describe('ParseGraphQLServer', () => { expect(result1.objectId).toEqual(country.id); expect(result1.companies.edges.length).toEqual(2); expect( - result1.companies.edges.some((o) => o.node.objectId === company1.id) + result1.companies.edges.some(o => o.node.objectId === company1.id) ).toBeTruthy(); expect( - result1.companies.edges.some((o) => o.node.objectId === company2.id) + result1.companies.edges.some(o => o.node.objectId === company2.id) ).toBeTruthy(); // With where @@ -9762,12 +9806,12 @@ describe('ParseGraphQLServer', () => { const { edges } = someClasses; expect(edges.length).toEqual(2); expect( - edges.find((result) => result.node.id === create1.someClass.id) - .node.someField + edges.find(result => result.node.id === create1.someClass.id).node + .someField ).toEqual(someFieldValue); expect( - edges.find((result) => result.node.id === create2.someClass.id) - .node.someField + edges.find(result => result.node.id === create2.someClass.id).node + .someField ).toEqual(someFieldValue2); } catch (e) { handleError(e); @@ -9859,7 +9903,7 @@ describe('ParseGraphQLServer', () => { const { someField } = getResult.data.someClass; expect(Array.isArray(someField)).toBeTruthy(); - expect(someField.map((element) => element.value)).toEqual( + expect(someField.map(element => element.value)).toEqual( someFieldValue ); expect(getResult.data.someClasses.edges.length).toEqual(1); @@ -10276,7 +10320,7 @@ describe('ParseGraphQLServer', () => { [46, 47], [48, 49], [44, 45], - ].map((point) => ({ + ].map(point => ({ latitude: point[0], longitude: point[1], })); @@ -10356,7 +10400,7 @@ describe('ParseGraphQLServer', () => { 'object' ); expect(getResult.data.someClass.somePolygonField).toEqual( - somePolygonFieldValue.map((geoPoint) => ({ + somePolygonFieldValue.map(geoPoint => ({ ...geoPoint, __typename: 'GeoPoint', })) @@ -10672,7 +10716,7 @@ describe('ParseGraphQLServer', () => { `, }); parseGraphQLServer.applyGraphQL(expressApp); - await new Promise((resolve) => + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve) ); const httpLink = createUploadLink({ @@ -10797,7 +10841,7 @@ describe('ParseGraphQLServer', () => { fields: { nameUpperCase: { type: new GraphQLNonNull(GraphQLString), - resolve: (p) => p.name.toUpperCase(), + resolve: p => p.name.toUpperCase(), }, type: { type: TypeEnum }, language: { @@ -10858,7 +10902,7 @@ describe('ParseGraphQLServer', () => { }); parseGraphQLServer.applyGraphQL(expressApp); - await new Promise((resolve) => + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve) ); const httpLink = createUploadLink({ @@ -10992,7 +11036,7 @@ describe('ParseGraphQLServer', () => { }); parseGraphQLServer.applyGraphQL(expressApp); - await new Promise((resolve) => + await new Promise(resolve => httpServer.listen({ port: 13377 }, resolve) ); const httpLink = createUploadLink({ diff --git a/src/GraphQL/loaders/usersMutations.js b/src/GraphQL/loaders/usersMutations.js index 701929677e..09d66bd90e 100644 --- a/src/GraphQL/loaders/usersMutations.js +++ b/src/GraphQL/loaders/usersMutations.js @@ -41,7 +41,7 @@ const load = parseGraphQLSchema => { const { fields } = args; const { config, auth, info } = context; - const { sessionToken } = await objectsMutations.createObject( + const { sessionToken, objectId } = await objectsMutations.createObject( '_User', fields, config, @@ -49,15 +49,14 @@ const load = parseGraphQLSchema => { info ); - info.sessionToken = sessionToken; + context.info.sessionToken = sessionToken; return { viewer: await getUserFromSessionToken( - config, - info, + context, mutationInfo, 'viewer.user.', - true + objectId ), }; } catch (e) { @@ -120,7 +119,7 @@ const load = parseGraphQLSchema => { const { fields, authData } = args; const { config, auth, info } = context; - const { sessionToken } = await objectsMutations.createObject( + const { sessionToken, objectId } = await objectsMutations.createObject( '_User', { ...fields, authData }, config, @@ -128,15 +127,14 @@ const load = parseGraphQLSchema => { info ); - info.sessionToken = sessionToken; + context.info.sessionToken = sessionToken; return { viewer: await getUserFromSessionToken( - config, - info, + context, mutationInfo, 'viewer.user.', - true + objectId ), }; } catch (e) { @@ -183,7 +181,7 @@ const load = parseGraphQLSchema => { const { username, password } = args; const { config, auth, info } = context; - const { sessionToken } = ( + const { sessionToken, objectId } = ( await usersRouter.handleLogIn({ body: { username, @@ -196,15 +194,14 @@ const load = parseGraphQLSchema => { }) ).response; - info.sessionToken = sessionToken; + context.info.sessionToken = sessionToken; return { viewer: await getUserFromSessionToken( - config, - info, + context, mutationInfo, 'viewer.user.', - true + objectId ), }; } catch (e) { @@ -236,11 +233,10 @@ const load = parseGraphQLSchema => { const { config, auth, info } = context; const viewer = await getUserFromSessionToken( - config, - info, + context, mutationInfo, 'viewer.user.', - true + auth.user.id ); await usersRouter.handleLogOut({ diff --git a/src/GraphQL/loaders/usersQueries.js b/src/GraphQL/loaders/usersQueries.js index 6a1d3ea945..976d0b3b02 100644 --- a/src/GraphQL/loaders/usersQueries.js +++ b/src/GraphQL/loaders/usersQueries.js @@ -2,16 +2,16 @@ import { GraphQLNonNull } from 'graphql'; import getFieldNames from 'graphql-list-fields'; import Parse from 'parse/node'; import rest from '../../rest'; -import Auth from '../../Auth'; import { extractKeysAndInclude } from './parseClassTypes'; +import { Auth } from '../../Auth'; const getUserFromSessionToken = async ( - config, - info, + context, queryInfo, keysPrefix, - validatedToken + userId ) => { + const { info, config } = context; if (!info || !info.sessionToken) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, @@ -27,7 +27,7 @@ const getUserFromSessionToken = async ( const { keys } = keysAndInclude; let { include } = keysAndInclude; - if (validatedToken && !keys && !include) { + if (userId && !keys && !include) { return { sessionToken, }; @@ -35,40 +35,47 @@ const getUserFromSessionToken = async ( include = 'user'; } + if (userId) { + // We need to re create the auth context + // to avoid security breach if userId is provided + context.auth = new Auth({ + config, + isMaster: context.auth.isMaster, + user: { id: userId }, + }); + } + const options = {}; if (keys) { options.keys = keys .split(',') - .map(key => `user.${key}`) + .map(key => `${key}`) .join(','); } if (include) { options.include = include .split(',') - .map(included => `user.${included}`) + .map(included => `${included}`) .join(','); } const response = await rest.find( config, - Auth.master(config), - '_Session', - { sessionToken }, + context.auth, + '_User', + // Get the user it self from auth object + { objectId: context.auth.user.id }, options, info.clientVersion, - info.context, + info.context ); - if ( - !response.results || - response.results.length == 0 || - !response.results[0].user - ) { + if (!response.results || response.results.length == 0) { throw new Parse.Error( Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token' ); } else { - const user = response.results[0].user; + const user = response.results[0]; return { sessionToken, user, @@ -89,10 +96,8 @@ const load = parseGraphQLSchema => { type: new GraphQLNonNull(parseGraphQLSchema.viewerType), async resolve(_source, _args, context, queryInfo) { try { - const { config, info } = context; return await getUserFromSessionToken( - config, - info, + context, queryInfo, 'user.', false From d69833332c5eba0d6ea44b90d6ca435b7614c500 Mon Sep 17 00:00:00 2001 From: mess-lelouch Date: Fri, 17 Jul 2020 14:14:43 -0400 Subject: [PATCH 13/33] Optimizing pointer CLP query decoration done by DatabaseController#addPointerPermissions (#6747) * Optimize CLP pointer query * remove console log * Update changelog * Fix flow type checker issues * Remove unused properties * Fix typo, add one more test case for coverage * Add support for CLP entry of type Object Co-authored-by: Musa Yassin-Fort Co-authored-by: Diamond Lewis --- CHANGELOG.md | 1 + spec/DatabaseController.spec.js | 274 +++++++++++++++++++++++++- src/Controllers/DatabaseController.js | 43 ++-- 3 files changed, 299 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee55a1936..24206b79ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/4.2.0...master) +- FIX: Optimize query decoration based on pointer CLPs by looking at the class schema to determine the field's type. - NEW (EXPERIMENTAL): Idempotency enforcement for client requests. This deduplicates requests where the client intends to send one request to Parse Server but due to network issues the server receives the request multiple times. **Caution, this is an experimental feature that may not be appropriate for production.** [#6744](https://github.com/parse-community/parse-server/issues/6744). Thanks to [Manuel Trezza](https://github.com/mtrezza). ### 4.2.0 diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index af373e64f2..e2df0f1e95 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -1,9 +1,9 @@ const DatabaseController = require('../lib/Controllers/DatabaseController.js'); const validateQuery = DatabaseController._validateQuery; -describe('DatabaseController', function() { - describe('validateQuery', function() { - it('should not restructure simple cases of SERVER-13732', done => { +describe('DatabaseController', function () { + describe('validateQuery', function () { + it('should not restructure simple cases of SERVER-13732', (done) => { const query = { $or: [{ a: 1 }, { a: 2 }], _rperm: { $in: ['a', 'b'] }, @@ -18,7 +18,7 @@ describe('DatabaseController', function() { done(); }); - it('should not restructure SERVER-13732 queries with $nears', done => { + it('should not restructure SERVER-13732 queries with $nears', (done) => { let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } }; validateQuery(query); expect(query).toEqual({ @@ -31,7 +31,7 @@ describe('DatabaseController', function() { done(); }); - it('should not push refactored keys down a tree for SERVER-13732', done => { + it('should not push refactored keys down a tree for SERVER-13732', (done) => { const query = { a: 1, $or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }], @@ -45,14 +45,274 @@ describe('DatabaseController', function() { done(); }); - it('should reject invalid queries', done => { + it('should reject invalid queries', (done) => { expect(() => validateQuery({ $or: { a: 1 } })).toThrow(); done(); }); - it('should accept valid queries', done => { + it('should accept valid queries', (done) => { expect(() => validateQuery({ $or: [{ a: 1 }, { b: 2 }] })).not.toThrow(); done(); }); }); + + describe('addPointerPermissions', function () { + const CLASS_NAME = 'Foo'; + const USER_ID = 'userId'; + const ACL_GROUP = [USER_ID]; + const OPERATION = 'find'; + + const databaseController = new DatabaseController(); + const schemaController = jasmine.createSpyObj('SchemaController', [ + 'testPermissionsForClassName', + 'getClassLevelPermissions', + 'getExpectedType', + ]); + + it('should not decorate query if no pointer CLPs are present', (done) => { + const clp = buildCLP(); + const query = { a: 'b' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(true); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + + const output = databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + + expect(output).toEqual({ ...query }); + + done(); + }); + + it('should decorate query if a pointer CLP entry is present', (done) => { + const clp = buildCLP(['user']); + const query = { a: 'b' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(false); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'user') + .and.returnValue({ type: 'Pointer' }); + + const output = databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + + expect(output).toEqual({ ...query, user: createUserPointer(USER_ID) }); + + done(); + }); + + it('should decorate query if an array CLP entry is present', (done) => { + const clp = buildCLP(['users']); + const query = { a: 'b' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(false); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'users') + .and.returnValue({ type: 'Array' }); + + const output = databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + + expect(output).toEqual({ + ...query, + users: { $all: [createUserPointer(USER_ID)] }, + }); + + done(); + }); + + it('should decorate query if an object CLP entry is present', (done) => { + const clp = buildCLP(['user']); + const query = { a: 'b' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(false); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'user') + .and.returnValue({ type: 'Object' }); + + const output = databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + + expect(output).toEqual({ + ...query, + user: createUserPointer(USER_ID), + }); + + done(); + }); + + it('should decorate query if a pointer CLP is present and the same field is part of the query', (done) => { + const clp = buildCLP(['user']); + const query = { a: 'b', user: 'a' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(false); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'user') + .and.returnValue({ type: 'Pointer' }); + + const output = databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + + expect(output).toEqual({ + $and: [{ user: createUserPointer(USER_ID) }, { ...query }], + }); + + done(); + }); + + it('should transform the query to an $or query if multiple array/pointer CLPs are present', (done) => { + const clp = buildCLP(['user', 'users', 'userObject']); + const query = { a: 'b' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(false); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'user') + .and.returnValue({ type: 'Pointer' }); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'users') + .and.returnValue({ type: 'Array' }); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'userObject') + .and.returnValue({ type: 'Object' }); + + const output = databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + + expect(output).toEqual({ + $or: [ + { ...query, user: createUserPointer(USER_ID) }, + { ...query, users: { $all: [createUserPointer(USER_ID)] } }, + { ...query, userObject: createUserPointer(USER_ID) }, + ], + }); + + done(); + }); + + it('should throw an error if for some unexpected reason the property specified in the CLP is neither a pointer nor an array', (done) => { + const clp = buildCLP(['user']); + const query = { a: 'b' }; + + schemaController.testPermissionsForClassName + .withArgs(CLASS_NAME, ACL_GROUP, OPERATION) + .and.returnValue(false); + schemaController.getClassLevelPermissions + .withArgs(CLASS_NAME) + .and.returnValue(clp); + schemaController.getExpectedType + .withArgs(CLASS_NAME, 'user') + .and.returnValue({ type: 'Number' }); + + expect(() => { + databaseController.addPointerPermissions( + schemaController, + CLASS_NAME, + OPERATION, + query, + ACL_GROUP + ); + }).toThrow( + Error( + `An unexpected condition occurred when resolving pointer permissions: ${CLASS_NAME} user` + ) + ); + + done(); + }); + }); }); + +function buildCLP(pointerNames) { + const OPERATIONS = [ + 'count', + 'find', + 'get', + 'create', + 'update', + 'delete', + 'addField', + ]; + + const clp = OPERATIONS.reduce((acc, op) => { + acc[op] = {}; + + if (pointerNames && pointerNames.length) { + acc[op].pointerFields = pointerNames; + } + + return acc; + }, {}); + + clp.protectedFields = {}; + clp.writeUserFields = []; + clp.readUserFields = []; + + return clp; +} + +function createUserPointer(userId) { + return { + __type: 'Pointer', + className: '_User', + objectId: userId, + }; +} diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 657b40db8a..7db5b52cb3 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1567,23 +1567,42 @@ class DatabaseController { objectId: userId, }; - const ors = permFields.flatMap(key => { - // constraint for single pointer setup - const q = { - [key]: userPointer, - }; - // constraint for users-array setup - const qa = { - [key]: { $all: [userPointer] }, - }; + const queries = permFields.map((key) => { + const fieldDescriptor = schema.getExpectedType(className, key); + const fieldType = + fieldDescriptor && + typeof fieldDescriptor === 'object' && + Object.prototype.hasOwnProperty.call(fieldDescriptor, 'type') + ? fieldDescriptor.type + : null; + + let queryClause; + + if (fieldType === 'Pointer') { + // constraint for single pointer setup + queryClause = { [key]: userPointer }; + } else if (fieldType === 'Array') { + // constraint for users-array setup + queryClause = { [key]: { $all: [userPointer] } }; + } else if (fieldType === 'Object') { + // constraint for object setup + queryClause = { [key]: userPointer }; + } else { + // This means that there is a CLP field of an unexpected type. This condition should not happen, which is + // why is being treated as an error. + throw Error( + `An unexpected condition occurred when resolving pointer permissions: ${className} ${key}` + ); + } // if we already have a constraint on the key, use the $and if (Object.prototype.hasOwnProperty.call(query, key)) { - return [{ $and: [q, query] }, { $and: [qa, query] }]; + return { $and: [queryClause, query] }; } // otherwise just add the constaint - return [Object.assign({}, query, q), Object.assign({}, query, qa)]; + return Object.assign({}, query, queryClause); }); - return { $or: ors }; + + return queries.length === 1 ? queries[0] : { $or: queries }; } else { return query; } From 6f060e090934207724e8814faf1baca09057ea2b Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 19 Jul 2020 10:37:36 -0700 Subject: [PATCH 14/33] Release 4.3.0 (#6811) * Release version 4.3.0 * Update CHANGELOG.md Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> * Update CHANGELOG.md Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> Co-authored-by: Tom Fox <13188249+TomWFox@users.noreply.github.com> --- CHANGELOG.md | 43 ++++++++++++- package-lock.json | 2 +- package.json | 2 +- spec/DatabaseController.spec.js | 24 ++++---- src/Controllers/DatabaseController.js | 87 +++++++++++++++------------ 5 files changed, 101 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24206b79ca..5146e84218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,46 @@ ## Parse Server Changelog ### master -[Full Changelog](https://github.com/parse-community/parse-server/compare/4.2.0...master) -- FIX: Optimize query decoration based on pointer CLPs by looking at the class schema to determine the field's type. -- NEW (EXPERIMENTAL): Idempotency enforcement for client requests. This deduplicates requests where the client intends to send one request to Parse Server but due to network issues the server receives the request multiple times. **Caution, this is an experimental feature that may not be appropriate for production.** [#6744](https://github.com/parse-community/parse-server/issues/6744). Thanks to [Manuel Trezza](https://github.com/mtrezza). +[Full Changelog](https://github.com/parse-community/parse-server/compare/4.3.0...master) + +### 4.3.0 +[Full Changelog](https://github.com/parse-community/parse-server/compare/4.2.0...4.3.0) +- PERFORMANCE: Optimizing pointer CLP query decoration done by DatabaseController#addPointerPermissions [#6747](https://github.com/parse-community/parse-server/pull/6747). Thanks to [mess-lelouch](https://github.com/mess-lelouch). +- SECURITY: Fix security breach on GraphQL viewer. Thanks to [Antoine Cormouls](https://github.com/Moumouls). +- FIX: Save context not present if direct access enabled [#6764](https://github.com/parse-community/parse-server/pull/6764). Thanks to [Omair Vaiyani](https://github.com/omairvaiyani). +- NEW: Before Connect + Before Subscribe [#6793](https://github.com/parse-community/parse-server/pull/6793). Thanks to [dblythy](https://github.com/dblythy). +- FIX: Add version to playground to fix CDN [#6804](https://github.com/parse-community/parse-server/pull/6804). Thanks to [Antoine Cormouls](https://github.com/Moumouls). +- NEW (EXPERIMENTAL): Idempotency enforcement for client requests. This deduplicates requests where the client intends to send one request to Parse Server but due to network issues the server receives the request multiple times. **Caution, this is an experimental feature that may not be appropriate for production.** [#6748](https://github.com/parse-community/parse-server/issues/6748). Thanks to [Manuel Trezza](https://github.com/mtrezza). +- FIX: Add production Google Auth Adapter instead of using the development url [#6734](https://github.com/parse-community/parse-server/pull/6734). Thanks to [SebC.](https://github.com/SebC99). +- IMPROVE: Run Prettier JS Again Without requiring () on arrow functions [#6796](https://github.com/parse-community/parse-server/pull/6796). Thanks to [Diamond Lewis](https://github.com/dplewis). +- IMPROVE: Run Prettier JS [#6795](https://github.com/parse-community/parse-server/pull/6795). Thanks to [Diamond Lewis](https://github.com/dplewis). +- IMPROVE: Replace bcrypt with @node-rs/bcrypt [#6794](https://github.com/parse-community/parse-server/pull/6794). Thanks to [LongYinan](https://github.com/Brooooooklyn). +- IMPROVE: Make clear description of anonymous user [#6655](https://github.com/parse-community/parse-server/pull/6655). Thanks to [Jerome De Leon](https://github.com/JeromeDeLeon). +- IMPROVE: Simplify GraphQL merge system to avoid js ref bugs [#6791](https://github.com/parse-community/parse-server/pull/6791). Thanks to [Manuel](https://github.com/mtrezza). +- NEW: Pass context in beforeDelete, afterDelete, beforeFind and Parse.Cloud.run [#6666](https://github.com/parse-community/parse-server/pull/6666). Thanks to [yog27ray](https://github.com/yog27ray). +- NEW: Allow passing custom gql schema function to ParseServer#start options [#6762](https://github.com/parse-community/parse-server/pull/6762). Thanks to [Luca](https://github.com/lucatk). +- NEW: Allow custom cors origin header [#6772](https://github.com/parse-community/parse-server/pull/6772). Thanks to [Kevin Yao](https://github.com/kzmeyao). +- FIX: Fix context for cascade-saving and saving existing object [#6735](https://github.com/parse-community/parse-server/pull/6735). Thanks to [Manuel](https://github.com/mtrezza). +- NEW: Add file bucket encryption using fileKey [#6765](https://github.com/parse-community/parse-server/pull/6765). Thanks to [Corey Baker](https://github.com/cbaker6). +- FIX: Removed gaze from dev dependencies and removed not working dev script [#6745](https://github.com/parse-community/parse-server/pull/6745). Thanks to [Vincent Semrau](https://github.com/vince1995). +- IMPROVE: Upgrade graphql-tools to v6 [#6701](https://github.com/parse-community/parse-server/pull/6701). Thanks to [Yaacov Rydzinski](https://github.com/yaacovCR). +- NEW: Support Metadata in GridFSAdapter [#6660](https://github.com/parse-community/parse-server/pull/6660). Thanks to [Diamond Lewis](https://github.com/dplewis). +- NEW: Allow to unset file from graphql [#6651](https://github.com/parse-community/parse-server/pull/6651). Thanks to [Antoine Cormouls](https://github.com/Moumouls). +- NEW: Handle shutdown for RedisCacheAdapter [#6658](https://github.com/parse-community/parse-server/pull/6658). Thanks to [promisenxu](https://github.com/promisenxu). +- FIX: Fix explain on user class [#6650](https://github.com/parse-community/parse-server/pull/6650). Thanks to [Manuel](https://github.com/mtrezza). +- FIX: Fix read preference for aggregate [#6585](https://github.com/parse-community/parse-server/pull/6585). Thanks to [Manuel](https://github.com/mtrezza). +- NEW: Add context to Parse.Object.save [#6626](https://github.com/parse-community/parse-server/pull/6626). Thanks to [Manuel](https://github.com/mtrezza). +- NEW: Adding ssl config params to Postgres URI [#6580](https://github.com/parse-community/parse-server/pull/6580). Thanks to [Corey Baker](https://github.com/cbaker6). +- FIX: Travis postgres update: removing unnecessary start of mongo-runner [#6594](https://github.com/parse-community/parse-server/pull/6594). Thanks to [Corey Baker](https://github.com/cbaker6). +- FIX: ObjectId size for Pointer in Postgres [#6619](https://github.com/parse-community/parse-server/pull/6619). Thanks to [Corey Baker](https://github.com/cbaker6). +- IMPROVE: Improve a test case [#6629](https://github.com/parse-community/parse-server/pull/6629). Thanks to [Gordon Sun](https://github.com/sunshineo). +- NEW: Allow to resolve automatically Parse Type fields from Custom Schema [#6562](https://github.com/parse-community/parse-server/pull/6562). Thanks to [Antoine Cormouls](https://github.com/Moumouls). +- FIX: Remove wrong console log in test [#6627](https://github.com/parse-community/parse-server/pull/6627). Thanks to [Gordon Sun](https://github.com/sunshineo). +- IMPROVE: Graphql tools v5 [#6611](https://github.com/parse-community/parse-server/pull/6611). Thanks to [Yaacov Rydzinski](https://github.com/yaacovCR). +- FIX: Catch JSON.parse and return 403 properly [#6589](https://github.com/parse-community/parse-server/pull/6589). Thanks to [Gordon Sun](https://github.com/sunshineo). +- PERFORMANCE: Allow covering relation queries with minimal index [#6581](https://github.com/parse-community/parse-server/pull/6581). Thanks to [Noah Silas](https://github.com/noahsilas). +- FIX: Fix Postgres group aggregation [#6522](https://github.com/parse-community/parse-server/pull/6522). Thanks to [Siddharth Ramesh](https://github.com/srameshr). +- NEW: Allow set user mapped from JWT directly on request [#6411](https://github.com/parse-community/parse-server/pull/6411). Thanks to [Gordon Sun](https://github.com/sunshineo). ### 4.2.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.1.0...4.2.0) diff --git a/package-lock.json b/package-lock.json index c186e9f836..87259fd17a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "4.2.0", + "version": "4.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index eb572d3c75..519b528d22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "4.2.0", + "version": "4.3.0", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index e2df0f1e95..77cd6d035a 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -3,7 +3,7 @@ const validateQuery = DatabaseController._validateQuery; describe('DatabaseController', function () { describe('validateQuery', function () { - it('should not restructure simple cases of SERVER-13732', (done) => { + it('should not restructure simple cases of SERVER-13732', done => { const query = { $or: [{ a: 1 }, { a: 2 }], _rperm: { $in: ['a', 'b'] }, @@ -18,7 +18,7 @@ describe('DatabaseController', function () { done(); }); - it('should not restructure SERVER-13732 queries with $nears', (done) => { + it('should not restructure SERVER-13732 queries with $nears', done => { let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } }; validateQuery(query); expect(query).toEqual({ @@ -31,7 +31,7 @@ describe('DatabaseController', function () { done(); }); - it('should not push refactored keys down a tree for SERVER-13732', (done) => { + it('should not push refactored keys down a tree for SERVER-13732', done => { const query = { a: 1, $or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }], @@ -45,12 +45,12 @@ describe('DatabaseController', function () { done(); }); - it('should reject invalid queries', (done) => { + it('should reject invalid queries', done => { expect(() => validateQuery({ $or: { a: 1 } })).toThrow(); done(); }); - it('should accept valid queries', (done) => { + it('should accept valid queries', done => { expect(() => validateQuery({ $or: [{ a: 1 }, { b: 2 }] })).not.toThrow(); done(); }); @@ -69,7 +69,7 @@ describe('DatabaseController', function () { 'getExpectedType', ]); - it('should not decorate query if no pointer CLPs are present', (done) => { + it('should not decorate query if no pointer CLPs are present', done => { const clp = buildCLP(); const query = { a: 'b' }; @@ -93,7 +93,7 @@ describe('DatabaseController', function () { done(); }); - it('should decorate query if a pointer CLP entry is present', (done) => { + it('should decorate query if a pointer CLP entry is present', done => { const clp = buildCLP(['user']); const query = { a: 'b' }; @@ -120,7 +120,7 @@ describe('DatabaseController', function () { done(); }); - it('should decorate query if an array CLP entry is present', (done) => { + it('should decorate query if an array CLP entry is present', done => { const clp = buildCLP(['users']); const query = { a: 'b' }; @@ -150,7 +150,7 @@ describe('DatabaseController', function () { done(); }); - it('should decorate query if an object CLP entry is present', (done) => { + it('should decorate query if an object CLP entry is present', done => { const clp = buildCLP(['user']); const query = { a: 'b' }; @@ -180,7 +180,7 @@ describe('DatabaseController', function () { done(); }); - it('should decorate query if a pointer CLP is present and the same field is part of the query', (done) => { + it('should decorate query if a pointer CLP is present and the same field is part of the query', done => { const clp = buildCLP(['user']); const query = { a: 'b', user: 'a' }; @@ -209,7 +209,7 @@ describe('DatabaseController', function () { done(); }); - it('should transform the query to an $or query if multiple array/pointer CLPs are present', (done) => { + it('should transform the query to an $or query if multiple array/pointer CLPs are present', done => { const clp = buildCLP(['user', 'users', 'userObject']); const query = { a: 'b' }; @@ -248,7 +248,7 @@ describe('DatabaseController', function () { done(); }); - it('should throw an error if for some unexpected reason the property specified in the CLP is neither a pointer nor an array', (done) => { + it('should throw an error if for some unexpected reason the property specified in the CLP is neither a pointer nor an array', done => { const clp = buildCLP(['user']); const query = { a: 'b' }; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 7db5b52cb3..334494b1b5 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1567,7 +1567,7 @@ class DatabaseController { objectId: userId, }; - const queries = permFields.map((key) => { + const queries = permFields.map(key => { const fieldDescriptor = schema.getExpectedType(className, key); const fieldType = fieldDescriptor && @@ -1769,9 +1769,12 @@ class DatabaseController { const roleClassPromise = this.loadSchema().then(schema => schema.enforceClassExists('_Role') ); - const idempotencyClassPromise = this.adapter instanceof MongoStorageAdapter - ? this.loadSchema().then((schema) => schema.enforceClassExists('_Idempotency')) - : Promise.resolve(); + const idempotencyClassPromise = + this.adapter instanceof MongoStorageAdapter + ? this.loadSchema().then(schema => + schema.enforceClassExists('_Idempotency') + ) + : Promise.resolve(); const usernameUniqueness = userClassPromise .then(() => @@ -1836,42 +1839,46 @@ class DatabaseController { throw error; }); - const idempotencyRequestIdIndex = this.adapter instanceof MongoStorageAdapter - ? idempotencyClassPromise - .then(() => - this.adapter.ensureUniqueness( - '_Idempotency', - requiredIdempotencyFields, - ['reqId'] - )) - .catch((error) => { - logger.warn( - 'Unable to ensure uniqueness for idempotency request ID: ', - error - ); - throw error; - }) - : Promise.resolve(); - - const idempotencyExpireIndex = this.adapter instanceof MongoStorageAdapter - ? idempotencyClassPromise - .then(() => - this.adapter.ensureIndex( - '_Idempotency', - requiredIdempotencyFields, - ['expire'], - 'ttl', - false, - { ttl: 0 }, - )) - .catch((error) => { - logger.warn( - 'Unable to create TTL index for idempotency expire date: ', - error - ); - throw error; - }) - : Promise.resolve(); + const idempotencyRequestIdIndex = + this.adapter instanceof MongoStorageAdapter + ? idempotencyClassPromise + .then(() => + this.adapter.ensureUniqueness( + '_Idempotency', + requiredIdempotencyFields, + ['reqId'] + ) + ) + .catch(error => { + logger.warn( + 'Unable to ensure uniqueness for idempotency request ID: ', + error + ); + throw error; + }) + : Promise.resolve(); + + const idempotencyExpireIndex = + this.adapter instanceof MongoStorageAdapter + ? idempotencyClassPromise + .then(() => + this.adapter.ensureIndex( + '_Idempotency', + requiredIdempotencyFields, + ['expire'], + 'ttl', + false, + { ttl: 0 } + ) + ) + .catch(error => { + logger.warn( + 'Unable to create TTL index for idempotency expire date: ', + error + ); + throw error; + }) + : Promise.resolve(); const indexPromise = this.adapter.updateSchemaWithIndexes(); From 3654f0280b1bd54d36118082461e9e92bdc0d9bb Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Sun, 19 Jul 2020 15:23:15 -0700 Subject: [PATCH 15/33] Fix message on 4.3.0 release (#6813) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5146e84218..280a4995b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - IMPROVE: Run Prettier JS [#6795](https://github.com/parse-community/parse-server/pull/6795). Thanks to [Diamond Lewis](https://github.com/dplewis). - IMPROVE: Replace bcrypt with @node-rs/bcrypt [#6794](https://github.com/parse-community/parse-server/pull/6794). Thanks to [LongYinan](https://github.com/Brooooooklyn). - IMPROVE: Make clear description of anonymous user [#6655](https://github.com/parse-community/parse-server/pull/6655). Thanks to [Jerome De Leon](https://github.com/JeromeDeLeon). -- IMPROVE: Simplify GraphQL merge system to avoid js ref bugs [#6791](https://github.com/parse-community/parse-server/pull/6791). Thanks to [Manuel](https://github.com/mtrezza). +- IMPROVE: Simplify GraphQL merge system to avoid js ref bugs [#6791](https://github.com/parse-community/parse-server/pull/6791). Thanks to [Antoine Cormouls](https://github.com/Moumouls). - NEW: Pass context in beforeDelete, afterDelete, beforeFind and Parse.Cloud.run [#6666](https://github.com/parse-community/parse-server/pull/6666). Thanks to [yog27ray](https://github.com/yog27ray). - NEW: Allow passing custom gql schema function to ParseServer#start options [#6762](https://github.com/parse-community/parse-server/pull/6762). Thanks to [Luca](https://github.com/lucatk). - NEW: Allow custom cors origin header [#6772](https://github.com/parse-community/parse-server/pull/6772). Thanks to [Kevin Yao](https://github.com/kzmeyao). From d652517bbed5126e7ea013093262f06fab193390 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Tue, 21 Jul 2020 04:34:31 +0100 Subject: [PATCH 16/33] fix: upgrade graphql from 15.1.0 to 15.2.0 (#6818) Snyk has created this PR to upgrade graphql from 15.1.0 to 15.2.0. See this package in npm: https://www.npmjs.com/package/graphql See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87259fd17a..9a43ccfc9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6961,9 +6961,9 @@ "dev": true }, "graphql": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.1.0.tgz", - "integrity": "sha512-0TVyfOlCGhv/DBczQkJmwXOK6fjWkjzY3Pt7wY8i0gcYXq8aogG3weCsg48m72lywKSeOqedEHvVPOvZvSD51Q==" + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.2.0.tgz", + "integrity": "sha512-tsceRyHfgzZo+ee0YK3o8f0CR0cXAXxRlxoORWFo/CoM1bVy3UXGWeyzBcf+Y6oqPvO27BDmOEVATcunOO/MrQ==" }, "graphql-extensions": { "version": "0.12.4", diff --git a/package.json b/package.json index 519b528d22..81fc02a3da 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "deepcopy": "2.0.0", "express": "4.17.1", "follow-redirects": "1.12.1", - "graphql": "15.1.0", + "graphql": "15.2.0", "graphql-list-fields": "2.0.2", "graphql-relay": "0.6.0", "graphql-upload": "11.0.0", From 0236efd113e5faad088cf739071628f74810c18b Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 22 Jul 2020 04:36:14 +0100 Subject: [PATCH 17/33] fix: upgrade apollo-server-express from 2.15.0 to 2.15.1 (#6819) Snyk has created this PR to upgrade apollo-server-express from 2.15.0 to 2.15.1. See this package in npm: https://www.npmjs.com/package/apollo-server-express See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 32 ++++++++++++++++---------------- package.json | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9a43ccfc9c..4cafaacfdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3559,12 +3559,12 @@ } }, "apollo-engine-reporting": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-2.2.1.tgz", - "integrity": "sha512-HPwf70p4VbxKEagHYWTwldqfYNekBE33BXcryHI9owxMm5B8/vutQfx67+4Bf351kOpndCG9I91aOiFBfC2/iQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-2.3.0.tgz", + "integrity": "sha512-SbcPLFuUZcRqDEZ6mSs8uHM9Ftr8yyt2IEu0JA8c3LNBmYXSLM7MHqFe80SVcosYSTBgtMz8mLJO8orhYoSYZw==", "requires": { "apollo-engine-reporting-protobuf": "^0.5.2", - "apollo-graphql": "^0.4.0", + "apollo-graphql": "^0.5.0", "apollo-server-caching": "^0.5.2", "apollo-server-env": "^2.4.5", "apollo-server-errors": "^2.4.2", @@ -3594,9 +3594,9 @@ } }, "apollo-graphql": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.4.5.tgz", - "integrity": "sha512-0qa7UOoq7E71kBYE7idi6mNQhHLVdMEDInWk6TNw3KsSWZE2/I68gARP84Mj+paFTO5NYuw1Dht66PVX76Cc2w==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.5.0.tgz", + "integrity": "sha512-YSdF/BKPbsnQpxWpmCE53pBJX44aaoif31Y22I/qKpB6ZSGzYijV5YBoCL5Q15H2oA/v/02Oazh9lbp4ek3eig==", "requires": { "apollo-env": "^0.6.5", "lodash.sortby": "^4.7.0" @@ -3667,9 +3667,9 @@ } }, "apollo-server-core": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.15.1.tgz", - "integrity": "sha512-ZRSK3uVPS6YkIV3brm2CjzVphg6NHY0PRhFojZD8BjoQlGo3+pPRP1IHFDvC3UzybGWfyCelcfF4YiVqh4GJHw==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.16.0.tgz", + "integrity": "sha512-mnvg2cPvsQtjFXIqIhEAbPqGyiSXDSbiBgNQ8rY8g7r2eRMhHKZePqGF03gP1/w87yVaSDRAZBDk6o+jiBXjVQ==", "requires": { "@apollographql/apollo-tools": "^0.4.3", "@apollographql/graphql-playground-html": "1.6.26", @@ -3677,7 +3677,7 @@ "@types/ws": "^7.0.0", "apollo-cache-control": "^0.11.1", "apollo-datasource": "^0.7.2", - "apollo-engine-reporting": "^2.2.1", + "apollo-engine-reporting": "^2.3.0", "apollo-server-caching": "^0.5.2", "apollo-server-env": "^2.4.5", "apollo-server-errors": "^2.4.2", @@ -3731,9 +3731,9 @@ "integrity": "sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ==" }, "apollo-server-express": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.15.0.tgz", - "integrity": "sha512-ECptVIrOVW2cmMWvqtpkZfyZrQL8yTSgbVvP4M8qcPV/3XxDJa6444zy7vxqN7lyYl8IJAsg/IwC0vodoXe//A==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.15.1.tgz", + "integrity": "sha512-anNb9HJo+KTpgvUqiPOjEl4wPq8y8NmWaIUz/QqPZlhIEDdf7wd/kQo3Sdbov++7J9JNJx6Ownnvw+wxfogUgA==", "requires": { "@apollographql/graphql-playground-html": "1.6.26", "@types/accepts": "^1.3.5", @@ -3741,8 +3741,8 @@ "@types/cors": "^2.8.4", "@types/express": "4.17.4", "accepts": "^1.3.5", - "apollo-server-core": "^2.15.0", - "apollo-server-types": "^0.5.0", + "apollo-server-core": "^2.15.1", + "apollo-server-types": "^0.5.1", "body-parser": "^1.18.3", "cors": "^2.8.4", "express": "^4.17.1", diff --git a/package.json b/package.json index 81fc02a3da..681645f1c4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@parse/push-adapter": "3.2.0", "@parse/s3-files-adapter": "1.4.0", "@parse/simple-mailgun-adapter": "1.1.0", - "apollo-server-express": "2.15.0", + "apollo-server-express": "2.15.1", "bcryptjs": "2.4.3", "body-parser": "1.19.0", "commander": "5.1.0", From a6597ac6691931c2b64683a3e8616858aa1b58b9 Mon Sep 17 00:00:00 2001 From: Antonio Davi Macedo Coelho de Castro Date: Wed, 22 Jul 2020 14:46:17 -0700 Subject: [PATCH 18/33] Fix NPM publish process (#6821) --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 681645f1c4..ff719f2be2 100644 --- a/package.json +++ b/package.json @@ -122,9 +122,6 @@ "url": "https://opencollective.com/parse-server", "logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary" }, - "publishConfig": { - "registry": "https://npm.pkg.github.com/" - }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parse-server" From 97f2456a28860c62391f873e609441d9a95bfc5c Mon Sep 17 00:00:00 2001 From: Tom Fox <13188249+TomWFox@users.noreply.github.com> Date: Thu, 23 Jul 2020 16:58:56 +0100 Subject: [PATCH 19/33] add details of security fix (#6822) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 280a4995b3..e5ed371030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ ### 4.3.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.2.0...4.3.0) - PERFORMANCE: Optimizing pointer CLP query decoration done by DatabaseController#addPointerPermissions [#6747](https://github.com/parse-community/parse-server/pull/6747). Thanks to [mess-lelouch](https://github.com/mess-lelouch). -- SECURITY: Fix security breach on GraphQL viewer. Thanks to [Antoine Cormouls](https://github.com/Moumouls). +- SECURITY: Fix security breach on GraphQL viewer [78239ac](https://github.com/parse-community/parse-server/commit/78239ac9071167fdf243c55ae4bc9a2c0b0d89aa), [secuity advisory](https://github.com/parse-community/parse-server/security/advisories/GHSA-236h-rqv8-8q73). Thanks to [Antoine Cormouls](https://github.com/Moumouls). - FIX: Save context not present if direct access enabled [#6764](https://github.com/parse-community/parse-server/pull/6764). Thanks to [Omair Vaiyani](https://github.com/omairvaiyani). - NEW: Before Connect + Before Subscribe [#6793](https://github.com/parse-community/parse-server/pull/6793). Thanks to [dblythy](https://github.com/dblythy). - FIX: Add version to playground to fix CDN [#6804](https://github.com/parse-community/parse-server/pull/6804). Thanks to [Antoine Cormouls](https://github.com/Moumouls). From dd41bf17394013bbd961179f1eb7ac75296594f1 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Fri, 24 Jul 2020 15:42:13 +0100 Subject: [PATCH 20/33] fix: upgrade @graphql-tools/utils from 6.0.11 to 6.0.12 (#6825) Snyk has created this PR to upgrade @graphql-tools/utils from 6.0.11 to 6.0.12. See this package in npm: https://www.npmjs.com/package/@graphql-tools/utils See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4cafaacfdd..2e5b06ba42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2827,9 +2827,9 @@ } }, "@graphql-tools/utils": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", - "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.12.tgz", + "integrity": "sha512-MuFSkxXCe2QoD5QJPJ/1WIm0YnBzzXpkq9d/XznVAWptHFRwtwIbZ1xcREjYquFvoZ7ddsjZfyvUN/5ulmHhhg==", "requires": { "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" diff --git a/package.json b/package.json index ff719f2be2..cd869d8f89 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@apollographql/graphql-playground-html": "1.6.26", "@graphql-tools/stitch": "6.0.11", - "@graphql-tools/utils": "6.0.11", + "@graphql-tools/utils": "6.0.12", "@parse/fs-files-adapter": "1.0.1", "@parse/push-adapter": "3.2.0", "@parse/s3-files-adapter": "1.4.0", From 6f1210c325be536214b00483e006014aea43537c Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Fri, 24 Jul 2020 19:05:27 +0100 Subject: [PATCH 21/33] fix: upgrade @graphql-tools/stitch from 6.0.11 to 6.0.12 (#6824) Snyk has created this PR to upgrade @graphql-tools/stitch from 6.0.11 to 6.0.12. See this package in npm: https://www.npmjs.com/package/@graphql-tools/stitch See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Antonio Davi Macedo Coelho de Castro --- package-lock.json | 88 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e5b06ba42..3667c1e301 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2720,20 +2720,20 @@ } }, "@graphql-tools/delegate": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.11.tgz", - "integrity": "sha512-c5mVcjCPUqWabe3oPpuXs1IxXj58xsJhuG48vJJjDrTRuRXNZCJb5aa2+VLJEQkkW4tq/qmLcU8zeOfo2wdGng==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-6.0.12.tgz", + "integrity": "sha512-52bac1Ct1s0c8aSTVCbnc5FI2LC+NqUFSs+5/mP1k5hIEW2GROGBeZdbRs2GQaHir1vKUYIyHzlZIIBMzOZ/gA==", "requires": { "@ardatan/aggregate-error": "0.0.1", - "@graphql-tools/schema": "6.0.11", - "@graphql-tools/utils": "6.0.11", + "@graphql-tools/schema": "6.0.12", + "@graphql-tools/utils": "6.0.12", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", - "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.12.tgz", + "integrity": "sha512-MuFSkxXCe2QoD5QJPJ/1WIm0YnBzzXpkq9d/XznVAWptHFRwtwIbZ1xcREjYquFvoZ7ddsjZfyvUN/5ulmHhhg==", "requires": { "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" @@ -2747,19 +2747,19 @@ } }, "@graphql-tools/merge": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.0.11.tgz", - "integrity": "sha512-jNXl5pOdjfTRm+JKMpD47hsafM44Ojt7oi25Cflydw9VaWlQ5twFUSXk2rydP0mx1Twdxozk9ZCj4qiTdzw9ag==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-6.0.12.tgz", + "integrity": "sha512-GGvdIoTad6PJk/d1omPlGQ25pCFWmjuGkARYZ71qWI/c4FEA8EdGoOoPz3shhaKXyLdRiu84S758z4ZtDQiYVw==", "requires": { - "@graphql-tools/schema": "6.0.11", - "@graphql-tools/utils": "6.0.11", + "@graphql-tools/schema": "6.0.12", + "@graphql-tools/utils": "6.0.12", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", - "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.12.tgz", + "integrity": "sha512-MuFSkxXCe2QoD5QJPJ/1WIm0YnBzzXpkq9d/XznVAWptHFRwtwIbZ1xcREjYquFvoZ7ddsjZfyvUN/5ulmHhhg==", "requires": { "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" @@ -2773,18 +2773,18 @@ } }, "@graphql-tools/schema": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-6.0.11.tgz", - "integrity": "sha512-Zl9LTwOnkMaNtgs1+LJEYtklywtn602kRbxkRFeA7nFGaDmFPFHZnfQqcLsfhaPA8S0jNCQnbucHERCz8pRUYA==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-6.0.12.tgz", + "integrity": "sha512-XUmKJ+ipENaxuXIX4GapsLAUl1dFQBUg+S4ZbgtKVlwrPhZJ9bkjIqnUHk3wg4S4VXqzLX97ol1e4g9N6XLkYg==", "requires": { - "@graphql-tools/utils": "6.0.11", + "@graphql-tools/utils": "6.0.12", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", - "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.12.tgz", + "integrity": "sha512-MuFSkxXCe2QoD5QJPJ/1WIm0YnBzzXpkq9d/XznVAWptHFRwtwIbZ1xcREjYquFvoZ7ddsjZfyvUN/5ulmHhhg==", "requires": { "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" @@ -2798,22 +2798,22 @@ } }, "@graphql-tools/stitch": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/stitch/-/stitch-6.0.11.tgz", - "integrity": "sha512-pXELKOJV56C6VkILeyavtGWqzCi0e2gGtOhPiWKcV4MW5RwFN0QsPF4xSLZ7vpX8PVXovyuf/xdI62IvKXoywg==", - "requires": { - "@graphql-tools/delegate": "6.0.11", - "@graphql-tools/merge": "6.0.11", - "@graphql-tools/schema": "6.0.11", - "@graphql-tools/utils": "6.0.11", - "@graphql-tools/wrap": "6.0.11", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/stitch/-/stitch-6.0.12.tgz", + "integrity": "sha512-I+9l5Ws30Fn3nx0CIDUDMGP0nhexMEJyzfQn1t9DuOTy2QHPQ5YpaZ8hxv6y5+X23EJBU9AebqvNSvWNEO6XJQ==", + "requires": { + "@graphql-tools/delegate": "6.0.12", + "@graphql-tools/merge": "6.0.12", + "@graphql-tools/schema": "6.0.12", + "@graphql-tools/utils": "6.0.12", + "@graphql-tools/wrap": "6.0.12", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", - "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.12.tgz", + "integrity": "sha512-MuFSkxXCe2QoD5QJPJ/1WIm0YnBzzXpkq9d/XznVAWptHFRwtwIbZ1xcREjYquFvoZ7ddsjZfyvUN/5ulmHhhg==", "requires": { "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" @@ -2836,21 +2836,21 @@ } }, "@graphql-tools/wrap": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.0.11.tgz", - "integrity": "sha512-zy6ftDahsgrsaTPXPJgNNCt+0BTtuq37bZ5K9Ayf58wuxxW06fNLUp76wCUJWzb7nsML5aECQF9+STw1iQJ5qg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-6.0.12.tgz", + "integrity": "sha512-x/t6004aNLzTbOFzZiau15fY2+TBy0wbFqP2du+I+yh8j6KmAU1YkPolBJ4bAI04WD3qcLNh7Rai+VhOxidOkw==", "requires": { - "@graphql-tools/delegate": "6.0.11", - "@graphql-tools/schema": "6.0.11", - "@graphql-tools/utils": "6.0.11", + "@graphql-tools/delegate": "6.0.12", + "@graphql-tools/schema": "6.0.12", + "@graphql-tools/utils": "6.0.12", "aggregate-error": "3.0.1", "tslib": "~2.0.0" }, "dependencies": { "@graphql-tools/utils": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.11.tgz", - "integrity": "sha512-BK6HO73FbB/Ufac6XX5H0O2q4tEZi//HaQ7DgmHFoda53GZSZ/ZckJ59wh/tUvHykEaSFUSmMBVQxKbXBhGhyg==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-6.0.12.tgz", + "integrity": "sha512-MuFSkxXCe2QoD5QJPJ/1WIm0YnBzzXpkq9d/XznVAWptHFRwtwIbZ1xcREjYquFvoZ7ddsjZfyvUN/5ulmHhhg==", "requires": { "@ardatan/aggregate-error": "0.0.1", "camel-case": "4.1.1" diff --git a/package.json b/package.json index cd869d8f89..8108e86826 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "license": "BSD-3-Clause", "dependencies": { "@apollographql/graphql-playground-html": "1.6.26", - "@graphql-tools/stitch": "6.0.11", + "@graphql-tools/stitch": "6.0.12", "@graphql-tools/utils": "6.0.12", "@parse/fs-files-adapter": "1.0.1", "@parse/push-adapter": "3.2.0", From 114d78e80ae63ef11d8f67725945494726592b39 Mon Sep 17 00:00:00 2001 From: Manuel Date: Sat, 25 Jul 2020 19:11:10 +0200 Subject: [PATCH 22/33] enabled MongoDB transaction test for MongoDB >= 4.0.4 (#6827) --- spec/MongoStorageAdapter.spec.js | 3 ++- spec/ParseServerRESTController.spec.js | 5 +++-- spec/batch.spec.js | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 45c7068341..4509e3685d 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -8,6 +8,7 @@ const databaseURI = const request = require('../lib/request'); const Config = require('../lib/Config'); const TestUtils = require('../lib/TestUtils'); +const semver = require('semver'); const fakeClient = { s: { options: { dbName: null } }, @@ -351,7 +352,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); if ( - process.env.MONGODB_VERSION === '4.0.4' && + semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger' ) { diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index 3e02cde71c..0e9e04e796 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -3,6 +3,7 @@ const ParseServerRESTController = require('../lib/ParseServerRESTController') const ParseServer = require('../lib/ParseServer').default; const Parse = require('parse/node').Parse; const TestUtils = require('../lib/TestUtils'); +const semver = require('semver'); let RESTController; @@ -101,7 +102,7 @@ describe('ParseServerRESTController', () => { }); if ( - (process.env.MONGODB_VERSION === '4.0.4' && + (semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') || process.env.PARSE_SERVER_TEST_DB === 'postgres' @@ -109,7 +110,7 @@ describe('ParseServerRESTController', () => { describe('transactions', () => { beforeAll(async () => { if ( - process.env.MONGODB_VERSION === '4.0.4' && + semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger' ) { diff --git a/spec/batch.spec.js b/spec/batch.spec.js index c225be320e..9bb0cf15e1 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -1,6 +1,7 @@ const batch = require('../lib/batch'); const request = require('../lib/request'); const TestUtils = require('../lib/TestUtils'); +const semver = require('semver'); const originalURL = '/parse/batch'; const serverURL = 'http://localhost:1234/parse'; @@ -153,7 +154,7 @@ describe('batch', () => { }); if ( - (process.env.MONGODB_VERSION === '4.0.4' && + (semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') || process.env.PARSE_SERVER_TEST_DB === 'postgres' @@ -161,7 +162,7 @@ describe('batch', () => { describe('transactions', () => { beforeAll(async () => { if ( - process.env.MONGODB_VERSION === '4.0.4' && + semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger' ) { From 9ba9620bdf378d7c5884115389fe9d045ba30124 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sun, 26 Jul 2020 23:22:06 +0100 Subject: [PATCH 23/33] fix: upgrade ws from 7.3.0 to 7.3.1 (#6831) Snyk has created this PR to upgrade ws from 7.3.0 to 7.3.1. See this package in npm: https://www.npmjs.com/package/ws See this project in Snyk: https://app.snyk.io/org/dplewis/project/704d127e-7a24-4789-a0cd-807b7b972c86?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 11 ++++++++--- package.json | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3667c1e301..82fcdd9d54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10117,6 +10117,11 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "ws": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" } } }, @@ -12404,9 +12409,9 @@ } }, "ws": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", - "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" }, "xml2js": { "version": "0.4.17", diff --git a/package.json b/package.json index 8108e86826..a439193673 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "uuid": "8.2.0", "winston": "3.2.1", "winston-daily-rotate-file": "4.5.0", - "ws": "7.3.0" + "ws": "7.3.1" }, "devDependencies": { "@babel/cli": "7.10.0", From 5b7199317550ebff4df639643094c669753afdd2 Mon Sep 17 00:00:00 2001 From: Manuel Date: Mon, 27 Jul 2020 02:22:04 +0200 Subject: [PATCH 24/33] improve field deletion in collection (#6823) * added filter to updateMany when deleting field * added test cases * added changelog entry --- CHANGELOG.md | 1 + spec/MongoStorageAdapter.spec.js | 37 +++++++++++++++++++ .../Storage/Mongo/MongoStorageAdapter.js | 7 +++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ed371030..4f54fdaf7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### master [Full Changelog](https://github.com/parse-community/parse-server/compare/4.3.0...master) +- IMPROVE: Optimized deletion of class field from schema by using an index if available to do an index scan instead of a collection scan. [#6815](https://github.com/parse-community/parse-server/issues/6815). Thanks to [Manuel Trezza](https://github.com/mtrezza). ### 4.3.0 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.2.0...4.3.0) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 4509e3685d..239f50b888 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -351,6 +351,43 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH'); }); + it('should delete field without index', async () => { + const database = Config.get(Parse.applicationId).database; + const obj = new Parse.Object('MyObject'); + obj.set("test", 1); + await obj.save(); + const schemaBeforeDeletion = await new Parse.Schema('MyObject').get(); + await database.adapter.deleteFields( + "MyObject", + schemaBeforeDeletion, + ["test"] + ); + const schemaAfterDeletion = await new Parse.Schema('MyObject').get(); + expect(schemaBeforeDeletion.fields.test).toBeDefined(); + expect(schemaAfterDeletion.fields.test).toBeUndefined(); + }); + + it('should delete field with index', async () => { + const database = Config.get(Parse.applicationId).database; + const obj = new Parse.Object('MyObject'); + obj.set("test", 1); + await obj.save(); + const schemaBeforeDeletion = await new Parse.Schema('MyObject').get(); + await database.adapter.ensureIndex( + 'MyObject', + schemaBeforeDeletion, + ['test'] + ); + await database.adapter.deleteFields( + "MyObject", + schemaBeforeDeletion, + ["test"] + ); + const schemaAfterDeletion = await new Parse.Schema('MyObject').get(); + expect(schemaBeforeDeletion.fields.test).toBeDefined(); + expect(schemaAfterDeletion.fields.test).toBeUndefined(); + }); + if ( semver.satisfies(process.env.MONGODB_VERSION, '>=4.0.4') && process.env.MONGODB_TOPOLOGY === 'replicaset' && diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 9c08d3a073..9e75f5bb8a 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -433,6 +433,11 @@ export class MongoStorageAdapter implements StorageAdapter { collectionUpdate['$unset'][name] = null; }); + const collectionFilter = { $or: [] }; + mongoFormatNames.forEach(name => { + collectionFilter['$or'].push({ [name]: { $exists: true } }); + }); + const schemaUpdate = { $unset: {} }; fieldNames.forEach((name) => { schemaUpdate['$unset'][name] = null; @@ -440,7 +445,7 @@ export class MongoStorageAdapter implements StorageAdapter { }); return this._adaptiveCollection(className) - .then((collection) => collection.updateMany({}, collectionUpdate)) + .then((collection) => collection.updateMany(collectionFilter, collectionUpdate)) .then(() => this._schemaCollection()) .then((schemaCollection) => schemaCollection.updateSchema(className, schemaUpdate) From 92afcca4ee9129a7a1684102e01dcfcac92f00bc Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Mon, 27 Jul 2020 04:37:30 +0100 Subject: [PATCH 25/33] fix: upgrade graphql from 15.2.0 to 15.3.0 (#6832) Snyk has created this PR to upgrade graphql from 15.2.0 to 15.3.0. See this package in npm: https://www.npmjs.com/package/graphql See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82fcdd9d54..d28ad19114 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6961,9 +6961,9 @@ "dev": true }, "graphql": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.2.0.tgz", - "integrity": "sha512-tsceRyHfgzZo+ee0YK3o8f0CR0cXAXxRlxoORWFo/CoM1bVy3UXGWeyzBcf+Y6oqPvO27BDmOEVATcunOO/MrQ==" + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz", + "integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==" }, "graphql-extensions": { "version": "0.12.4", diff --git a/package.json b/package.json index a439193673..879f598d79 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "deepcopy": "2.0.0", "express": "4.17.1", "follow-redirects": "1.12.1", - "graphql": "15.2.0", + "graphql": "15.3.0", "graphql-list-fields": "2.0.2", "graphql-relay": "0.6.0", "graphql-upload": "11.0.0", From 42f75d6d947a86c3987042a0cc1523e553b39ae0 Mon Sep 17 00:00:00 2001 From: Arjun Vedak Date: Wed, 29 Jul 2020 20:25:59 +0530 Subject: [PATCH 26/33] fix(auth): Properly handle google token issuer (#6836) * Updated TOKEN_ISSUER to 'accounts.google.com' Hi, I was getting this issue from today morning parse-server/Adapters/Auth/google.js was expecting the TOKEN_ISSUER to be prefixed with https:// but on debugging the original value was not having the prefix, removing https:// from TOKEN_ISSUER solved this bug. This issue is introduced in 4.3.0 as in 4.2.0 it is working fine currently I have downgraded the version to 4.2.0 for it to work properly and suggesting the changes please merge this PR. * Update google.js * Update AuthenticationAdapters.spec.js * Update google.js * Update google.js --- spec/AuthenticationAdapters.spec.js | 2 +- src/Adapters/Auth/google.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 53b701f544..a42017769b 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -701,7 +701,7 @@ describe('google auth adapter', () => { fail(); } catch (e) { expect(e.message).toBe( - 'id token not issued by correct provider - expected: https://accounts.google.com | from: https://not.google.com' + 'id token not issued by correct provider - expected: accounts.google.com or https://accounts.google.com | from: https://not.google.com' ); } }); diff --git a/src/Adapters/Auth/google.js b/src/Adapters/Auth/google.js index 267aebb6df..e156eb1afb 100644 --- a/src/Adapters/Auth/google.js +++ b/src/Adapters/Auth/google.js @@ -6,7 +6,8 @@ var Parse = require('parse/node').Parse; const https = require('https'); const jwt = require('jsonwebtoken'); -const TOKEN_ISSUER = 'https://accounts.google.com'; +const TOKEN_ISSUER = 'accounts.google.com'; +const HTTPS_TOKEN_ISSUER = 'https://accounts.google.com'; let cache = {}; @@ -67,8 +68,8 @@ async function verifyIdToken({id_token: token, id}, {clientId}) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `${message}`); } - if (jwtClaims.iss !== TOKEN_ISSUER) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token not issued by correct provider - expected: ${TOKEN_ISSUER} | from: ${jwtClaims.iss}`); + if (jwtClaims.iss !== TOKEN_ISSUER && jwtClaims.iss !== HTTPS_TOKEN_ISSUER) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, `id token not issued by correct provider - expected: ${TOKEN_ISSUER} or ${HTTPS_TOKEN_ISSUER} | from: ${jwtClaims.iss}`); } if (jwtClaims.sub !== id) { From d4beda14af9ae92524972fca71b0af24cda77105 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 29 Jul 2020 16:13:53 +0100 Subject: [PATCH 27/33] fix: upgrade pg-promise from 10.5.7 to 10.5.8 (#6838) Snyk has created this PR to upgrade pg-promise from 10.5.7 to 10.5.8. See this package in npm: https://www.npmjs.com/package/pg-promise See this project in Snyk: https://app.snyk.io/org/dplewis/project/704d127e-7a24-4789-a0cd-807b7b972c86?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 24 ++++++++++++------------ package.json | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index d28ad19114..ff3dfb449a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10227,15 +10227,15 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pg": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.2.1.tgz", - "integrity": "sha512-DKzffhpkWRr9jx7vKxA+ur79KG+SKw+PdjMb1IRhMiKI9zqYUGczwFprqy+5Veh/DCcFs1Y6V8lRLN5I1DlleQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.2.2.tgz", + "integrity": "sha512-Uni50U0W2CNPM68+zfC/1WWjSO3q/uBSF/Nl7D+1npZGsPSM4/EZt0xSMW2jox1Bn0EfDlnTWnTsM/TrSOtBEA==", "requires": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.2.3", "pg-pool": "^3.2.1", - "pg-protocol": "^1.2.4", + "pg-protocol": "^1.2.5", "pg-types": "^2.1.0", "pgpass": "1.x", "semver": "4.3.2" @@ -10269,14 +10269,14 @@ "integrity": "sha512-BQDPWUeKenVrMMDN9opfns/kZo4lxmSWhIqo+cSAF7+lfi9ZclQbr9vfnlNaPr8wYF3UYjm5X0yPAhbcgqNOdA==" }, "pg-promise": { - "version": "10.5.7", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.5.7.tgz", - "integrity": "sha512-feCpn4J4MsNnR5Ve3fpbIlmbohwRirvZEI1Dcy72zwKvIKKRHPk7TJZFQHP4YQhaZ3sT3VGgg0o1/I+uhht/1g==", + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.5.8.tgz", + "integrity": "sha512-EdLyPd/XlmNsfA2uRKHuCnyLhk5DHPdKGPZmjzpcKfdx6dDZB+nEfSuaNSjReRrM7BmPaV/hSGppt9kG/W2Umw==", "requires": { "assert-options": "0.6.2", - "pg": "8.2.1", + "pg": "8.2.2", "pg-minify": "1.6.1", - "spex": "3.0.1" + "spex": "3.0.2" } }, "pg-protocol": { @@ -11382,9 +11382,9 @@ } }, "spex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spex/-/spex-3.0.1.tgz", - "integrity": "sha512-priWZUrXBmVPHTOmtUeS7gZzCOUwRK87OHJw5K8bTC6MLOq93mQocx+vWccNyKPT2EY+goZvKGguGn2lx8TBDA==" + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spex/-/spex-3.0.2.tgz", + "integrity": "sha512-ZNCrOso+oNv5P01HCO4wuxV9Og5rS6ms7gGAqugfBPjx1QwfNXJI3T02ldfaap1O0dlT1sB0Rk+mhDqxt3Z27w==" }, "split": { "version": "1.0.1", diff --git a/package.json b/package.json index 879f598d79..082ebf77ae 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "mime": "2.4.6", "mongodb": "3.5.9", "parse": "2.15.0", - "pg-promise": "10.5.7", + "pg-promise": "10.5.8", "pluralize": "8.0.0", "redis": "3.0.2", "semver": "7.3.2", From 19264ba9ffb417b4b165da9484089bc2b43ddb05 Mon Sep 17 00:00:00 2001 From: Tom Fox <13188249+TomWFox@users.noreply.github.com> Date: Wed, 29 Jul 2020 21:26:09 +0100 Subject: [PATCH 28/33] Further improve issue template (#6816) * Update ---report-an-issue.md * mtrezza's suggestions * remove support from readme * Rename ---report-an-issue.md to ---1-report-an-issue.md * Update ---feature-request.md * Rename ---feature-request.md to ---2-feature-request.md * Delete ---getting-help.md * Delete ---push-notifications.md * Delete ---parse-server-3-0-0.md * Create config.yml * change support link to org wide doc --- .../ISSUE_TEMPLATE/---1-report-an-issue.md | 48 +++++++++++++++ ...ure-request.md => ---2-feature-request.md} | 4 +- .github/ISSUE_TEMPLATE/---getting-help.md | 10 ---- .../ISSUE_TEMPLATE/---parse-server-3-0-0.md | 59 ------------------- .../ISSUE_TEMPLATE/---push-notifications.md | 51 ---------------- .github/ISSUE_TEMPLATE/---report-an-issue.md | 54 ----------------- .github/ISSUE_TEMPLATE/config.yml | 8 +++ README.md | 12 ---- 8 files changed, 58 insertions(+), 188 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/---1-report-an-issue.md rename .github/ISSUE_TEMPLATE/{---feature-request.md => ---2-feature-request.md} (82%) delete mode 100644 .github/ISSUE_TEMPLATE/---getting-help.md delete mode 100644 .github/ISSUE_TEMPLATE/---parse-server-3-0-0.md delete mode 100644 .github/ISSUE_TEMPLATE/---push-notifications.md delete mode 100644 .github/ISSUE_TEMPLATE/---report-an-issue.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/---1-report-an-issue.md b/.github/ISSUE_TEMPLATE/---1-report-an-issue.md new file mode 100644 index 0000000000..085f3fbe36 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/---1-report-an-issue.md @@ -0,0 +1,48 @@ +--- +name: "\U0001F41B Report an issue" +about: Report an issue on parse-server +title: '' +labels: '' +assignees: '' + +--- + +### New Issue Checklist + + +- [ ] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md). +- [ ] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md). +- [ ] I have searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue). +- [ ] I can reproduce the issue with the [latest version of Parse Server](https://github.com/parse-community/parse-server/releases). + +### Issue Description + + +### Steps to reproduce + + +### Actual Outcome + + +### Expected Outcome + + +### Environment + + +Server +- Parse Server version: `FILL_THIS_OUT` +- Operating system: `FILL_THIS_OUT` +- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): `FILL_THIS_OUT` + +Database +- System (MongoDB or Postgres): `FILL_THIS_OUT` +- Database version: `FILL_THIS_OUT` +- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): `FILL_THIS_OUT` + +Client +- SDK (iOS, Android, JavaScript, PHP, Unity, etc): `FILL_THIS_OUT` +- SDK version: `FILL_THIS_OUT` + +### Logs + diff --git a/.github/ISSUE_TEMPLATE/---feature-request.md b/.github/ISSUE_TEMPLATE/---2-feature-request.md similarity index 82% rename from .github/ISSUE_TEMPLATE/---feature-request.md rename to .github/ISSUE_TEMPLATE/---2-feature-request.md index 9ce4b51612..c2756fb952 100644 --- a/.github/ISSUE_TEMPLATE/---feature-request.md +++ b/.github/ISSUE_TEMPLATE/---2-feature-request.md @@ -1,6 +1,6 @@ --- -name: "\U0001F4A1 Feature request" -about: Suggest an idea for this project +name: "\U0001F4A1 Request a feature" +about: Suggest new functionality or an enhancement of existing functionality. title: '' labels: '' assignees: '' diff --git a/.github/ISSUE_TEMPLATE/---getting-help.md b/.github/ISSUE_TEMPLATE/---getting-help.md deleted file mode 100644 index 4b75b325de..0000000000 --- a/.github/ISSUE_TEMPLATE/---getting-help.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: "\U0001F64B‍Getting Help" -about: Join https://community.parseplatform.org -title: '' -labels: '' -assignees: '' - ---- - - diff --git a/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md b/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md deleted file mode 100644 index 72373ca25e..0000000000 --- a/.github/ISSUE_TEMPLATE/---parse-server-3-0-0.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -name: "\U0001F525 parse-server 3.0.0" -about: Report an issue while migrating to parse-server 3.0.0 -title: '' -labels: '' -assignees: '' - ---- - - - -# Before opening the issue please ensure that you have: - -- [ ] [Read the migration guide](https://github.com/parse-community/parse-server/blob/master/3.0.0.md) to parse-server 3.0.0 -- [ ] [Read the migration guide](https://github.com/parse-community/Parse-SDK-JS/blob/master/2.0.0.md) to Parse SDK JS 2.0.0 - -### Issue Description - - - -### Steps to reproduce - - - -### Expected Results - - - -### Actual Outcome - - - -### Environment Setup - -- **Server** - - parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT] - - Operating System: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT] - -- **Database** - - MongoDB version: [FILL THIS OUT] - - Storage engine: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT] - -### Logs/Trace - - diff --git a/.github/ISSUE_TEMPLATE/---push-notifications.md b/.github/ISSUE_TEMPLATE/---push-notifications.md deleted file mode 100644 index 9654d7c23f..0000000000 --- a/.github/ISSUE_TEMPLATE/---push-notifications.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -name: "\U0001F4F2 Push Notifications" -about: Issues with setting up or delivering push notifications -title: '' -labels: '' -assignees: '' - ---- - - - -### Issue Description - - - -### Push Configuration - -Please provide a copy of your `push` configuration here, obfuscating any sensitive part. - -### Environment Setup - -- **Server** - - parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT] - - Operating System: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT] - -- **Database** - - MongoDB version: [FILL THIS OUT] - - Storage engine: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT] - -### Logs/Trace - - diff --git a/.github/ISSUE_TEMPLATE/---report-an-issue.md b/.github/ISSUE_TEMPLATE/---report-an-issue.md deleted file mode 100644 index b8c8fed9b0..0000000000 --- a/.github/ISSUE_TEMPLATE/---report-an-issue.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: "\U0001F41B Report an issue" -about: Report an issue on parse-server -title: '' -labels: '' -assignees: '' - ---- - -We use GitHub Issues for reporting bugs with Parse Server. - -Make sure these boxes are checked before submitting your issue - thanks for reporting issues back to Parse Server! - -- [ ] This isn't a vulnerability disclosure, if it is please follow our [security policy](https://github.com/parse-community/parse-server/blob/master/SECURITY.md). - -- [ ] You're running version >=2.3.2 of Parse Server, we can't accept issues for very outdated releases, please update to a newer version. - -- [ ] This isn't a question, if you need if you have questions about code please use Stack Overflow with the [parse-platform tag](https://stackoverflow.com/questions/tagged/parse-platform) & other questions can be posted on the [community forum](https://community.parseplatform.org). - -- [ ] You've searched through [existing issues](https://github.com/parse-community/parse-server/issues?q=is%3Aissue), your issue may have been reported or resolved before. - -### Issue Description - - - -### Steps to reproduce - - - -### Expected Results - - - -### Actual Outcome - - - -### Environment Setup - -- **Server** - - parse-server version (Be specific! Don't say 'latest'.) : [FILL THIS OUT] - - Operating System: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, Heroku, Azure, Digital Ocean, etc): [FILL THIS OUT] - -- **Database** - - MongoDB version: [FILL THIS OUT] - - Storage engine: [FILL THIS OUT] - - Hardware: [FILL THIS OUT] - - Localhost or remote server? (AWS, mLab, ObjectRocket, Digital Ocean, etc): [FILL THIS OUT] - -### Logs/Trace - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..e5a8c3caa9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 🙋🏽‍♀️ Getting help with code + url: https://stackoverflow.com/questions/tagged/parse-platform + about: Get help with code-level questions on Stack Overflow. + - name: 🙋 Getting general help + url: https://community.parseplatform.org + about: Get help with other questions on our Community Forum. diff --git a/README.md b/README.md index 7dde76a41f..ed3dab8f86 100644 --- a/README.md +++ b/README.md @@ -766,18 +766,6 @@ All the Cloud Code interfaces also have been updated to reflect those changes, a We have written up a [migration guide](3.0.0.md), hoping this will help you transition to the next major release. -# Support - -Please take a look at our [support document](https://github.com/parse-community/.github/blob/master/SUPPORT.md). - -If you believe you've found an issue with Parse Server, make sure these boxes are checked before [reporting an issue](https://github.com/parse-community/parse-server/issues): - -- [ ] You've met the [prerequisites](http://docs.parseplatform.org/parse-server/guide/#prerequisites). - -- [ ] You're running the [latest version](https://github.com/parse-community/parse-server/releases) of Parse Server. - -- [ ] You've searched through [existing issues](https://github.com/parse-community/parse-server/issues?utf8=%E2%9C%93&q=). Chances are that your issue has been reported or resolved before. - # Want to ride the bleeding edge? It is recommend to use builds deployed npm for many reasons, but if you want to use From b4c8542a7e12b7b64059982d988a6bb3c3d27912 Mon Sep 17 00:00:00 2001 From: Tom Fox <13188249+TomWFox@users.noreply.github.com> Date: Wed, 29 Jul 2020 22:06:13 +0100 Subject: [PATCH 29/33] Small issue template change (#6839) --- .github/ISSUE_TEMPLATE/---1-report-an-issue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/---1-report-an-issue.md b/.github/ISSUE_TEMPLATE/---1-report-an-issue.md index 085f3fbe36..dbfa97106a 100644 --- a/.github/ISSUE_TEMPLATE/---1-report-an-issue.md +++ b/.github/ISSUE_TEMPLATE/---1-report-an-issue.md @@ -1,6 +1,6 @@ --- name: "\U0001F41B Report an issue" -about: Report an issue on parse-server +about: A feature of Parse Server is not working as expected. title: '' labels: '' assignees: '' From 4cec333cf96eb10f836a83932797daba57a07144 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Sun, 2 Aug 2020 04:34:58 +0100 Subject: [PATCH 30/33] fix: upgrade subscriptions-transport-ws from 0.9.16 to 0.9.17 (#6841) Snyk has created this PR to upgrade subscriptions-transport-ws from 0.9.16 to 0.9.17. See this package in npm: https://www.npmjs.com/package/subscriptions-transport-ws See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff3dfb449a..a9220cf3ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11568,9 +11568,9 @@ } }, "subscriptions-transport-ws": { - "version": "0.9.16", - "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz", - "integrity": "sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw==", + "version": "0.9.17", + "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz", + "integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==", "requires": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", diff --git a/package.json b/package.json index 082ebf77ae..3b4182e9f6 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "pluralize": "8.0.0", "redis": "3.0.2", "semver": "7.3.2", - "subscriptions-transport-ws": "0.9.16", + "subscriptions-transport-ws": "0.9.17", "tv4": "1.3.0", "uuid": "8.2.0", "winston": "3.2.1", From 5fd63931c7cca03bb119d0a8432ef2dbbde054d9 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 12 Aug 2020 21:36:03 +1000 Subject: [PATCH 31/33] afterLiveQueryEvent --- spec/ParseLiveQuery.spec.js | 251 +++++++++++++++++++++++++- src/LiveQuery/ParseLiveQueryServer.js | 80 +++++--- src/cloud-code/Parse.Cloud.js | 10 + src/triggers.js | 20 ++ 4 files changed, 331 insertions(+), 30 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 80eebaa733..de1d6b8bbd 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -16,10 +16,257 @@ describe('ParseLiveQuery', function () { const query = new Parse.Query(TestObject); query.equalTo('objectId', object.id); const subscription = await query.subscribe(); - subscription.on('update', async object => { + subscription.on('update', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + it('expect afterEvent create', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Create'); + expect(req.user).toBeUndefined(); + expect(req.current.get('foo')).toBe('bar'); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + subscription.on('create', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + }); + + it('expect afterEvent payload', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Update'); + expect(req.user).toBeUndefined(); + expect(req.current.get('foo')).toBe('bar'); + expect(req.original.get('foo')).toBeUndefined(); + done(); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + await query.subscribe(); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('expect afterEvent enter', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Enter'); + expect(req.user).toBeUndefined(); + expect(req.current.get('foo')).toBe('bar'); + expect(req.original.get('foo')).toBeUndefined(); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('enter', object => { expect(object.get('foo')).toBe('bar'); done(); }); + + object.set('foo', 'bar'); + await object.save(); + }); + + it('expect afterEvent leave', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Leave'); + expect(req.user).toBeUndefined(); + expect(req.current.get('foo')).toBeUndefined(); + expect(req.original.get('foo')).toBe('bar'); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('leave', object => { + expect(object.get('foo')).toBeUndefined(); + done(); + }); + + object.unset('foo'); + await object.save(); + }); + + it('can handle afterEvent modification', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + const current = req.current; + current.set('foo', 'yolo'); + + const original = req.original; + original.set('yolo', 'foo'); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', (object, original) => { + expect(object.get('foo')).toBe('yolo'); + expect(original.get('yolo')).toBe('foo'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can return different object in afterEvent', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', () => { + const object = new Parse.Object('Yolo'); + return object; + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', object => { + expect(object.className).toBe('Yolo'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can handle async afterEvent modification', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const parent = new TestObject(); + const child = new TestObject(); + child.set('bar', 'foo'); + await Parse.Object.saveAll([parent, child]); + + Parse.Cloud.afterLiveQueryEvent('TestObject', async req => { + const current = req.current; + const pointer = current.get('child'); + await pointer.fetch(); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', parent.id); + const subscription = await query.subscribe(); + subscription.on('update', object => { + expect(object.get('child')).toBeDefined(); + expect(object.get('child').get('bar')).toBe('foo'); + done(); + }); + parent.set('child', child); + await parent.save(); + }); + + it('can handle afterEvent throw', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + const current = req.current; + const original = req.original; + + setTimeout(() => { + done(); + }, 2000); + + if (current.get('foo') != original.get('foo')) { + throw "Don't pass an update trigger, or message"; + } + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', () => { + fail('update should not have been called.'); + }); + subscription.on('error', () => { + fail('error should not have been called.'); + }); object.set({ foo: 'bar' }); await object.save(); }); @@ -56,7 +303,7 @@ describe('ParseLiveQuery', function () { const query = new Parse.Query(TestObject); query.equalTo('objectId', object.id); const subscription = await query.subscribe(); - subscription.on('update', async object => { + subscription.on('update', object => { expect(object.get('foo')).toBe('bar'); done(); }); diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 5d28367961..978fc09194 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -14,6 +14,7 @@ import { runLiveQueryEventHandlers, maybeRunConnectTrigger, maybeRunSubscribeTrigger, + maybeRunAfterEventTrigger, } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; @@ -193,7 +194,7 @@ class ParseLiveQueryServer { originalParseObject = message.originalParseObject.toJSON(); } const classLevelPermissions = message.classLevelPermissions; - const currentParseObject = message.currentParseObject.toJSON(); + let currentParseObject = message.currentParseObject.toJSON(); const className = currentParseObject.className; logger.verbose( 'ClassName: %s | ObjectId: %s', @@ -243,6 +244,7 @@ class ParseLiveQueryServer { // Set current ParseObject ACL checking promise, if the object does not match // subscription, we do not need to check ACL let currentACLCheckingPromise; + let res; if (!isCurrentSubscriptionMatched) { currentACLCheckingPromise = Promise.resolve(false); } else { @@ -267,35 +269,57 @@ class ParseLiveQueryServer { currentACLCheckingPromise, ]); }) - .then( - ([isOriginalMatched, isCurrentMatched]) => { - logger.verbose( - 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', - originalParseObject, - currentParseObject, - isOriginalSubscriptionMatched, - isCurrentSubscriptionMatched, - isOriginalMatched, - isCurrentMatched, - subscription.hash - ); - - // Decide event type - let type; - if (isOriginalMatched && isCurrentMatched) { - type = 'Update'; - } else if (isOriginalMatched && !isCurrentMatched) { - type = 'Leave'; - } else if (!isOriginalMatched && isCurrentMatched) { - if (originalParseObject) { - type = 'Enter'; - } else { - type = 'Create'; - } + .then(([isOriginalMatched, isCurrentMatched]) => { + logger.verbose( + 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', + originalParseObject, + currentParseObject, + isOriginalSubscriptionMatched, + isCurrentSubscriptionMatched, + isOriginalMatched, + isCurrentMatched, + subscription.hash + ); + + // Decide event type + let type; + if (isOriginalMatched && isCurrentMatched) { + type = 'Update'; + } else if (isOriginalMatched && !isCurrentMatched) { + type = 'Leave'; + } else if (!isOriginalMatched && isCurrentMatched) { + if (originalParseObject) { + type = 'Enter'; } else { - return null; + type = 'Create'; + } + } else { + return null; + } + message.event = type; + res = { + event: type, + sessionToken: client.sessionToken, + current: currentParseObject, + original: originalParseObject, + }; + return maybeRunAfterEventTrigger('afterEvent', className, res); + }) + .then( + newObj => { + if (res.current != currentParseObject) { + currentParseObject = res.current.toJSON(); + currentParseObject.className = className; + } + if (res.original != originalParseObject) { + originalParseObject = res.original.toJSON(); + originalParseObject.className = className; + } + if (newObj) { + currentParseObject = newObj.toJSON(); + currentParseObject.className = newObj.className; } - const functionName = 'push' + type; + const functionName = 'push' + message.event; client[functionName]( requestId, currentParseObject, diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 088c4dc3c1..2ea210bccc 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -511,6 +511,16 @@ ParseCloud.onLiveQueryEvent = function (handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; +ParseCloud.afterLiveQueryEvent = function (parseClass, handler) { + const className = getClassName(parseClass); + triggers.addTrigger( + triggers.Types.afterEvent, + className, + handler, + Parse.applicationId + ); +}; + ParseCloud._removeAllHooks = () => { triggers._unregisterAll(); }; diff --git a/src/triggers.js b/src/triggers.js index 96dcb65e47..c231d742ee 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -18,6 +18,7 @@ export const Types = { afterDeleteFile: 'afterDeleteFile', beforeConnect: 'beforeConnect', beforeSubscribe: 'beforeSubscribe', + afterEvent: 'afterEvent', }; const FileClassName = '@File'; @@ -797,6 +798,25 @@ export async function maybeRunSubscribeTrigger( return trigger(request); } +export async function maybeRunAfterEventTrigger( + triggerType, + className, + request +) { + const trigger = getTrigger(className, triggerType, Parse.applicationId); + if (!trigger) { + return; + } + if (request.current) { + request.current = Parse.Object.fromJSON(request.current); + } + if (request.original) { + request.original = Parse.Object.fromJSON(request.original); + } + request.user = await userForSessionToken(request.sessionToken); + return trigger(request); +} + async function userForSessionToken(sessionToken) { if (!sessionToken) { return; From 55ac6cb2725232e2136013323d8bb6c5141c7a36 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 12 Aug 2020 22:02:05 +1000 Subject: [PATCH 32/33] Add delete event --- spec/ParseLiveQuery.spec.js | 44 ++++++++++++++++++++++----- src/LiveQuery/ParseLiveQueryServer.js | 25 ++++++++++++--- src/triggers.js | 4 +-- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index de1d6b8bbd..460e490a7e 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -35,7 +35,7 @@ describe('ParseLiveQuery', function () { Parse.Cloud.afterLiveQueryEvent('TestObject', req => { expect(req.event).toBe('Create'); expect(req.user).toBeUndefined(); - expect(req.current.get('foo')).toBe('bar'); + expect(req.object.get('foo')).toBe('bar'); }); const query = new Parse.Query(TestObject); @@ -65,7 +65,7 @@ describe('ParseLiveQuery', function () { Parse.Cloud.afterLiveQueryEvent('TestObject', req => { expect(req.event).toBe('Update'); expect(req.user).toBeUndefined(); - expect(req.current.get('foo')).toBe('bar'); + expect(req.object.get('foo')).toBe('bar'); expect(req.original.get('foo')).toBeUndefined(); done(); }); @@ -89,7 +89,7 @@ describe('ParseLiveQuery', function () { Parse.Cloud.afterLiveQueryEvent('TestObject', req => { expect(req.event).toBe('Enter'); expect(req.user).toBeUndefined(); - expect(req.current.get('foo')).toBe('bar'); + expect(req.object.get('foo')).toBe('bar'); expect(req.original.get('foo')).toBeUndefined(); }); @@ -120,7 +120,7 @@ describe('ParseLiveQuery', function () { Parse.Cloud.afterLiveQueryEvent('TestObject', req => { expect(req.event).toBe('Leave'); expect(req.user).toBeUndefined(); - expect(req.current.get('foo')).toBeUndefined(); + expect(req.object.get('foo')).toBeUndefined(); expect(req.original.get('foo')).toBe('bar'); }); @@ -140,6 +140,36 @@ describe('ParseLiveQuery', function () { await object.save(); }); + it('expect afterEvent delete', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Delete'); + expect(req.user).toBeUndefined(); + req.object.set('foo', 'bar'); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + + const subscription = await query.subscribe(); + subscription.on('delete', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + await object.destroy(); + }); + it('can handle afterEvent modification', async done => { await reconfigureServer({ liveQuery: { @@ -153,7 +183,7 @@ describe('ParseLiveQuery', function () { await object.save(); Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - const current = req.current; + const current = req.object; current.set('foo', 'yolo'); const original = req.original; @@ -215,7 +245,7 @@ describe('ParseLiveQuery', function () { await Parse.Object.saveAll([parent, child]); Parse.Cloud.afterLiveQueryEvent('TestObject', async req => { - const current = req.current; + const current = req.object; const pointer = current.get('child'); await pointer.fetch(); }); @@ -246,7 +276,7 @@ describe('ParseLiveQuery', function () { await object.save(); Parse.Cloud.afterLiveQueryEvent('TestObject', req => { - const current = req.current; + const current = req.object; const original = req.original; setTimeout(() => { diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 978fc09194..ca45b9497b 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -125,7 +125,7 @@ class ParseLiveQueryServer { _onAfterDelete(message: any): void { logger.verbose(Parse.applicationId + 'afterDelete is triggered'); - const deletedParseObject = message.currentParseObject.toJSON(); + let deletedParseObject = message.currentParseObject.toJSON(); const classLevelPermissions = message.classLevelPermissions; const className = deletedParseObject.className; logger.verbose( @@ -159,6 +159,7 @@ class ParseLiveQueryServer { const acl = message.currentParseObject.getACL(); // Check CLP const op = this._getCLPOperation(subscription.query); + let res; this._matchesCLP( classLevelPermissions, message.currentParseObject, @@ -174,6 +175,22 @@ class ParseLiveQueryServer { if (!isMatched) { return null; } + res = { + event: 'Delete', + sessionToken: client.sessionToken, + object: deletedParseObject, + }; + return maybeRunAfterEventTrigger('afterEvent', className, res); + }) + .then(newObj => { + if (res.object != deletedParseObject) { + deletedParseObject = res.object.toJSON(); + deletedParseObject.className = className; + } + if (newObj) { + deletedParseObject = newObj.toJSON(); + deletedParseObject.className = newObj.className; + } client.pushDelete(requestId, deletedParseObject); }) .catch(error => { @@ -300,15 +317,15 @@ class ParseLiveQueryServer { res = { event: type, sessionToken: client.sessionToken, - current: currentParseObject, + object: currentParseObject, original: originalParseObject, }; return maybeRunAfterEventTrigger('afterEvent', className, res); }) .then( newObj => { - if (res.current != currentParseObject) { - currentParseObject = res.current.toJSON(); + if (res.object != currentParseObject) { + currentParseObject = res.object.toJSON(); currentParseObject.className = className; } if (res.original != originalParseObject) { diff --git a/src/triggers.js b/src/triggers.js index c231d742ee..75c26ff039 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -807,8 +807,8 @@ export async function maybeRunAfterEventTrigger( if (!trigger) { return; } - if (request.current) { - request.current = Parse.Object.fromJSON(request.current); + if (request.object) { + request.object = Parse.Object.fromJSON(request.object); } if (request.original) { request.original = Parse.Object.fromJSON(request.original); From 8a977d1aa4aefd5eb286bb8b87b4c679e172ae33 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 12 Aug 2020 22:28:25 +1000 Subject: [PATCH 33/33] Fix failing tests --- src/LiveQuery/ParseLiveQueryServer.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index ca45b9497b..12af52a5a0 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -183,11 +183,12 @@ class ParseLiveQueryServer { return maybeRunAfterEventTrigger('afterEvent', className, res); }) .then(newObj => { - if (res.object != deletedParseObject) { + if (res.object && typeof res.object.toJSON === 'function') { deletedParseObject = res.object.toJSON(); deletedParseObject.className = className; } - if (newObj) { + + if (newObj && typeof newObj.toJSON === 'function') { deletedParseObject = newObj.toJSON(); deletedParseObject.className = newObj.className; } @@ -324,24 +325,29 @@ class ParseLiveQueryServer { }) .then( newObj => { - if (res.object != currentParseObject) { + if (res.object && typeof res.object.toJSON === 'function') { currentParseObject = res.object.toJSON(); currentParseObject.className = className; } - if (res.original != originalParseObject) { + + if (res.original && typeof res.original.toJSON === 'function') { originalParseObject = res.original.toJSON(); originalParseObject.className = className; } - if (newObj) { + + if (newObj && typeof newObj.toJSON === 'function') { currentParseObject = newObj.toJSON(); currentParseObject.className = newObj.className; } + const functionName = 'push' + message.event; - client[functionName]( - requestId, - currentParseObject, - originalParseObject - ); + if (client[functionName]) { + client[functionName]( + requestId, + currentParseObject, + originalParseObject + ); + } }, error => { logger.error('Matching ACL error : ', error);