Skip to content

Commit 457d51a

Browse files
committed
Adds context object in Cloud Code hooks (#4939)
* wip * Refactors triggers a bit - Adds testing for hooks and context * comment nit * nits
1 parent 488b2ff commit 457d51a

File tree

4 files changed

+145
-42
lines changed

4 files changed

+145
-42
lines changed

spec/CloudCode.spec.js

+42
Original file line numberDiff line numberDiff line change
@@ -1809,4 +1809,46 @@ describe('afterFind hooks', () => {
18091809
done();
18101810
});
18111811
});
1812+
1813+
it('should expose context in before and afterSave', async () => {
1814+
let calledBefore = false;
1815+
let calledAfter = false;
1816+
Parse.Cloud.beforeSave('MyClass', (req) => {
1817+
req.context = {
1818+
key: 'value',
1819+
otherKey: 1,
1820+
}
1821+
calledBefore = true;
1822+
});
1823+
Parse.Cloud.afterSave('MyClass', (req) => {
1824+
expect(req.context.otherKey).toBe(1);
1825+
expect(req.context.key).toBe('value');
1826+
calledAfter = true;
1827+
});
1828+
1829+
const object = new Parse.Object('MyClass');
1830+
await object.save();
1831+
expect(calledBefore).toBe(true);
1832+
expect(calledAfter).toBe(true);
1833+
});
1834+
1835+
it('should expose context in before and afterSave and let keys be set individually', async () => {
1836+
let calledBefore = false;
1837+
let calledAfter = false;
1838+
Parse.Cloud.beforeSave('MyClass', (req) => {
1839+
req.context.some = 'value';
1840+
req.context.yolo = 1;
1841+
calledBefore = true;
1842+
});
1843+
Parse.Cloud.afterSave('MyClass', (req) => {
1844+
expect(req.context.yolo).toBe(1);
1845+
expect(req.context.some).toBe('value');
1846+
calledAfter = true;
1847+
});
1848+
1849+
const object = new Parse.Object('MyClass');
1850+
await object.save();
1851+
expect(calledBefore).toBe(true);
1852+
expect(calledAfter).toBe(true);
1853+
});
18121854
});

spec/ParseHooks.spec.js

+39
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ const triggers = require('../lib/triggers');
55
const HooksController = require('../lib/Controllers/HooksController').default;
66
const express = require("express");
77
const bodyParser = require('body-parser');
8+
const auth = require('../lib/Auth');
9+
const Config = require('../lib/Config');
10+
811

912
const port = 12345;
1013
const hookServerURL = "http://localhost:" + port;
@@ -503,3 +506,39 @@ describe('Hooks', () => {
503506
});
504507
});
505508
});
509+
510+
describe('triggers', () => {
511+
it('should produce a proper request object with context in beforeSave', () => {
512+
const config = Config.get('test');
513+
const master = auth.master(config);
514+
const context = {
515+
originalKey: 'original'
516+
};
517+
const req = triggers.getRequestObject(triggers.Types.beforeSave, master, {}, {}, config, context);
518+
expect(req.context.originalKey).toBe('original');
519+
req.context = {
520+
key: 'value'
521+
};
522+
expect(context.key).toBe(undefined);
523+
req.context = {
524+
key: 'newValue'
525+
};
526+
expect(context.key).toBe(undefined);
527+
});
528+
529+
it('should produce a proper request object with context in afterSave', () => {
530+
const config = Config.get('test');
531+
const master = auth.master(config);
532+
const context = {};
533+
const req = triggers.getRequestObject(triggers.Types.afterSave, master, {}, {}, config, context);
534+
expect(req.context).not.toBeUndefined();
535+
});
536+
537+
it('should not set context on beforeFind', () => {
538+
const config = Config.get('test');
539+
const master = auth.master(config);
540+
const context = {};
541+
const req = triggers.getRequestObject(triggers.Types.beforeFind, master, {}, {}, config, context);
542+
expect(req.context).toBeUndefined();
543+
});
544+
});

