From 3b3329f8023651fa9c9bd5b98472e84348262a12 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 03:33:26 +1000 Subject: [PATCH 01/12] cleanup code --- spec/CloudCode.spec.js | 639 +++++++++++---------------------- src/Routers/FunctionsRouter.js | 166 ++++----- 2 files changed, 268 insertions(+), 537 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 86a7627427..4868bda93d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -19,78 +19,53 @@ const mockAdapter = { }; describe('Cloud Code', () => { - it('can load absolute cloud code file', done => { - reconfigureServer({ - cloud: __dirname + '/cloud/cloudCodeRelativeFile.js', - }).then(() => { - Parse.Cloud.run('cloudCodeInFile', {}).then(result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); + it('can load absolute cloud code file', async () => { + await reconfigureServer({ + cloud: `${__dirname}/cloud/cloudCodeRelativeFile.js`, }); + await expectAsync(Parse.Cloud.run('cloudCodeInFile')).toBeResolvedTo( + 'It is possible to define cloud code in a file.' + ); }); - it('can load relative cloud code file', done => { - reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' }).then(() => { - Parse.Cloud.run('cloudCodeInFile', {}).then(result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); - }); + it('can load relative cloud code file', async () => { + await reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' }); + await expectAsync(Parse.Cloud.run('cloudCodeInFile')).toBeResolvedTo( + 'It is possible to define cloud code in a file.' + ); }); - it('can create functions', done => { - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); - - Parse.Cloud.run('hello', {}).then(result => { - expect(result).toEqual('Hello world!'); - done(); - }); + it('can create functions', async () => { + Parse.Cloud.define('hello', () => 'Hello world!'); + await expectAsync(Parse.Cloud.run('hello')).toBeResolvedTo('Hello world!'); }); - it('show warning on duplicate cloud functions', done => { - const logger = require('../lib/logger').logger; + it('show warning on duplicate cloud functions', () => { + const { logger } = require('../lib/logger'); spyOn(logger, 'warn').and.callFake(() => {}); - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); + Parse.Cloud.define('hello', () => 'Hello world!'); + Parse.Cloud.define('hello', () => 'Hello world!'); expect(logger.warn).toHaveBeenCalledWith( 'Warning: Duplicate cloud functions exist for hello. Only the last one will be used and the others will be ignored.' ); - done(); }); - it('is cleared cleared after the previous test', done => { - Parse.Cloud.run('hello', {}).catch(error => { - expect(error.code).toEqual(141); - done(); - }); + it('is cleared cleared after the previous test', async () => { + await expectAsync(Parse.Cloud.run('hello')).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "hello"`) + ); }); - it('basic beforeSave rejection', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFail', function () { + it('basic beforeSave rejection', async () => { + Parse.Cloud.beforeSave('BeforeSaveFail', () => { throw new Error('You shall not pass!'); }); - - const obj = new Parse.Object('BeforeSaveFail'); - obj.set('foo', 'bar'); - obj.save().then( - () => { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, - () => { - done(); - } + await expectAsync(new Parse.Object('BeforeSaveFail').save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, `You shall not pass!`) ); }); - it('returns an error', done => { + it('returns an error', async () => { Parse.Cloud.define('cloudCodeWithError', () => { /* eslint-disable no-undef */ foo.bar(); @@ -98,145 +73,77 @@ describe('Cloud Code', () => { return 'I better throw an error.'; }); - Parse.Cloud.run('cloudCodeWithError').then( - () => done.fail('should not succeed'), - e => { - expect(e).toEqual(new Parse.Error(141, 'foo is not defined')); - done(); - } + await expectAsync(Parse.Cloud.run('cloudCodeWithError')).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'foo is not defined') ); }); - it('returns an empty error', done => { + it('returns an empty error', async () => { 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(); - } + await expectAsync(Parse.Cloud.run('cloudCodeWithError')).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Script failed.') ); }); - it('beforeFind can throw string', async function (done) { + it('beforeFind can throw string', async () => { Parse.Cloud.beforeFind('beforeFind', () => { throw 'throw beforeFind'; }); - const obj = new Parse.Object('beforeFind'); - obj.set('foo', 'bar'); - await obj.save(); - expect(obj.get('foo')).toBe('bar'); - try { - const query = new Parse.Query('beforeFind'); - await query.first(); - } catch (e) { - expect(e.code).toBe(141); - expect(e.message).toBe('throw beforeFind'); - done(); - } + await expectAsync(new Parse.Query('beforeFind').first()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'throw beforeFind') + ); }); - it('beforeSave rejection with custom error code', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { - throw new Parse.Error(999, 'Nope'); + it('beforeSave rejection with custom error code', async () => { + const error = new Parse.Error(999, 'Nope'); + Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', () => { + throw error; }); - - const obj = new Parse.Object('BeforeSaveFailWithErrorCode'); - obj.set('foo', 'bar'); - obj.save().then( - function () { - fail('Should not have been able to save BeforeSaveFailWithErrorCode class.'); - done(); - }, - function (error) { - expect(error.code).toEqual(999); - expect(error.message).toEqual('Nope'); - done(); - } + await expectAsync(new Parse.Object('BeforeSaveFailWithErrorCode').save()).toBeRejectedWith( + error ); }); - it('basic beforeSave rejection via promise', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function () { + it('basic beforeSave rejection via promise', async () => { + Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', async () => { const query = new Parse.Query('Yolo'); - return query.find().then( - () => { - throw 'Nope'; - }, - () => { - return Promise.response(); - } - ); + await query.find(); + throw 'Nope'; }); - - const obj = new Parse.Object('BeforeSaveFailWithPromise'); - obj.set('foo', 'bar'); - obj.save().then( - function () { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, - function (error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - done(); - } + await expectAsync(new Parse.Object('BeforeSaveFailWithPromise').save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Nope') ); }); - it('test beforeSave changed object success', function (done) { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { - req.object.set('foo', 'baz'); + it('test beforeSave changed object success', async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', ({ object }) => { + object.set('foo', 'baz'); }); const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bar'); - obj.save().then( - function () { - const query = new Parse.Query('BeforeSaveChanged'); - query.get(obj.id).then( - function (objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - }, - function (error) { - fail(error); - done(); - } - ); + await obj.save(); + expect(obj.get('foo')).toBe('baz'); + const objAgain = await new Parse.Query('BeforeSaveChanged').get(obj.id); + expect(objAgain.get('foo')).toBe('baz'); }); it('test beforeSave with invalid field', async () => { Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { req.object.set('length', 0); }); - - const obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bar'); - try { - await obj.save(); - fail('should not succeed'); - } catch (e) { - expect(e.message).toBe('Invalid field name: length.'); - } + await expectAsync(new Parse.Object('BeforeSaveChanged').save()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: length.') + ); }); - it("test beforeSave changed object fail doesn't change object", async function () { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { + it("test beforeSave changed object fail doesn't change object", async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { if (req.object.has('fail')) { return Promise.reject(new Error('something went wrong')); } - return Promise.resolve(); }); @@ -244,47 +151,40 @@ describe('Cloud Code', () => { obj.set('foo', 'bar'); await obj.save(); obj.set('foo', 'baz').set('fail', true); - try { - await obj.save(); - } catch (e) { - await obj.fetch(); - expect(obj.get('foo')).toBe('bar'); - } + await expectAsync(obj.save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'something went wrong') + ); + await obj.fetch(); + expect(obj.get('foo')).toBe('bar'); }); - it('test beforeSave returns value on create and update', done => { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { + it('test beforeSave returns value on create and update', async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { req.object.set('foo', 'baz'); }); - const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bing'); - obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - obj.set('foo', 'bar'); - return obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - done(); - }); - }); + await obj.save(); + expect(obj.get('foo')).toEqual('baz'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.get('foo')).toEqual('baz'); }); - it('test beforeSave applies changes when beforeSave returns true', done => { - Parse.Cloud.beforeSave('Insurance', function (req) { + it('test beforeSave applies changes when beforeSave returns true', async () => { + Parse.Cloud.beforeSave('Insurance', req => { req.object.set('rate', '$49.99/Month'); return true; }); const insurance = new Parse.Object('Insurance'); insurance.set('rate', '$5.00/Month'); - insurance.save().then(insurance => { - expect(insurance.get('rate')).toEqual('$49.99/Month'); - done(); - }); + await insurance.save(); + expect(insurance.get('rate')).toEqual('$49.99/Month'); }); - it('test beforeSave applies changes and resolves returned promise', done => { - Parse.Cloud.beforeSave('Insurance', function (req) { + it('test beforeSave applies changes and resolves returned promise', async () => { + Parse.Cloud.beforeSave('Insurance', req => { req.object.set('rate', '$49.99/Month'); return new Parse.Query('Pet').get(req.object.get('pet').id).then(pet => { pet.set('healthy', true); @@ -294,45 +194,26 @@ describe('Cloud Code', () => { const pet = new Parse.Object('Pet'); pet.set('healthy', false); - pet.save().then(pet => { - const insurance = new Parse.Object('Insurance'); - insurance.set('pet', pet); - insurance.set('rate', '$5.00/Month'); - insurance.save().then(insurance => { - expect(insurance.get('rate')).toEqual('$49.99/Month'); - new Parse.Query('Pet').get(insurance.get('pet').id).then(pet => { - expect(pet.get('healthy')).toEqual(true); - done(); - }); - }); - }); + await pet.save(); + const insurance = new Parse.Object('Insurance'); + insurance.set('pet', pet); + insurance.set('rate', '$5.00/Month'); + await insurance.save(); + expect(insurance.get('rate')).toEqual('$49.99/Month'); + const _pet = await new Parse.Query('Pet').get(insurance.get('pet').id); + expect(_pet.get('healthy')).toEqual(true); }); it('beforeSave should be called only if user fulfills permissions', async () => { - const triggeruser = new Parse.User(); - triggeruser.setUsername('triggeruser'); - triggeruser.setPassword('triggeruser'); - await triggeruser.signUp(); - - const triggeruser2 = new Parse.User(); - triggeruser2.setUsername('triggeruser2'); - triggeruser2.setPassword('triggeruser2'); - await triggeruser2.signUp(); - - const triggeruser3 = new Parse.User(); - triggeruser3.setUsername('triggeruser3'); - triggeruser3.setPassword('triggeruser3'); - await triggeruser3.signUp(); - - const triggeruser4 = new Parse.User(); - triggeruser4.setUsername('triggeruser4'); - triggeruser4.setPassword('triggeruser4'); - await triggeruser4.signUp(); - - const triggeruser5 = new Parse.User(); - triggeruser5.setUsername('triggeruser5'); - triggeruser5.setPassword('triggeruser5'); - await triggeruser5.signUp(); + const [triggeruser, triggeruser2, triggeruser3, triggeruser4, triggeruser5] = await Promise.all( + [1, 2, 3, 4, 5].map(async num => { + const user = new Parse.User(); + user.setUsername(`triggeruser${num}`); + user.setPassword(`triggeruser${num}`); + await user.signUp(); + return user; + }) + ); const triggerroleacl = new Parse.ACL(); triggerroleacl.setPublicReadAccess(true); @@ -389,11 +270,11 @@ describe('Cloud Code', () => { {} ); - let called = 0; - Parse.Cloud.beforeSave('triggerclass', () => { - called++; - }); - + const caller = { + beforeSave: () => {}, + }; + const spy = spyOn(caller, 'beforeSave').and.callThrough(); + Parse.Cloud.beforeSave('triggerclass', caller.beforeSave); const triggerobject = new Parse.Object('triggerclass'); triggerobject.set('someField', 'someValue'); triggerobject.set('someField2', 'someValue'); @@ -411,19 +292,19 @@ describe('Cloud Code', () => { await triggerobject.save(undefined, { sessionToken: triggeruser.getSessionToken(), }); - expect(called).toBe(1); + expect(spy).toHaveBeenCalledTimes(1); await triggerobject.save(undefined, { sessionToken: triggeruser.getSessionToken(), }); - expect(called).toBe(2); + expect(spy).toHaveBeenCalledTimes(2); await triggerobject.save(undefined, { sessionToken: triggeruser2.getSessionToken(), }); - expect(called).toBe(3); + expect(spy).toHaveBeenCalledTimes(3); await triggerobject.save(undefined, { sessionToken: triggeruser3.getSessionToken(), }); - expect(called).toBe(4); + expect(spy).toHaveBeenCalledTimes(4); const triggerobject2 = new Parse.Object('triggerclass'); triggerobject2.set('someField', 'someValue'); @@ -442,109 +323,89 @@ describe('Cloud Code', () => { await triggerobject2.save(undefined, { sessionToken: triggeruser2.getSessionToken(), }); - expect(called).toBe(5); + expect(spy).toHaveBeenCalledTimes(5); await triggerobject2.save(undefined, { sessionToken: triggeruser2.getSessionToken(), }); - expect(called).toBe(6); + expect(spy).toHaveBeenCalledTimes(6); await triggerobject2.save(undefined, { sessionToken: triggeruser.getSessionToken(), }); - expect(called).toBe(7); + expect(spy).toHaveBeenCalledTimes(7); - let catched = false; - try { - await triggerobject2.save(undefined, { + await expectAsync( + triggerobject2.save(undefined, { sessionToken: triggeruser3.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(101); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); - catched = false; - try { - await triggerobject2.save(undefined, { + expect(spy).toHaveBeenCalledTimes(7); + + await expectAsync( + triggerobject2.save(undefined, { sessionToken: triggeruser4.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(101); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); - catched = false; - try { - await triggerobject2.save(undefined, { + expect(spy).toHaveBeenCalledTimes(7); + + await expectAsync( + triggerobject2.save(undefined, { sessionToken: triggeruser5.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(101); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); + + expect(spy).toHaveBeenCalledTimes(7); const triggerobject3 = new Parse.Object('triggerclass'); triggerobject3.set('someField', 'someValue'); triggerobject3.set('someField33', 'someValue'); - catched = false; - try { - await triggerobject3.save(undefined, { + await expectAsync( + triggerobject3.save(undefined, { sessionToken: triggeruser4.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(119); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Permission denied for action create on class triggerclass.' + ) + ); - catched = false; - try { - await triggerobject3.save(undefined, { + expect(spy).toHaveBeenCalledTimes(7); + + await expectAsync( + triggerobject3.save(undefined, { sessionToken: triggeruser5.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(119); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Permission denied for action create on class triggerclass.' + ) + ); + expect(spy).toHaveBeenCalledTimes(7); }); - it('test afterSave ran and created an object', function (done) { - Parse.Cloud.afterSave('AfterSaveTest', function (req) { + it('test afterSave ran and created an object', done => { + const obj = new Parse.Object('AfterSaveTest'); + const test = async () => { + const query = new Parse.Query('AfterSaveProof'); + query.equalTo('proof', obj.id); + const results = await query.find(); + expect(results.length).toEqual(1); + done(); + }; + Parse.Cloud.afterSave('AfterSaveTest', req => { const obj = new Parse.Object('AfterSaveProof'); obj.set('proof', req.object.id); obj.save().then(test); }); - - const obj = new Parse.Object('AfterSaveTest'); obj.save(); - - function test() { - const query = new Parse.Query('AfterSaveProof'); - query.equalTo('proof', obj.id); - query.find().then( - function (results) { - expect(results.length).toEqual(1); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - } }); - it('test afterSave ran on created object and returned a promise', function (done) { - Parse.Cloud.afterSave('AfterSaveTest2', function (req) { + it('test afterSave ran on created object and returned a promise', async () => { + Parse.Cloud.afterSave('AfterSaveTest2', req => { const obj = req.object; if (!obj.existed()) { return new Promise(resolve => { @@ -558,23 +419,11 @@ describe('Cloud Code', () => { } }); - const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then(function () { - const query = new Parse.Query('AfterSaveTest2'); - query.equalTo('proof', obj.id); - query.find().then( - function (results) { - expect(results.length).toEqual(1); - const savedObject = results[0]; - expect(savedObject.get('proof')).toEqual(obj.id); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - }); + const obj = await new Parse.Object('AfterSaveTest2').save(); + const results = await new Parse.Query('AfterSaveTest2').equalTo('proof', obj.id).find(); + expect(results.length).toEqual(1); + const [savedObject] = results; + expect(savedObject.get('proof')).toEqual(obj.id); }); // TODO: Fails on CI randomly as racing @@ -610,28 +459,18 @@ describe('Cloud Code', () => { ); }); - it('test afterSave rejecting promise', function (done) { - Parse.Cloud.afterSave('AfterSaveTest2', function () { + it('test afterSave rejecting promise', async () => { + Parse.Cloud.afterSave('AfterSaveTest2', () => { return new Promise((resolve, reject) => { setTimeout(function () { reject('THIS SHOULD BE IGNORED'); }, 1000); }); }); - - const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then( - function () { - done(); - }, - function (error) { - fail(error); - done(); - } - ); + await new Parse.Object('AfterSaveTest2').save(); }); - it('test afterDelete returning promise, object is deleted when destroy resolves', function (done) { + it('test afterDelete returning promise, object is deleted when destroy resolves', async () => { Parse.Cloud.afterDelete('AfterDeleteTest2', function (req) { return new Promise(resolve => { setTimeout(function () { @@ -644,28 +483,17 @@ describe('Cloud Code', () => { }); }); - const errorHandler = function (error) { - fail(error); - done(); - }; - const obj = new Parse.Object('AfterDeleteTest2'); - obj.save().then(function () { - obj.destroy().then(function () { - const query = new Parse.Query('AfterDeleteTestProof'); - query.equalTo('proof', obj.id); - query.find().then(function (results) { - expect(results.length).toEqual(1); - const deletedObject = results[0]; - expect(deletedObject.get('proof')).toEqual(obj.id); - done(); - }, errorHandler); - }, errorHandler); - }, errorHandler); + await obj.save(); + await obj.destroy(); + const results = await new Parse.Query('AfterDeleteTestProof').equalTo('proof', obj.id).find(); + expect(results.length).toEqual(1); + const [deletedObject] = results; + expect(deletedObject.get('proof')).toEqual(obj.id); }); - it('test afterDelete ignoring promise, object is not yet deleted', function (done) { - Parse.Cloud.afterDelete('AfterDeleteTest2', function (req) { + it('test afterDelete ignoring promise, object is not yet deleted', async () => { + Parse.Cloud.afterDelete('AfterDeleteTest2', req => { return new Promise(resolve => { setTimeout(function () { const obj = new Parse.Object('AfterDeleteTestProof'); @@ -677,119 +505,62 @@ describe('Cloud Code', () => { }); }); - const errorHandler = function (error) { - fail(error); - done(); - }; - const obj = new Parse.Object('AfterDeleteTest2'); - obj.save().then(function () { - obj.destroy().then(function () { - done(); - }); + await obj.save(); + const query = new Parse.Query('AfterDeleteTestProof'); + query.equalTo('proof', obj.id); + const [results] = await Promise.all([query.find(), obj.destroy()]); - const query = new Parse.Query('AfterDeleteTestProof'); - query.equalTo('proof', obj.id); - query.find().then(function (results) { - expect(results.length).toEqual(0); - }, errorHandler); - }, errorHandler); + expect(results.length).toEqual(0); }); - it('test beforeSave happens on update', function (done) { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { + it('test beforeSave happens on update', async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { req.object.set('foo', 'baz'); }); const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bar'); - obj - .save() - .then(function () { - obj.set('foo', 'bar'); - return obj.save(); - }) - .then( - function () { - const query = new Parse.Query('BeforeSaveChanged'); - return query.get(obj.id).then(function (objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }); - }, - function (error) { - fail(error); - done(); - } - ); + await obj.save(); + expect(obj.get('foo')).toBe('baz'); + obj.set('foo', 'bar'); + await obj.save(); + obj.set('foo', 'bar'); + const objAgain = await new Parse.Query('BeforeSaveChanged').get(obj.id); + expect(objAgain.get('foo')).toEqual('baz'); }); - it('test beforeDelete failure', function (done) { - Parse.Cloud.beforeDelete('BeforeDeleteFail', function () { + it('test beforeDelete failure', async () => { + Parse.Cloud.beforeDelete('BeforeDeleteFail', () => { throw 'Nope'; }); const obj = new Parse.Object('BeforeDeleteFail'); - let id; obj.set('foo', 'bar'); - obj - .save() - .then(() => { - id = obj.id; - return obj.destroy(); - }) - .then( - () => { - fail('obj.destroy() should have failed, but it succeeded'); - done(); - }, - error => { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); + await obj.save(); + const id = `${obj.id}`; + await expectAsync(obj.destroy()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Nope') + ); - const objAgain = new Parse.Object('BeforeDeleteFail', { - objectId: id, - }); - return objAgain.fetch(); - } - ) - .then( - objAgain => { - if (objAgain) { - expect(objAgain.get('foo')).toEqual('bar'); - } else { - fail('unable to fetch the object ', id); - } - done(); - }, - error => { - // We should have been able to fetch the object again - fail(error); - } - ); + const objAgain = new Parse.Object('BeforeDeleteFail', { + objectId: id, + }); + await objAgain.fetch(); + expect(objAgain.get('foo')).toEqual('bar'); }); - it('basic beforeDelete rejection via promise', function (done) { - Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function () { + it('basic beforeDelete rejection via promise', async () => { + Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', () => { const query = new Parse.Query('Yolo'); return query.find().then(() => { throw 'Nope'; }); }); - const obj = new Parse.Object('BeforeDeleteFailWithPromise'); obj.set('foo', 'bar'); - obj.save().then( - function () { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, - function (error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - done(); - } + await expectAsync(obj.save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Nope') ); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d239908103..2b86a26b5e 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -1,8 +1,8 @@ // FunctionsRouter.js -var Parse = require('parse/node').Parse, - triggers = require('../triggers'); +var Parse = require('parse/node').Parse; +import { getJob, getFunction, maybeRunValidator, resolveError } from '../triggers.js'; import PromiseRouter from '../PromiseRouter'; import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; import { jobStatusHandler } from '../StatusHandler'; @@ -11,18 +11,18 @@ import { logger } from '../logger'; function parseObject(obj) { if (Array.isArray(obj)) { - return obj.map(item => { - return parseObject(item); - }); - } else if (obj && obj.__type == 'Date') { + return obj.map(item => parseObject(item)); + } + if (obj?.__type === 'Date') { return Object.assign(new Date(obj.iso), obj); - } else if (obj && obj.__type == 'File') { + } + if (obj?.__type === 'File') { return Parse.File.fromJSON(obj); - } else if (obj && typeof obj === 'object') { + } + if (typeof obj === 'object') { return parseParams(obj); - } else { - return obj; } + return obj; } function parseParams(params) { @@ -51,18 +51,17 @@ export class FunctionsRouter extends PromiseRouter { }); } - static handleCloudJob(req) { + static async handleCloudJob(req) { const jobName = req.params.jobName || req.body.jobName; const applicationId = req.config.applicationId; const jobHandler = jobStatusHandler(req.config); - const jobFunction = triggers.getJob(jobName, applicationId); + const jobFunction = getJob(jobName, applicationId); if (!jobFunction) { throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.'); } - let params = Object.assign({}, req.body, req.query); - params = parseParams(params); + const params = parseParams({ ...req.body, ...req.query }); const request = { - params: params, + params, log: req.config.loggerController, headers: req.config.headers, ip: req.config.ip, @@ -70,61 +69,40 @@ export class FunctionsRouter extends PromiseRouter { message: jobHandler.setMessage.bind(jobHandler), }; - return jobHandler.setRunning(jobName, params).then(jobStatus => { - request.jobId = jobStatus.objectId; - // run the function async - process.nextTick(() => { - Promise.resolve() - .then(() => { - return jobFunction(request); - }) - .then( - result => { - jobHandler.setSucceeded(result); - }, - error => { - jobHandler.setFailed(error); - } - ); - }); - return { - headers: { - 'X-Parse-Job-Status-Id': jobStatus.objectId, - }, - response: {}, - }; + const jobStatus = await jobHandler.setRunning(jobName, params); + request.jobId = jobStatus.objectId; + // run the function async + process.nextTick(() => { + (async () => { + try { + const result = await jobFunction(request); + jobHandler.setSucceeded(result); + } catch (error) { + jobHandler.setFailed(error); + } + })(); }); - } - - static createResponseObject(resolve, reject) { return { - success: function (result) { - resolve({ - response: { - result: Parse._encode(result), - }, - }); - }, - error: function (message) { - const error = triggers.resolveError(message); - reject(error); + headers: { + 'X-Parse-Job-Status-Id': jobStatus.objectId, }, + response: {}, }; } - static handleCloudFunction(req) { + + static async handleCloudFunction(req) { const functionName = req.params.functionName; const applicationId = req.config.applicationId; - const theFunction = triggers.getFunction(functionName, applicationId); + const theFunction = getFunction(functionName, applicationId); if (!theFunction) { throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`); } - let params = Object.assign({}, req.body, req.query); - params = parseParams(params); + const params = parseParams({ ...req.body, ...req.query }); const request = { - params: params, - master: req.auth && req.auth.isMaster, - user: req.auth && req.auth.user, + params, + master: req.auth?.isMaster, + user: req.auth?.user, installationId: req.info.installationId, log: req.config.loggerController, headers: req.config.headers, @@ -133,52 +111,34 @@ export class FunctionsRouter extends PromiseRouter { context: req.info.context, }; - return new Promise(function (resolve, reject) { - const userString = req.auth && req.auth.user ? req.auth.user.id : undefined; - const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); - const { success, error } = FunctionsRouter.createResponseObject( - result => { - try { - const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result)); - logger.info( - `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, - { - functionName, - params, - user: userString, - } - ); - resolve(result); - } catch (e) { - reject(e); - } - }, - error => { - try { - logger.error( - `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + - JSON.stringify(error), - { - functionName, - error, - params, - user: userString, - } - ); - reject(error); - } catch (e) { - reject(e); - } + const userString = req.auth?.user?.id; + const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); + try { + await maybeRunValidator(request, functionName, req.auth); + const response = await theFunction(request); + const result = Parse._encode(response); + const cleanResult = logger.truncateLogMessage(JSON.stringify(result)); + logger.info( + `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, + { + functionName, + params, + user: userString, } ); - return Promise.resolve() - .then(() => { - return triggers.maybeRunValidator(request, functionName, req.auth); - }) - .then(() => { - return theFunction(request); - }) - .then(success, error); - }); + return { + response: { + result, + }, + }; + } catch (e) { + const error = resolveError(e); + logger.error( + `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ${JSON.stringify( + error + )}\n Stack: ${error.stack}\n` + ); + throw error; + } } } From efd7d8ea8a59669e5a708a264ba6483789b00aec Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 03:35:26 +1000 Subject: [PATCH 02/12] 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 2b86a26b5e..cef5b80c96 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -1,6 +1,6 @@ // FunctionsRouter.js -var Parse = require('parse/node').Parse; +const { Parse } = require('parse/node'); import { getJob, getFunction, maybeRunValidator, resolveError } from '../triggers.js'; import PromiseRouter from '../PromiseRouter'; From ecc7d1de14edb655278d974d9c10accf3699942f Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 09:58:43 +1000 Subject: [PATCH 03/12] fix tests --- src/ParseServer.js | 2 +- src/Routers/FunctionsRouter.js | 6 +- src/cloud-code/Parse.Cloud.Validator.js | 255 ++++++++++++++++++++++++ src/cloud-code/Parse.Cloud.js | 211 ++++---------------- src/triggers.js | 194 +----------------- 5 files changed, 301 insertions(+), 367 deletions(-) create mode 100644 src/cloud-code/Parse.Cloud.Validator.js diff --git a/src/ParseServer.js b/src/ParseServer.js index 43996ac751..11e2be0738 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -368,7 +368,7 @@ class ParseServer { } function addParseCloud() { - const ParseCloud = require('./cloud-code/Parse.Cloud'); + const { ParseCloud } = require('./cloud-code/Parse.Cloud'); Object.assign(Parse.Cloud, ParseCloud); global.Parse = Parse; } diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index cef5b80c96..40dc022eaa 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -2,7 +2,8 @@ const { Parse } = require('parse/node'); -import { getJob, getFunction, maybeRunValidator, resolveError } from '../triggers.js'; +import { getJob, getFunction, resolveError } from '../triggers.js'; +import { maybeRunValidator } from './../cloud-code/Parse.Cloud.Validator.js'; import PromiseRouter from '../PromiseRouter'; import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; import { jobStatusHandler } from '../StatusHandler'; @@ -10,6 +11,9 @@ import _ from 'lodash'; import { logger } from '../logger'; function parseObject(obj) { + if (obj === null || obj === undefined) { + return obj; + } if (Array.isArray(obj)) { return obj.map(item => parseObject(item)); } diff --git a/src/cloud-code/Parse.Cloud.Validator.js b/src/cloud-code/Parse.Cloud.Validator.js new file mode 100644 index 0000000000..5af828e59d --- /dev/null +++ b/src/cloud-code/Parse.Cloud.Validator.js @@ -0,0 +1,255 @@ +import { getValidator, resolveError } from '../triggers'; +import Parse from 'parse/node'; +export function validateValidator(validator) { + if (!validator || typeof validator === 'function') { + return; + } + const fieldOptions = { + type: ['Any'], + constant: [Boolean], + default: ['Any'], + options: [Array, 'function', 'Any'], + required: [Boolean], + error: [String], + }; + const allowedKeys = { + requireUser: [Boolean], + requireAnyUserRoles: [Array, 'function'], + requireAllUserRoles: [Array, 'function'], + requireMaster: [Boolean], + validateMasterKey: [Boolean], + skipWithMasterKey: [Boolean], + requireUserKeys: [Array, Object], + fields: [Array, Object], + }; + const getType = fn => { + if (Array.isArray(fn)) { + return 'array'; + } + if (fn === 'Any' || fn === 'function') { + return fn; + } + const type = typeof fn; + if (typeof fn === 'function') { + const match = fn && fn.toString().match(/^\s*function (\w+)/); + return (match ? match[1] : 'function').toLowerCase(); + } + return type; + }; + const checkKey = (key, data, validatorParam) => { + const parameter = data[key]; + if (!parameter) { + throw `${key} is not a supported parameter for Cloud Function validations.`; + } + const types = parameter.map(type => getType(type)); + const type = getType(validatorParam); + if (!types.includes(type) && !types.includes('Any')) { + throw `Invalid type for Cloud Function validation key ${key}. Expected ${types.join( + '|' + )}, actual ${type}`; + } + }; + for (const key in validator) { + checkKey(key, allowedKeys, validator[key]); + if (key === 'fields' || key === 'requireUserKeys') { + const values = validator[key]; + if (Array.isArray(values)) { + continue; + } + for (const value in values) { + const data = values[value]; + for (const subKey in data) { + checkKey(subKey, fieldOptions, data[subKey]); + } + } + } + } +} +export async function maybeRunValidator(request, functionName, auth) { + const theValidator = getValidator(functionName, Parse.applicationId); + if (!theValidator) { + return; + } + if (typeof theValidator === 'object' && theValidator.skipWithMasterKey && request.master) { + request.skipWithMasterKey = true; + } + try { + if (typeof theValidator === 'object') { + await builtInTriggerValidator(theValidator, request, auth); + } else { + await theValidator(request); + } + } catch (e) { + const error = resolveError(e, { + code: Parse.Error.VALIDATION_ERROR, + message: 'Validation failed.', + }); + throw error; + } +} +async function builtInTriggerValidator(options, request, auth) { + if (request.master && !options.validateMasterKey) { + return; + } + let reqUser = request.user; + if ( + !reqUser && + request.object && + request.object.className === '_User' && + !request.object.existed() + ) { + reqUser = request.object; + } + if ( + (options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) && + !reqUser + ) { + throw 'Validation failed. Please login to continue.'; + } + 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 = params[key]; + if (value == null) { + throw `Validation failed. Please specify data for ${key}.`; + } + }; + + const validateOptions = async (opt, key, val) => { + let opts = opt.options; + if (typeof opts === 'function') { + try { + const result = await 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(); + }; + if (Array.isArray(options.fields)) { + for (const key of options.fields) { + requiredParam(key); + } + } else { + const optionPromises = []; + 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; + 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 != null) { + request.object.set(key, opt.default); + } + } + if (opt.required) { + requiredParam(key); + } + const optional = !opt.required && val === undefined; + if (!optional) { + if (opt.type) { + const type = getType(opt.type); + const valType = Array.isArray(val) ? 'array' : typeof val; + if (valType !== type) { + throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; + } + } + if (opt.options) { + optionPromises.push(validateOptions(opt, key, val)); + } + } + } + } + await Promise.all(optionPromises); + } + let userRoles = options.requireAnyUserRoles; + let requireAllRoles = options.requireAllUserRoles; + const promises = [Promise.resolve(), Promise.resolve(), Promise.resolve()]; + if (userRoles || requireAllRoles) { + promises[0] = auth.getUserRoles(); + } + if (typeof userRoles === 'function') { + promises[1] = userRoles(); + } + if (typeof requireAllRoles === 'function') { + promises[2] = requireAllRoles(); + } + const [roles, resolvedUserRoles, resolvedRequireAll] = await Promise.all(promises); + if (resolvedUserRoles && Array.isArray(resolvedUserRoles)) { + userRoles = resolvedUserRoles; + } + if (resolvedRequireAll && Array.isArray(resolvedRequireAll)) { + requireAllRoles = resolvedRequireAll; + } + if (userRoles) { + const hasRole = userRoles.some(requiredRole => roles.includes(`role:${requiredRole}`)); + if (!hasRole) { + throw `Validation failed. User does not match the required roles.`; + } + } + if (requireAllRoles) { + for (const requiredRole of requireAllRoles) { + if (!roles.includes(`role:${requiredRole}`)) { + throw `Validation failed. User does not match all the required roles.`; + } + } + } + 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.`; + } + } + } else if (typeof userKeys === 'object') { + const optionPromises = []; + for (const key in options.requireUserKeys) { + const opt = options.requireUserKeys[key]; + if (opt.options) { + optionPromises.push(validateOptions(opt, key, reqUser.get(key))); + } + } + await Promise.all(optionPromises); + } +} diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index d16fe28ad4..83163fa5f2 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -1,5 +1,15 @@ -import { Parse } from 'parse/node'; -import * as triggers from '../triggers'; +import Parse from 'parse/node'; +import { + addFunction, + addTrigger, + addJob, + addFileTrigger, + addConnectTrigger, + addLiveQueryEventHandler, + _unregisterAll, + Types, +} from '../triggers'; +import { validateValidator } from './Parse.Cloud.Validator.js'; const Config = require('../Config'); function isParseObjectConstructor(object) { @@ -7,77 +17,9 @@ function isParseObjectConstructor(object) { } function getClassName(parseClass) { - if (parseClass && parseClass.className) { - return parseClass.className; - } - return parseClass; + return parseClass?.className || parseClass; } -function validateValidator(validator) { - if (!validator || typeof validator === 'function') { - return; - } - const fieldOptions = { - type: ['Any'], - constant: [Boolean], - default: ['Any'], - options: [Array, 'function', 'Any'], - required: [Boolean], - error: [String], - }; - const allowedKeys = { - requireUser: [Boolean], - requireAnyUserRoles: [Array, 'function'], - requireAllUserRoles: [Array, 'function'], - requireMaster: [Boolean], - validateMasterKey: [Boolean], - skipWithMasterKey: [Boolean], - requireUserKeys: [Array, Object], - fields: [Array, Object], - }; - const getType = fn => { - if (Array.isArray(fn)) { - return 'array'; - } - if (fn === 'Any' || fn === 'function') { - return fn; - } - const type = typeof fn; - if (typeof fn === 'function') { - const match = fn && fn.toString().match(/^\s*function (\w+)/); - return (match ? match[1] : 'function').toLowerCase(); - } - return type; - }; - const checkKey = (key, data, validatorParam) => { - const parameter = data[key]; - if (!parameter) { - throw `${key} is not a supported parameter for Cloud Function validations.`; - } - const types = parameter.map(type => getType(type)); - const type = getType(validatorParam); - if (!types.includes(type) && !types.includes('Any')) { - throw `Invalid type for Cloud Function validation key ${key}. Expected ${types.join( - '|' - )}, actual ${type}`; - } - }; - for (const key in validator) { - checkKey(key, allowedKeys, validator[key]); - if (key === 'fields' || key === 'requireUserKeys') { - const values = validator[key]; - if (Array.isArray(values)) { - continue; - } - for (const value in values) { - const data = values[value]; - for (const subKey in data) { - checkKey(subKey, fieldOptions, data[subKey]); - } - } - } - } -} /** @namespace * @name Parse * @description The Parse SDK. @@ -90,7 +32,7 @@ function validateValidator(validator) { * @description The Parse Cloud Code SDK. */ -var ParseCloud = {}; +export const ParseCloud = {}; /** * Defines a Cloud Function. * @@ -116,7 +58,7 @@ var ParseCloud = {}; */ ParseCloud.define = function (functionName, handler, validationHandler) { validateValidator(validationHandler); - triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId); + addFunction(functionName, handler, validationHandler, Parse.applicationId); }; /** @@ -131,7 +73,7 @@ ParseCloud.define = function (functionName, handler, validationHandler) { * */ ParseCloud.job = function (functionName, handler) { - triggers.addJob(functionName, handler, Parse.applicationId); + addJob(functionName, handler, Parse.applicationId); }; /** @@ -161,15 +103,9 @@ ParseCloud.job = function (functionName, handler) { * @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); + const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.beforeSave, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.beforeSave, className, handler, Parse.applicationId, validationHandler); }; /** @@ -197,15 +133,9 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * @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); + const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.beforeDelete, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.beforeDelete, className, handler, Parse.applicationId, validationHandler); }; /** @@ -239,7 +169,7 @@ ParseCloud.beforeLogin = function (handler) { className = getClassName(handler); handler = arguments[1]; } - triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId); + addTrigger(Types.beforeLogin, className, handler, Parse.applicationId); }; /** @@ -269,7 +199,7 @@ ParseCloud.afterLogin = function (handler) { className = getClassName(handler); handler = arguments[1]; } - triggers.addTrigger(triggers.Types.afterLogin, className, handler, Parse.applicationId); + addTrigger(Types.afterLogin, className, handler, Parse.applicationId); }; /** @@ -298,7 +228,7 @@ ParseCloud.afterLogout = function (handler) { className = getClassName(handler); handler = arguments[1]; } - triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId); + addTrigger(Types.afterLogout, className, handler, Parse.applicationId); }; /** @@ -327,15 +257,9 @@ ParseCloud.afterLogout = function (handler) { * @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); + const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.afterSave, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.afterSave, className, handler, Parse.applicationId, validationHandler); }; /** @@ -363,15 +287,9 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * @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); + const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.afterDelete, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.afterDelete, className, handler, Parse.applicationId, validationHandler); }; /** @@ -399,15 +317,9 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * @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); + const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.beforeFind, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.beforeFind, className, handler, Parse.applicationId, validationHandler); }; /** @@ -437,13 +349,7 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { ParseCloud.afterFind = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.afterFind, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.afterFind, className, handler, Parse.applicationId, validationHandler); }; /** @@ -470,12 +376,7 @@ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { */ ParseCloud.beforeSaveFile = function (handler, validationHandler) { validateValidator(validationHandler); - triggers.addFileTrigger( - triggers.Types.beforeSaveFile, - handler, - Parse.applicationId, - validationHandler - ); + addFileTrigger(Types.beforeSaveFile, handler, Parse.applicationId, validationHandler); }; /** @@ -502,12 +403,7 @@ ParseCloud.beforeSaveFile = function (handler, validationHandler) { */ ParseCloud.afterSaveFile = function (handler, validationHandler) { validateValidator(validationHandler); - triggers.addFileTrigger( - triggers.Types.afterSaveFile, - handler, - Parse.applicationId, - validationHandler - ); + addFileTrigger(Types.afterSaveFile, handler, Parse.applicationId, validationHandler); }; /** @@ -534,12 +430,7 @@ ParseCloud.afterSaveFile = function (handler, validationHandler) { */ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { validateValidator(validationHandler); - triggers.addFileTrigger( - triggers.Types.beforeDeleteFile, - handler, - Parse.applicationId, - validationHandler - ); + addFileTrigger(Types.beforeDeleteFile, handler, Parse.applicationId, validationHandler); }; /** @@ -566,12 +457,7 @@ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { */ ParseCloud.afterDeleteFile = function (handler, validationHandler) { validateValidator(validationHandler); - triggers.addFileTrigger( - triggers.Types.afterDeleteFile, - handler, - Parse.applicationId, - validationHandler - ); + addFileTrigger(Types.afterDeleteFile, handler, Parse.applicationId, validationHandler); }; /** @@ -598,12 +484,7 @@ ParseCloud.afterDeleteFile = function (handler, validationHandler) { */ ParseCloud.beforeConnect = function (handler, validationHandler) { validateValidator(validationHandler); - triggers.addConnectTrigger( - triggers.Types.beforeConnect, - handler, - Parse.applicationId, - validationHandler - ); + addConnectTrigger(Types.beforeConnect, handler, Parse.applicationId, validationHandler); }; /** @@ -664,17 +545,11 @@ ParseCloud.sendEmail = function (data) { ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { validateValidator(validationHandler); var className = getClassName(parseClass); - triggers.addTrigger( - triggers.Types.beforeSubscribe, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.beforeSubscribe, className, handler, Parse.applicationId, validationHandler); }; ParseCloud.onLiveQueryEvent = function (handler) { - triggers.addLiveQueryEventHandler(handler, Parse.applicationId); + addLiveQueryEventHandler(handler, Parse.applicationId); }; /** @@ -703,17 +578,11 @@ ParseCloud.onLiveQueryEvent = function (handler) { ParseCloud.afterLiveQueryEvent = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); validateValidator(validationHandler); - triggers.addTrigger( - triggers.Types.afterEvent, - className, - handler, - Parse.applicationId, - validationHandler - ); + addTrigger(Types.afterEvent, className, handler, Parse.applicationId, validationHandler); }; ParseCloud._removeAllHooks = () => { - triggers._unregisterAll(); + _unregisterAll(); }; ParseCloud.useMasterKey = () => { @@ -725,8 +594,6 @@ ParseCloud.useMasterKey = () => { ParseCloud.httpRequest = require('./httpRequest'); -module.exports = ParseCloud; - /** * @interface Parse.Cloud.TriggerRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/triggers.js b/src/triggers.js index cdd4353128..182756c663 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,7 @@ // triggers.js import Parse from 'parse/node'; import { logger } from './logger'; +import { maybeRunValidator } from './cloud-code/Parse.Cloud.Validator.js'; export const Types = { beforeLogin: 'beforeLogin', @@ -602,199 +603,6 @@ export function resolveError(message, defaultOpts) { } return error; } -export function maybeRunValidator(request, functionName, auth) { - const theValidator = getValidator(functionName, Parse.applicationId); - if (!theValidator) { - return; - } - if (typeof theValidator === 'object' && theValidator.skipWithMasterKey && request.master) { - request.skipWithMasterKey = true; - } - return new Promise((resolve, reject) => { - return Promise.resolve() - .then(() => { - return typeof theValidator === 'object' - ? builtInTriggerValidator(theValidator, request, auth) - : theValidator(request); - }) - .then(() => { - resolve(); - }) - .catch(e => { - const error = resolveError(e, { - code: Parse.Error.VALIDATION_ERROR, - message: 'Validation failed.', - }); - reject(error); - }); - }); -} -async function builtInTriggerValidator(options, request, auth) { - if (request.master && !options.validateMasterKey) { - return; - } - let reqUser = request.user; - if ( - !reqUser && - request.object && - request.object.className === '_User' && - !request.object.existed() - ) { - reqUser = request.object; - } - if ( - (options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) && - !reqUser - ) { - throw 'Validation failed. Please login to continue.'; - } - 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 = params[key]; - if (value == null) { - throw `Validation failed. Please specify data for ${key}.`; - } - }; - - const validateOptions = async (opt, key, val) => { - let opts = opt.options; - if (typeof opts === 'function') { - try { - const result = await 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(); - }; - if (Array.isArray(options.fields)) { - for (const key of options.fields) { - requiredParam(key); - } - } else { - const optionPromises = []; - 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; - 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 != null) { - request.object.set(key, opt.default); - } - } - if (opt.required) { - requiredParam(key); - } - const optional = !opt.required && val === undefined; - if (!optional) { - if (opt.type) { - const type = getType(opt.type); - const valType = Array.isArray(val) ? 'array' : typeof val; - if (valType !== type) { - throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; - } - } - if (opt.options) { - optionPromises.push(validateOptions(opt, key, val)); - } - } - } - } - await Promise.all(optionPromises); - } - let userRoles = options.requireAnyUserRoles; - let requireAllRoles = options.requireAllUserRoles; - const promises = [Promise.resolve(), Promise.resolve(), Promise.resolve()]; - if (userRoles || requireAllRoles) { - promises[0] = auth.getUserRoles(); - } - if (typeof userRoles === 'function') { - promises[1] = userRoles(); - } - if (typeof requireAllRoles === 'function') { - promises[2] = requireAllRoles(); - } - const [roles, resolvedUserRoles, resolvedRequireAll] = await Promise.all(promises); - if (resolvedUserRoles && Array.isArray(resolvedUserRoles)) { - userRoles = resolvedUserRoles; - } - if (resolvedRequireAll && Array.isArray(resolvedRequireAll)) { - requireAllRoles = resolvedRequireAll; - } - if (userRoles) { - const hasRole = userRoles.some(requiredRole => roles.includes(`role:${requiredRole}`)); - if (!hasRole) { - throw `Validation failed. User does not match the required roles.`; - } - } - if (requireAllRoles) { - for (const requiredRole of requireAllRoles) { - if (!roles.includes(`role:${requiredRole}`)) { - throw `Validation failed. User does not match all the required roles.`; - } - } - } - 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.`; - } - } - } else if (typeof userKeys === 'object') { - const optionPromises = []; - for (const key in options.requireUserKeys) { - const opt = options.requireUserKeys[key]; - if (opt.options) { - optionPromises.push(validateOptions(opt, key, reqUser.get(key))); - } - } - await Promise.all(optionPromises); - } -} // To be used as part of the promise chain when saving/deleting an object // Will resolve successfully if no trigger is configured From fa109b67ad2bb053a34e2c7978dec035b135ce9e Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 10:09:24 +1000 Subject: [PATCH 04/12] Revert "fix tests" This reverts commit ecc7d1de14edb655278d974d9c10accf3699942f. --- src/ParseServer.js | 2 +- src/Routers/FunctionsRouter.js | 6 +- src/cloud-code/Parse.Cloud.Validator.js | 255 ------------------------ src/cloud-code/Parse.Cloud.js | 211 ++++++++++++++++---- src/triggers.js | 194 +++++++++++++++++- 5 files changed, 367 insertions(+), 301 deletions(-) delete mode 100644 src/cloud-code/Parse.Cloud.Validator.js diff --git a/src/ParseServer.js b/src/ParseServer.js index 11e2be0738..43996ac751 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -368,7 +368,7 @@ class ParseServer { } function addParseCloud() { - const { ParseCloud } = require('./cloud-code/Parse.Cloud'); + const ParseCloud = require('./cloud-code/Parse.Cloud'); Object.assign(Parse.Cloud, ParseCloud); global.Parse = Parse; } diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 40dc022eaa..cef5b80c96 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -2,8 +2,7 @@ const { Parse } = require('parse/node'); -import { getJob, getFunction, resolveError } from '../triggers.js'; -import { maybeRunValidator } from './../cloud-code/Parse.Cloud.Validator.js'; +import { getJob, getFunction, maybeRunValidator, resolveError } from '../triggers.js'; import PromiseRouter from '../PromiseRouter'; import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; import { jobStatusHandler } from '../StatusHandler'; @@ -11,9 +10,6 @@ import _ from 'lodash'; import { logger } from '../logger'; function parseObject(obj) { - if (obj === null || obj === undefined) { - return obj; - } if (Array.isArray(obj)) { return obj.map(item => parseObject(item)); } diff --git a/src/cloud-code/Parse.Cloud.Validator.js b/src/cloud-code/Parse.Cloud.Validator.js deleted file mode 100644 index 5af828e59d..0000000000 --- a/src/cloud-code/Parse.Cloud.Validator.js +++ /dev/null @@ -1,255 +0,0 @@ -import { getValidator, resolveError } from '../triggers'; -import Parse from 'parse/node'; -export function validateValidator(validator) { - if (!validator || typeof validator === 'function') { - return; - } - const fieldOptions = { - type: ['Any'], - constant: [Boolean], - default: ['Any'], - options: [Array, 'function', 'Any'], - required: [Boolean], - error: [String], - }; - const allowedKeys = { - requireUser: [Boolean], - requireAnyUserRoles: [Array, 'function'], - requireAllUserRoles: [Array, 'function'], - requireMaster: [Boolean], - validateMasterKey: [Boolean], - skipWithMasterKey: [Boolean], - requireUserKeys: [Array, Object], - fields: [Array, Object], - }; - const getType = fn => { - if (Array.isArray(fn)) { - return 'array'; - } - if (fn === 'Any' || fn === 'function') { - return fn; - } - const type = typeof fn; - if (typeof fn === 'function') { - const match = fn && fn.toString().match(/^\s*function (\w+)/); - return (match ? match[1] : 'function').toLowerCase(); - } - return type; - }; - const checkKey = (key, data, validatorParam) => { - const parameter = data[key]; - if (!parameter) { - throw `${key} is not a supported parameter for Cloud Function validations.`; - } - const types = parameter.map(type => getType(type)); - const type = getType(validatorParam); - if (!types.includes(type) && !types.includes('Any')) { - throw `Invalid type for Cloud Function validation key ${key}. Expected ${types.join( - '|' - )}, actual ${type}`; - } - }; - for (const key in validator) { - checkKey(key, allowedKeys, validator[key]); - if (key === 'fields' || key === 'requireUserKeys') { - const values = validator[key]; - if (Array.isArray(values)) { - continue; - } - for (const value in values) { - const data = values[value]; - for (const subKey in data) { - checkKey(subKey, fieldOptions, data[subKey]); - } - } - } - } -} -export async function maybeRunValidator(request, functionName, auth) { - const theValidator = getValidator(functionName, Parse.applicationId); - if (!theValidator) { - return; - } - if (typeof theValidator === 'object' && theValidator.skipWithMasterKey && request.master) { - request.skipWithMasterKey = true; - } - try { - if (typeof theValidator === 'object') { - await builtInTriggerValidator(theValidator, request, auth); - } else { - await theValidator(request); - } - } catch (e) { - const error = resolveError(e, { - code: Parse.Error.VALIDATION_ERROR, - message: 'Validation failed.', - }); - throw error; - } -} -async function builtInTriggerValidator(options, request, auth) { - if (request.master && !options.validateMasterKey) { - return; - } - let reqUser = request.user; - if ( - !reqUser && - request.object && - request.object.className === '_User' && - !request.object.existed() - ) { - reqUser = request.object; - } - if ( - (options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) && - !reqUser - ) { - throw 'Validation failed. Please login to continue.'; - } - 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 = params[key]; - if (value == null) { - throw `Validation failed. Please specify data for ${key}.`; - } - }; - - const validateOptions = async (opt, key, val) => { - let opts = opt.options; - if (typeof opts === 'function') { - try { - const result = await 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(); - }; - if (Array.isArray(options.fields)) { - for (const key of options.fields) { - requiredParam(key); - } - } else { - const optionPromises = []; - 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; - 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 != null) { - request.object.set(key, opt.default); - } - } - if (opt.required) { - requiredParam(key); - } - const optional = !opt.required && val === undefined; - if (!optional) { - if (opt.type) { - const type = getType(opt.type); - const valType = Array.isArray(val) ? 'array' : typeof val; - if (valType !== type) { - throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; - } - } - if (opt.options) { - optionPromises.push(validateOptions(opt, key, val)); - } - } - } - } - await Promise.all(optionPromises); - } - let userRoles = options.requireAnyUserRoles; - let requireAllRoles = options.requireAllUserRoles; - const promises = [Promise.resolve(), Promise.resolve(), Promise.resolve()]; - if (userRoles || requireAllRoles) { - promises[0] = auth.getUserRoles(); - } - if (typeof userRoles === 'function') { - promises[1] = userRoles(); - } - if (typeof requireAllRoles === 'function') { - promises[2] = requireAllRoles(); - } - const [roles, resolvedUserRoles, resolvedRequireAll] = await Promise.all(promises); - if (resolvedUserRoles && Array.isArray(resolvedUserRoles)) { - userRoles = resolvedUserRoles; - } - if (resolvedRequireAll && Array.isArray(resolvedRequireAll)) { - requireAllRoles = resolvedRequireAll; - } - if (userRoles) { - const hasRole = userRoles.some(requiredRole => roles.includes(`role:${requiredRole}`)); - if (!hasRole) { - throw `Validation failed. User does not match the required roles.`; - } - } - if (requireAllRoles) { - for (const requiredRole of requireAllRoles) { - if (!roles.includes(`role:${requiredRole}`)) { - throw `Validation failed. User does not match all the required roles.`; - } - } - } - 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.`; - } - } - } else if (typeof userKeys === 'object') { - const optionPromises = []; - for (const key in options.requireUserKeys) { - const opt = options.requireUserKeys[key]; - if (opt.options) { - optionPromises.push(validateOptions(opt, key, reqUser.get(key))); - } - } - await Promise.all(optionPromises); - } -} diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 83163fa5f2..d16fe28ad4 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -1,15 +1,5 @@ -import Parse from 'parse/node'; -import { - addFunction, - addTrigger, - addJob, - addFileTrigger, - addConnectTrigger, - addLiveQueryEventHandler, - _unregisterAll, - Types, -} from '../triggers'; -import { validateValidator } from './Parse.Cloud.Validator.js'; +import { Parse } from 'parse/node'; +import * as triggers from '../triggers'; const Config = require('../Config'); function isParseObjectConstructor(object) { @@ -17,9 +7,77 @@ function isParseObjectConstructor(object) { } function getClassName(parseClass) { - return parseClass?.className || parseClass; + if (parseClass && parseClass.className) { + return parseClass.className; + } + return parseClass; } +function validateValidator(validator) { + if (!validator || typeof validator === 'function') { + return; + } + const fieldOptions = { + type: ['Any'], + constant: [Boolean], + default: ['Any'], + options: [Array, 'function', 'Any'], + required: [Boolean], + error: [String], + }; + const allowedKeys = { + requireUser: [Boolean], + requireAnyUserRoles: [Array, 'function'], + requireAllUserRoles: [Array, 'function'], + requireMaster: [Boolean], + validateMasterKey: [Boolean], + skipWithMasterKey: [Boolean], + requireUserKeys: [Array, Object], + fields: [Array, Object], + }; + const getType = fn => { + if (Array.isArray(fn)) { + return 'array'; + } + if (fn === 'Any' || fn === 'function') { + return fn; + } + const type = typeof fn; + if (typeof fn === 'function') { + const match = fn && fn.toString().match(/^\s*function (\w+)/); + return (match ? match[1] : 'function').toLowerCase(); + } + return type; + }; + const checkKey = (key, data, validatorParam) => { + const parameter = data[key]; + if (!parameter) { + throw `${key} is not a supported parameter for Cloud Function validations.`; + } + const types = parameter.map(type => getType(type)); + const type = getType(validatorParam); + if (!types.includes(type) && !types.includes('Any')) { + throw `Invalid type for Cloud Function validation key ${key}. Expected ${types.join( + '|' + )}, actual ${type}`; + } + }; + for (const key in validator) { + checkKey(key, allowedKeys, validator[key]); + if (key === 'fields' || key === 'requireUserKeys') { + const values = validator[key]; + if (Array.isArray(values)) { + continue; + } + for (const value in values) { + const data = values[value]; + for (const subKey in data) { + checkKey(subKey, fieldOptions, data[subKey]); + } + } + } + } +} /** @namespace * @name Parse * @description The Parse SDK. @@ -32,7 +90,7 @@ function getClassName(parseClass) { * @description The Parse Cloud Code SDK. */ -export const ParseCloud = {}; +var ParseCloud = {}; /** * Defines a Cloud Function. * @@ -58,7 +116,7 @@ export const ParseCloud = {}; */ ParseCloud.define = function (functionName, handler, validationHandler) { validateValidator(validationHandler); - addFunction(functionName, handler, validationHandler, Parse.applicationId); + triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId); }; /** @@ -73,7 +131,7 @@ ParseCloud.define = function (functionName, handler, validationHandler) { * */ ParseCloud.job = function (functionName, handler) { - addJob(functionName, handler, Parse.applicationId); + triggers.addJob(functionName, handler, Parse.applicationId); }; /** @@ -103,9 +161,15 @@ ParseCloud.job = function (functionName, handler) { * @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) { - const className = getClassName(parseClass); + var className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.beforeSave, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.beforeSave, + className, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -133,9 +197,15 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * @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) { - const className = getClassName(parseClass); + var className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.beforeDelete, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.beforeDelete, + className, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -169,7 +239,7 @@ ParseCloud.beforeLogin = function (handler) { className = getClassName(handler); handler = arguments[1]; } - addTrigger(Types.beforeLogin, className, handler, Parse.applicationId); + triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId); }; /** @@ -199,7 +269,7 @@ ParseCloud.afterLogin = function (handler) { className = getClassName(handler); handler = arguments[1]; } - addTrigger(Types.afterLogin, className, handler, Parse.applicationId); + triggers.addTrigger(triggers.Types.afterLogin, className, handler, Parse.applicationId); }; /** @@ -228,7 +298,7 @@ ParseCloud.afterLogout = function (handler) { className = getClassName(handler); handler = arguments[1]; } - addTrigger(Types.afterLogout, className, handler, Parse.applicationId); + triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId); }; /** @@ -257,9 +327,15 @@ ParseCloud.afterLogout = function (handler) { * @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) { - const className = getClassName(parseClass); + var className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.afterSave, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.afterSave, + className, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -287,9 +363,15 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * @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) { - const className = getClassName(parseClass); + var className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.afterDelete, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.afterDelete, + className, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -317,9 +399,15 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * @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) { - const className = getClassName(parseClass); + var className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.beforeFind, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.beforeFind, + className, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -349,7 +437,13 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { ParseCloud.afterFind = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.afterFind, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.afterFind, + className, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -376,7 +470,12 @@ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { */ ParseCloud.beforeSaveFile = function (handler, validationHandler) { validateValidator(validationHandler); - addFileTrigger(Types.beforeSaveFile, handler, Parse.applicationId, validationHandler); + triggers.addFileTrigger( + triggers.Types.beforeSaveFile, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -403,7 +502,12 @@ ParseCloud.beforeSaveFile = function (handler, validationHandler) { */ ParseCloud.afterSaveFile = function (handler, validationHandler) { validateValidator(validationHandler); - addFileTrigger(Types.afterSaveFile, handler, Parse.applicationId, validationHandler); + triggers.addFileTrigger( + triggers.Types.afterSaveFile, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -430,7 +534,12 @@ ParseCloud.afterSaveFile = function (handler, validationHandler) { */ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { validateValidator(validationHandler); - addFileTrigger(Types.beforeDeleteFile, handler, Parse.applicationId, validationHandler); + triggers.addFileTrigger( + triggers.Types.beforeDeleteFile, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -457,7 +566,12 @@ ParseCloud.beforeDeleteFile = function (handler, validationHandler) { */ ParseCloud.afterDeleteFile = function (handler, validationHandler) { validateValidator(validationHandler); - addFileTrigger(Types.afterDeleteFile, handler, Parse.applicationId, validationHandler); + triggers.addFileTrigger( + triggers.Types.afterDeleteFile, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -484,7 +598,12 @@ ParseCloud.afterDeleteFile = function (handler, validationHandler) { */ ParseCloud.beforeConnect = function (handler, validationHandler) { validateValidator(validationHandler); - addConnectTrigger(Types.beforeConnect, handler, Parse.applicationId, validationHandler); + triggers.addConnectTrigger( + triggers.Types.beforeConnect, + handler, + Parse.applicationId, + validationHandler + ); }; /** @@ -545,11 +664,17 @@ ParseCloud.sendEmail = function (data) { ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { validateValidator(validationHandler); var className = getClassName(parseClass); - addTrigger(Types.beforeSubscribe, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.beforeSubscribe, + className, + handler, + Parse.applicationId, + validationHandler + ); }; ParseCloud.onLiveQueryEvent = function (handler) { - addLiveQueryEventHandler(handler, Parse.applicationId); + triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; /** @@ -578,11 +703,17 @@ ParseCloud.onLiveQueryEvent = function (handler) { ParseCloud.afterLiveQueryEvent = function (parseClass, handler, validationHandler) { const className = getClassName(parseClass); validateValidator(validationHandler); - addTrigger(Types.afterEvent, className, handler, Parse.applicationId, validationHandler); + triggers.addTrigger( + triggers.Types.afterEvent, + className, + handler, + Parse.applicationId, + validationHandler + ); }; ParseCloud._removeAllHooks = () => { - _unregisterAll(); + triggers._unregisterAll(); }; ParseCloud.useMasterKey = () => { @@ -594,6 +725,8 @@ ParseCloud.useMasterKey = () => { ParseCloud.httpRequest = require('./httpRequest'); +module.exports = ParseCloud; + /** * @interface Parse.Cloud.TriggerRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/triggers.js b/src/triggers.js index 182756c663..cdd4353128 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,7 +1,6 @@ // triggers.js import Parse from 'parse/node'; import { logger } from './logger'; -import { maybeRunValidator } from './cloud-code/Parse.Cloud.Validator.js'; export const Types = { beforeLogin: 'beforeLogin', @@ -603,6 +602,199 @@ export function resolveError(message, defaultOpts) { } return error; } +export function maybeRunValidator(request, functionName, auth) { + const theValidator = getValidator(functionName, Parse.applicationId); + if (!theValidator) { + return; + } + if (typeof theValidator === 'object' && theValidator.skipWithMasterKey && request.master) { + request.skipWithMasterKey = true; + } + return new Promise((resolve, reject) => { + return Promise.resolve() + .then(() => { + return typeof theValidator === 'object' + ? builtInTriggerValidator(theValidator, request, auth) + : theValidator(request); + }) + .then(() => { + resolve(); + }) + .catch(e => { + const error = resolveError(e, { + code: Parse.Error.VALIDATION_ERROR, + message: 'Validation failed.', + }); + reject(error); + }); + }); +} +async function builtInTriggerValidator(options, request, auth) { + if (request.master && !options.validateMasterKey) { + return; + } + let reqUser = request.user; + if ( + !reqUser && + request.object && + request.object.className === '_User' && + !request.object.existed() + ) { + reqUser = request.object; + } + if ( + (options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) && + !reqUser + ) { + throw 'Validation failed. Please login to continue.'; + } + 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 = params[key]; + if (value == null) { + throw `Validation failed. Please specify data for ${key}.`; + } + }; + + const validateOptions = async (opt, key, val) => { + let opts = opt.options; + if (typeof opts === 'function') { + try { + const result = await 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(); + }; + if (Array.isArray(options.fields)) { + for (const key of options.fields) { + requiredParam(key); + } + } else { + const optionPromises = []; + 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; + 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 != null) { + request.object.set(key, opt.default); + } + } + if (opt.required) { + requiredParam(key); + } + const optional = !opt.required && val === undefined; + if (!optional) { + if (opt.type) { + const type = getType(opt.type); + const valType = Array.isArray(val) ? 'array' : typeof val; + if (valType !== type) { + throw `Validation failed. Invalid type for ${key}. Expected: ${type}`; + } + } + if (opt.options) { + optionPromises.push(validateOptions(opt, key, val)); + } + } + } + } + await Promise.all(optionPromises); + } + let userRoles = options.requireAnyUserRoles; + let requireAllRoles = options.requireAllUserRoles; + const promises = [Promise.resolve(), Promise.resolve(), Promise.resolve()]; + if (userRoles || requireAllRoles) { + promises[0] = auth.getUserRoles(); + } + if (typeof userRoles === 'function') { + promises[1] = userRoles(); + } + if (typeof requireAllRoles === 'function') { + promises[2] = requireAllRoles(); + } + const [roles, resolvedUserRoles, resolvedRequireAll] = await Promise.all(promises); + if (resolvedUserRoles && Array.isArray(resolvedUserRoles)) { + userRoles = resolvedUserRoles; + } + if (resolvedRequireAll && Array.isArray(resolvedRequireAll)) { + requireAllRoles = resolvedRequireAll; + } + if (userRoles) { + const hasRole = userRoles.some(requiredRole => roles.includes(`role:${requiredRole}`)); + if (!hasRole) { + throw `Validation failed. User does not match the required roles.`; + } + } + if (requireAllRoles) { + for (const requiredRole of requireAllRoles) { + if (!roles.includes(`role:${requiredRole}`)) { + throw `Validation failed. User does not match all the required roles.`; + } + } + } + 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.`; + } + } + } else if (typeof userKeys === 'object') { + const optionPromises = []; + for (const key in options.requireUserKeys) { + const opt = options.requireUserKeys[key]; + if (opt.options) { + optionPromises.push(validateOptions(opt, key, reqUser.get(key))); + } + } + await Promise.all(optionPromises); + } +} // To be used as part of the promise chain when saving/deleting an object // Will resolve successfully if no trigger is configured From 5b80aba146b6912853df0e53c986bf5416f277fb Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 10:19:06 +1000 Subject: [PATCH 05/12] fix tests --- spec/CloudCode.spec.js | 31 +++++++++++-------------------- src/Routers/FunctionsRouter.js | 3 +++ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 4868bda93d..8645918fc9 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -564,32 +564,23 @@ describe('Cloud Code', () => { ); }); - it('test afterDelete ran and created an object', function (done) { + it('test afterDelete ran and created an object', async done => { + const obj = new Parse.Object('AfterDeleteTest'); + const test = async () => { + const query = new Parse.Query('AfterDeleteProof'); + query.equalTo('proof', obj.id); + const results = await query.find(); + expect(results.length).toEqual(1); + done(); + }; Parse.Cloud.afterDelete('AfterDeleteTest', function (req) { const obj = new Parse.Object('AfterDeleteProof'); obj.set('proof', req.object.id); obj.save().then(test); }); - const obj = new Parse.Object('AfterDeleteTest'); - obj.save().then(function () { - obj.destroy(); - }); - - function test() { - const query = new Parse.Query('AfterDeleteProof'); - query.equalTo('proof', obj.id); - query.find().then( - function (results) { - expect(results.length).toEqual(1); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - } + await obj.save(); + await obj.destroy(); }); it('test cloud function return types', function (done) { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index cef5b80c96..64b29cfd21 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -10,6 +10,9 @@ import _ from 'lodash'; import { logger } from '../logger'; function parseObject(obj) { + if (!(obj ?? false)) { + return obj; + } if (Array.isArray(obj)) { return obj.map(item => parseObject(item)); } From dd19f49708e7242244291e6345a22b7538d10de0 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 10:21:15 +1000 Subject: [PATCH 06/12] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b329847255..597f2da1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,6 +152,7 @@ ___ - ci: bump ci environment (Manuel Trezza) [#7539](https://github.com/parse-community/parse-server/pull/7539) - CI now pushes docker images to Docker Hub (Corey Baker) [#7548](https://github.com/parse-community/parse-server/pull/7548) - docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562) +- cleanup FunctionRouter.js (Daniel Blyth) [#7564](https://github.com/parse-community/parse-server/pull/7564) ## 4.10.3 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.2...4.10.3) @@ -178,15 +179,15 @@ ___ *Versions >4.5.2 and <4.10.0 are skipped.* -> ⚠️ A security incident caused a number of incorrect version tags to be pushed to the Parse Server repository. These version tags linked to a personal fork of a contributor who had write access to the repository. The code to which these tags linked has not been reviewed or approved by Parse Platform. Even though no releases were published with these incorrect versions, it was possible to define a Parse Server dependency that pointed to these version tags, for example if you defined this dependency: +> ⚠️ A security incident caused a number of incorrect version tags to be pushed to the Parse Server repository. These version tags linked to a personal fork of a contributor who had write access to the repository. The code to which these tags linked has not been reviewed or approved by Parse Platform. Even though no releases were published with these incorrect versions, it was possible to define a Parse Server dependency that pointed to these version tags, for example if you defined this dependency: > ```js > "parse-server": "git@github.com:parse-community/parse-server.git#4.9.3" > ``` -> +> > We have since deleted the incorrect version tags, but they may still show up if your personal fork on GitHub or locally. We do not know when these tags have been pushed to the Parse Server repository, but we first became aware of this issue on July 21, 2021. We are not aware of any malicious code or concerns related to privacy, security or legality (e.g. proprietary code). However, it has been reported that some functionality does not work as expected and the introduction of security vulnerabilities cannot be ruled out. > -> You may be also affected if you used the Bitnami image for Parse Server. Bitnami picked up the incorrect version tag `4.9.3` and published a new Bitnami image for Parse Server. -> +> You may be also affected if you used the Bitnami image for Parse Server. Bitnami picked up the incorrect version tag `4.9.3` and published a new Bitnami image for Parse Server. +> >**If you are using any of the affected versions, we urgently recommend to upgrade to version `4.10.0`.** ## 4.5.2 From 3ab2f49809146ace7470c6fc117ea07e753ad635 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 10:40:04 +1000 Subject: [PATCH 07/12] coverage --- spec/CloudCode.spec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 8645918fc9..491bae625b 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1338,6 +1338,19 @@ describe('Cloud Code', () => { .catch(done.fail); }); + it('run job should fail', async () => { + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/jobs/failedJob', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + }) + ).toBeRejected(); + }); + it('should not run without master key', done => { expect(() => { Parse.Cloud.job('myJob', () => {}); From 7abc2cf7dcb348d25bfe32b2c4f68ad8c557f434 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 10 Sep 2021 11:16:24 +1000 Subject: [PATCH 08/12] coverage --- spec/CloudCode.spec.js | 36 ++++++++++++++++++++++++++++++++++ src/Routers/FunctionsRouter.js | 4 ++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 491bae625b..b92783252e 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1408,6 +1408,42 @@ describe('Cloud Code', () => { .catch(done.fail); }); + it('should run a job as body param', done => { + expect(() => { + Parse.Cloud.job('myJob', (req, res) => { + expect(req.functionName).toBeUndefined(); + expect(req.jobName).toBe('myJob'); + expect(typeof req.jobId).toBe('string'); + expect(typeof req.message).toBe('function'); + expect(typeof res).toBe('undefined'); + }); + }).not.toThrow(); + + request({ + method: 'POST', + url: 'http://localhost:8378/1/jobs', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + body: { + jobName: 'myJob', + }, + }) + .then(async response => { + const jobStatusId = response.headers['x-parse-job-status-id']; + const checkJobStatus = async () => { + const jobStatus = await getJobStatus(jobStatusId); + return jobStatus.get('finishedAt'); + }; + while (!(await checkJobStatus())) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + }) + .then(done) + .catch(done.fail); + }); + it('should run with master key basic auth', done => { expect(() => { Parse.Cloud.job('myJob', (req, res) => { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 64b29cfd21..8f70f6241d 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -16,10 +16,10 @@ function parseObject(obj) { if (Array.isArray(obj)) { return obj.map(item => parseObject(item)); } - if (obj?.__type === 'Date') { + if (obj.__type === 'Date') { return Object.assign(new Date(obj.iso), obj); } - if (obj?.__type === 'File') { + if (obj.__type === 'File') { return Parse.File.fromJSON(obj); } if (typeof obj === 'object') { From fc18d8dd56351b266c2d23990583b19ae472de1d Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 14 Sep 2021 17:46:40 +1000 Subject: [PATCH 09/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 597f2da1a8..9b05cbcacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -152,7 +152,7 @@ ___ - ci: bump ci environment (Manuel Trezza) [#7539](https://github.com/parse-community/parse-server/pull/7539) - CI now pushes docker images to Docker Hub (Corey Baker) [#7548](https://github.com/parse-community/parse-server/pull/7548) - docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562) -- cleanup FunctionRouter.js (Daniel Blyth) [#7564](https://github.com/parse-community/parse-server/pull/7564) +- refactor: simplify Cloud Code tests and FunctionsRouter (Daniel Blyth) [#7564](https://github.com/parse-community/parse-server/pull/7564) ## 4.10.3 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.2...4.10.3) From 727d54d4c9d0c175f3a9a887844b2576e5708008 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 14 Sep 2021 23:17:34 +1000 Subject: [PATCH 10/12] Update CloudCode.spec.js --- spec/CloudCode.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 24cd7ef5e1..a3555f7de2 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -38,6 +38,7 @@ describe('Cloud Code', () => { it('can create functions', async () => { Parse.Cloud.define('hello', () => 'Hello world!'); await expectAsync(Parse.Cloud.run('hello')).toBeResolvedTo('Hello world!'); + }); it('can load cloud code as a module', async () => { process.env.npm_package_type = 'module'; From 2e47fb59c7044c4b932e3ae832d910acea8ae965 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 15 Sep 2021 10:56:45 +1000 Subject: [PATCH 11/12] Update FunctionsRouter.js --- src/Routers/FunctionsRouter.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 8f70f6241d..7cce0152d2 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -1,7 +1,6 @@ // FunctionsRouter.js -const { Parse } = require('parse/node'); - +import Parse from 'parse/node'; import { getJob, getFunction, maybeRunValidator, resolveError } from '../triggers.js'; import PromiseRouter from '../PromiseRouter'; import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; @@ -45,13 +44,9 @@ export class FunctionsRouter extends PromiseRouter { '/jobs/:jobName', promiseEnsureIdempotency, promiseEnforceMasterKeyAccess, - function (req) { - return FunctionsRouter.handleCloudJob(req); - } + FunctionsRouter.handleCloudJob ); - this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, function (req) { - return FunctionsRouter.handleCloudJob(req); - }); + this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, FunctionsRouter.handleCloudJob); } static async handleCloudJob(req) { From ddcddc52b08eb3f3e2b6b47e88f7d89a407be9ed Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 9 Oct 2021 12:28:01 +1100 Subject: [PATCH 12/12] increase coverage --- spec/CloudCode.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index a3555f7de2..fa97d0f680 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -40,6 +40,21 @@ describe('Cloud Code', () => { await expectAsync(Parse.Cloud.run('hello')).toBeResolvedTo('Hello world!'); }); + it('can load cloud code from function', async () => { + reconfigureServer({ + cloud: () => { + Parse.Cloud.define('hello', () => 'Hello world...'); + }, + }); + await expectAsync(Parse.Cloud.run('hello')).toBeResolvedTo('Hello world...'); + }); + + it('cloud code should be a function or string', async () => { + await expectAsync(reconfigureServer({ cloud: [] })).toBeRejectedWith( + "argument 'cloud' must either be a string or a function" + ); + }); + it('can load cloud code as a module', async () => { process.env.npm_package_type = 'module'; await reconfigureServer({ cloud: './spec/cloud/cloudCodeModuleFile.js' });