From 5519f4053feca582f0ee986f1e05d146d0716fc5 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 11:55:47 +1100 Subject: [PATCH 01/16] Initial Commit --- spec/CloudCode.spec.js | 785 +++++++++++++++++++++++++++++++++ spec/ParseLiveQuery.spec.js | 75 ++++ src/Routers/FilesRouter.js | 25 +- src/Routers/FunctionsRouter.js | 36 +- src/cloud-code/Parse.Cloud.js | 82 ++-- src/triggers.js | 207 ++++++++- 6 files changed, 1133 insertions(+), 77 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 0255de352d..676b84b0fb 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1693,6 +1693,791 @@ describe('cloud functions', () => { Parse.Cloud.run('myFunction', {}).then(() => done()); }); + + it('existing validator functionality', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + return false; + } + ); + try { + await Parse.Cloud.run('myFunction', {}); + fail('should have thrown error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('complete validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => {} + ); + try { + const result = await Parse.Cloud.run('myFunction', {}); + expect(result).toBe('myFunc'); + done(); + } catch (e) { + fail('should not have thrown error'); + } + }); + it('Throw from validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + throw 'error'; + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + it('validator can throw parse error', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('should have validation error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(e.message).toBe('It should fail'); + done(); + } + }); + + it('async validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + async () => { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + throw 'async error'; + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('should have validation error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + expect(e.message).toBe('async error'); + done(); + } + }); + + it('pass function to validator', async done => { + const validator = request => { + expect(request).toBeDefined(); + expect(request.params).toBeDefined(); + expect(request.master).toBe(false); + expect(request.user).toBeUndefined(); + expect(request.installationId).toBeDefined(); + expect(request.log).toBeDefined(); + expect(request.headers).toBeDefined(); + expect(request.functionName).toBeDefined(); + expect(request.context).toBeDefined(); + done(); + }; + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + validator + ); + await Parse.Cloud.run('myFunction'); + }); + + it('require user on cloud functions', done => { + Parse.Cloud.define( + 'hello1', + () => { + return 'Hello world!'; + }, + { + requireUser: true, + } + ); + + Parse.Cloud.run('hello1', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please login to continue.' + ); + done(); + }); + }); + + it('require master on cloud functions', done => { + Parse.Cloud.define( + 'hello2', + () => { + return 'Hello world!'; + }, + { + requireMaster: true, + } + ); + Parse.Cloud.run('hello2', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Master key is required to complete this request.' + ); + done(); + }); + }); + + it('set params on cloud functions', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + params: ['a'], + } + ); + Parse.Cloud.run('hello', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for a.' + ); + done(); + }); + }); + + it('set params on cloud functions', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + params: ['a'], + } + ); + Parse.Cloud.run('hello', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for a.' + ); + done(); + }); + }); + + it('allow params on cloud functions', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.a).toEqual('yolo'); + return 'Hello world!'; + }, + { + params: ['a'], + } + ); + Parse.Cloud.run('hello', { a: 'yolo' }) + .then(() => { + done(); + }) + .catch(() => { + fail('Error should not have been called.'); + }); + }); + + it('set params type', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + params: { + data: { + type: String, + }, + }, + } + ); + Parse.Cloud.run('hello', { data: [] }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid type for data. Expected: string' + ); + done(); + }); + }); + + it('set params default', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + params: { + data: { + type: String, + default: 'yolo', + }, + }, + } + ); + Parse.Cloud.run('hello') + .then(() => { + done(); + }) + .catch(() => { + fail('function should not have failed.'); + }); + }); + + it('set params required', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + params: { + data: { + type: String, + required: true, + }, + }, + } + ); + Parse.Cloud.run('hello', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for data.' + ); + done(); + }); + }); + + it('set params option', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + params: { + data: { + type: String, + required: true, + options: 'a', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'f' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid option for data. Expected: a' + ); + done(); + }); + }); + + it('set params options', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + params: { + data: { + type: String, + required: true, + options: ['a', 'b'], + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'f' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid option for data. Expected: a, b' + ); + done(); + }); + }); + + it('can create functions', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + requireUser: false, + requireMaster: false, + params: { + data: { + type: String, + }, + data1: { + type: String, + default: 'default', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'str' }).then(result => { + expect(result).toEqual('Hello world!'); + done(); + }); + }); + + it('basic beforeSave requireUser', function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUser: true, + }); + + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj + .save() + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please login to continue.' + ); + done(); + }); + }); + + it('basic beforeSave requireMaster', function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireMaster: true, + }); + + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj + .save() + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Master key is required to complete this request.' + ); + done(); + }); + }); + + it('basic beforeSave requireKeys', function (done) { + Parse.Cloud.beforeSave('beforeSaveRequire', () => {}, { + requireKeys: ['foo', 'bar'], + }); + const obj = new Parse.Object('beforeSaveRequire'); + obj.set('foo', 'bar'); + obj + .save() + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for bar.' + ); + done(); + }); + }); + it('basic beforeSave constantKeys', async function (done) { + Parse.Cloud.beforeSave('BeforeSave', () => {}, { + constantKeys: ['foo'], + }); + + const obj = new Parse.Object('BeforeSave'); + obj.set('foo', 'bar'); + await obj.save(); + obj.set('foo', 'yolo'); + await obj.save(); + expect(obj.get('foo')).toBe('bar'); + done(); + }); + + const validatorFail = () => { + throw 'you are not authorised'; + }; + const validatorSuccess = () => { + return true; + }; + it('validate beforeSave', async done => { + Parse.Cloud.beforeSave('MyObject', () => {}, validatorSuccess); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + try { + await myObject.save(); + done(); + } catch (e) { + fail('before save should not have failed.'); + } + }); + it('validate beforeSave fail', async done => { + Parse.Cloud.beforeSave('MyObject', () => {}, validatorFail); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + try { + await myObject.save(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterSave', async done => { + Parse.Cloud.afterSave( + 'MyObject', + () => { + done(); + }, + validatorSuccess + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + try { + await myObject.save(); + } catch (e) { + fail('before save should not have failed.'); + } + }); + it('validate afterSave fail', async done => { + Parse.Cloud.afterSave( + 'MyObject', + () => { + fail('this should not be called.'); + }, + validatorFail + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + setTimeout(() => { + done(); + }, 1000); + }); + + it('validate beforeDelete', async done => { + Parse.Cloud.beforeDelete('MyObject', () => {}, validatorSuccess); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + done(); + } catch (e) { + fail('before delete should not have failed.'); + } + }); + it('validate beforeDelete fail', async done => { + Parse.Cloud.beforeDelete( + 'MyObject', + () => { + fail('this should not be called.'); + }, + validatorFail + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterDelete', async done => { + Parse.Cloud.afterDelete( + 'MyObject', + () => { + done(); + }, + validatorSuccess + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + } catch (e) { + fail('after delete should not have failed.'); + } + }); + it('validate afterDelete fail', async done => { + Parse.Cloud.afterDelete( + 'MyObject', + () => { + fail('this should not be called.'); + }, + validatorFail + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate beforeFind', async done => { + Parse.Cloud.beforeFind('MyObject', () => {}, validatorSuccess); + try { + const MyObject = Parse.Object.extend('MyObject'); + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + done(); + } catch (e) { + fail('beforeFind should not have failed.'); + } + }); + it('validate beforeFind fail', async done => { + Parse.Cloud.beforeFind('MyObject', () => {}, validatorFail); + try { + const MyObject = Parse.Object.extend('MyObject'); + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterFind', async done => { + Parse.Cloud.afterFind('MyObject', () => {}, validatorSuccess); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + done(); + } catch (e) { + fail('beforeFind should not have failed.'); + } + }); + it('validate afterFind fail', async done => { + Parse.Cloud.afterFind('MyObject', () => {}, validatorFail); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('throw custom error from beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + }); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + done(); + } + }); + + it('validate beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + done(); + }); + + it('validate beforeSaveFile fail', async done => { + Parse.Cloud.beforeSaveFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterSaveFile', async done => { + Parse.Cloud.afterSaveFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + done(); + }); + + it('validate afterSaveFile fail', async done => { + Parse.Cloud.beforeSaveFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate beforeDeleteFile', async done => { + Parse.Cloud.beforeDeleteFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + done(); + }); + + it('validate beforeDeleteFile fail', async done => { + Parse.Cloud.beforeDeleteFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterDeleteFile', async done => { + Parse.Cloud.afterDeleteFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + done(); + }); + + it('validate afterDeleteFile fail', async done => { + Parse.Cloud.afterDeleteFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('Should have validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => {}, + () => { + throw 'error'; + } + ); + try { + await Parse.Cloud.run('myFunction'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); }); describe('beforeSave hooks', () => { diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index bb98306e97..efb57998d3 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -550,6 +550,81 @@ describe('ParseLiveQuery', function () { object.set({ foo: 'bar' }); await object.save(); }); + const validatorFail = () => { + throw 'you are not authorised'; + }; + it('can handle beforeConnect validation function', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeConnect(() => {}, validatorFail); + let complete = false; + Parse.LiveQuery.on('error', error => { + if (complete) { + return; + } + complete = true; + expect(error).toBe('you are not authorised'); + done(); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + await query.subscribe(); + }); + + it('can handle beforeSubscribe validation function', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeSubscribe(TestObject, () => {}, validatorFail); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('error', error => { + expect(error).toBe('you are not authorised'); + done(); + }); + }); + + it('can handle afterEvent validation function', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', () => {}, validatorFail); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + subscription.on('error', error => { + expect(error).toBe('you are not authorised'); + done(); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + }); it('can handle beforeConnect error', async done => { await reconfigureServer({ diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 705d7d0c84..01d1103f48 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -33,15 +33,6 @@ const addFileDataIfNeeded = async file => { return file; }; -const errorMessageFromError = e => { - if (typeof e === 'string') { - return e; - } else if (e && e.message) { - return e.message; - } - return undefined; -}; - export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { var router = express.Router(); @@ -192,10 +183,11 @@ export class FilesRouter { res.json(saveResult); } catch (e) { logger.error('Error creating a file: ', e); - const errorMessage = - errorMessageFromError(e) || - `Could not store file: ${fileObject.file._name}.`; - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, errorMessage)); + const error = triggers.resolveError(e, { + code: Parse.Error.FILE_SAVE_ERROR, + message: `Could not store file: ${fileObject.file._name}.`, + }); + next(error); } } @@ -227,8 +219,11 @@ export class FilesRouter { res.end(); } catch (e) { logger.error('Error deleting a file: ', e); - const errorMessage = errorMessageFromError(e) || `Could not delete file.`; - next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, errorMessage)); + const error = triggers.resolveError(e, { + code: Parse.Error.FILE_DELETE_ERROR, + message: 'Could not delete file.', + }); + next(error); } } diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index e40ede0502..7e08764910 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -110,36 +110,17 @@ export class FunctionsRouter extends PromiseRouter { }, error: function (message) { // parse error, process away - if (message instanceof Parse.Error) { - return reject(message); - } - - const code = Parse.Error.SCRIPT_FAILED; - // If it's an error, mark it as a script failed - if (typeof message === 'string') { - return reject(new Parse.Error(code, message)); - } - const error = new Parse.Error( - code, - (message && message.message) || message - ); - if (message instanceof Error) { - error.stack = message.stack; - } + const error = triggers.resolveError(message); reject(error); }, message: message, }; } - static handleCloudFunction(req) { const functionName = req.params.functionName; const applicationId = req.config.applicationId; const theFunction = triggers.getFunction(functionName, applicationId); - const theValidator = triggers.getValidator( - req.params.functionName, - applicationId - ); + if (!theFunction) { throw new Parse.Error( Parse.Error.SCRIPT_FAILED, @@ -160,16 +141,6 @@ export class FunctionsRouter extends PromiseRouter { context: req.info.context, }; - if (theValidator && typeof theValidator === 'function') { - var result = theValidator(request); - if (!result) { - throw new Parse.Error( - Parse.Error.VALIDATION_ERROR, - 'Validation failed.' - ); - } - } - return new Promise(function (resolve, reject) { const userString = req.auth && req.auth.user ? req.auth.user.id : undefined; @@ -212,6 +183,9 @@ export class FunctionsRouter extends PromiseRouter { } ); return Promise.resolve() + .then(() => { + return triggers.maybeRunValidator(request, functionName); + }) .then(() => { return theFunction(request, { message }); }) diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 01d1569bc1..e78264c1eb 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -37,6 +37,7 @@ var ParseCloud = {}; * @memberof Parse.Cloud * @param {String} name The name of the Cloud Function * @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ ParseCloud.define = function (functionName, handler, validationHandler) { triggers.addFunction( @@ -84,14 +85,16 @@ ParseCloud.job = function (functionName, handler) { * @name Parse.Cloud.beforeSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a save. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeSave = function (parseClass, handler) { +ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeSave, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -115,14 +118,16 @@ ParseCloud.beforeSave = function (parseClass, handler) { * @name Parse.Cloud.beforeDelete * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before delete function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a delete. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeDelete = function (parseClass, handler) { +ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeDelete, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -257,14 +262,16 @@ ParseCloud.afterLogout = function (handler) { * @name Parse.Cloud.afterSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a save. This function can be an async function and should take just one parameter, {@link Parse.Cloud.TriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.afterSave = function (parseClass, handler) { +ParseCloud.afterSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.afterSave, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -288,14 +295,16 @@ ParseCloud.afterSave = function (parseClass, handler) { * @name Parse.Cloud.afterDelete * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after delete function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a delete. This function can be async and should take just one parameter, {@link Parse.Cloud.TriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.afterDelete = function (parseClass, handler) { +ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.afterDelete, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -319,14 +328,16 @@ ParseCloud.afterDelete = function (parseClass, handler) { * @name Parse.Cloud.beforeFind * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before find function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.BeforeFindRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeFind = function (parseClass, handler) { +ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeFind, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -350,14 +361,16 @@ ParseCloud.beforeFind = function (parseClass, handler) { * @name Parse.Cloud.afterFind * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after find function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.AfterFindRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.afterFind = function (parseClass, handler) { +ParseCloud.afterFind = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); triggers.addTrigger( triggers.Types.afterFind, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -375,12 +388,14 @@ ParseCloud.afterFind = function (parseClass, handler) { * @method beforeSaveFile * @name Parse.Cloud.beforeSaveFile * @param {Function} func The function to run before saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeSaveFile = function (handler) { +ParseCloud.beforeSaveFile = function (handler, validationHandler) { triggers.addFileTrigger( triggers.Types.beforeSaveFile, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -398,12 +413,14 @@ ParseCloud.beforeSaveFile = function (handler) { * @method afterSaveFile * @name Parse.Cloud.afterSaveFile * @param {Function} func The function to run after saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.afterSaveFile = function (handler) { +ParseCloud.afterSaveFile = function (handler, validationHandler) { triggers.addFileTrigger( triggers.Types.afterSaveFile, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -421,12 +438,14 @@ ParseCloud.afterSaveFile = function (handler) { * @method beforeDeleteFile * @name Parse.Cloud.beforeDeleteFile * @param {Function} func The function to run before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeDeleteFile = function (handler) { +ParseCloud.beforeDeleteFile = function (handler, validationHandler) { triggers.addFileTrigger( triggers.Types.beforeDeleteFile, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -444,12 +463,14 @@ ParseCloud.beforeDeleteFile = function (handler) { * @method afterDeleteFile * @name Parse.Cloud.afterDeleteFile * @param {Function} func The function to after before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.afterDeleteFile = function (handler) { +ParseCloud.afterDeleteFile = function (handler, validationHandler) { triggers.addFileTrigger( triggers.Types.afterDeleteFile, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -467,12 +488,14 @@ ParseCloud.afterDeleteFile = function (handler) { * @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}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeConnect = function (handler) { +ParseCloud.beforeConnect = function (handler, validationHandler) { triggers.addConnectTrigger( triggers.Types.beforeConnect, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -496,14 +519,16 @@ ParseCloud.beforeConnect = function (handler) { * @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}. + * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. */ -ParseCloud.beforeSubscribe = function (parseClass, handler) { +ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeSubscribe, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; @@ -527,13 +552,18 @@ ParseCloud.onLiveQueryEvent = function (handler) { * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after live query event function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a live query event. This function can be async and should take one parameter, a {@link Parse.Cloud.LiveQueryEventTrigger}. */ -ParseCloud.afterLiveQueryEvent = function (parseClass, handler) { +ParseCloud.afterLiveQueryEvent = function ( + parseClass, + handler, + validationHandler +) { const className = getClassName(parseClass); triggers.addTrigger( triggers.Types.afterEvent, className, handler, - Parse.applicationId + Parse.applicationId, + validationHandler ); }; diff --git a/src/triggers.js b/src/triggers.js index a806d095c3..6cd98c9f95 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -25,7 +25,10 @@ const FileClassName = '@File'; const ConnectClassName = '@Connect'; const baseStore = function () { - const Validators = {}; + const Validators = Object.keys(Types).reduce(function (base, key) { + base[key] = {}; + return base; + }, {}); const Functions = {}; const Jobs = {}; const LiveQuery = []; @@ -125,24 +128,66 @@ export function addFunction( applicationId ) { add(Category.Functions, functionName, handler, applicationId); - add(Category.Validators, functionName, validationHandler, applicationId); + if (validationHandler) { + add(Category.Validators, functionName, validationHandler, applicationId); + } } export function addJob(jobName, handler, applicationId) { add(Category.Jobs, jobName, handler, applicationId); } -export function addTrigger(type, className, handler, applicationId) { +export function addTrigger( + type, + className, + handler, + applicationId, + validationHandler +) { validateClassNameForTriggers(className, type); add(Category.Triggers, `${type}.${className}`, handler, applicationId); + if (validationHandler) { + add( + Category.Validators, + `${type}.${className}`, + validationHandler, + applicationId + ); + } } -export function addFileTrigger(type, handler, applicationId) { +export function addFileTrigger( + type, + handler, + applicationId, + validationHandler +) { add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId); + if (validationHandler) { + add( + Category.Validators, + `${type}.${FileClassName}`, + validationHandler, + applicationId + ); + } } -export function addConnectTrigger(type, handler, applicationId) { +export function addConnectTrigger( + type, + handler, + applicationId, + validationHandler +) { add(Category.Triggers, `${type}.${ConnectClassName}`, handler, applicationId); + if (validationHandler) { + add( + Category.Validators, + `${type}.${ConnectClassName}`, + validationHandler, + applicationId + ); + } } export function addLiveQueryEventHandler(handler, applicationId) { @@ -455,6 +500,9 @@ export function maybeRunAfterFindTrigger( return Parse.Object.fromJSON(object); }); return Promise.resolve() + .then(() => { + return maybeRunValidator(request, `${triggerType}.${className}`); + }) .then(() => { const response = trigger(request); if (response && typeof response.then === 'function') { @@ -514,6 +562,9 @@ export function maybeRunQueryTrigger( isGet ); return Promise.resolve() + .then(() => { + return maybeRunValidator(requestObject, `${triggerType}.${className}`); + }) .then(() => { return trigger(requestObject); }) @@ -588,6 +639,142 @@ export function maybeRunQueryTrigger( ); } +export function resolveError(message, defaultOpts) { + if (!defaultOpts) { + defaultOpts = {}; + } + if (message instanceof Parse.Error) { + if (!message.message && defaultOpts.message) { + message.message = defaultOpts.message; + } + return message; + } + + const code = defaultOpts.code || Parse.Error.SCRIPT_FAILED; + // If it's an error, mark it as a script failed + if (typeof message === 'string') { + return new Parse.Error(code, message); + } + const error = new Parse.Error(code, (message && message.message) || message); + if (message instanceof Error) { + error.stack = message.stack; + } + return error; +} +export function maybeRunValidator(request, functionName) { + const theValidator = getValidator(functionName, Parse.applicationId); + if (!theValidator) { + return; + } + return new Promise((resolve, reject) => { + return Promise.resolve() + .then(() => { + return typeof theValidator === 'object' + ? inbuiltTriggerValidator(theValidator, request) + : theValidator(request); + }) + .then(result => { + if (result != null && !result) { + throw 'Validation failed.'; + } + resolve(); + }) + .catch(e => { + const error = resolveError(e, { + code: Parse.Error.VALIDATION_ERROR, + message: 'Validation failed.', + }); + reject(error); + }); + }); +} +function inbuiltTriggerValidator(options, request) { + if (!options) { + return; + } + if (options.requireUser && !request.user) { + throw 'Validation failed. Please login to continue.'; + } + if (options.requireMaster && !request.master) { + throw 'Validation failed. Master key is required to complete this request.'; + } + const requiredParam = key => { + const value = request.params[key]; + if (value == null) { + throw `Validation failed. Please specify data for ${key}.`; + } + }; + const getType = fn => { + const match = fn && fn.toString().match(/^\s*function (\w+)/); + return (match ? match[1] : '').toLowerCase(); + }; + if (Array.isArray(options.params)) { + for (const key of options.params) { + requiredParam(key); + } + } else { + for (const key in options.params) { + const opt = options.params[key]; + let val = request.params[key]; + if (typeof opt === 'string') { + requiredParam(opt); + } + if (typeof opt === 'object') { + if (opt.default != null && val == null) { + val = opt.default; + request.params[key] = val; + } + if (opt.required) { + requiredParam(key); + } + const type = getType(opt.type); + if (type == 'Array' && Array.isArray(val)) { + throw `Validation failed. Invalid type for ${key}. Expected Array.`; + } else if (typeof val !== type) { + throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; + } + let options = opt.options; + if (options) { + if (typeof opt.options == 'string') { + options = [opt.options]; + } + if (!options.includes(val)) { + throw `Validation failed. Invalid option for ${key}. Expected: ${options.join( + ', ' + )}`; + } + } + } + } + } + for (const key of options.requireKeys || []) { + if (!request.object) { + break; + } + if (request.object.get(key) == null) { + throw `Validation failed. Please specify data for ${key}.`; + } + } + for (const key of options.constantKeys || []) { + if ( + !request.object || + !request.original || + request.original.get(key) === null + ) { + break; + } + request.object.set(key, request.original.get(key)); + } + for (const key of options.requireUserKeys || []) { + if (!request.user) { + break; + } + if (request.user.get(key) == null) { + throw `Validation failed. Please set data for ${key} on your account.`; + } + } +} + // To be used as part of the promise chain when saving/deleting an object // Will resolve successfully if no trigger is configured // Resolves to an object, empty or containing an object key. A beforeSave @@ -657,6 +844,12 @@ export function maybeRunTrigger( // If triggers do not return a promise, they can run async code parallel // to the RestWrite.execute() call. return Promise.resolve() + .then(() => { + return maybeRunValidator( + request, + `${triggerType}.${parseObject.className}` + ); + }) .then(() => { const promise = trigger(request); if ( @@ -755,6 +948,7 @@ export async function maybeRunFileTrigger( fileObject, config ); + await maybeRunValidator(request, `${triggerType}.${FileClassName}`); const result = await fileTrigger(request); logTriggerSuccessBeforeHook( triggerType, @@ -788,6 +982,7 @@ export async function maybeRunConnectTrigger(triggerType, request) { return; } request.user = await userForSessionToken(request.sessionToken); + await maybeRunValidator(request, `${triggerType}.${ConnectClassName}`); return trigger(request); } @@ -804,6 +999,7 @@ export async function maybeRunSubscribeTrigger( parseQuery.withJSON(request.query); request.query = parseQuery; request.user = await userForSessionToken(request.sessionToken); + await maybeRunValidator(request, `${triggerType}.${className}`); await trigger(request); const query = request.query.toJSON(); if (query.keys) { @@ -828,6 +1024,7 @@ export async function maybeRunAfterEventTrigger( request.original = Parse.Object.fromJSON(request.original); } request.user = await userForSessionToken(request.sessionToken); + await maybeRunValidator(request, `${triggerType}.${className}`); return trigger(request); } From 7101e91cbdaa12eda4278f0632e6a10014e0a881 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 13:44:03 +1100 Subject: [PATCH 02/16] Update FunctionsRouter.js --- src/Routers/FunctionsRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 7e08764910..d3d6e013d8 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -187,7 +187,7 @@ export class FunctionsRouter extends PromiseRouter { return triggers.maybeRunValidator(request, functionName); }) .then(() => { - return theFunction(request, { message }); + return theFunction(request); }) .then(success, error); }); From ed07cd8586cf6ac2dcc7da8f1f39246968471d37 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 14:12:12 +1100 Subject: [PATCH 03/16] Update FunctionsRouter.js --- src/Routers/FunctionsRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d3d6e013d8..7e08764910 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -187,7 +187,7 @@ export class FunctionsRouter extends PromiseRouter { return triggers.maybeRunValidator(request, functionName); }) .then(() => { - return theFunction(request); + return theFunction(request, { message }); }) .then(success, error); }); From 3bc587bc9cf544e771333aed6b473cc651906c6d Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 15:17:04 +1100 Subject: [PATCH 04/16] Change params to fields --- spec/CloudCode.spec.js | 71 +++++++++++++++++++++++++++------ src/Routers/FunctionsRouter.js | 2 +- src/triggers.js | 73 +++++++++++++++++++--------------- 3 files changed, 99 insertions(+), 47 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 676b84b0fb..b0ca13d37f 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1869,7 +1869,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: ['a'], + fields: ['a'], } ); Parse.Cloud.run('hello', {}) @@ -1892,7 +1892,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: ['a'], + fields: ['a'], } ); Parse.Cloud.run('hello', {}) @@ -1916,7 +1916,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: ['a'], + fields: ['a'], } ); Parse.Cloud.run('hello', { a: 'yolo' }) @@ -1935,7 +1935,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: { + fields: { data: { type: String, }, @@ -1963,7 +1963,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: { + fields: { data: { type: String, default: 'yolo', @@ -1988,7 +1988,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: { + fields: { data: { type: String, required: true, @@ -2017,7 +2017,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: { + fields: { data: { type: String, required: true, @@ -2047,7 +2047,7 @@ describe('cloud functions', () => { return 'Hello world!'; }, { - params: { + fields: { data: { type: String, required: true, @@ -2069,6 +2069,39 @@ describe('cloud functions', () => { }); }); + it('set params options function', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + fields: { + data: { + type: Number, + required: true, + options: val => { + return val > 1 && val < 5; + }, + error: 'Validation failed. Expected data to be 1 and 5.', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 7 }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Expected data to be 1 and 5.' + ); + done(); + }); + }); + it('can create functions', done => { Parse.Cloud.define( 'hello', @@ -2078,7 +2111,7 @@ describe('cloud functions', () => { { requireUser: false, requireMaster: false, - params: { + fields: { data: { type: String, }, @@ -2139,7 +2172,14 @@ describe('cloud functions', () => { it('basic beforeSave requireKeys', function (done) { Parse.Cloud.beforeSave('beforeSaveRequire', () => {}, { - requireKeys: ['foo', 'bar'], + fields: { + foo: { + required: true, + }, + bar: { + required: true, + }, + }, }); const obj = new Parse.Object('beforeSaveRequire'); obj.set('foo', 'bar'); @@ -2158,12 +2198,17 @@ describe('cloud functions', () => { }); it('basic beforeSave constantKeys', async function (done) { Parse.Cloud.beforeSave('BeforeSave', () => {}, { - constantKeys: ['foo'], + fields: { + foo: { + constant: true, + default: 'bar', + }, + }, }); - const obj = new Parse.Object('BeforeSave'); - obj.set('foo', 'bar'); + obj.set('foo', 'far'); await obj.save(); + expect(obj.get('foo')).toBe('bar'); obj.set('foo', 'yolo'); await obj.save(); expect(obj.get('foo')).toBe('bar'); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d3d6e013d8..7e08764910 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -187,7 +187,7 @@ export class FunctionsRouter extends PromiseRouter { return triggers.maybeRunValidator(request, functionName); }) .then(() => { - return theFunction(request); + return theFunction(request, { message }); }) .then(success, error); }); diff --git a/src/triggers.js b/src/triggers.js index 6cd98c9f95..f99938b0a5 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -698,8 +698,12 @@ function inbuiltTriggerValidator(options, request) { if (options.requireMaster && !request.master) { throw 'Validation failed. Master key is required to complete this request.'; } + let params = request.params || {}; + if (request.object) { + params = request.object.toJSON(); + } const requiredParam = key => { - const value = request.params[key]; + const value = params[key]; if (value == null) { throw `Validation failed. Please specify data for ${key}.`; } @@ -708,63 +712,66 @@ function inbuiltTriggerValidator(options, request) { const match = fn && fn.toString().match(/^\s*function (\w+)/); return (match ? match[1] : '').toLowerCase(); }; - if (Array.isArray(options.params)) { - for (const key of options.params) { + if (Array.isArray(options.fields)) { + for (const key of options.fields) { requiredParam(key); } } else { - for (const key in options.params) { - const opt = options.params[key]; - let val = request.params[key]; + for (const key in options.fields) { + const opt = options.fields[key]; + let val = params[key]; if (typeof opt === 'string') { requiredParam(opt); } if (typeof opt === 'object') { if (opt.default != null && val == null) { val = opt.default; - request.params[key] = val; + params[key] = val; + if (request.object) { + request.object.set(key, val); + } + } + if (opt.constant && request.object) { + if (request.original) { + request.object.set(key, request.original.get(key)); + } else if (opt.default) { + request.object.set(key, opt.default); + } } if (opt.required) { requiredParam(key); } - const type = getType(opt.type); - if (type == 'Array' && Array.isArray(val)) { - throw `Validation failed. Invalid type for ${key}. Expected Array.`; - } else if (typeof val !== type) { - throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; + if (opt.type) { + const type = getType(opt.type); + if (type == 'Array' && Array.isArray(val)) { + throw `Validation failed. Invalid type for ${key}. Expected Array.`; + } else if (typeof val !== type) { + throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; + } } let options = opt.options; if (options) { + if (typeof options === 'function') { + const result = options(val); + if (!result) { + throw opt.error || `Validation failed. Invalid value for ${key}.`; + } + } if (typeof opt.options == 'string') { options = [opt.options]; } if (!options.includes(val)) { - throw `Validation failed. Invalid option for ${key}. Expected: ${options.join( - ', ' - )}`; + throw ( + opt.error || + `Validation failed. Invalid option for ${key}. Expected: ${options.join( + ', ' + )}` + ); } } } } } - for (const key of options.requireKeys || []) { - if (!request.object) { - break; - } - if (request.object.get(key) == null) { - throw `Validation failed. Please specify data for ${key}.`; - } - } - for (const key of options.constantKeys || []) { - if ( - !request.object || - !request.original || - request.original.get(key) === null - ) { - break; - } - request.object.set(key, request.original.get(key)); - } for (const key of options.requireUserKeys || []) { if (!request.user) { break; From 598af7a57708e58b04cd028b53418317035c4f69 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 16:06:17 +1100 Subject: [PATCH 05/16] Changes requested --- spec/CloudCode.Validator.spec.js | 854 +++++++++++++++++++++++++++++++ spec/CloudCode.spec.js | 830 ------------------------------ spec/ParseLiveQuery.spec.js | 12 +- src/Routers/FunctionsRouter.js | 1 - src/triggers.js | 80 ++- 5 files changed, 895 insertions(+), 882 deletions(-) create mode 100644 spec/CloudCode.Validator.spec.js diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js new file mode 100644 index 0000000000..c2a5e18714 --- /dev/null +++ b/spec/CloudCode.Validator.spec.js @@ -0,0 +1,854 @@ +'use strict'; +const Parse = require('parse/node'); +const validatorFail = () => { + throw 'you are not authorized'; +}; +const validatorSuccess = () => { + return true; +}; + +describe('cloud validator', () => { + it('existing validator functionality', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + return false; + } + ); + try { + await Parse.Cloud.run('myFunction', {}); + fail('should have thrown error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('complete validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => {} + ); + try { + const result = await Parse.Cloud.run('myFunction', {}); + expect(result).toBe('myFunc'); + done(); + } catch (e) { + fail('should not have thrown error'); + } + }); + it('Throw from validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + throw 'error'; + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + it('validator can throw parse error', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('should have validation error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(e.message).toBe('It should fail'); + done(); + } + }); + + it('validator can throw parse error with no message', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + () => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED); + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('should have validation error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(e.message).toBe('Validation failed.'); + done(); + } + }); + + it('async validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + async () => { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + throw 'async error'; + } + ); + try { + await Parse.Cloud.run('myFunction'); + fail('should have validation error'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + expect(e.message).toBe('async error'); + done(); + } + }); + + it('pass function to validator', async done => { + const validator = request => { + expect(request).toBeDefined(); + expect(request.params).toBeDefined(); + expect(request.master).toBe(false); + expect(request.user).toBeUndefined(); + expect(request.installationId).toBeDefined(); + expect(request.log).toBeDefined(); + expect(request.headers).toBeDefined(); + expect(request.functionName).toBeDefined(); + expect(request.context).toBeDefined(); + done(); + }; + Parse.Cloud.define( + 'myFunction', + () => { + return 'myFunc'; + }, + validator + ); + await Parse.Cloud.run('myFunction'); + }); + + it('require user on cloud functions', done => { + Parse.Cloud.define( + 'hello1', + () => { + return 'Hello world!'; + }, + { + requireUser: true, + } + ); + + Parse.Cloud.run('hello1', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please login to continue.' + ); + done(); + }); + }); + + it('require master on cloud functions', done => { + Parse.Cloud.define( + 'hello2', + () => { + return 'Hello world!'; + }, + { + requireMaster: true, + } + ); + Parse.Cloud.run('hello2', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Master key is required to complete this request.' + ); + done(); + }); + }); + + it('set params on cloud functions', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + fields: ['a'], + } + ); + Parse.Cloud.run('hello', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for a.' + ); + done(); + }); + }); + + it('set params on cloud functions', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + fields: ['a'], + } + ); + Parse.Cloud.run('hello', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for a.' + ); + done(); + }); + }); + + it('allow params on cloud functions', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.a).toEqual('yolo'); + return 'Hello world!'; + }, + { + fields: ['a'], + } + ); + Parse.Cloud.run('hello', { a: 'yolo' }) + .then(() => { + done(); + }) + .catch(() => { + fail('Error should not have been called.'); + }); + }); + + it('set params type', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + }, + }, + } + ); + Parse.Cloud.run('hello', { data: [] }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid type for data. Expected: string' + ); + done(); + }); + }); + + it('set params default', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + default: 'yolo', + }, + }, + } + ); + Parse.Cloud.run('hello') + .then(() => { + done(); + }) + .catch(() => { + fail('function should not have failed.'); + }); + }); + + it('set params required', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + required: true, + }, + }, + } + ); + Parse.Cloud.run('hello', {}) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for data.' + ); + done(); + }); + }); + + it('set params option', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + required: true, + options: 'a', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'f' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid option for data. Expected: a' + ); + done(); + }); + }); + + it('set params options', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + fields: { + data: { + type: String, + required: true, + options: ['a', 'b'], + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'f' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid option for data. Expected: a, b' + ); + done(); + }); + }); + + it('set params options function', done => { + Parse.Cloud.define( + 'hello', + req => { + expect(req.params.data).toBe('yolo'); + return 'Hello world!'; + }, + { + fields: { + data: { + type: Number, + required: true, + options: val => { + return val > 1 && val < 5; + }, + error: 'Validation failed. Expected data to be between 1 and 5.', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 7 }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Expected data to be between 1 and 5.' + ); + done(); + }); + }); + + it('can create functions', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + requireUser: false, + requireMaster: false, + fields: { + data: { + type: String, + }, + data1: { + type: String, + default: 'default', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'str' }).then(result => { + expect(result).toEqual('Hello world!'); + done(); + }); + }); + + it('basic beforeSave requireUser', function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUser: true, + }); + + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj + .save() + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please login to continue.' + ); + done(); + }); + }); + + it('basic beforeSave requireMaster', function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireMaster: true, + }); + + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj + .save() + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Master key is required to complete this request.' + ); + done(); + }); + }); + + it('basic beforeSave requireKeys', function (done) { + Parse.Cloud.beforeSave('beforeSaveRequire', () => {}, { + fields: { + foo: { + required: true, + }, + bar: { + required: true, + }, + }, + }); + const obj = new Parse.Object('beforeSaveRequire'); + obj.set('foo', 'bar'); + obj + .save() + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please specify data for bar.' + ); + done(); + }); + }); + it('basic beforeSave constantKeys', async function (done) { + Parse.Cloud.beforeSave('BeforeSave', () => {}, { + fields: { + foo: { + constant: true, + default: 'bar', + }, + }, + }); + const obj = new Parse.Object('BeforeSave'); + obj.set('foo', 'far'); + await obj.save(); + expect(obj.get('foo')).toBe('bar'); + obj.set('foo', 'yolo'); + await obj.save(); + expect(obj.get('foo')).toBe('bar'); + done(); + }); + + it('validate beforeSave', async done => { + Parse.Cloud.beforeSave('MyObject', () => {}, validatorSuccess); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + try { + await myObject.save(); + done(); + } catch (e) { + fail('before save should not have failed.'); + } + }); + it('validate beforeSave fail', async done => { + Parse.Cloud.beforeSave('MyObject', () => {}, validatorFail); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + try { + await myObject.save(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterSave', async done => { + Parse.Cloud.afterSave( + 'MyObject', + () => { + done(); + }, + validatorSuccess + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + try { + await myObject.save(); + } catch (e) { + fail('before save should not have failed.'); + } + }); + it('validate afterSave fail', async done => { + Parse.Cloud.afterSave( + 'MyObject', + () => { + fail('this should not be called.'); + }, + validatorFail + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + setTimeout(() => { + done(); + }, 1000); + }); + + it('validate beforeDelete', async done => { + Parse.Cloud.beforeDelete('MyObject', () => {}, validatorSuccess); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + done(); + } catch (e) { + fail('before delete should not have failed.'); + } + }); + it('validate beforeDelete fail', async done => { + Parse.Cloud.beforeDelete( + 'MyObject', + () => { + fail('this should not be called.'); + }, + validatorFail + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterDelete', async done => { + Parse.Cloud.afterDelete( + 'MyObject', + () => { + done(); + }, + validatorSuccess + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + } catch (e) { + fail('after delete should not have failed.'); + } + }); + it('validate afterDelete fail', async done => { + Parse.Cloud.afterDelete( + 'MyObject', + () => { + fail('this should not be called.'); + }, + validatorFail + ); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + await myObject.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate beforeFind', async done => { + Parse.Cloud.beforeFind('MyObject', () => {}, validatorSuccess); + try { + const MyObject = Parse.Object.extend('MyObject'); + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + done(); + } catch (e) { + fail('beforeFind should not have failed.'); + } + }); + it('validate beforeFind fail', async done => { + Parse.Cloud.beforeFind('MyObject', () => {}, validatorFail); + try { + const MyObject = Parse.Object.extend('MyObject'); + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterFind', async done => { + Parse.Cloud.afterFind('MyObject', () => {}, validatorSuccess); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + done(); + } catch (e) { + fail('beforeFind should not have failed.'); + } + }); + it('validate afterFind fail', async done => { + Parse.Cloud.afterFind('MyObject', () => {}, validatorFail); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + try { + const myObjectQuery = new Parse.Query(MyObject); + await myObjectQuery.find(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('throw custom error from beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + }); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + done(); + } + }); + + it('validate beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + done(); + }); + + it('validate beforeSaveFile fail', async done => { + Parse.Cloud.beforeSaveFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterSaveFile', async done => { + Parse.Cloud.afterSaveFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + done(); + }); + + it('validate afterSaveFile fail', async done => { + Parse.Cloud.beforeSaveFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate beforeDeleteFile', async done => { + Parse.Cloud.beforeDeleteFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + done(); + }); + + it('validate beforeDeleteFile fail', async done => { + Parse.Cloud.beforeDeleteFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('validate afterDeleteFile', async done => { + Parse.Cloud.afterDeleteFile(() => {}, validatorSuccess); + + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + done(); + }); + + it('validate afterDeleteFile fail', async done => { + Parse.Cloud.afterDeleteFile(() => {}, validatorFail); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save(); + await file.destroy(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); + + it('Should have validator', async done => { + Parse.Cloud.define( + 'myFunction', + () => {}, + () => { + throw 'error'; + } + ); + try { + await Parse.Cloud.run('myFunction'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + done(); + } + }); +}); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index b0ca13d37f..0255de352d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1693,836 +1693,6 @@ describe('cloud functions', () => { Parse.Cloud.run('myFunction', {}).then(() => done()); }); - - it('existing validator functionality', async done => { - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - () => { - return false; - } - ); - try { - await Parse.Cloud.run('myFunction', {}); - fail('should have thrown error'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('complete validator', async done => { - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - () => {} - ); - try { - const result = await Parse.Cloud.run('myFunction', {}); - expect(result).toBe('myFunc'); - done(); - } catch (e) { - fail('should not have thrown error'); - } - }); - it('Throw from validator', async done => { - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - () => { - throw 'error'; - } - ); - try { - await Parse.Cloud.run('myFunction'); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - it('validator can throw parse error', async done => { - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - () => { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); - } - ); - try { - await Parse.Cloud.run('myFunction'); - fail('should have validation error'); - } catch (e) { - expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); - expect(e.message).toBe('It should fail'); - done(); - } - }); - - it('async validator', async done => { - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - async () => { - await new Promise(resolve => { - setTimeout(() => { - resolve(); - }, 1000); - }); - throw 'async error'; - } - ); - try { - await Parse.Cloud.run('myFunction'); - fail('should have validation error'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - expect(e.message).toBe('async error'); - done(); - } - }); - - it('pass function to validator', async done => { - const validator = request => { - expect(request).toBeDefined(); - expect(request.params).toBeDefined(); - expect(request.master).toBe(false); - expect(request.user).toBeUndefined(); - expect(request.installationId).toBeDefined(); - expect(request.log).toBeDefined(); - expect(request.headers).toBeDefined(); - expect(request.functionName).toBeDefined(); - expect(request.context).toBeDefined(); - done(); - }; - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - validator - ); - await Parse.Cloud.run('myFunction'); - }); - - it('require user on cloud functions', done => { - Parse.Cloud.define( - 'hello1', - () => { - return 'Hello world!'; - }, - { - requireUser: true, - } - ); - - Parse.Cloud.run('hello1', {}) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Please login to continue.' - ); - done(); - }); - }); - - it('require master on cloud functions', done => { - Parse.Cloud.define( - 'hello2', - () => { - return 'Hello world!'; - }, - { - requireMaster: true, - } - ); - Parse.Cloud.run('hello2', {}) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Master key is required to complete this request.' - ); - done(); - }); - }); - - it('set params on cloud functions', done => { - Parse.Cloud.define( - 'hello', - () => { - return 'Hello world!'; - }, - { - fields: ['a'], - } - ); - Parse.Cloud.run('hello', {}) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Please specify data for a.' - ); - done(); - }); - }); - - it('set params on cloud functions', done => { - Parse.Cloud.define( - 'hello', - () => { - return 'Hello world!'; - }, - { - fields: ['a'], - } - ); - Parse.Cloud.run('hello', {}) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Please specify data for a.' - ); - done(); - }); - }); - - it('allow params on cloud functions', done => { - Parse.Cloud.define( - 'hello', - req => { - expect(req.params.a).toEqual('yolo'); - return 'Hello world!'; - }, - { - fields: ['a'], - } - ); - Parse.Cloud.run('hello', { a: 'yolo' }) - .then(() => { - done(); - }) - .catch(() => { - fail('Error should not have been called.'); - }); - }); - - it('set params type', done => { - Parse.Cloud.define( - 'hello', - () => { - return 'Hello world!'; - }, - { - fields: { - data: { - type: String, - }, - }, - } - ); - Parse.Cloud.run('hello', { data: [] }) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Invalid type for data. Expected: string' - ); - done(); - }); - }); - - it('set params default', done => { - Parse.Cloud.define( - 'hello', - req => { - expect(req.params.data).toBe('yolo'); - return 'Hello world!'; - }, - { - fields: { - data: { - type: String, - default: 'yolo', - }, - }, - } - ); - Parse.Cloud.run('hello') - .then(() => { - done(); - }) - .catch(() => { - fail('function should not have failed.'); - }); - }); - - it('set params required', done => { - Parse.Cloud.define( - 'hello', - req => { - expect(req.params.data).toBe('yolo'); - return 'Hello world!'; - }, - { - fields: { - data: { - type: String, - required: true, - }, - }, - } - ); - Parse.Cloud.run('hello', {}) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Please specify data for data.' - ); - done(); - }); - }); - - it('set params option', done => { - Parse.Cloud.define( - 'hello', - req => { - expect(req.params.data).toBe('yolo'); - return 'Hello world!'; - }, - { - fields: { - data: { - type: String, - required: true, - options: 'a', - }, - }, - } - ); - Parse.Cloud.run('hello', { data: 'f' }) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Invalid option for data. Expected: a' - ); - done(); - }); - }); - - it('set params options', done => { - Parse.Cloud.define( - 'hello', - req => { - expect(req.params.data).toBe('yolo'); - return 'Hello world!'; - }, - { - fields: { - data: { - type: String, - required: true, - options: ['a', 'b'], - }, - }, - } - ); - Parse.Cloud.run('hello', { data: 'f' }) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Invalid option for data. Expected: a, b' - ); - done(); - }); - }); - - it('set params options function', done => { - Parse.Cloud.define( - 'hello', - req => { - expect(req.params.data).toBe('yolo'); - return 'Hello world!'; - }, - { - fields: { - data: { - type: Number, - required: true, - options: val => { - return val > 1 && val < 5; - }, - error: 'Validation failed. Expected data to be 1 and 5.', - }, - }, - } - ); - Parse.Cloud.run('hello', { data: 7 }) - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Expected data to be 1 and 5.' - ); - done(); - }); - }); - - it('can create functions', done => { - Parse.Cloud.define( - 'hello', - () => { - return 'Hello world!'; - }, - { - requireUser: false, - requireMaster: false, - fields: { - data: { - type: String, - }, - data1: { - type: String, - default: 'default', - }, - }, - } - ); - Parse.Cloud.run('hello', { data: 'str' }).then(result => { - expect(result).toEqual('Hello world!'); - done(); - }); - }); - - it('basic beforeSave requireUser', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { - requireUser: true, - }); - - const obj = new Parse.Object('BeforeSaveFail'); - obj.set('foo', 'bar'); - obj - .save() - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Please login to continue.' - ); - done(); - }); - }); - - it('basic beforeSave requireMaster', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { - requireMaster: true, - }); - - const obj = new Parse.Object('BeforeSaveFail'); - obj.set('foo', 'bar'); - obj - .save() - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Master key is required to complete this request.' - ); - done(); - }); - }); - - it('basic beforeSave requireKeys', function (done) { - Parse.Cloud.beforeSave('beforeSaveRequire', () => {}, { - fields: { - foo: { - required: true, - }, - bar: { - required: true, - }, - }, - }); - const obj = new Parse.Object('beforeSaveRequire'); - obj.set('foo', 'bar'); - obj - .save() - .then(() => { - fail('function should have failed.'); - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - expect(error.message).toEqual( - 'Validation failed. Please specify data for bar.' - ); - done(); - }); - }); - it('basic beforeSave constantKeys', async function (done) { - Parse.Cloud.beforeSave('BeforeSave', () => {}, { - fields: { - foo: { - constant: true, - default: 'bar', - }, - }, - }); - const obj = new Parse.Object('BeforeSave'); - obj.set('foo', 'far'); - await obj.save(); - expect(obj.get('foo')).toBe('bar'); - obj.set('foo', 'yolo'); - await obj.save(); - expect(obj.get('foo')).toBe('bar'); - done(); - }); - - const validatorFail = () => { - throw 'you are not authorised'; - }; - const validatorSuccess = () => { - return true; - }; - it('validate beforeSave', async done => { - Parse.Cloud.beforeSave('MyObject', () => {}, validatorSuccess); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - try { - await myObject.save(); - done(); - } catch (e) { - fail('before save should not have failed.'); - } - }); - it('validate beforeSave fail', async done => { - Parse.Cloud.beforeSave('MyObject', () => {}, validatorFail); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - try { - await myObject.save(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate afterSave', async done => { - Parse.Cloud.afterSave( - 'MyObject', - () => { - done(); - }, - validatorSuccess - ); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - try { - await myObject.save(); - } catch (e) { - fail('before save should not have failed.'); - } - }); - it('validate afterSave fail', async done => { - Parse.Cloud.afterSave( - 'MyObject', - () => { - fail('this should not be called.'); - }, - validatorFail - ); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - setTimeout(() => { - done(); - }, 1000); - }); - - it('validate beforeDelete', async done => { - Parse.Cloud.beforeDelete('MyObject', () => {}, validatorSuccess); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - try { - await myObject.destroy(); - done(); - } catch (e) { - fail('before delete should not have failed.'); - } - }); - it('validate beforeDelete fail', async done => { - Parse.Cloud.beforeDelete( - 'MyObject', - () => { - fail('this should not be called.'); - }, - validatorFail - ); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - try { - await myObject.destroy(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate afterDelete', async done => { - Parse.Cloud.afterDelete( - 'MyObject', - () => { - done(); - }, - validatorSuccess - ); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - try { - await myObject.destroy(); - } catch (e) { - fail('after delete should not have failed.'); - } - }); - it('validate afterDelete fail', async done => { - Parse.Cloud.afterDelete( - 'MyObject', - () => { - fail('this should not be called.'); - }, - validatorFail - ); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - try { - await myObject.destroy(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate beforeFind', async done => { - Parse.Cloud.beforeFind('MyObject', () => {}, validatorSuccess); - try { - const MyObject = Parse.Object.extend('MyObject'); - const myObjectQuery = new Parse.Query(MyObject); - await myObjectQuery.find(); - done(); - } catch (e) { - fail('beforeFind should not have failed.'); - } - }); - it('validate beforeFind fail', async done => { - Parse.Cloud.beforeFind('MyObject', () => {}, validatorFail); - try { - const MyObject = Parse.Object.extend('MyObject'); - const myObjectQuery = new Parse.Query(MyObject); - await myObjectQuery.find(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate afterFind', async done => { - Parse.Cloud.afterFind('MyObject', () => {}, validatorSuccess); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - try { - const myObjectQuery = new Parse.Query(MyObject); - await myObjectQuery.find(); - done(); - } catch (e) { - fail('beforeFind should not have failed.'); - } - }); - it('validate afterFind fail', async done => { - Parse.Cloud.afterFind('MyObject', () => {}, validatorFail); - - const MyObject = Parse.Object.extend('MyObject'); - const myObject = new MyObject(); - await myObject.save(); - try { - const myObjectQuery = new Parse.Query(MyObject); - await myObjectQuery.find(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('throw custom error from beforeSaveFile', async done => { - Parse.Cloud.beforeSaveFile(() => { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); - }); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save({ useMasterKey: true }); - fail('error should have thrown'); - } catch (e) { - expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); - done(); - } - }); - - it('validate beforeSaveFile', async done => { - Parse.Cloud.beforeSaveFile(() => {}, validatorSuccess); - - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - const result = await file.save({ useMasterKey: true }); - expect(result).toBe(file); - done(); - }); - - it('validate beforeSaveFile fail', async done => { - Parse.Cloud.beforeSaveFile(() => {}, validatorFail); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save({ useMasterKey: true }); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate afterSaveFile', async done => { - Parse.Cloud.afterSaveFile(() => {}, validatorSuccess); - - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - const result = await file.save({ useMasterKey: true }); - expect(result).toBe(file); - done(); - }); - - it('validate afterSaveFile fail', async done => { - Parse.Cloud.beforeSaveFile(() => {}, validatorFail); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save({ useMasterKey: true }); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate beforeDeleteFile', async done => { - Parse.Cloud.beforeDeleteFile(() => {}, validatorSuccess); - - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save(); - await file.destroy(); - done(); - }); - - it('validate beforeDeleteFile fail', async done => { - Parse.Cloud.beforeDeleteFile(() => {}, validatorFail); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save(); - await file.destroy(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('validate afterDeleteFile', async done => { - Parse.Cloud.afterDeleteFile(() => {}, validatorSuccess); - - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save(); - await file.destroy(); - done(); - }); - - it('validate afterDeleteFile fail', async done => { - Parse.Cloud.afterDeleteFile(() => {}, validatorFail); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save(); - await file.destroy(); - fail('cloud function should have failed.'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - - it('Should have validator', async done => { - Parse.Cloud.define( - 'myFunction', - () => {}, - () => { - throw 'error'; - } - ); - try { - await Parse.Cloud.run('myFunction'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); }); describe('beforeSave hooks', () => { diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index efb57998d3..ff3795005c 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -2,6 +2,9 @@ const UserController = require('../lib/Controllers/UserController') .UserController; const Config = require('../lib/Config'); +const validatorFail = () => { + throw 'you are not authorized'; +}; describe('ParseLiveQuery', function () { it('can subscribe to query', async done => { await reconfigureServer({ @@ -550,9 +553,6 @@ describe('ParseLiveQuery', function () { object.set({ foo: 'bar' }); await object.save(); }); - const validatorFail = () => { - throw 'you are not authorised'; - }; it('can handle beforeConnect validation function', async done => { await reconfigureServer({ liveQuery: { @@ -573,7 +573,7 @@ describe('ParseLiveQuery', function () { return; } complete = true; - expect(error).toBe('you are not authorised'); + expect(error).toBe('you are not authorized'); done(); }); const query = new Parse.Query(TestObject); @@ -598,7 +598,7 @@ describe('ParseLiveQuery', function () { query.equalTo('objectId', object.id); const subscription = await query.subscribe(); subscription.on('error', error => { - expect(error).toBe('you are not authorised'); + expect(error).toBe('you are not authorized'); done(); }); }); @@ -617,7 +617,7 @@ describe('ParseLiveQuery', function () { const query = new Parse.Query(TestObject); const subscription = await query.subscribe(); subscription.on('error', error => { - expect(error).toBe('you are not authorised'); + expect(error).toBe('you are not authorized'); done(); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 7e08764910..85f5960196 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -109,7 +109,6 @@ export class FunctionsRouter extends PromiseRouter { }); }, error: function (message) { - // parse error, process away const error = triggers.resolveError(message); reject(error); }, diff --git a/src/triggers.js b/src/triggers.js index f99938b0a5..52494acad6 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -128,9 +128,7 @@ export function addFunction( applicationId ) { add(Category.Functions, functionName, handler, applicationId); - if (validationHandler) { - add(Category.Validators, functionName, validationHandler, applicationId); - } + add(Category.Validators, functionName, validationHandler, applicationId); } export function addJob(jobName, handler, applicationId) { @@ -146,14 +144,12 @@ export function addTrigger( ) { validateClassNameForTriggers(className, type); add(Category.Triggers, `${type}.${className}`, handler, applicationId); - if (validationHandler) { - add( - Category.Validators, - `${type}.${className}`, - validationHandler, - applicationId - ); - } + add( + Category.Validators, + `${type}.${className}`, + validationHandler, + applicationId + ); } export function addFileTrigger( @@ -163,14 +159,12 @@ export function addFileTrigger( validationHandler ) { add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId); - if (validationHandler) { - add( - Category.Validators, - `${type}.${FileClassName}`, - validationHandler, - applicationId - ); - } + add( + Category.Validators, + `${type}.${FileClassName}`, + validationHandler, + applicationId + ); } export function addConnectTrigger( @@ -180,14 +174,12 @@ export function addConnectTrigger( validationHandler ) { add(Category.Triggers, `${type}.${ConnectClassName}`, handler, applicationId); - if (validationHandler) { - add( - Category.Validators, - `${type}.${ConnectClassName}`, - validationHandler, - applicationId - ); - } + add( + Category.Validators, + `${type}.${ConnectClassName}`, + validationHandler, + applicationId + ); } export function addLiveQueryEventHandler(handler, applicationId) { @@ -639,27 +631,28 @@ export function maybeRunQueryTrigger( ); } -export function resolveError(message, defaultOpts) { +export function resolveError(error, defaultOpts) { if (!defaultOpts) { defaultOpts = {}; } - if (message instanceof Parse.Error) { - if (!message.message && defaultOpts.message) { - message.message = defaultOpts.message; - } - return message; + if (!error) { + return new Parse.Error( + defaultOpts.code || Parse.Error.SCRIPT_FAILED, + defaultOpts.message || 'Script failed.' + ); } - - const code = defaultOpts.code || Parse.Error.SCRIPT_FAILED; - // If it's an error, mark it as a script failed - if (typeof message === 'string') { - return new Parse.Error(code, message); + let errorStr = null; + if (typeof error === 'string') { + errorStr = error; } - const error = new Parse.Error(code, (message && message.message) || message); - if (message instanceof Error) { - error.stack = message.stack; + const code = error.code || defaultOpts.code || Parse.Error.SCRIPT_FAILED; + const message = + error.message || errorStr || defaultOpts.message || 'Script failed.'; + const resolvedError = new Parse.Error(code, message); + if (error.stack) { + resolvedError.stack = error.stack; } - return error; + return resolvedError; } export function maybeRunValidator(request, functionName) { const theValidator = getValidator(functionName, Parse.applicationId); @@ -689,9 +682,6 @@ export function maybeRunValidator(request, functionName) { }); } function inbuiltTriggerValidator(options, request) { - if (!options) { - return; - } if (options.requireUser && !request.user) { throw 'Validation failed. Please login to continue.'; } From ab1e21eeed1daca738d10da76c447f7db0e16b34 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 16:30:14 +1100 Subject: [PATCH 06/16] Fix failing tests --- spec/CloudCode.Validator.spec.js | 16 +--------------- spec/CloudCode.spec.js | 14 ++++++++++++++ src/triggers.js | 26 ++++++++++++++------------ 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index c2a5e18714..8293504410 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -96,7 +96,7 @@ describe('cloud validator', () => { fail('should have validation error'); } catch (e) { expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); - expect(e.message).toBe('Validation failed.'); + expect(e.message).toBeUndefined(); done(); } }); @@ -736,20 +736,6 @@ describe('cloud validator', () => { } }); - it('throw custom error from beforeSaveFile', async done => { - Parse.Cloud.beforeSaveFile(() => { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); - }); - try { - const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); - await file.save({ useMasterKey: true }); - fail('error should have thrown'); - } catch (e) { - expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); - done(); - } - }); - it('validate beforeSaveFile', async done => { Parse.Cloud.beforeSaveFile(() => {}, validatorSuccess); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 0255de352d..443dcf5161 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2675,6 +2675,20 @@ describe('beforeLogin hook', () => { expect(result).toBe(file); }); + it('throw custom error from beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + }); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + done(); + } + }); + it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); diff --git a/src/triggers.js b/src/triggers.js index 52494acad6..7bfc40cdae 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -631,28 +631,30 @@ export function maybeRunQueryTrigger( ); } -export function resolveError(error, defaultOpts) { +export function resolveError(message, defaultOpts) { if (!defaultOpts) { defaultOpts = {}; } - if (!error) { + if (!message) { return new Parse.Error( defaultOpts.code || Parse.Error.SCRIPT_FAILED, defaultOpts.message || 'Script failed.' ); } - let errorStr = null; - if (typeof error === 'string') { - errorStr = error; + if (message instanceof Parse.Error) { + return message; } - const code = error.code || defaultOpts.code || Parse.Error.SCRIPT_FAILED; - const message = - error.message || errorStr || defaultOpts.message || 'Script failed.'; - const resolvedError = new Parse.Error(code, message); - if (error.stack) { - resolvedError.stack = error.stack; + + const code = defaultOpts.code || Parse.Error.SCRIPT_FAILED; + // If it's an error, mark it as a script failed + if (typeof message === 'string') { + return new Parse.Error(code, message); + } + const error = new Parse.Error(code, message.message || message); + if (message instanceof Error) { + error.stack = message.stack; } - return resolvedError; + return error; } export function maybeRunValidator(request, functionName) { const theValidator = getValidator(functionName, Parse.applicationId); From 410c941e703790b5e6d025de52aa8ac63f1e56a1 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 17:52:10 +1100 Subject: [PATCH 07/16] More tests --- spec/CloudCode.Validator.spec.js | 201 ++++++++++++++++++++++++++++++- src/cloud-code/Parse.Cloud.js | 50 +++++++- src/triggers.js | 34 ++++-- 3 files changed, 275 insertions(+), 10 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 8293504410..f188e54089 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -262,6 +262,33 @@ describe('cloud validator', () => { }); }); + it('set params type array', done => { + Parse.Cloud.define( + 'hello', + () => { + return 'Hello world!'; + }, + { + fields: { + data: { + type: Array, + }, + }, + } + ); + Parse.Cloud.run('hello', { data: '' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid type for data. Expected: array' + ); + done(); + }); + }); + it('set params type', done => { Parse.Cloud.define( 'hello', @@ -406,8 +433,8 @@ describe('cloud validator', () => { it('set params options function', done => { Parse.Cloud.define( 'hello', - req => { - expect(req.params.data).toBe('yolo'); + () => { + fail('cloud function should not run.'); return 'Hello world!'; }, { @@ -436,6 +463,95 @@ describe('cloud validator', () => { }); }); + it('can run params function on null', done => { + Parse.Cloud.define( + 'hello', + () => { + fail('cloud function should not run.'); + return 'Hello world!'; + }, + { + fields: { + data: { + options: val => { + return val.length > 5; + }, + error: 'Validation failed. String should be at least 5 characters', + }, + }, + } + ); + Parse.Cloud.run('hello', { data: null }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. String should be at least 5 characters' + ); + done(); + }); + }); + + it('can throw from options validator', done => { + Parse.Cloud.define( + 'hello', + () => { + fail('cloud function should not run.'); + return 'Hello world!'; + }, + { + fields: { + data: { + options: () => { + throw 'validation failed.'; + }, + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'a' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('validation failed.'); + done(); + }); + }); + + it('can throw null from options validator', done => { + Parse.Cloud.define( + 'hello', + () => { + fail('cloud function should not run.'); + return 'Hello world!'; + }, + { + fields: { + data: { + options: () => { + throw null; + }, + }, + }, + } + ); + Parse.Cloud.run('hello', { data: 'a' }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Invalid value for data.' + ); + done(); + }); + }); + it('can create functions', done => { Parse.Cloud.define( 'hello', @@ -462,6 +578,70 @@ describe('cloud validator', () => { }); }); + it('basic beforeSave requireUserKey', async function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUser: true, + requireUserKeys: ['name'], + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + user.set('name', 'foo'); + await user.save(null, { sessionToken: user.getSessionToken() }); + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + await obj.save(null, { sessionToken: user.getSessionToken() }); + expect(obj.get('foo')).toBe('bar'); + done(); + }); + + it('basic beforeSave requireUserKey on User Class', async function (done) { + Parse.Cloud.beforeSave(Parse.User, () => {}, { + requireUser: true, + requireUserKeys: ['name'], + }); + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'p@ssword'); + user.set('name', 'foo'); + expect(user.get('name')).toBe('foo'); + done(); + }); + + it('basic beforeSave requireUserKey rejection', async function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUser: true, + requireUserKeys: ['name'], + }); + const user = await Parse.User.signUp('testuser', 'p@ssword'); + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + try { + await obj.save(null, { sessionToken: user.getSessionToken() }); + fail('should not have been able to save without userkey'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please set data for name on your account.' + ); + done(); + } + }); + + it('basic beforeSave requireUserKey without user', async function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUserKeys: ['name'], + }); + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + try { + await obj.save(); + fail('should not have been able to save without user'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Please login to make this request.'); + done(); + } + }); + it('basic beforeSave requireUser', function (done) { Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { requireUser: true, @@ -549,6 +729,23 @@ describe('cloud validator', () => { done(); }); + it('basic beforeSave defaultKeys', async function (done) { + Parse.Cloud.beforeSave('BeforeSave', () => {}, { + fields: { + foo: { + default: 'bar', + }, + }, + }); + const obj = new Parse.Object('BeforeSave'); + await obj.save(); + expect(obj.get('foo')).toBe('bar'); + obj.set('foo', 'yolo'); + await obj.save(); + expect(obj.get('foo')).toBe('yolo'); + done(); + }); + it('validate beforeSave', async done => { Parse.Cloud.beforeSave('MyObject', () => {}, validatorSuccess); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index e78264c1eb..b095ccc587 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -85,7 +85,7 @@ ParseCloud.job = function (functionName, handler) { * @name Parse.Cloud.beforeSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a save. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function or object to help validating cloud code. This object should be a {@link Parse.Cloud.ValidatorObject}, and this function can be an async function and should take one parameter a {@link Parse.Cloud.ValidatorFunction}. Returning false from this function will return a "validation fail" error. */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -667,6 +667,54 @@ module.exports = ParseCloud; * @property {Object} params The params passed to the cloud function. */ +/** + * @interface Parse.Cloud.ValidatorFunction + * @property {String} installationId If set, the installationId triggering the request. + * @property {Boolean} master If true, means the master key was used. + * @property {Parse.User} user If set, the user that made the request. + * @property {Object} params The params passed to the cloud function. + */ + +/** + * @interface Parse.Cloud.ValidatorObject + * @property {Boolean} requireUser whether the cloud trigger requires a user. + * @property {Boolean} requireMaster whether the cloud trigger requires a master key. + * @property {Object|Array} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, {@link Parse.Cloud.ValidatorFields} to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`. + * @property {Array} requireUserKeys If set, keys required on request.user to make the request. + */ + +/** + * +``` +Parse.Cloud.beforeSave('ClassName', request => { + // request.object.get('name') is defined and longer than 5 +},{ + fields: { + name: { + type: String, + default: 'default', + options: val => { + return val.length > 5; + }, + error: 'name must be longer than 5 characters' + }, + admin: { + default: false, + constant: true + } + } +}) + ``` + * @interface Parse.Cloud.ValidatorFields + * @property {String} field name of field to validate. + * @property {String} field.type expected type of data for field. + * @property {Boolean} field.constant whether the field can be modified on the object. + * @property {Any} field.default default value if field is `null`, or initial value `constant` is `true`. + * @property {Array|function} field.options array of options that the field can be, or function to validate field. Return false if value is invalid. + * @property {String} field.error custom error message if field is invalid. + * + */ + /** * @interface Parse.Cloud.JobRequest * @property {Object} params The params passed to the background job. diff --git a/src/triggers.js b/src/triggers.js index 7bfc40cdae..7371585fa8 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -684,7 +684,16 @@ export function maybeRunValidator(request, functionName) { }); } function inbuiltTriggerValidator(options, request) { - if (options.requireUser && !request.user) { + let reqUser = request.user; + if ( + !reqUser && + request.object && + request.object.className === '_User' && + !request.object.existed() + ) { + reqUser = request.object; + } + if (options.requireUser && !reqUser) { throw 'Validation failed. Please login to continue.'; } if (options.requireMaster && !request.master) { @@ -744,9 +753,20 @@ function inbuiltTriggerValidator(options, request) { let options = opt.options; if (options) { if (typeof options === 'function') { - const result = options(val); - if (!result) { - throw opt.error || `Validation failed. Invalid value for ${key}.`; + try { + const result = options(val); + if (!result) { + throw ( + opt.error || `Validation failed. Invalid value for ${key}.` + ); + } + } catch (e) { + if (!e) { + throw ( + opt.error || `Validation failed. Invalid value for ${key}.` + ); + } + throw opt.error || e.message || e; } } if (typeof opt.options == 'string') { @@ -765,10 +785,10 @@ function inbuiltTriggerValidator(options, request) { } } for (const key of options.requireUserKeys || []) { - if (!request.user) { - break; + if (!reqUser) { + throw 'Please login to make this request.'; } - if (request.user.get(key) == null) { + if (reqUser.get(key) == null) { throw `Validation failed. Please set data for ${key} on your account.`; } } From 8b50fe25038aa0c5799032e01fa0662185aa2723 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 18:07:02 +1100 Subject: [PATCH 08/16] More tests --- spec/CloudCode.spec.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 443dcf5161..01af45055f 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -112,7 +112,20 @@ describe('Cloud Code', () => { } ); }); + it('returns an empty error', done => { + Parse.Cloud.define('cloudCodeWithError', () => { + throw null; + }); + Parse.Cloud.run('cloudCodeWithError').then( + () => done.fail('should not succeed'), + e => { + expect(e.code).toEqual(141); + expect(e.message).toEqual('Script failed.'); + done(); + } + ); + }); it('beforeSave rejection with custom error code', function (done) { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { throw new Parse.Error(999, 'Nope'); @@ -2689,6 +2702,20 @@ describe('beforeLogin hook', () => { } }); + it('throw empty error from beforeSaveFile', async done => { + Parse.Cloud.beforeSaveFile(() => { + throw null; + }); + try { + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(130); + done(); + } + }); + it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); From ce0987e82bbc2fd47e829da8cbbe40f825edd49c Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 18:14:12 +1100 Subject: [PATCH 09/16] Remove existing functionality --- spec/CloudCode.Validator.spec.js | 19 ------------------- src/cloud-code/Parse.Cloud.js | 2 +- src/triggers.js | 5 +---- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index f188e54089..4178b2dcd1 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -8,25 +8,6 @@ const validatorSuccess = () => { }; describe('cloud validator', () => { - it('existing validator functionality', async done => { - Parse.Cloud.define( - 'myFunction', - () => { - return 'myFunc'; - }, - () => { - return false; - } - ); - try { - await Parse.Cloud.run('myFunction', {}); - fail('should have thrown error'); - } catch (e) { - expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); - done(); - } - }); - it('complete validator', async done => { Parse.Cloud.define( 'myFunction', diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index b095ccc587..a82b43decb 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -710,7 +710,7 @@ Parse.Cloud.beforeSave('ClassName', request => { * @property {String} field.type expected type of data for field. * @property {Boolean} field.constant whether the field can be modified on the object. * @property {Any} field.default default value if field is `null`, or initial value `constant` is `true`. - * @property {Array|function} field.options array of options that the field can be, or function to validate field. Return false if value is invalid. + * @property {Array|function} field.options array of options that the field can be, or function to validate field. Throw an error if value is invalid. * @property {String} field.error custom error message if field is invalid. * */ diff --git a/src/triggers.js b/src/triggers.js index 7371585fa8..c82d62c72d 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -668,10 +668,7 @@ export function maybeRunValidator(request, functionName) { ? inbuiltTriggerValidator(theValidator, request) : theValidator(request); }) - .then(result => { - if (result != null && !result) { - throw 'Validation failed.'; - } + .then(() => { resolve(); }) .catch(e => { From f62ccad573d176f5439498820ee6bee2e7e7c202 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 18:18:46 +1100 Subject: [PATCH 10/16] Remove legacy tests --- spec/ParseAPI.spec.js | 118 ++++++++++++++++-------------------------- 1 file changed, 44 insertions(+), 74 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 8e0af10ac6..dd98716458 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -24,7 +24,7 @@ const headers = { }; describe_only_db('mongo')('miscellaneous', () => { - it('test rest_create_app', function(done) { + it('test rest_create_app', function (done) { let appId; Parse._request('POST', 'rest_create_app') .then(res => { @@ -57,19 +57,19 @@ describe_only_db('mongo')('miscellaneous', () => { }); }); -describe('miscellaneous', function() { - it('create a GameScore object', function(done) { +describe('miscellaneous', function () { + it('create a GameScore object', function (done) { const obj = new Parse.Object('GameScore'); obj.set('score', 1337); - obj.save().then(function(obj) { + obj.save().then(function (obj) { expect(typeof obj.id).toBe('string'); expect(typeof obj.createdAt.toGMTString()).toBe('string'); done(); }, done.fail); }); - it('get a TestObject', function(done) { - create({ bloop: 'blarg' }, async function(obj) { + it('get a TestObject', function (done) { + create({ bloop: 'blarg' }, async function (obj) { const t2 = new TestObject({ objectId: obj.id }); const obj2 = await t2.fetch(); expect(obj2.get('bloop')).toEqual('blarg'); @@ -79,8 +79,8 @@ describe('miscellaneous', function() { }); }); - it('create a valid parse user', function(done) { - createTestUser().then(function(data) { + it('create a valid parse user', function (done) { + createTestUser().then(function (data) { expect(data.id).not.toBeUndefined(); expect(data.getSessionToken()).not.toBeUndefined(); expect(data.get('password')).toBeUndefined(); @@ -297,8 +297,8 @@ describe('miscellaneous', function() { }); }); - it('succeed in logging in', function(done) { - createTestUser().then(async function(u) { + it('succeed in logging in', function (done) { + createTestUser().then(async function (u) { expect(typeof u.id).toEqual('string'); const user = await Parse.User.logIn('test', 'moon-y'); @@ -310,7 +310,7 @@ describe('miscellaneous', function() { }, fail); }); - it('increment with a user object', function(done) { + it('increment with a user object', function (done) { createTestUser() .then(user => { user.increment('foo'); @@ -338,7 +338,7 @@ describe('miscellaneous', function() { ); }); - it('save various data types', function(done) { + it('save various data types', function (done) { const obj = new TestObject(); obj.set('date', new Date()); obj.set('array', [1, 2, 3]); @@ -358,7 +358,7 @@ describe('miscellaneous', function() { }); }); - it('query with limit', function(done) { + it('query with limit', function (done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); baz @@ -383,7 +383,7 @@ describe('miscellaneous', function() { ); }); - it('query without limit get default 100 records', function(done) { + it('query without limit get default 100 records', function (done) { const objects = []; for (let i = 0; i < 150; i++) { objects.push(new TestObject({ name: 'name' + i })); @@ -404,7 +404,7 @@ describe('miscellaneous', function() { ); }); - it('basic saveAll', function(done) { + it('basic saveAll', function (done) { const alpha = new TestObject({ letter: 'alpha' }); const beta = new TestObject({ letter: 'beta' }); Parse.Object.saveAll([alpha, beta]) @@ -425,26 +425,26 @@ describe('miscellaneous', function() { ); }); - it('test beforeSave set object acl success', function(done) { + it('test beforeSave set object acl success', function (done) { const acl = new Parse.ACL({ '*': { read: true, write: false }, }); - Parse.Cloud.beforeSave('BeforeSaveAddACL', function(req) { + Parse.Cloud.beforeSave('BeforeSaveAddACL', function (req) { req.object.setACL(acl); }); const obj = new Parse.Object('BeforeSaveAddACL'); obj.set('lol', true); obj.save().then( - function() { + function () { const query = new Parse.Query('BeforeSaveAddACL'); query.get(obj.id).then( - function(objAgain) { + function (objAgain) { expect(objAgain.get('lol')).toBeTruthy(); expect(objAgain.getACL().equals(acl)); done(); }, - function(error) { + function (error) { fail(error); done(); } @@ -667,10 +667,10 @@ describe('miscellaneous', function() { }); }); - it('test afterSave get full object on create and update', function(done) { + it('test afterSave get full object on create and update', function (done) { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.afterSave('GameScore', function(req) { + Parse.Cloud.afterSave('GameScore', function (req) { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.id).not.toBeUndefined(); @@ -694,29 +694,29 @@ describe('miscellaneous', function() { obj.set('fooAgain', 'barAgain'); obj .save() - .then(function() { + .then(function () { // We only update foo obj.set('foo', 'baz'); return obj.save(); }) .then( - function() { + function () { // Make sure the checking has been triggered expect(triggerTime).toBe(2); done(); }, - function(error) { + function (error) { fail(error); done(); } ); }); - it('test afterSave get original object on update', function(done) { + it('test afterSave get original object on update', function (done) { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.afterSave('GameScore', function(req) { + Parse.Cloud.afterSave('GameScore', function (req) { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.get('fooAgain')).toEqual('barAgain'); @@ -750,18 +750,18 @@ describe('miscellaneous', function() { obj.set('fooAgain', 'barAgain'); obj .save() - .then(function() { + .then(function () { // We only update foo obj.set('foo', 'baz'); return obj.save(); }) .then( - function() { + function () { // Make sure the checking has been triggered expect(triggerTime).toBe(2); done(); }, - function(error) { + function (error) { jfail(error); done(); } @@ -771,7 +771,7 @@ describe('miscellaneous', function() { it('test afterSave get full original object even req auth can not query it', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.afterSave('GameScore', function(req) { + Parse.Cloud.afterSave('GameScore', function (req) { const object = req.object; const originalObject = req.original; if (triggerTime == 0) { @@ -802,18 +802,18 @@ describe('miscellaneous', function() { obj.setACL(acl); obj .save() - .then(function() { + .then(function () { // We only update foo obj.set('foo', 'baz'); return obj.save(); }) .then( - function() { + function () { // Make sure the checking has been triggered expect(triggerTime).toBe(2); done(); }, - function(error) { + function (error) { jfail(error); done(); } @@ -823,7 +823,7 @@ describe('miscellaneous', function() { it('afterSave flattens custom operations', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.afterSave('GameScore', function(req) { + Parse.Cloud.afterSave('GameScore', function (req) { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); const originalObject = req.original; @@ -865,7 +865,7 @@ describe('miscellaneous', function() { it('beforeSave receives ACL', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', function(req) { + Parse.Cloud.beforeSave('GameScore', function (req) { const object = req.object; if (triggerTime == 0) { const acl = object.getACL(); @@ -909,7 +909,7 @@ describe('miscellaneous', function() { it('afterSave receives ACL', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.afterSave('GameScore', function(req) { + Parse.Cloud.afterSave('GameScore', function (req) { const object = req.object; if (triggerTime == 0) { const acl = object.getACL(); @@ -1057,14 +1057,14 @@ describe('miscellaneous', function() { ); }); - it('test beforeSave/afterSave get installationId', function(done) { + it('test beforeSave/afterSave get installationId', function (done) { let triggerTime = 0; - Parse.Cloud.beforeSave('GameScore', function(req) { + Parse.Cloud.beforeSave('GameScore', function (req) { triggerTime++; expect(triggerTime).toEqual(1); expect(req.installationId).toEqual('yolo'); }); - Parse.Cloud.afterSave('GameScore', function(req) { + Parse.Cloud.afterSave('GameScore', function (req) { triggerTime++; expect(triggerTime).toEqual(2); expect(req.installationId).toEqual('yolo'); @@ -1087,14 +1087,14 @@ describe('miscellaneous', function() { }); }); - it('test beforeDelete/afterDelete get installationId', function(done) { + it('test beforeDelete/afterDelete get installationId', function (done) { let triggerTime = 0; - Parse.Cloud.beforeDelete('GameScore', function(req) { + Parse.Cloud.beforeDelete('GameScore', function (req) { triggerTime++; expect(triggerTime).toEqual(1); expect(req.installationId).toEqual('yolo'); }); - Parse.Cloud.afterDelete('GameScore', function(req) { + Parse.Cloud.afterDelete('GameScore', function (req) { triggerTime++; expect(triggerTime).toEqual(2); expect(req.installationId).toEqual('yolo'); @@ -1170,33 +1170,6 @@ describe('miscellaneous', function() { }); }); - it('test cloud function parameter validation', done => { - // Register a function with validation - Parse.Cloud.define( - 'functionWithParameterValidationFailure', - () => { - return 'noway'; - }, - request => { - return request.params.success === 100; - } - ); - - Parse.Cloud.run('functionWithParameterValidationFailure', { - success: 500, - }).then( - () => { - fail('Validation should not have succeeded'); - done(); - }, - e => { - expect(e.code).toEqual(142); - expect(e.message).toEqual('Validation failed.'); - done(); - } - ); - }); - it('can handle null params in cloud functions (regression test for #1742)', done => { Parse.Cloud.define('func', request => { expect(request.params.nullParam).toEqual(null); @@ -1715,10 +1688,7 @@ describe('miscellaneous', function() { it('purge empty class', done => { const testSchema = new Parse.Schema('UnknownClass'); - testSchema - .purge() - .then(done) - .catch(done.fail); + testSchema.purge().then(done).catch(done.fail); }); it('should not update schema beforeSave #2672', done => { From c11d4b91a3391af3644193e113241ffb591a5ca3 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 18:45:35 +1100 Subject: [PATCH 11/16] fix array typo --- src/triggers.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/triggers.js b/src/triggers.js index c82d62c72d..f24452eabf 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -741,8 +741,8 @@ function inbuiltTriggerValidator(options, request) { } if (opt.type) { const type = getType(opt.type); - if (type == 'Array' && Array.isArray(val)) { - throw `Validation failed. Invalid type for ${key}. Expected Array.`; + if (type == 'array' && !Array.isArray(val)) { + throw `Validation failed. Invalid type for ${key}. Expected: array.`; } else if (typeof val !== type) { throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; } From 05ab004601b485507d0d23814025294253c49690 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 24 Oct 2020 18:55:34 +1100 Subject: [PATCH 12/16] Update triggers.js --- src/triggers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/triggers.js b/src/triggers.js index f24452eabf..46281c2969 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -742,7 +742,7 @@ function inbuiltTriggerValidator(options, request) { if (opt.type) { const type = getType(opt.type); if (type == 'array' && !Array.isArray(val)) { - throw `Validation failed. Invalid type for ${key}. Expected: array.`; + throw `Validation failed. Invalid type for ${key}. Expected: array`; } else if (typeof val !== type) { throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; } From 32ced98353f2548b86292bce25e4960cf7ab5276 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 25 Oct 2020 12:07:00 +1100 Subject: [PATCH 13/16] Docs --- src/cloud-code/Parse.Cloud.js | 70 +++++++++++++++-------------------- src/triggers.js | 4 +- 2 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index a82b43decb..3d18a2f86c 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -37,7 +37,7 @@ var ParseCloud = {}; * @memberof Parse.Cloud * @param {String} name The name of the Cloud Function * @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.define = function (functionName, handler, validationHandler) { triggers.addFunction( @@ -85,7 +85,7 @@ ParseCloud.job = function (functionName, handler) { * @name Parse.Cloud.beforeSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a save. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; - * @param {(Object|Function)} validator An optional function or object to help validating cloud code. This object should be a {@link Parse.Cloud.ValidatorObject}, and this function can be an async function and should take one parameter a {@link Parse.Cloud.ValidatorFunction}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -118,7 +118,7 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * @name Parse.Cloud.beforeDelete * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before delete function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a delete. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -262,7 +262,7 @@ ParseCloud.afterLogout = function (handler) { * @name Parse.Cloud.afterSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a save. This function can be an async function and should take just one parameter, {@link Parse.Cloud.TriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -295,7 +295,7 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * @name Parse.Cloud.afterDelete * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after delete function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a delete. This function can be async and should take just one parameter, {@link Parse.Cloud.TriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -328,7 +328,7 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * @name Parse.Cloud.beforeFind * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before find function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.BeforeFindRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -361,7 +361,7 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { * @name Parse.Cloud.afterFind * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after find function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.AfterFindRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); @@ -388,7 +388,7 @@ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { * @method beforeSaveFile * @name Parse.Cloud.beforeSaveFile * @param {Function} func The function to run before saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSaveFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -413,7 +413,7 @@ ParseCloud.beforeSaveFile = function (handler, validationHandler) { * @method afterSaveFile * @name Parse.Cloud.afterSaveFile * @param {Function} func The function to run after saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSaveFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -438,7 +438,7 @@ ParseCloud.afterSaveFile = function (handler, validationHandler) { * @method beforeDeleteFile * @name Parse.Cloud.beforeDeleteFile * @param {Function} func The function to run before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -463,7 +463,7 @@ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { * @method afterDeleteFile * @name Parse.Cloud.afterDeleteFile * @param {Function} func The function to after before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDeleteFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -488,7 +488,7 @@ ParseCloud.afterDeleteFile = function (handler, validationHandler) { * @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}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeConnect = function (handler, validationHandler) { triggers.addConnectTrigger( @@ -519,7 +519,7 @@ ParseCloud.beforeConnect = function (handler, validationHandler) { * @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}. - * @param {Function} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. Returning false from this function will return a "validation fail" error. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -551,6 +551,7 @@ ParseCloud.onLiveQueryEvent = function (handler) { * @name Parse.Cloud.afterLiveQueryEvent * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after live query event function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a live query event. This function can be async and should take one parameter, a {@link Parse.Cloud.LiveQueryEventTrigger}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterLiveQueryEvent = function ( parseClass, @@ -668,27 +669,13 @@ module.exports = ParseCloud; */ /** - * @interface Parse.Cloud.ValidatorFunction - * @property {String} installationId If set, the installationId triggering the request. - * @property {Boolean} master If true, means the master key was used. - * @property {Parse.User} user If set, the user that made the request. - * @property {Object} params The params passed to the cloud function. - */ - -/** - * @interface Parse.Cloud.ValidatorObject - * @property {Boolean} requireUser whether the cloud trigger requires a user. - * @property {Boolean} requireMaster whether the cloud trigger requires a master key. - * @property {Object|Array} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, {@link Parse.Cloud.ValidatorFields} to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`. - * @property {Array} requireUserKeys If set, keys required on request.user to make the request. - */ - -/** - * -``` + ``` Parse.Cloud.beforeSave('ClassName', request => { - // request.object.get('name') is defined and longer than 5 + // request.object.get('admin') cannot be changed and will default to 'false' + // request.object.get('name') is defined and longer than 5, and user making the object will have set firstName, lastName, and email. },{ + requireUser: true, + requireUserKeys:['firstName','lastName','email'], fields: { name: { type: String, @@ -705,14 +692,17 @@ Parse.Cloud.beforeSave('ClassName', request => { } }) ``` - * @interface Parse.Cloud.ValidatorFields - * @property {String} field name of field to validate. - * @property {String} field.type expected type of data for field. - * @property {Boolean} field.constant whether the field can be modified on the object. - * @property {Any} field.default default value if field is `null`, or initial value `constant` is `true`. - * @property {Array|function} field.options array of options that the field can be, or function to validate field. Throw an error if value is invalid. - * @property {String} field.error custom error message if field is invalid. - * + * @interface Parse.Cloud.ValidatorObject + * @property {Boolean} requireUser whether the cloud trigger requires a user. + * @property {Boolean} requireMaster whether the cloud trigger requires a master key. + * @property {Array} requireUserKeys If set, keys required on request.user to make the request. + * @property {Object|Array} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, fields to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`. + * @property {String} fields.field name of field to validate. + * @property {String} fields.field.type expected type of data for field. + * @property {Boolean} fields.field.constant whether the field can be modified on the object. + * @property {Any} fields.field.default default value if field is `null`, or initial value `constant` is `true`. + * @property {Array|function} fields.field.options array of options that the field can be, or function to validate field. Throw an error if value is invalid. + * @property {String} fields.field.error custom error message if field is invalid. */ /** diff --git a/src/triggers.js b/src/triggers.js index 46281c2969..5bff896d99 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -665,7 +665,7 @@ export function maybeRunValidator(request, functionName) { return Promise.resolve() .then(() => { return typeof theValidator === 'object' - ? inbuiltTriggerValidator(theValidator, request) + ? builtInTriggerValidator(theValidator, request) : theValidator(request); }) .then(() => { @@ -680,7 +680,7 @@ export function maybeRunValidator(request, functionName) { }); }); } -function inbuiltTriggerValidator(options, request) { +function builtInTriggerValidator(options, request) { let reqUser = request.user; if ( !reqUser && From 742482a18fc80f08b9bf4acf9e387ab3f63d5716 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 25 Oct 2020 13:23:29 +1100 Subject: [PATCH 14/16] Allow requireUserKeys to be object --- spec/CloudCode.Validator.spec.js | 115 +++++++++++++++++++++++++++++++ src/cloud-code/Parse.Cloud.js | 9 ++- src/triggers.js | 87 +++++++++++++---------- 3 files changed, 173 insertions(+), 38 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 4178b2dcd1..9711febb0f 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -623,6 +623,121 @@ describe('cloud validator', () => { } }); + it('basic beforeSave requireUserKey as admin', async function (done) { + Parse.Cloud.beforeSave(Parse.User, () => {}, { + fields: { + admin: { + default: false, + constant: true, + }, + }, + }); + Parse.Cloud.define( + 'secureFunction', + () => { + return "Here's all the secure data!"; + }, + { + requireUserKeys: { + admin: { + options: true, + error: 'Unauthorized.', + }, + }, + } + ); + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'p@ssword'); + user.set('admin', true); + await user.signUp(); + expect(user.get('admin')).toBe(false); + try { + await Parse.Cloud.run('secureFunction'); + fail('function should only be available to admin users'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Unauthorized.'); + } + done(); + }); + + it('basic beforeSave requireUserKey as custom function', async function (done) { + Parse.Cloud.beforeSave(Parse.User, () => {}, { + fields: { + accType: { + default: 'normal', + constant: true, + }, + }, + }); + Parse.Cloud.define( + 'secureFunction', + () => { + return "Here's all the secure data!"; + }, + { + requireUserKeys: { + accType: { + options: val => { + return ['admin', 'admin2'].includes(val); + }, + error: 'Unauthorized.', + }, + }, + } + ); + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'p@ssword'); + user.set('accType', 'admin'); + await user.signUp(); + expect(user.get('accType')).toBe('normal'); + try { + await Parse.Cloud.run('secureFunction'); + fail('function should only be available to admin users'); + } catch (error) { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual('Unauthorized.'); + } + done(); + }); + + it('basic beforeSave allow requireUserKey as custom function', async function (done) { + Parse.Cloud.beforeSave(Parse.User, () => {}, { + fields: { + accType: { + default: 'admin', + constant: true, + }, + }, + }); + Parse.Cloud.define( + 'secureFunction', + () => { + return "Here's all the secure data!"; + }, + { + requireUserKeys: { + accType: { + options: val => { + return ['admin', 'admin2'].includes(val); + }, + error: 'Unauthorized.', + }, + }, + } + ); + const user = new Parse.User(); + user.set('username', 'testuser'); + user.set('password', 'p@ssword'); + await user.signUp(); + expect(user.get('accType')).toBe('admin'); + const result = await Parse.Cloud.run('secureFunction'); + expect(result).toBe("Here's all the secure data!"); + done(); + }); + it('basic beforeSave requireUser', function (done) { Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { requireUser: true, diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 3d18a2f86c..254e916c69 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -695,13 +695,18 @@ Parse.Cloud.beforeSave('ClassName', request => { * @interface Parse.Cloud.ValidatorObject * @property {Boolean} requireUser whether the cloud trigger requires a user. * @property {Boolean} requireMaster whether the cloud trigger requires a master key. - * @property {Array} requireUserKeys If set, keys required on request.user to make the request. + * + * @property {Array|Object} requireUserKeys If set, keys required on request.user to make the request. + * @property {String} requireUserKeys.field If requireUserKeys is an object, name of field to validate on request user + * @property {Array|function|Any} requireUserKeys.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid. + * @property {String} requireUserKeys.field.error custom error message if field is invalid. + * * @property {Object|Array} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, fields to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`. * @property {String} fields.field name of field to validate. * @property {String} fields.field.type expected type of data for field. * @property {Boolean} fields.field.constant whether the field can be modified on the object. * @property {Any} fields.field.default default value if field is `null`, or initial value `constant` is `true`. - * @property {Array|function} fields.field.options array of options that the field can be, or function to validate field. Throw an error if value is invalid. + * @property {Array|function|Any} fields.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid. * @property {String} fields.field.error custom error message if field is invalid. */ diff --git a/src/triggers.js b/src/triggers.js index 5bff896d99..4486b68760 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -706,6 +706,38 @@ function builtInTriggerValidator(options, request) { throw `Validation failed. Please specify data for ${key}.`; } }; + + const validateOptions = (opt, key, val) => { + let opts = opt.options; + if (typeof opts === 'function') { + try { + const result = opts(val); + if (!result && result != null) { + throw opt.error || `Validation failed. Invalid value for ${key}.`; + } + } catch (e) { + if (!e) { + throw opt.error || `Validation failed. Invalid value for ${key}.`; + } + + throw opt.error || e.message || e; + } + return; + } + if (!Array.isArray(opts)) { + opts = [opt.options]; + } + + if (!opts.includes(val)) { + throw ( + opt.error || + `Validation failed. Invalid option for ${key}. Expected: ${opts.join( + ', ' + )}` + ); + } + }; + const getType = fn => { const match = fn && fn.toString().match(/^\s*function (\w+)/); return (match ? match[1] : '').toLowerCase(); @@ -732,7 +764,7 @@ function builtInTriggerValidator(options, request) { if (opt.constant && request.object) { if (request.original) { request.object.set(key, request.original.get(key)); - } else if (opt.default) { + } else if (opt.default != null) { request.object.set(key, opt.default); } } @@ -747,46 +779,29 @@ function builtInTriggerValidator(options, request) { throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; } } - let options = opt.options; - if (options) { - if (typeof options === 'function') { - try { - const result = options(val); - if (!result) { - throw ( - opt.error || `Validation failed. Invalid value for ${key}.` - ); - } - } catch (e) { - if (!e) { - throw ( - opt.error || `Validation failed. Invalid value for ${key}.` - ); - } - throw opt.error || e.message || e; - } - } - if (typeof opt.options == 'string') { - options = [opt.options]; - } - if (!options.includes(val)) { - throw ( - opt.error || - `Validation failed. Invalid option for ${key}. Expected: ${options.join( - ', ' - )}` - ); - } + if (opt.options) { + validateOptions(opt, key, val); } } } } - for (const key of options.requireUserKeys || []) { - if (!reqUser) { - throw 'Please login to make this request.'; + const userKeys = options.requireUserKeys || []; + if (Array.isArray(userKeys)) { + for (const key of userKeys) { + if (!reqUser) { + throw 'Please login to make this request.'; + } + + if (reqUser.get(key) == null) { + throw `Validation failed. Please set data for ${key} on your account.`; + } } - if (reqUser.get(key) == null) { - throw `Validation failed. Please set data for ${key} on your account.`; + } else if (typeof userKeys === 'object') { + for (const key in options.requireUserKeys) { + const opt = options.requireUserKeys[key]; + if (opt.options) { + validateOptions(opt, key, reqUser.get(key)); + } } } } From 8429bb0021db377e306a2d9d27b1a14fe7eab452 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 25 Oct 2020 13:58:54 +1100 Subject: [PATCH 15/16] validateMasterKey --- spec/CloudCode.Validator.spec.js | 33 ++++++++++++++++++++++++++++++++ src/cloud-code/Parse.Cloud.js | 1 + src/triggers.js | 3 +++ 3 files changed, 37 insertions(+) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 9711febb0f..67f66f6737 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -780,6 +780,39 @@ describe('cloud validator', () => { }); }); + it('basic beforeSave master', async function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUser: true, + }); + + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + await obj.save(null, { useMasterKey: true }); + done(); + }); + + it('basic beforeSave validateMasterKey', function (done) { + Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, { + requireUser: true, + validateMasterKey: true, + }); + + const obj = new Parse.Object('BeforeSaveFail'); + obj.set('foo', 'bar'); + obj + .save(null, { useMasterKey: true }) + .then(() => { + fail('function should have failed.'); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + expect(error.message).toEqual( + 'Validation failed. Please login to continue.' + ); + done(); + }); + }); + it('basic beforeSave requireKeys', function (done) { Parse.Cloud.beforeSave('beforeSaveRequire', () => {}, { fields: { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 254e916c69..872547c674 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -695,6 +695,7 @@ Parse.Cloud.beforeSave('ClassName', request => { * @interface Parse.Cloud.ValidatorObject * @property {Boolean} requireUser whether the cloud trigger requires a user. * @property {Boolean} requireMaster whether the cloud trigger requires a master key. + * @property {Boolean} validateMasterKey whether the validator should run if masterKey is provided. Defaults to false. * * @property {Array|Object} requireUserKeys If set, keys required on request.user to make the request. * @property {String} requireUserKeys.field If requireUserKeys is an object, name of field to validate on request user diff --git a/src/triggers.js b/src/triggers.js index 4486b68760..19e8a20536 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -681,6 +681,9 @@ export function maybeRunValidator(request, functionName) { }); } function builtInTriggerValidator(options, request) { + if (request.master && !options.validateMasterKey) { + return; + } let reqUser = request.user; if ( !reqUser && From 014e8212e36f01b303bf16b256adba73d66556dd Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Sun, 25 Oct 2020 12:16:11 -0500 Subject: [PATCH 16/16] Improve documentation --- spec/CloudCode.Validator.spec.js | 8 ++ spec/CloudCode.spec.js | 2 + spec/ParseLiveQuery.spec.js | 4 + src/cloud-code/Parse.Cloud.js | 171 +++++++++++++++++++------------ 4 files changed, 118 insertions(+), 67 deletions(-) diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index 67f66f6737..b21c71e101 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -24,6 +24,7 @@ describe('cloud validator', () => { fail('should not have thrown error'); } }); + it('Throw from validator', async done => { Parse.Cloud.define( 'myFunction', @@ -42,6 +43,7 @@ describe('cloud validator', () => { done(); } }); + it('validator can throw parse error', async done => { Parse.Cloud.define( 'myFunction', @@ -839,6 +841,7 @@ describe('cloud validator', () => { done(); }); }); + it('basic beforeSave constantKeys', async function (done) { Parse.Cloud.beforeSave('BeforeSave', () => {}, { fields: { @@ -887,6 +890,7 @@ describe('cloud validator', () => { fail('before save should not have failed.'); } }); + it('validate beforeSave fail', async done => { Parse.Cloud.beforeSave('MyObject', () => {}, validatorFail); @@ -918,6 +922,7 @@ describe('cloud validator', () => { fail('before save should not have failed.'); } }); + it('validate afterSave fail', async done => { Parse.Cloud.afterSave( 'MyObject', @@ -948,6 +953,7 @@ describe('cloud validator', () => { fail('before delete should not have failed.'); } }); + it('validate beforeDelete fail', async done => { Parse.Cloud.beforeDelete( 'MyObject', @@ -987,6 +993,7 @@ describe('cloud validator', () => { fail('after delete should not have failed.'); } }); + it('validate afterDelete fail', async done => { Parse.Cloud.afterDelete( 'MyObject', @@ -1046,6 +1053,7 @@ describe('cloud validator', () => { fail('beforeFind should not have failed.'); } }); + it('validate afterFind fail', async done => { Parse.Cloud.afterFind('MyObject', () => {}, validatorFail); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 01af45055f..a5a89f334d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -112,6 +112,7 @@ describe('Cloud Code', () => { } ); }); + it('returns an empty error', done => { Parse.Cloud.define('cloudCodeWithError', () => { throw null; @@ -126,6 +127,7 @@ describe('Cloud Code', () => { } ); }); + it('beforeSave rejection with custom error code', function (done) { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { throw new Parse.Error(999, 'Nope'); diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index ff3795005c..cb43b32d97 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -5,6 +5,7 @@ const Config = require('../lib/Config'); const validatorFail = () => { throw 'you are not authorized'; }; + describe('ParseLiveQuery', function () { it('can subscribe to query', async done => { await reconfigureServer({ @@ -234,6 +235,7 @@ describe('ParseLiveQuery', function () { object.set({ foo: 'bar' }); await object.save(); }); + it('can handle afterEvent throw', async done => { await reconfigureServer({ liveQuery: { @@ -303,6 +305,7 @@ describe('ParseLiveQuery', function () { object.set({ foo: 'bar' }); await object.save(); }); + it('expect afterEvent create', async done => { await reconfigureServer({ liveQuery: { @@ -553,6 +556,7 @@ describe('ParseLiveQuery', function () { object.set({ foo: 'bar' }); await object.save(); }); + it('can handle beforeConnect validation function', async done => { await reconfigureServer({ liveQuery: { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 872547c674..ddd31bd2e5 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -32,7 +32,19 @@ var ParseCloud = {}; * Defines a Cloud Function. * * **Available in Cloud Code only.** - + * + * ``` + * Parse.Cloud.define('functionName', (request) => { + * // code here + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.define('functionName', (request) => { + * // code here + * }, { ...validationObject }); + * ``` + * * @static * @memberof Parse.Cloud * @param {String} name The name of the Cloud Function @@ -74,18 +86,20 @@ ParseCloud.job = function (functionName, handler) { * ``` * Parse.Cloud.beforeSave('MyCustomClass', (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.beforeSave(Parse.User, (request) => { * // code here - * }) + * }, { ...validationObject }) * ``` * * @method beforeSave * @name Parse.Cloud.beforeSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a save. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -107,18 +121,20 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * ``` * Parse.Cloud.beforeDelete('MyCustomClass', (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.beforeDelete(Parse.User, (request) => { * // code here - * }) + * }, { ...validationObject }) *``` * * @method beforeDelete * @name Parse.Cloud.beforeDelete * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before delete function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a delete. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -182,8 +198,7 @@ ParseCloud.beforeLogin = function (handler) { * ``` * Parse.Cloud.afterLogin((request) => { * // code here - * }) - * + * }); * ``` * * @method afterLogin @@ -217,8 +232,7 @@ ParseCloud.afterLogin = function (handler) { * ``` * Parse.Cloud.afterLogout((request) => { * // code here - * }) - * + * }); * ``` * * @method afterLogout @@ -251,18 +265,20 @@ ParseCloud.afterLogout = function (handler) { * ``` * Parse.Cloud.afterSave('MyCustomClass', async function(request) { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.afterSave(Parse.User, async function(request) { * // code here - * }) + * }, { ...validationObject }); * ``` * * @method afterSave * @name Parse.Cloud.afterSave * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a save. This function can be an async function and should take just one parameter, {@link Parse.Cloud.TriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -284,18 +300,20 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * ``` * Parse.Cloud.afterDelete('MyCustomClass', async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.afterDelete(Parse.User, async (request) => { * // code here - * }) + * }, { ...validationObject }); *``` * * @method afterDelete * @name Parse.Cloud.afterDelete * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after delete function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a delete. This function can be async and should take just one parameter, {@link Parse.Cloud.TriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -317,18 +335,20 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * ``` * Parse.Cloud.beforeFind('MyCustomClass', async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.beforeFind(Parse.User, async (request) => { * // code here - * }) + * }, { ...validationObject }); *``` * * @method beforeFind * @name Parse.Cloud.beforeFind * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before find function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.BeforeFindRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.BeforeFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -350,18 +370,20 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { * ``` * Parse.Cloud.afterFind('MyCustomClass', async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.afterFind(Parse.User, async (request) => { * // code here - * }) + * }, { ...validationObject }); *``` * * @method afterFind * @name Parse.Cloud.afterFind * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after find function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.AfterFindRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.AfterFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); @@ -382,13 +404,19 @@ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { * ``` * Parse.Cloud.beforeSaveFile(async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.beforeSaveFile(async (request) => { + * // code here + * }, { ...validationObject }); *``` * * @method beforeSaveFile * @name Parse.Cloud.beforeSaveFile * @param {Function} func The function to run before saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSaveFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -407,13 +435,19 @@ ParseCloud.beforeSaveFile = function (handler, validationHandler) { * ``` * Parse.Cloud.afterSaveFile(async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.afterSaveFile(async (request) => { + * // code here + * }, { ...validationObject }); *``` * * @method afterSaveFile * @name Parse.Cloud.afterSaveFile * @param {Function} func The function to run after saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSaveFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -432,13 +466,19 @@ ParseCloud.afterSaveFile = function (handler, validationHandler) { * ``` * Parse.Cloud.beforeDeleteFile(async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.beforeDeleteFile(async (request) => { + * // code here + * }, { ...validationObject }); *``` * * @method beforeDeleteFile * @name Parse.Cloud.beforeDeleteFile * @param {Function} func The function to run before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -457,13 +497,19 @@ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { * ``` * Parse.Cloud.afterDeleteFile(async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.afterDeleteFile(async (request) => { + * // code here + * }, { ...validationObject }); *``` * * @method afterDeleteFile * @name Parse.Cloud.afterDeleteFile * @param {Function} func The function to after before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDeleteFile = function (handler, validationHandler) { triggers.addFileTrigger( @@ -482,13 +528,19 @@ ParseCloud.afterDeleteFile = function (handler, validationHandler) { * ``` * Parse.Cloud.beforeConnect(async (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.beforeConnect(async (request) => { + * // code here + * }, { ...validationObject }); *``` * * @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}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.ConnectTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeConnect = function (handler, validationHandler) { triggers.addConnectTrigger( @@ -508,18 +560,20 @@ ParseCloud.beforeConnect = function (handler, validationHandler) { * ``` * Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); * * Parse.Cloud.beforeSubscribe(Parse.User, (request) => { * // code here - * }) + * }, { ...validationObject }); *``` * * @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}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { var className = getClassName(parseClass); @@ -544,14 +598,20 @@ ParseCloud.onLiveQueryEvent = function (handler) { * ``` * Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => { * // code here - * }) + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => { + * // code here + * }, { ...validationObject }); *``` * * @method afterLiveQueryEvent * @name Parse.Cloud.afterLiveQueryEvent * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after live query event function for. This can instead be a String that is the className of the subclass. * @param {Function} func The function to run after a live query event. This function can be async and should take one parameter, a {@link Parse.Cloud.LiveQueryEventTrigger}. - * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.LiveQueryEventTrigger}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterLiveQueryEvent = function ( parseClass, @@ -669,29 +729,12 @@ module.exports = ParseCloud; */ /** - ``` -Parse.Cloud.beforeSave('ClassName', request => { - // request.object.get('admin') cannot be changed and will default to 'false' - // request.object.get('name') is defined and longer than 5, and user making the object will have set firstName, lastName, and email. -},{ - requireUser: true, - requireUserKeys:['firstName','lastName','email'], - fields: { - name: { - type: String, - default: 'default', - options: val => { - return val.length > 5; - }, - error: 'name must be longer than 5 characters' - }, - admin: { - default: false, - constant: true - } - } -}) - ``` + * @interface Parse.Cloud.JobRequest + * @property {Object} params The params passed to the background job. + * @property {function} message If message is called with a string argument, will update the current message to be stored in the job status. + */ + +/** * @interface Parse.Cloud.ValidatorObject * @property {Boolean} requireUser whether the cloud trigger requires a user. * @property {Boolean} requireMaster whether the cloud trigger requires a master key. @@ -710,9 +753,3 @@ Parse.Cloud.beforeSave('ClassName', request => { * @property {Array|function|Any} fields.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid. * @property {String} fields.field.error custom error message if field is invalid. */ - -/** - * @interface Parse.Cloud.JobRequest - * @property {Object} params The params passed to the background job. - * @property {function} message If message is called with a string argument, will update the current message to be stored in the job status. - */