diff --git a/spec/CloudCode.Validator.spec.js b/spec/CloudCode.Validator.spec.js index f9e7a44c82..dfec5c1e49 100644 --- a/spec/CloudCode.Validator.spec.js +++ b/spec/CloudCode.Validator.spec.js @@ -6,6 +6,9 @@ const validatorFail = () => { const validatorSuccess = () => { return true; }; +function testConfig() { + return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true }); +} describe('cloud validator', () => { it('complete validator', async done => { @@ -731,6 +734,38 @@ describe('cloud validator', () => { done(); }); + it('basic beforeSave Parse.Config skipWithMasterKey', async () => { + Parse.Cloud.beforeSave( + Parse.Config, + () => { + throw 'beforeSaveFile should have resolved using master key.'; + }, + { + skipWithMasterKey: true, + } + ); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + }); + + it('basic afterSave Parse.Config skipWithMasterKey', async () => { + Parse.Cloud.afterSave( + Parse.Config, + () => { + throw 'beforeSaveFile should have resolved using master key.'; + }, + { + skipWithMasterKey: true, + } + ); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + }); + it('beforeSave validateMasterKey and skipWithMasterKey fail', async function (done) { Parse.Cloud.beforeSave( 'BeforeSave', @@ -1441,7 +1476,7 @@ describe('cloud validator', () => { }); it('validate afterSaveFile fail', async done => { - Parse.Cloud.beforeSave(Parse.File, () => {}, validatorFail); + Parse.Cloud.afterSave(Parse.File, () => {}, validatorFail); try { const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); await file.save({ useMasterKey: true }); @@ -1496,6 +1531,42 @@ describe('cloud validator', () => { } }); + it('validate beforeSave Parse.Config', async () => { + Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorSuccess); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + }); + + it('validate beforeSave Parse.Config fail', async () => { + Parse.Cloud.beforeSave(Parse.Config, () => {}, validatorFail); + try { + await testConfig(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + } + }); + + it('validate afterSave Parse.Config', async () => { + Parse.Cloud.afterSave(Parse.Config, () => {}, validatorSuccess); + const config = await testConfig(); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + }); + + it('validate afterSave Parse.Config fail', async () => { + Parse.Cloud.afterSave(Parse.Config, () => {}, validatorFail); + try { + await testConfig(); + fail('cloud function should have failed.'); + } catch (e) { + expect(e.code).toBe(Parse.Error.VALIDATION_ERROR); + } + }); + it('Should have validator', async done => { Parse.Cloud.define( 'myFunction', diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 04540fcd4a..7dbec67d60 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3921,6 +3921,162 @@ describe('saveFile hooks', () => { }); }); +describe('Cloud Config hooks', () => { + function testConfig() { + return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true }); + } + + it('beforeSave(Parse.Config) can run hook with new config', async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, (req) => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + expect(count).toBe(1); + }); + + it('beforeSave(Parse.Config) can run hook with existing config', async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, (req) => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); + await testConfig(); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); + }); + + it('beforeSave(Parse.Config) should not change config if nothing is returned', async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, () => { + count += 1; + return; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + expect(count).toBe(1); + }); + + it('beforeSave(Parse.Config) throw custom error', async () => { + Parse.Cloud.beforeSave(Parse.Config, () => { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(e.message).toBe('It should fail'); + } + }); + + it('beforeSave(Parse.Config) throw string error', async () => { + Parse.Cloud.beforeSave(Parse.Config, () => { + throw 'before save failed'; + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(e.message).toBe('before save failed'); + } + }); + + it('beforeSave(Parse.Config) throw empty error', async () => { + Parse.Cloud.beforeSave(Parse.Config, () => { + throw null; + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(Parse.Error.SCRIPT_FAILED); + expect(e.message).toBe('Script failed. Unknown error.'); + } + }); + + it('afterSave(Parse.Config) can run hook with new config', async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, (req) => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + expect(count).toBe(1); + }); + + it('afterSave(Parse.Config) can run hook with existing config', async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, (req) => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); + await testConfig(); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); + }); + + it('afterSave(Parse.Config) should throw error', async () => { + Parse.Cloud.afterSave(Parse.Config, () => { + throw new Parse.Error(400, 'It should fail'); + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(400); + expect(e.message).toBe('It should fail'); + } + }); +}); + describe('sendEmail', () => { it('can send email via Parse.Cloud', async done => { const emailAdapter = { diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index c30619fc21..1fcdfe968a 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -2,6 +2,15 @@ import Parse from 'parse/node'; import PromiseRouter from '../PromiseRouter'; import * as middleware from '../middlewares'; +import * as triggers from '../triggers'; + +const getConfigFromParams = params => { + const config = new Parse.Config(); + for (const attr in params) { + config.attributes[attr] = Parse._decode(undefined, params[attr]); + } + return config; +}; export class GlobalConfigRouter extends PromiseRouter { getGlobalConfig(req) { @@ -30,7 +39,7 @@ export class GlobalConfigRouter extends PromiseRouter { }); } - updateGlobalConfig(req) { + async updateGlobalConfig(req) { if (req.auth.isReadOnly) { throw new Parse.Error( Parse.Error.OPERATION_FORBIDDEN, @@ -45,9 +54,37 @@ export class GlobalConfigRouter extends PromiseRouter { acc[`masterKeyOnly.${key}`] = masterKeyOnly[key] || false; return acc; }, {}); - return req.config.database - .update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true) - .then(() => ({ response: { result: true } })); + const className = triggers.getClassName(Parse.Config); + const hasBeforeSaveHook = triggers.triggerExists(className, triggers.Types.beforeSave, req.config.applicationId); + const hasAfterSaveHook = triggers.triggerExists(className, triggers.Types.afterSave, req.config.applicationId); + let originalConfigObject; + let updatedConfigObject; + const configObject = new Parse.Config(); + configObject.attributes = params; + + const results = await req.config.database.find('_GlobalConfig', { objectId: '1' }, { limit: 1 }); + const isNew = results.length !== 1; + if (!isNew && (hasBeforeSaveHook || hasAfterSaveHook)) { + originalConfigObject = getConfigFromParams(results[0].params); + } + try { + await triggers.maybeRunGlobalConfigTrigger(triggers.Types.beforeSave, req.auth, configObject, originalConfigObject, req.config, req.context); + if (isNew) { + await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }, true) + updatedConfigObject = configObject; + } else { + const result = await req.config.database.update('_GlobalConfig', { objectId: '1' }, update, {}, true); + updatedConfigObject = getConfigFromParams(result.params); + } + await triggers.maybeRunGlobalConfigTrigger(triggers.Types.afterSave, req.auth, updatedConfigObject, originalConfigObject, req.config, req.context); + return { response: { result: true } } + } catch (err) { + const error = triggers.resolveError(err, { + code: Parse.Error.SCRIPT_FAILED, + message: 'Script failed. Unknown error.', + }); + throw error; + } } mountRoutes() { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index e37cc9b225..3f33e5100d 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -79,10 +79,14 @@ const getRoute = parseClass => { _User: 'users', _Session: 'sessions', '@File': 'files', + '@Config' : 'config', }[parseClass] || 'classes'; if (parseClass === '@File') { return `/${route}/:id?(.*)`; } + if (parseClass === '@Config') { + return `/${route}`; + } return `/${route}/${parseClass}/:id?(.*)`; }; /** @namespace diff --git a/src/triggers.js b/src/triggers.js index db3ee6ddb0..e34c5fd3a8 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1027,3 +1027,38 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) } return fileObject; } + +export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObject, originalConfigObject, config, context) { + const GlobalConfigClassName = getClassName(Parse.Config); + const configTrigger = getTrigger(GlobalConfigClassName, triggerType, config.applicationId); + if (typeof configTrigger === 'function') { + try { + const request = getRequestObject(triggerType, auth, configObject, originalConfigObject, config, context); + await maybeRunValidator(request, `${triggerType}.${GlobalConfigClassName}`, auth); + if (request.skipWithMasterKey) { + return configObject; + } + const result = await configTrigger(request); + logTriggerSuccessBeforeHook( + triggerType, + 'Parse.Config', + configObject, + result, + auth, + config.logLevels.triggerBeforeSuccess + ); + return result || configObject; + } catch (error) { + logTriggerErrorBeforeHook( + triggerType, + 'Parse.Config', + configObject, + auth, + error, + config.logLevels.triggerBeforeError + ); + throw error; + } + } + return configObject; +}