Adds context object in Cloud Code hooks (#4939)

* wip

* Refactors triggers a bit

- Adds testing for hooks and context

* comment nit

* nits
This commit is contained in:
Florent Vilmart
2018-08-09 11:08:18 -04:00
parent 488b2ff231
commit 457d51a972
4 changed files with 145 additions and 42 deletions

View File

@@ -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);
});
});

View File

@@ -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();
});
});

View File

@@ -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);
})

View File

@@ -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(