src/RestWrite.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK
3434
this.clientSDK = clientSDK;
3535
this.storage = {};
3636
this.runOptions = {};
37+
this.context = {};
3738
if (!query && data.objectId) {
3839
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId is an invalid field name.');
3940
}
@@ -165,7 +166,7 @@ RestWrite.prototype.runBeforeTrigger = function() {
165166
}
166167

167168
return Promise.resolve().then(() => {
168-
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config);
169+
return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config, this.context);
169170
}).then((response) => {
170171
if (response && response.object) {
171172
this.storage.fieldsChangedByTrigger = _.reduce(response.object, (result, value, key) => {
@@ -1142,7 +1143,7 @@ RestWrite.prototype.runAfterTrigger = function() {
11421143
this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject);
11431144

11441145
// Run afterSave trigger
1145-
return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config)
1146+
return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config, this.context)
11461147
.catch(function(err) {
11471148
logger.warn('afterSave caught an error', err);
11481149
})

src/triggers.js

+61-40
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,58 @@ function validateClassNameForTriggers(className, type) {
4646

4747
const _triggerStore = {};
4848

49-
export function addFunction(functionName, handler, validationHandler, applicationId) {
49+
const Category = {
50+
Functions: 'Functions',
51+
Validators: 'Validators',
52+
Jobs: 'Jobs',
53+
Triggers: 'Triggers'
54+
}
55+
56+
function getStore(category, name, applicationId) {
57+
const path = name.split('.');
58+
path.splice(-1); // remove last component
5059
applicationId = applicationId || Parse.applicationId;
5160
_triggerStore[applicationId] = _triggerStore[applicationId] || baseStore();
52-
_triggerStore[applicationId].Functions[functionName] = handler;
53-
_triggerStore[applicationId].Validators[functionName] = validationHandler;
61+
let store = _triggerStore[applicationId][category];
62+
for (const component of path) {
63+
store = store[component];
64+
if (!store) {
65+
return undefined;
66+
}
67+
}
68+
return store;
69+
}
70+
71+
function add(category, name, handler, applicationId) {
72+
const lastComponent = name.split('.').splice(-1);
73+
const store = getStore(category, name, applicationId);
74+
store[lastComponent] = handler;
75+
}
76+
77+
function remove(category, name, applicationId) {
78+
const lastComponent = name.split('.').splice(-1);
79+
const store = getStore(category, name, applicationId);
80+
delete store[lastComponent];
81+
}
82+
83+
function get(category, name, applicationId) {
84+
const lastComponent = name.split('.').splice(-1);
85+
const store = getStore(category, name, applicationId);
86+
return store[lastComponent];
87+
}
88+
89+
export function addFunction(functionName, handler, validationHandler, applicationId) {
90+
add(Category.Functions, functionName, handler, applicationId);
91+
add(Category.Validators, functionName, validationHandler, applicationId);
5492
}
5593

5694
export function addJob(jobName, handler, applicationId) {
57-
applicationId = applicationId || Parse.applicationId;
58-
_triggerStore[applicationId] = _triggerStore[applicationId] || baseStore();
59-
_triggerStore[applicationId].Jobs[jobName] = handler;
95+
add(Category.Jobs, jobName, handler, applicationId);
6096
}
6197

6298
export function addTrigger(type, className, handler, applicationId) {
6399
validateClassNameForTriggers(className, type);
64-
applicationId = applicationId || Parse.applicationId;
65-
_triggerStore[applicationId] = _triggerStore[applicationId] || baseStore();
66-
_triggerStore[applicationId].Triggers[type][className] = handler;
100+
add(Category.Triggers, `${type}.${className}`, handler, applicationId);
67101
}
68102

69103
export function addLiveQueryEventHandler(handler, applicationId) {
@@ -73,13 +107,11 @@ export function addLiveQueryEventHandler(handler, applicationId) {
73107
}
74108

75109
export function removeFunction(functionName, applicationId) {
76-
applicationId = applicationId || Parse.applicationId;
77-
delete _triggerStore[applicationId].Functions[functionName]
110+
remove(Category.Functions, functionName, applicationId);
78111
}
79112

80113
export function removeTrigger(type, className, applicationId) {
81-
applicationId = applicationId || Parse.applicationId;
82-
delete _triggerStore[applicationId].Triggers[type][className]
114+
remove(Category.Triggers, `${type}.${className}`, applicationId);
83115
}
84116

85117
export function _unregisterAll() {
@@ -90,34 +122,19 @@ export function getTrigger(className, triggerType, applicationId) {
90122
if (!applicationId) {
91123
throw "Missing ApplicationID";
92124
}
93-
var manager = _triggerStore[applicationId]
94-
if (manager
95-
&& manager.Triggers
96-
&& manager.Triggers[triggerType]
97-
&& manager.Triggers[triggerType][className]) {
98-
return manager.Triggers[triggerType][className];
99-
}
100-
return undefined;
125+
return get(Category.Triggers, `${triggerType}.${className}`, applicationId);
101126
}
102127

103128
export function triggerExists(className: string, type: string, applicationId: string): boolean {
104129
return (getTrigger(className, type, applicationId) != undefined);
105130
}
106131

107132
export function getFunction(functionName, applicationId) {
108-
var manager = _triggerStore[applicationId];
109-
if (manager && manager.Functions) {
110-
return manager.Functions[functionName];
111-
}
112-
return undefined;
133+
return get(Category.Functions, functionName, applicationId);
113134
}
114135

115136
export function getJob(jobName, applicationId) {
116-
var manager = _triggerStore[applicationId];
117-
if (manager && manager.Jobs) {
118-
return manager.Jobs[jobName];
119-
}
120-
return undefined;
137+
return get(Category.Jobs, jobName, applicationId);
121138
}
122139

123140
export function getJobs(applicationId) {
@@ -130,15 +147,11 @@ export function getJobs(applicationId) {
130147

131148

132149
export function getValidator(functionName, applicationId) {
133-
var manager = _triggerStore[applicationId];
134-
if (manager && manager.Validators) {
135-
return manager.Validators[functionName];
136-
}
137-
return undefined;
150+
return get(Category.Validators, functionName, applicationId);
138151
}
139152

140-
export function getRequestObject(triggerType, auth, parseObject, originalParseObject, config) {
141-
var request = {
153+
export function getRequestObject(triggerType, auth, parseObject, originalParseObject, config, context) {
154+
const request = {
142155
triggerName: triggerType,
143156
object: parseObject,
144157
master: false,
@@ -151,6 +164,11 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb
151164
request.original = originalParseObject;
152165
}
153166

167+
if (triggerType === Types.beforeSave || triggerType === Types.afterSave) {
168+
// Set a copy of the context on the request object.
169+
request.context = Object.assign({}, context);
170+
}
171+
154172
if (!auth) {
155173
return request;
156174
}
@@ -390,17 +408,20 @@ export function maybeRunQueryTrigger(triggerType, className, restWhere, restOpti
390408
// Resolves to an object, empty or containing an object key. A beforeSave
391409
// trigger will set the object key to the rest format object to save.
392410
// originalParseObject is optional, we only need that for before/afterSave functions
393-
export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config) {
411+
export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config, context) {
394412
if (!parseObject) {
395413
return Promise.resolve({});
396414
}
397415
return new Promise(function (resolve, reject) {
398416
var trigger = getTrigger(parseObject.className, triggerType, config.applicationId);
399417
if (!trigger) return resolve();
400-
var request = getRequestObject(triggerType, auth, parseObject, originalParseObject, config);
418+
var request = getRequestObject(triggerType, auth, parseObject, originalParseObject, config, context);
401419
var { success, error } = getResponseObject(request, (object) => {
402420
logTriggerSuccessBeforeHook(
403421
triggerType, parseObject.className, parseObject.toJSON(), object, auth);
422+
if (triggerType === Types.beforeSave || triggerType === Types.afterSave) {
423+
Object.assign(context, request.context);
424+
}
404425
resolve(object);
405426
}, (error) => {
406427
logTriggerErrorBeforeHook(

0 commit comments

Comments
 (0)