diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index be8ec425..7d2184cf 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1809,4 +1809,46 @@ describe('afterFind hooks', () => { done(); }); }); + + it('should expose context in before and afterSave', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('MyClass', (req) => { + req.context = { + key: 'value', + otherKey: 1, + } + calledBefore = true; + }); + Parse.Cloud.afterSave('MyClass', (req) => { + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledAfter = true; + }); + + const object = new Parse.Object('MyClass'); + await object.save(); + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + }); + + it('should expose context in before and afterSave and let keys be set individually', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('MyClass', (req) => { + req.context.some = 'value'; + req.context.yolo = 1; + calledBefore = true; + }); + Parse.Cloud.afterSave('MyClass', (req) => { + expect(req.context.yolo).toBe(1); + expect(req.context.some).toBe('value'); + calledAfter = true; + }); + + const object = new Parse.Object('MyClass'); + await object.save(); + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + }); }); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index b8c941eb..5c3eae8b 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -5,6 +5,9 @@ const triggers = require('../lib/triggers'); const HooksController = require('../lib/Controllers/HooksController').default; const express = require("express"); const bodyParser = require('body-parser'); +const auth = require('../lib/Auth'); +const Config = require('../lib/Config'); + const port = 12345; const hookServerURL = "http://localhost:" + port; @@ -503,3 +506,39 @@ describe('Hooks', () => { }); }); }); + +describe('triggers', () => { + it('should produce a proper request object with context in beforeSave', () => { + const config = Config.get('test'); + const master = auth.master(config); + const context = { + originalKey: 'original' + }; + const req = triggers.getRequestObject(triggers.Types.beforeSave, master, {}, {}, config, context); + expect(req.context.originalKey).toBe('original'); + req.context = { + key: 'value' + }; + expect(context.key).toBe(undefined); + req.context = { + key: 'newValue' + }; + expect(context.key).toBe(undefined); + }); + + it('should produce a proper request object with context in afterSave', () => { + const config = Config.get('test'); + const master = auth.master(config); + const context = {}; + const req = triggers.getRequestObject(triggers.Types.afterSave, master, {}, {}, config, context); + expect(req.context).not.toBeUndefined(); + }); + + it('should not set context on beforeFind', () => { + const config = Config.get('test'); + const master = auth.master(config); + const context = {}; + const req = triggers.getRequestObject(triggers.Types.beforeFind, master, {}, {}, config, context); + expect(req.context).toBeUndefined(); + }); +}); diff --git a/src/RestWrite.js b/src/RestWrite.js index b91491ed..e7bb8eef 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -34,6 +34,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK this.clientSDK = clientSDK; this.storage = {}; this.runOptions = {}; + this.context = {}; if (!query && data.objectId) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId is an invalid field name.'); } @@ -165,7 +166,7 @@ RestWrite.prototype.runBeforeTrigger = function() { } return Promise.resolve().then(() => { - return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config); + return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config, this.context); }).then((response) => { if (response && response.object) { this.storage.fieldsChangedByTrigger = _.reduce(response.object, (result, value, key) => { @@ -1142,7 +1143,7 @@ RestWrite.prototype.runAfterTrigger = function() { this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject); // Run afterSave trigger - return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config) + return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config, this.context) .catch(function(err) { logger.warn('afterSave caught an error', err); }) diff --git a/src/triggers.js b/src/triggers.js index 0fb03564..c8c5435e 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -46,24 +46,58 @@ function validateClassNameForTriggers(className, type) { const _triggerStore = {}; -export function addFunction(functionName, handler, validationHandler, applicationId) { +const Category = { + Functions: 'Functions', + Validators: 'Validators', + Jobs: 'Jobs', + Triggers: 'Triggers' +} + +function getStore(category, name, applicationId) { + const path = name.split('.'); + path.splice(-1); // remove last component applicationId = applicationId || Parse.applicationId; _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); - _triggerStore[applicationId].Functions[functionName] = handler; - _triggerStore[applicationId].Validators[functionName] = validationHandler; + let store = _triggerStore[applicationId][category]; + for (const component of path) { + store = store[component]; + if (!store) { + return undefined; + } + } + return store; +} + +function add(category, name, handler, applicationId) { + const lastComponent = name.split('.').splice(-1); + const store = getStore(category, name, applicationId); + store[lastComponent] = handler; +} + +function remove(category, name, applicationId) { + const lastComponent = name.split('.').splice(-1); + const store = getStore(category, name, applicationId); + delete store[lastComponent]; +} + +function get(category, name, applicationId) { + const lastComponent = name.split('.').splice(-1); + const store = getStore(category, name, applicationId); + return store[lastComponent]; +} + +export function addFunction(functionName, handler, validationHandler, applicationId) { + add(Category.Functions, functionName, handler, applicationId); + add(Category.Validators, functionName, validationHandler, applicationId); } export function addJob(jobName, handler, applicationId) { - applicationId = applicationId || Parse.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); - _triggerStore[applicationId].Jobs[jobName] = handler; + add(Category.Jobs, jobName, handler, applicationId); } export function addTrigger(type, className, handler, applicationId) { validateClassNameForTriggers(className, type); - applicationId = applicationId || Parse.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); - _triggerStore[applicationId].Triggers[type][className] = handler; + add(Category.Triggers, `${type}.${className}`, handler, applicationId); } export function addLiveQueryEventHandler(handler, applicationId) { @@ -73,13 +107,11 @@ export function addLiveQueryEventHandler(handler, applicationId) { } export function removeFunction(functionName, applicationId) { - applicationId = applicationId || Parse.applicationId; - delete _triggerStore[applicationId].Functions[functionName] + remove(Category.Functions, functionName, applicationId); } export function removeTrigger(type, className, applicationId) { - applicationId = applicationId || Parse.applicationId; - delete _triggerStore[applicationId].Triggers[type][className] + remove(Category.Triggers, `${type}.${className}`, applicationId); } export function _unregisterAll() { @@ -90,14 +122,7 @@ export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { throw "Missing ApplicationID"; } - var manager = _triggerStore[applicationId] - if (manager - && manager.Triggers - && manager.Triggers[triggerType] - && manager.Triggers[triggerType][className]) { - return manager.Triggers[triggerType][className]; - } - return undefined; + return get(Category.Triggers, `${triggerType}.${className}`, applicationId); } export function triggerExists(className: string, type: string, applicationId: string): boolean { @@ -105,19 +130,11 @@ export function triggerExists(className: string, type: string, applicationId: st } export function getFunction(functionName, applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Functions) { - return manager.Functions[functionName]; - } - return undefined; + return get(Category.Functions, functionName, applicationId); } export function getJob(jobName, applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Jobs) { - return manager.Jobs[jobName]; - } - return undefined; + return get(Category.Jobs, jobName, applicationId); } export function getJobs(applicationId) { @@ -130,15 +147,11 @@ export function getJobs(applicationId) { export function getValidator(functionName, applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Validators) { - return manager.Validators[functionName]; - } - return undefined; + return get(Category.Validators, functionName, applicationId); } -export function getRequestObject(triggerType, auth, parseObject, originalParseObject, config) { - var request = { +export function getRequestObject(triggerType, auth, parseObject, originalParseObject, config, context) { + const request = { triggerName: triggerType, object: parseObject, master: false, @@ -151,6 +164,11 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb request.original = originalParseObject; } + if (triggerType === Types.beforeSave || triggerType === Types.afterSave) { + // Set a copy of the context on the request object. + request.context = Object.assign({}, context); + } + if (!auth) { return request; } @@ -390,17 +408,20 @@ export function maybeRunQueryTrigger(triggerType, className, restWhere, restOpti // Resolves to an object, empty or containing an object key. A beforeSave // trigger will set the object key to the rest format object to save. // originalParseObject is optional, we only need that for before/afterSave functions -export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config) { +export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config, context) { if (!parseObject) { return Promise.resolve({}); } return new Promise(function (resolve, reject) { var trigger = getTrigger(parseObject.className, triggerType, config.applicationId); if (!trigger) return resolve(); - var request = getRequestObject(triggerType, auth, parseObject, originalParseObject, config); + var request = getRequestObject(triggerType, auth, parseObject, originalParseObject, config, context); var { success, error } = getResponseObject(request, (object) => { logTriggerSuccessBeforeHook( triggerType, parseObject.className, parseObject.toJSON(), object, auth); + if (triggerType === Types.beforeSave || triggerType === Types.afterSave) { + Object.assign(context, request.context); + } resolve(object); }, (error) => { logTriggerErrorBeforeHook(