From a80d876639dda2af314eb9349b9316a4a356d3ce Mon Sep 17 00:00:00 2001 From: Thomas Foricher Date: Wed, 1 Jul 2026 21:58:14 +0200 Subject: [PATCH 1/4] feat: Add a new beforeLiveQueryEvent trigger Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/ParseLiveQuery.spec.js | 139 ++++++++++++++++++++++++++++++++++ src/RestWrite.js | 49 +++++++++--- src/cloud-code/Parse.Cloud.js | 49 ++++++++++++ src/triggers.js | 9 ++- 4 files changed, 234 insertions(+), 12 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 9029eed977..b54ff24b18 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -2005,3 +2005,142 @@ describe('ParseLiveQuery ACL transition disclosure', function () { expect(leave.object.secretField).toBe('VISIBLE_NEW'); }); }); + +describe('ParseLiveQuery beforeLiveQueryEvent', function () { + beforeEach(() => { + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); + }); + afterEach(async () => { + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + await client.close(); + }); + + it('runs beforeLiveQueryEvent when creating an object', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const triggerPromise = resolvingPromise(); + Parse.Cloud.beforeLiveQueryEvent('TestObject', req => { + expect(req.user).toBeUndefined(); + expect(req.object.get('foo')).toBe('bar'); + triggerPromise.resolve(); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + const createPromise = resolvingPromise(); + subscription.on('create', object => { + createPromise.resolve(object); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + await triggerPromise; + const created = await createPromise; + expect(created.get('foo')).toBe('bar'); + // The published object must keep its identifier so LiveQuery clients can + // correlate the event with the object. + expect(created.id).toBe(object.id); + }); + + it('runs beforeLiveQueryEvent when updating an object', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + const triggerPromise = resolvingPromise(); + Parse.Cloud.beforeLiveQueryEvent('TestObject', async req => { + expect(req.object.get('foo')).toBe('baz'); + triggerPromise.resolve(); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + const updatePromise = resolvingPromise(); + subscription.on('update', updated => { + updatePromise.resolve(updated); + }); + + object.set('foo', 'baz'); + await object.save(); + + await triggerPromise; + const updated = await updatePromise; + expect(updated.get('foo')).toBe('baz'); + expect(updated.id).toBe(object.id); + }); + + it('prevents a LiveQuery create event when beforeLiveQueryEvent returns false', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + Parse.Cloud.beforeLiveQueryEvent('TestObject', req => { + expect(req.object.get('foo')).toBe('bar'); + return false; + }); + + const query = new Parse.Query(TestObject).equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + await sleep(500); + expect(createSpy).not.toHaveBeenCalled(); + }); + + it('prevents a LiveQuery update event when beforeLiveQueryEvent returns false', async () => { + 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'); + return false; + }); + + const query = new Parse.Query(TestObject).equalTo('foo', 'baz'); + const subscription = await query.subscribe(); + const updateSpy = jasmine.createSpy('update'); + subscription.on('update', updateSpy); + + object.set('foo', 'baz'); + await object.save(); + + await sleep(500); + expect(updateSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/RestWrite.js b/src/RestWrite.js index 98c9fd4656..2511599ce5 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1764,7 +1764,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; } @@ -1784,16 +1784,43 @@ RestWrite.prototype.runAfterSaveTrigger = function () { 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 - ); - }); + // Run the beforeLiveQueryEvent trigger, if defined, to let Cloud Code decide + // whether the event should be published. Returning `false` from the trigger + // prevents the event from being sent to the LiveQuery server, which saves + // network and CPU resources for events that no client needs to receive. + let preventLiveQuery = false; + const hasBeforeLiveQueryEventHook = triggers.triggerExists( + this.className, + triggers.Types.beforeEvent, + this.config.applicationId + ); + if (hasBeforeLiveQueryEventHook) { + try { + const result = await triggers.maybeRunTrigger( + triggers.Types.beforeEvent, + this.auth, + updatedObject, + originalObject, + this.config, + this.context + ); + preventLiveQuery = result === false; + } catch (err) { + logger.warn('beforeLiveQueryEvent caught an error', err); + } + } + if (!preventLiveQuery) { + 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 + ); + }); + } } if (!hasAfterSaveHook) { return Promise.resolve(); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index e829a492e0..a6e7f9b98c 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -673,6 +673,55 @@ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { ); }; +/** + * Registers a before live query event function. + * + * **Available in Cloud Code only.** + * + * The function runs on the Parse Server instance before an object change is + * published to the LiveQuery server, once per object write. This is different + * from {@link Parse.Cloud.afterLiveQueryEvent}, which runs on the LiveQuery + * server once per matching subscription. + * + * Return `false` from the function to prevent the event from being published to + * the LiveQuery server. This is useful to avoid publishing events that no client + * needs to receive, saving network and CPU resources. Returning any other value + * (including `undefined`) publishes the event as usual. + * + * 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) => { + * if (request.object.get('status') === 'draft') { + * // Prevent the event from being published to the LiveQuery server. + * return false; + * } + * }, (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}. Return `false` to prevent the event from being published. + * @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 f66d96f942..f66274654a 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -16,6 +16,7 @@ export const Types = { afterFind: 'afterFind', beforeConnect: 'beforeConnect', beforeSubscribe: 'beforeSubscribe', + beforeEvent: 'beforeEvent', afterEvent: 'afterEvent', }; @@ -307,7 +308,8 @@ export function getRequestObject( triggerType === Types.beforeLogin || triggerType === Types.afterLogin || triggerType === Types.beforePasswordResetRequest || - triggerType === Types.afterFind + triggerType === Types.afterFind || + triggerType === Types.beforeEvent ) { // Set a copy of the context on the request object. request.context = Object.assign(Object.create(null), context); @@ -373,6 +375,11 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c export function getResponseObject(request, resolve, reject) { return { success: function (response) { + if (request.triggerName === Types.beforeEvent) { + // Pass the handler's return value through unchanged so that the caller + // can react to it (e.g. returning `false` to prevent a LiveQuery event). + return resolve(response); + } if (request.triggerName === Types.afterFind) { if (!response) { response = request.objects; From dd6279ff6da4368dd4523c6161cc8eb0639b08f5 Mon Sep 17 00:00:00 2001 From: Thomas Foricher Date: Wed, 1 Jul 2026 23:48:40 +0200 Subject: [PATCH 2/4] docs: Clarify beforeLiveQueryEvent must not mutate the object Co-Authored-By: Claude Opus 4.8 (1M context) --- src/cloud-code/Parse.Cloud.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index a6e7f9b98c..757ffbcff9 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -688,6 +688,13 @@ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { * needs to receive, saving network and CPU resources. Returning any other value * (including `undefined`) publishes the event as usual. * + * This trigger is intended only to allow or prevent publishing an event. Do not + * mutate `request.object` here: it is the same object instance that is passed to + * the `afterSave` trigger, so changes would leak into `afterSave` and the save + * response. `request.context` is provided read-only and is not merged back. To + * transform the payload delivered to LiveQuery clients, use + * {@link Parse.Cloud.afterLiveQueryEvent} instead. + * * 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) => { From 5661275485038905d79cc0bcedfa6a579273b76c Mon Sep 17 00:00:00 2001 From: Thomas Foricher Date: Wed, 1 Jul 2026 23:53:52 +0200 Subject: [PATCH 3/4] test: Cover beforeLiveQueryEvent trigger error handling Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/ParseLiveQuery.spec.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index b54ff24b18..ba495a3210 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -2143,4 +2143,38 @@ describe('ParseLiveQuery beforeLiveQueryEvent', function () { await sleep(500); expect(updateSpy).not.toHaveBeenCalled(); }); + + it('still publishes the event and logs when beforeLiveQueryEvent throws', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const logger = require('../lib/logger').logger; + const warnSpy = spyOn(logger, 'warn').and.callThrough(); + + Parse.Cloud.beforeLiveQueryEvent('TestObject', () => { + throw new Error('beforeLiveQueryEvent failure'); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + const createPromise = resolvingPromise(); + subscription.on('create', object => { + createPromise.resolve(object); + }); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + // The event is still published even though the trigger threw. + const created = await createPromise; + expect(created.get('foo')).toBe('bar'); + expect(warnSpy).toHaveBeenCalledWith('beforeLiveQueryEvent caught an error', jasmine.anything()); + }); }); From 6d9745b4020392e47cb04d0cf201b00de6df91e8 Mon Sep 17 00:00:00 2001 From: Thomas Foricher Date: Thu, 2 Jul 2026 00:28:18 +0200 Subject: [PATCH 4/4] fix: Run beforeLiveQueryEvent fire-and-forget and cover delete events - Extract the trigger invocation into a shared triggers.maybeRunBeforeLiveQueryEventTrigger helper that returns whether the event should be published and never rejects. - Run the trigger detached from the save response: RestWrite.runAfterSaveTrigger is no longer async, so a slow trigger cannot delay the client's write request (restores the pre-existing fire-and-forget behavior of the LiveQuery notification). - Gate the delete path too: rest.js del() now consults the trigger before calling liveQueryController.onAfterDelete, so returning false also suppresses delete/leave events. - Fail closed on validator errors: a failing validator (e.g. requireMaster) prevents the publish instead of being swallowed; handler errors remain fail-open as documented. - Drop the redundant triggerExists pre-check (maybeRunTrigger already short-circuits). - Add tests for delete/leave suppression, the delete trigger request, and validator gating. Co-Authored-By: Claude Fable 5 --- spec/ParseLiveQuery.spec.js | 128 ++++++++++++++++++++++++++++++++++ src/RestWrite.js | 58 +++++++-------- src/cloud-code/Parse.Cloud.js | 12 ++-- src/rest.js | 12 +++- src/triggers.js | 33 +++++++++ 5 files changed, 203 insertions(+), 40 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index ba495a3210..dbce2cc649 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -2177,4 +2177,132 @@ describe('ParseLiveQuery beforeLiveQueryEvent', function () { expect(created.get('foo')).toBe('bar'); expect(warnSpy).toHaveBeenCalledWith('beforeLiveQueryEvent caught an error', jasmine.anything()); }); + + it('runs beforeLiveQueryEvent when deleting an object', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + const triggerPromise = resolvingPromise(); + Parse.Cloud.beforeLiveQueryEvent('TestObject', req => { + expect(req.object.id).toBe(object.id); + expect(req.object.get('foo')).toBe('bar'); + triggerPromise.resolve(); + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + const deletePromise = resolvingPromise(); + subscription.on('delete', deleted => { + deletePromise.resolve(deleted); + }); + + await object.destroy(); + + await triggerPromise; + const deleted = await deletePromise; + expect(deleted.id).toBe(object.id); + }); + + it('prevents a LiveQuery delete event when beforeLiveQueryEvent returns false', async () => { + 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', req => { + expect(req.object.id).toBe(object.id); + return false; + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + const deleteSpy = jasmine.createSpy('delete'); + subscription.on('delete', deleteSpy); + + await object.destroy(); + + await sleep(500); + expect(deleteSpy).not.toHaveBeenCalled(); + }); + + it('prevents a LiveQuery leave event when beforeLiveQueryEvent returns false', async () => { + 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', req => { + expect(req.object.get('foo')).toBe('baz'); + return false; + }); + + const query = new Parse.Query(TestObject).equalTo('foo', 'bar'); + const subscription = await query.subscribe(); + const leaveSpy = jasmine.createSpy('leave'); + subscription.on('leave', leaveSpy); + + // The object leaves the subscription's query, which would publish a + // leave event if the trigger did not prevent it. + object.set('foo', 'baz'); + await object.save(); + + await sleep(500); + expect(leaveSpy).not.toHaveBeenCalled(); + }); + + it('does not publish the event when the beforeLiveQueryEvent validator fails', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const logger = require('../lib/logger').logger; + const warnSpy = spyOn(logger, 'warn').and.callThrough(); + + const handlerSpy = jasmine.createSpy('handler'); + Parse.Cloud.beforeLiveQueryEvent('TestObject', handlerSpy, { + requireMaster: true, + }); + + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + const createSpy = jasmine.createSpy('create'); + subscription.on('create', createSpy); + + const object = new TestObject(); + object.set('foo', 'bar'); + await object.save(); + + await sleep(500); + expect(createSpy).not.toHaveBeenCalled(); + expect(handlerSpy).not.toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledWith('beforeLiveQueryEvent validation failed', jasmine.anything()); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 2511599ce5..0d9d53e3b0 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1764,7 +1764,7 @@ RestWrite.prototype.runDatabaseOperation = function () { }; // Returns nothing - doesn't wait for the trigger. -RestWrite.prototype.runAfterSaveTrigger = async function () { +RestWrite.prototype.runAfterSaveTrigger = function () { if (!this.response || !this.response.response || this.runOptions.many) { return; } @@ -1788,39 +1788,31 @@ RestWrite.prototype.runAfterSaveTrigger = async function () { // whether the event should be published. Returning `false` from the trigger // prevents the event from being sent to the LiveQuery server, which saves // network and CPU resources for events that no client needs to receive. - let preventLiveQuery = false; - const hasBeforeLiveQueryEventHook = triggers.triggerExists( - this.className, - triggers.Types.beforeEvent, - this.config.applicationId - ); - if (hasBeforeLiveQueryEventHook) { - try { - const result = await triggers.maybeRunTrigger( - triggers.Types.beforeEvent, - this.auth, - updatedObject, - originalObject, - this.config, - this.context - ); - preventLiveQuery = result === false; - } catch (err) { - logger.warn('beforeLiveQueryEvent caught an error', err); - } - } - if (!preventLiveQuery) { - 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 - ); + // Fire-and-forget: the trigger and the notification must not delay the + // save response. + triggers + .maybeRunBeforeLiveQueryEventTrigger( + this.auth, + updatedObject, + originalObject, + this.config, + this.context + ) + .then(publish => { + if (!publish) { + return; + } + return 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 + ); + }); }); - } } if (!hasAfterSaveHook) { return Promise.resolve(); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 757ffbcff9..8e1c326131 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -678,15 +678,17 @@ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { * * **Available in Cloud Code only.** * - * The function runs on the Parse Server instance before an object change is - * published to the LiveQuery server, once per object write. This is different - * from {@link Parse.Cloud.afterLiveQueryEvent}, which runs on the LiveQuery - * server once per matching subscription. + * The function runs on the Parse Server instance before an object create, + * update or delete is published to the LiveQuery server, once per object write. + * This is different from {@link Parse.Cloud.afterLiveQueryEvent}, which runs on + * the LiveQuery server once per matching subscription. * * Return `false` from the function to prevent the event from being published to * the LiveQuery server. This is useful to avoid publishing events that no client * needs to receive, saving network and CPU resources. Returning any other value - * (including `undefined`) publishes the event as usual. + * (including `undefined`) publishes the event as usual. If a validator is + * defined and fails, the event is not published; if the function itself throws, + * the error is logged and the event is published as usual. * * This trigger is intended only to allow or prevent publishing an event. Do not * mutate `request.object` here: it is the same object instance that is passed to diff --git a/src/rest.js b/src/rest.js index 7a78f2f8b5..cd27402257 100644 --- a/src/rest.js +++ b/src/rest.js @@ -241,9 +241,17 @@ function del(config, auth, className, objectId, context) { ); }) .then(() => { - // Notify LiveQuery server if possible + // Notify LiveQuery server if possible, unless the beforeLiveQueryEvent + // trigger prevents it. Fire-and-forget: the trigger and the notification + // must not delay the delete response. const perms = schemaController.getClassLevelPermissions(className); - config.liveQueryController.onAfterDelete(className, inflatedObject, null, perms); + triggers + .maybeRunBeforeLiveQueryEventTrigger(auth, inflatedObject, null, config, context) + .then(publish => { + if (publish) { + config.liveQueryController.onAfterDelete(className, inflatedObject, null, perms); + } + }); return triggers.maybeRunTrigger( triggers.Types.afterDelete, auth, diff --git a/src/triggers.js b/src/triggers.js index f66274654a..a3e224caf4 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1029,6 +1029,39 @@ export function maybeRunTrigger( }); } +// Runs the beforeLiveQueryEvent trigger, if defined, and returns whether the +// LiveQuery event should be published. The event is not published when the +// trigger returns `false` or when its validator fails (a failing validator is a +// deliberate gate, so it fails closed). Any other trigger error is logged and +// the event is published as usual, so a faulty trigger cannot silently drop +// events. This function never rejects. +export async function maybeRunBeforeLiveQueryEventTrigger( + auth, + parseObject, + originalParseObject, + config, + context +) { + try { + const result = await maybeRunTrigger( + Types.beforeEvent, + auth, + parseObject, + originalParseObject, + config, + context + ); + return result !== false; + } catch (error) { + if (error && error.code === Parse.Error.VALIDATION_ERROR) { + logger.warn('beforeLiveQueryEvent validation failed', error); + return false; + } + logger.warn('beforeLiveQueryEvent caught an error', error); + return true; + } +} + // Converts a REST-format object to a Parse.Object // data is either className or an object export function inflate(data, restObject) {