From bf39cd68ef2cdd3067f3a1f94cda1467e5e7768f Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 20 Oct 2020 02:38:55 +1100 Subject: [PATCH] Create Cloud function afterLiveQueryEvent (#6859) * Before Connect + Before Subscribe #1 * Cleanup and Documentation * Add E2E tests * Bump parse to 2.15.0 * Create afterLiveQueryEvent * Revert "Create afterLiveQueryEvent" This reverts commit 828c678a6995216b843a75f5b3c864aec063ba43. * afterLiveQueryEvent * Add delete event * Fix failing tests * Fix lint * Update ParseLiveQueryServer.js * Remove Facebook AccountKit auth (#6870) * Remove Facebook AccountKit auth Account Kit services are no longer available. https://developers.facebook.com/blog/post/2019/09/09/account-kit-services-no-longer-available-starting-march/ https://www.sinch.com/blog/facebook-account-kit-is-closing-down-are-your-apps-covered/ * remove flaky test * fix: upgrade uuid from 8.2.0 to 8.3.0 (#6865) Snyk has created this PR to upgrade uuid from 8.2.0 to 8.3.0. See this package in npm: https://www.npmjs.com/package/uuid See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Diamond Lewis * fix: package.json & package-lock.json to reduce vulnerabilities (#6864) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-LODASH-590103 Co-authored-by: Diamond Lewis * fix: upgrade ldapjs from 2.0.0 to 2.1.0 (#6857) Snyk has created this PR to upgrade ldapjs from 2.0.0 to 2.1.0. See this package in npm: https://www.npmjs.com/package/ldapjs See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Diamond Lewis * fix: upgrade apollo-server-express from 2.15.1 to 2.16.0 (#6851) Snyk has created this PR to upgrade apollo-server-express from 2.15.1 to 2.16.0. See this package in npm: https://www.npmjs.com/package/apollo-server-express See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Diamond Lewis * fix: upgrade @graphql-tools/stitch from 6.0.12 to 6.0.13 (#6845) Snyk has created this PR to upgrade @graphql-tools/stitch from 6.0.12 to 6.0.13. See this package in npm: https://www.npmjs.com/package/@graphql-tools/stitch See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Diamond Lewis * fix: upgrade @graphql-tools/utils from 6.0.12 to 6.0.13 (#6846) Snyk has created this PR to upgrade @graphql-tools/utils from 6.0.12 to 6.0.13. See this package in npm: https://www.npmjs.com/package/@graphql-tools/utils See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr Co-authored-by: Diamond Lewis * [Snyk] Upgrade winston from 3.2.1 to 3.3.2 (#6799) * fix: upgrade winston from 3.2.1 to 3.3.2 Snyk has created this PR to upgrade winston from 3.2.1 to 3.3.2. See this package in NPM: https://www.npmjs.com/package/winston See this project in Snyk: https://app.snyk.io/org/acinader/project/8c1a9edb-c8f5-4dc1-b221-4d6030a323eb?utm_source=github&utm_medium=upgrade-pr * fix tests Co-authored-by: Diamond Lewis * afterLiveQueryEvent * Add delete event * Fix failing tests * Before Connect + Before Subscribe #1 * Cleanup and Documentation * Create afterLiveQueryEvent * Revert "Create afterLiveQueryEvent" This reverts commit 828c678a6995216b843a75f5b3c864aec063ba43. * Update ParseLiveQueryServer.js * Rebase * Remove return value / deduplicate tests * Add docs * Add additional data to trigger Co-authored-by: Diamond Lewis Co-authored-by: Snyk bot --- spec/ParseLiveQuery.spec.js | 459 +++++++++++++++++++++++++- src/LiveQuery/ParseLiveQueryServer.js | 114 +++++-- src/cloud-code/Parse.Cloud.js | 39 +++ src/triggers.js | 20 ++ 4 files changed, 596 insertions(+), 36 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 32e20b9744..496f9fb922 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -18,14 +18,469 @@ describe('ParseLiveQuery', function () { const query = new Parse.Query(TestObject); query.equalTo('objectId', object.id); const subscription = await query.subscribe(); - subscription.on('update', async object => { + subscription.on('update', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + it('expect afterEvent create', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Create'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + subscription.on('create', object => { expect(object.get('foo')).toBe('bar'); done(); }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + }); + + it('expect afterEvent payload', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Update'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + expect(req.original.get('foo')).toBeUndefined(); + done(); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + await query.subscribe(); object.set({ foo: 'bar' }); await object.save(); }); + it('expect afterEvent enter', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Enter'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + expect(req.original.get('foo')).toBeUndefined(); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('enter', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + object.set('foo', 'bar'); + await object.save(); + }); + + it('expect afterEvent leave', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Leave'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBeUndefined(); + expect(req.original.get('foo')).toBe('bar'); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('leave', object => { + expect(object.get('foo')).toBeUndefined(); + done(); + }); + + object.unset('foo'); + await object.save(); + }); + + it('expect afterEvent delete', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Delete'); + expect(req.user).toBeUndefined(); + req.object.set('foo', 'bar'); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + + const subscription = await query.subscribe(); + subscription.on('delete', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + await object.destroy(); + }); + + it('can handle afterEvent modification', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + const current = req.object; + current.set('foo', 'yolo'); + + const original = req.original; + original.set('yolo', 'foo'); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', (object, original) => { + expect(object.get('foo')).toBe('yolo'); + expect(original.get('yolo')).toBe('foo'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can return different object in afterEvent', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + const object = new Parse.Object('Yolo'); + req.object = object; + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', object => { + expect(object.className).toBe('Yolo'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can handle afterEvent throw', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + const current = req.object; + const original = req.original; + + setTimeout(() => { + done(); + }, 2000); + + if (current.get('foo') != original.get('foo')) { + throw "Don't pass an update trigger, or message"; + } + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', () => { + fail('update should not have been called.'); + }); + subscription.on('error', () => { + fail('error should not have been called.'); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + it('expect afterEvent create', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Create'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + subscription.on('create', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + }); + + it('expect afterEvent payload', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Update'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + expect(req.original.get('foo')).toBeUndefined(); + done(); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + await query.subscribe(); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('expect afterEvent enter', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Enter'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + expect(req.original.get('foo')).toBeUndefined(); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('enter', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + object.set('foo', 'bar'); + await object.save(); + }); + + it('expect afterEvent leave', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Leave'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBeUndefined(); + expect(req.original.get('foo')).toBe('bar'); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('leave', object => { + expect(object.get('foo')).toBeUndefined(); + done(); + }); + + object.unset('foo'); + await object.save(); + }); + + it('expect afterEvent delete', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('Delete'); + expect(req.user).toBeUndefined(); + req.object.set('foo', 'bar'); + }); + + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + + const subscription = await query.subscribe(); + subscription.on('delete', object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + + await object.destroy(); + }); + + it('can handle afterEvent modification', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + const current = req.object; + current.set('foo', 'yolo'); + + const original = req.original; + original.set('yolo', 'foo'); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', (object, original) => { + expect(object.get('foo')).toBe('yolo'); + expect(original.get('yolo')).toBe('foo'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can handle async afterEvent modification', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const parent = new TestObject(); + const child = new TestObject(); + child.set('bar', 'foo'); + await Parse.Object.saveAll([parent, child]); + + Parse.Cloud.afterLiveQueryEvent('TestObject', async req => { + const current = req.object; + const pointer = current.get('child'); + await pointer.fetch(); + }); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', parent.id); + const subscription = await query.subscribe(); + subscription.on('update', object => { + expect(object.get('child')).toBeDefined(); + expect(object.get('child').get('bar')).toBe('foo'); + done(); + }); + parent.set('child', child); + await parent.save(); + }); + it('can handle beforeConnect / beforeSubscribe hooks', async done => { await reconfigureServer({ liveQuery: { @@ -58,7 +513,7 @@ describe('ParseLiveQuery', function () { const query = new Parse.Query(TestObject); query.equalTo('objectId', object.id); const subscription = await query.subscribe(); - subscription.on('update', async object => { + subscription.on('update', object => { expect(object.get('foo')).toBe('bar'); done(); }); diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 5d28367961..e535480a71 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -14,6 +14,7 @@ import { runLiveQueryEventHandlers, maybeRunConnectTrigger, maybeRunSubscribeTrigger, + maybeRunAfterEventTrigger, } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; @@ -124,7 +125,7 @@ class ParseLiveQueryServer { _onAfterDelete(message: any): void { logger.verbose(Parse.applicationId + 'afterDelete is triggered'); - const deletedParseObject = message.currentParseObject.toJSON(); + let deletedParseObject = message.currentParseObject.toJSON(); const classLevelPermissions = message.classLevelPermissions; const className = deletedParseObject.className; logger.verbose( @@ -158,6 +159,7 @@ class ParseLiveQueryServer { const acl = message.currentParseObject.getACL(); // Check CLP const op = this._getCLPOperation(subscription.query); + let res = {}; this._matchesCLP( classLevelPermissions, message.currentParseObject, @@ -173,6 +175,22 @@ class ParseLiveQueryServer { if (!isMatched) { return null; } + res = { + event: 'Delete', + sessionToken: client.sessionToken, + object: deletedParseObject, + clients: this.clients.size, + subscriptions: this.subscriptions.size, + useMasterKey: client.hasMasterKey, + installationId: client.installationId + }; + return maybeRunAfterEventTrigger('afterEvent', className, res); + }) + .then(() => { + if (res.object && typeof res.object.toJSON === 'function') { + deletedParseObject = res.object.toJSON(); + deletedParseObject.className = className; + } client.pushDelete(requestId, deletedParseObject); }) .catch(error => { @@ -193,7 +211,7 @@ class ParseLiveQueryServer { originalParseObject = message.originalParseObject.toJSON(); } const classLevelPermissions = message.classLevelPermissions; - const currentParseObject = message.currentParseObject.toJSON(); + let currentParseObject = message.currentParseObject.toJSON(); const className = currentParseObject.className; logger.verbose( 'ClassName: %s | ObjectId: %s', @@ -243,6 +261,7 @@ class ParseLiveQueryServer { // Set current ParseObject ACL checking promise, if the object does not match // subscription, we do not need to check ACL let currentACLCheckingPromise; + let res = {}; if (!isCurrentSubscriptionMatched) { currentACLCheckingPromise = Promise.resolve(false); } else { @@ -267,40 +286,67 @@ class ParseLiveQueryServer { currentACLCheckingPromise, ]); }) - .then( - ([isOriginalMatched, isCurrentMatched]) => { - logger.verbose( - 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', - originalParseObject, - currentParseObject, - isOriginalSubscriptionMatched, - isCurrentSubscriptionMatched, - isOriginalMatched, - isCurrentMatched, - subscription.hash - ); - - // Decide event type - let type; - if (isOriginalMatched && isCurrentMatched) { - type = 'Update'; - } else if (isOriginalMatched && !isCurrentMatched) { - type = 'Leave'; - } else if (!isOriginalMatched && isCurrentMatched) { - if (originalParseObject) { - type = 'Enter'; - } else { - type = 'Create'; - } + .then(([isOriginalMatched, isCurrentMatched]) => { + logger.verbose( + 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', + originalParseObject, + currentParseObject, + isOriginalSubscriptionMatched, + isCurrentSubscriptionMatched, + isOriginalMatched, + isCurrentMatched, + subscription.hash + ); + + // Decide event type + let type; + if (isOriginalMatched && isCurrentMatched) { + type = 'Update'; + } else if (isOriginalMatched && !isCurrentMatched) { + type = 'Leave'; + } else if (!isOriginalMatched && isCurrentMatched) { + if (originalParseObject) { + type = 'Enter'; } else { - return null; + type = 'Create'; + } + } else { + return null; + } + message.event = type; + res = { + event: type, + sessionToken: client.sessionToken, + object: currentParseObject, + original: originalParseObject, + clients: this.clients.size, + subscriptions: this.subscriptions.size, + useMasterKey: client.hasMasterKey, + installationId: client.installationId + }; + return maybeRunAfterEventTrigger('afterEvent', className, res); + }) + .then( + () => { + if (res.object && typeof res.object.toJSON === 'function') { + currentParseObject = res.object.toJSON(); + currentParseObject.className = + res.object.className || className; + } + + if (res.original && typeof res.original.toJSON === 'function') { + originalParseObject = res.original.toJSON(); + originalParseObject.className = + res.original.className || className; + } + const functionName = 'push' + message.event; + if (client[functionName]) { + client[functionName]( + requestId, + currentParseObject, + originalParseObject + ); } - const functionName = 'push' + type; - client[functionName]( - requestId, - currentParseObject, - originalParseObject - ); }, error => { logger.error('Matching ACL error : ', error); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 088c4dc3c1..c3f0536c2b 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -511,6 +511,32 @@ ParseCloud.onLiveQueryEvent = function (handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; +/** + * Registers an after live query server event function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => { + * // code here + * }) + *``` + * + * @method afterLiveQueryEvent + * @name Parse.Cloud.afterLiveQueryEvent + * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after live query event function for. This can instead be a String that is the className of the subclass. + * @param {Function} func The function to run after a live query event. This function can be async and should take one parameter, a {@link Parse.Cloud.LiveQueryEventTrigger}. + */ +ParseCloud.afterLiveQueryEvent = function (parseClass, handler) { + const className = getClassName(parseClass); + triggers.addTrigger( + triggers.Types.afterEvent, + className, + handler, + Parse.applicationId + ); +}; + ParseCloud._removeAllHooks = () => { triggers._unregisterAll(); }; @@ -563,6 +589,19 @@ module.exports = ParseCloud; * @property {String} sessionToken If set, the session of the user that made the request. */ +/** + * @interface Parse.Cloud.LiveQueryEventTrigger + * @property {String} installationId If set, the installationId triggering the request. + * @property {Boolean} useMasterKey If true, means the master key was used. + * @property {Parse.User} user If set, the user that made the request. + * @property {String} sessionToken If set, the session of the user that made the request. + * @property {String} event The live query event that triggered the request. + * @property {Parse.Object} object The object triggering the hook. + * @property {Parse.Object} original If set, the object, as currently stored. + * @property {Integer} clients The number of clients connected. + * @property {Integer} subscriptions The number of subscriptions connected. + */ + /** * @interface Parse.Cloud.BeforeFindRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/triggers.js b/src/triggers.js index de9856841e..48464c2ff4 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -18,6 +18,7 @@ export const Types = { afterDeleteFile: 'afterDeleteFile', beforeConnect: 'beforeConnect', beforeSubscribe: 'beforeSubscribe', + afterEvent: 'afterEvent', }; const FileClassName = '@File'; @@ -802,6 +803,25 @@ export async function maybeRunSubscribeTrigger( request.query = query; } +export async function maybeRunAfterEventTrigger( + triggerType, + className, + request +) { + const trigger = getTrigger(className, triggerType, Parse.applicationId); + if (!trigger) { + return; + } + if (request.object) { + request.object = Parse.Object.fromJSON(request.object); + } + if (request.original) { + request.original = Parse.Object.fromJSON(request.original); + } + request.user = await userForSessionToken(request.sessionToken); + return trigger(request); +} + async function userForSessionToken(sessionToken) { if (!sessionToken) { return;