From 2a82525c7ea5e414dd8483254415a2d792e6e015 Mon Sep 17 00:00:00 2001 From: Thomas Foricher Date: Tue, 19 Nov 2024 20:56:37 +0100 Subject: [PATCH] feat: Add a new `beforeLiveQueryEvent` trigger --- spec/ParseLiveQuery.spec.js | 124 ++++++++++++++++++++++++++++++++++ src/RestWrite.js | 34 ++++++---- src/cloud-code/Parse.Cloud.js | 37 ++++++++++ src/triggers.js | 7 +- 4 files changed, 188 insertions(+), 14 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index a65eef6084..773bcf27ce 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1230,4 +1230,128 @@ describe('ParseLiveQuery', function () { await new Promise(resolve => setTimeout(resolve, 100)); expect(createSpy).toHaveBeenCalledTimes(1); }); + + it('test beforeLiveQueryEvent ran while creating an object', async function () { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + + Parse.Cloud.beforeLiveQueryEvent('TestObject', req => { + 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('test beforeLiveQueryEvent ran while updating an object', async function (done) { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + Parse.Cloud.afterSave('TestObject', async req => { + expect(req.object.get('foo')).toBe('baz'); + }) + Parse.Cloud.beforeLiveQueryEvent('TestObject', async req => { + expect(req.object.get('foo')).toBe('baz'); + req.object.set('foo', 'rebaz'); + }) + Parse.Cloud.afterLiveQueryEvent('TestObject', req => { + expect(req.event).toBe('update'); + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('rebaz'); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + subscription.on('update', object => { + expect(object.get('foo')).toBe('rebaz'); + done(); + }); + + object.set('foo', 'baz') + await object.save(); + }); + + it('test beforeLiveQueryEvent should filter specific object creation', async function () { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + + Parse.Cloud.beforeLiveQueryEvent('TestObject', req => { + expect(req.object.get('foo')).toBe('bar'); + if (req.object.get('foo') === 'bar') { + req.context.preventLiveQuery === true; + } + }) + + const query = new Parse.Query(TestObject).equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + subscription.on('create', () => { + fail('create should not have been called.'); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + }); + + it('test beforeLiveQueryEvent should filter specific object update', async function () { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + Parse.Cloud.beforeLiveQueryEvent('TestObject', async req => { + expect(req.object.get('foo')).toBe('baz'); + if (req.object.get('foo') === 'baz') { + req.context.preventLiveQuery === true; + } + }) + + const query = new Parse.Query(TestObject).equalTo('foo', 'baz'); + const subscription = await query.subscribe(); + subscription.on('update', object => { + fail('update should not have been called.'); + }); + + object.set('foo', 'baz') + await object.save(); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 255c55f24c..f522937439 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1627,7 +1627,7 @@ RestWrite.prototype.runDatabaseOperation = function () { }; // Returns nothing - doesn't wait for the trigger. -RestWrite.prototype.runAfterSaveTrigger = function () { +RestWrite.prototype.runAfterSaveTrigger = async function () { if (!this.response || !this.response.response || this.runOptions.many) { return; } @@ -1642,21 +1642,31 @@ RestWrite.prototype.runAfterSaveTrigger = function () { if (!hasAfterSaveHook && !hasLiveQuery) { return Promise.resolve(); } - const { originalObject, updatedObject } = this.buildParseObjects(); updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); if (hasLiveQuery) { - this.config.database.loadSchema().then(schemaController => { - // Notify LiveQueryServer if possible - const perms = schemaController.getClassLevelPermissions(updatedObject.className); - this.config.liveQueryController.onAfterSave( - updatedObject.className, - updatedObject, - originalObject, - perms - ); - }); + const hasBeforeEventHook = triggers.triggerExists( + this.className, + triggers.Types.beforeEvent, + this.config.applicationId + ); + const publishedObject = updatedObject.clone(); + if (hasBeforeEventHook) { + await triggers.maybeRunTrigger(triggers.Types.beforeEvent, this.auth, publishedObject, originalObject, this.config, this.context); + } + if (this.context.preventLiveQuery !== true) { + this.config.database.loadSchema().then(schemaController => { + // Notify LiveQueryServer if possible + const perms = schemaController.getClassLevelPermissions(publishedObject.className); + this.config.liveQueryController.onAfterSave( + publishedObject.className, + publishedObject, + originalObject, + perms + ); + }); + } } if (!hasAfterSaveHook) { return Promise.resolve(); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 3f33e5100d..2f80cd04f0 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -604,6 +604,43 @@ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { ); }; +/** + * Registers a before live query event function. + * + * **Available in Cloud Code only.** + * + * If you want to use beforeLiveQueryEvent for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User} or {@link Parse.File}), you should pass the class itself and not the String for arg1. + * ``` + * Parse.Cloud.beforeLiveQueryEvent('MyCustomClass', (request) => { + * // code here + * }, (request) => { + * // validation code here + * }); + * + * Parse.Cloud.beforeLiveQueryEvent(Parse.User, (request) => { + * // code here + * }, { ...validationObject }); + *``` + * + * @method beforeLiveQueryEvent + * @name Parse.Cloud.beforeLiveQueryEvent + * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before 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 before a live query event (publish) is made. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. + * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. + */ + ParseCloud.beforeLiveQueryEvent = function (parseClass, handler, validationHandler) { + validateValidator(validationHandler); + const className = triggers.getClassName(parseClass); + triggers.addTrigger( + triggers.Types.beforeEvent, + className, + handler, + Parse.applicationId, + validationHandler + ); +}; + + ParseCloud.onLiveQueryEvent = function (handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; diff --git a/src/triggers.js b/src/triggers.js index 0f1b632078..933c572c28 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -14,6 +14,7 @@ export const Types = { afterFind: 'afterFind', beforeConnect: 'beforeConnect', beforeSubscribe: 'beforeSubscribe', + beforeEvent: 'beforeEvent', afterEvent: 'afterEvent', }; @@ -278,7 +279,8 @@ export function getRequestObject( triggerType === Types.afterDelete || triggerType === Types.beforeLogin || triggerType === Types.afterLogin || - triggerType === Types.afterFind + triggerType === Types.afterFind || + triggerType === Types.beforeEvent ) { // Set a copy of the context on the request object. request.context = Object.assign({}, context); @@ -885,7 +887,8 @@ export function maybeRunTrigger( triggerType === Types.beforeSave || triggerType === Types.afterSave || triggerType === Types.beforeDelete || - triggerType === Types.afterDelete + triggerType === Types.afterDelete || + triggerType === Types.beforeEvent ) { Object.assign(context, request.context); }