Skip to content

feat: Add a new beforeLiveQueryEvent trigger #9445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: alpha
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1308,4 +1308,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();
});
Comment on lines +1312 to +1338
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix test callback handling.

This test is using done() on line 1332, but the function doesn't receive the done parameter. The test should either:

  1. Receive done as a parameter and call it at the end, or
  2. Return a promise or use async/await properly
-  it('test beforeLiveQueryEvent ran while creating an object', async function () {
+  it('test beforeLiveQueryEvent ran while creating an object', async function (done) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 creating an object', async function (done) {
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();
});
🧰 Tools
🪛 ESLint

[error] 1312-1312: 'it' is not defined.

(no-undef)


[error] 1313-1313: 'reconfigureServer' is not defined.

(no-undef)


[error] 1324-1324: 'expect' is not defined.

(no-undef)


[error] 1325-1325: 'expect' is not defined.

(no-undef)


[error] 1328-1328: 'TestObject' is not defined.

(no-undef)


[error] 1331-1331: 'expect' is not defined.

(no-undef)


[error] 1332-1332: 'done' is not defined.

(no-undef)


[error] 1335-1335: 'TestObject' is not defined.

(no-undef)

🤖 Prompt for AI Agents (early access)
In spec/ParseLiveQuery.spec.js around lines 1312 to 1338, the test uses done() without receiving done as a parameter, causing callback handling issues. Fix this by adding done as a parameter to the test function and calling done() inside the subscription event handler after assertions, or refactor the test to properly use async/await and return a promise without using done().


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;
}
})
Comment on lines +1388 to +1393
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix critical bug in preventLiveQuery logic.

You're using a comparison operator (===) instead of an assignment operator (=), which means you're comparing the value rather than setting it. This would cause the filtering functionality to fail.

-      if (req.object.get('foo') === 'bar') {
-        req.context.preventLiveQuery === true;
+      if (req.object.get('foo') === 'bar') {
+        req.context.preventLiveQuery = true;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Parse.Cloud.beforeLiveQueryEvent('TestObject', req => {
expect(req.object.get('foo')).toBe('bar');
if (req.object.get('foo') === 'bar') {
req.context.preventLiveQuery === true;
}
})
Parse.Cloud.beforeLiveQueryEvent('TestObject', req => {
expect(req.object.get('foo')).toBe('bar');
if (req.object.get('foo') === 'bar') {
req.context.preventLiveQuery = true;
}
})
🧰 Tools
🪛 ESLint

[error] 1389-1389: 'expect' is not defined.

(no-undef)

🤖 Prompt for AI Agents (early access)
In spec/ParseLiveQuery.spec.js around lines 1388 to 1393, replace the comparison operator (===) with the assignment operator (=) when setting req.context.preventLiveQuery to true. This change will correctly assign the value and enable the intended filtering functionality.


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;
}
})
Comment on lines +1419 to +1424
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix critical bug in preventLiveQuery logic.

Same issue as in the previous test - you're using a comparison operator (===) instead of an assignment operator (=).

-      if (req.object.get('foo') === 'baz') {
-        req.context.preventLiveQuery === true;
+      if (req.object.get('foo') === 'baz') {
+        req.context.preventLiveQuery = true;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Parse.Cloud.beforeLiveQueryEvent('TestObject', async req => {
expect(req.object.get('foo')).toBe('baz');
if (req.object.get('foo') === 'baz') {
req.context.preventLiveQuery === true;
}
})
Parse.Cloud.beforeLiveQueryEvent('TestObject', async req => {
expect(req.object.get('foo')).toBe('baz');
if (req.object.get('foo') === 'baz') {
req.context.preventLiveQuery = true;
}
})
🧰 Tools
🪛 ESLint

[error] 1420-1420: 'expect' is not defined.

(no-undef)

🤖 Prompt for AI Agents (early access)
In spec/ParseLiveQuery.spec.js around lines 1419 to 1424, the code mistakenly uses the comparison operator (===) instead of the assignment operator (=) when setting req.context.preventLiveQuery. Change req.context.preventLiveQuery === true to req.context.preventLiveQuery = true to correctly assign the value and fix the preventLiveQuery logic.


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();
});
});
34 changes: 22 additions & 12 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,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;
}
Expand All @@ -1660,21 +1660,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
);
});
Comment on lines +1677 to +1686
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Await the schema call to avoid silent LiveQuery failures

this.config.database.loadSchema() returns a promise, but the surrounding async function does not await or return it.
If the DB lookup rejects (e.g. transient connectivity, schema inconsistency) the rejection becomes an unhandled promise, the catch in the caller never fires and LiveQuery delivery silently fails.

-      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
-        );
-      });
+      const schemaController = await this.config.database.loadSchema();
+      // Notify LiveQueryServer if possible
+      const perms = schemaController.getClassLevelPermissions(publishedObject.className);
+      this.config.liveQueryController.onAfterSave(
+        publishedObject.className,
+        publishedObject,
+        originalObject,
+        perms
+      );

This keeps the method “fire-and-forget” semantics for LiveQuery success but still propagates unexpected errors up the promise chain for proper logging / retries.
Please consider the change or add an explicit .catch() to prevent unhandled rejections.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
);
});
const schemaController = await this.config.database.loadSchema();
// Notify LiveQueryServer if possible
const perms = schemaController.getClassLevelPermissions(publishedObject.className);
this.config.liveQueryController.onAfterSave(
publishedObject.className,
publishedObject,
originalObject,
perms
);
🤖 Prompt for AI Agents (early access)
In src/RestWrite.js around lines 1677 to 1686, the call to this.config.database.loadSchema() returns a promise that is neither awaited nor returned, causing unhandled promise rejections if it fails. To fix this, modify the code to either await the loadSchema() call or return the promise chain, and add a .catch() handler to properly handle and propagate errors, ensuring that any failures in loading the schema do not silently fail and are logged or retried as needed.

}
}
if (!hasAfterSaveHook) {
return Promise.resolve();
Expand Down
37 changes: 37 additions & 0 deletions src/cloud-code/Parse.Cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
7 changes: 5 additions & 2 deletions src/triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const Types = {
afterFind: 'afterFind',
beforeConnect: 'beforeConnect',
beforeSubscribe: 'beforeSubscribe',
beforeEvent: 'beforeEvent',
afterEvent: 'afterEvent',
};

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