From f0630e193bc24988ce87293b804c8c2990e77e8d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Oct 2025 20:50:51 +1000 Subject: [PATCH 1/5] feature: run query via livequery --- spec/ParseLiveQueryQuery.spec.js | 287 ++++++++++++++++++++++++++ src/LiveQuery/Client.js | 23 +++ src/LiveQuery/ParseLiveQueryServer.ts | 72 ++++++- src/LiveQuery/RequestSchema.js | 16 +- 4 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 spec/ParseLiveQueryQuery.spec.js diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js new file mode 100644 index 0000000000..c839a5457a --- /dev/null +++ b/spec/ParseLiveQueryQuery.spec.js @@ -0,0 +1,287 @@ +'use strict'; + +describe('ParseLiveQuery query operation', function () { + beforeEach(() => { + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); + }); + + afterEach(async () => { + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (client) { + await client.close(); + } + }); + + it('can execute query on existing subscription and receive results', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create test objects + const obj1 = new TestObject(); + obj1.set('name', 'object1'); + await obj1.save(); + + const obj2 = new TestObject(); + obj2.set('name', 'object2'); + await obj2.save(); + + // Subscribe to query + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + + // Wait for subscription to be ready + await new Promise(resolve => subscription.on('open', resolve)); + + // Set up result listener + const resultPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout waiting for result')), 5000); + subscription.on('result', results => { + clearTimeout(timeout); + resolve(results); + }); + }); + + // Get the LiveQuery client and send query message + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + const message = { + op: 'query', + requestId: subscription.id, + }; + client.socket.send(JSON.stringify(message)); + + // Wait for and verify results + const results = await resultPromise; + expect(Array.isArray(results)).toBe(true); + expect(results.length).toBe(2); + expect(results.some(r => r.name === 'object1')).toBe(true); + expect(results.some(r => r.name === 'object2')).toBe(true); + + await subscription.unsubscribe(); + }); + + it('respects field filtering (keys) when executing query', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create test object with multiple fields + const obj = new TestObject(); + obj.set('name', 'test'); + obj.set('secret', 'confidential'); + obj.set('public', 'visible'); + await obj.save(); + + // Subscribe with field selection + const query = new Parse.Query(TestObject); + query.select('name', 'public'); // Only select these fields + const subscription = await query.subscribe(); + + // Wait for subscription to be ready + await new Promise(resolve => subscription.on('open', resolve)); + + // Set up result listener + const resultPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); + subscription.on('result', results => { + clearTimeout(timeout); + resolve(results); + }); + }); + + // Send query message + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + const message = { + op: 'query', + requestId: subscription.id, + }; + client.socket.send(JSON.stringify(message)); + + // Wait for and verify results + const results = await resultPromise; + expect(results.length).toBe(1); + const result = results[0]; + expect(result.name).toBe('test'); + expect(result.public).toBe('visible'); + expect(result.secret).toBeUndefined(); // Should be filtered out + + await subscription.unsubscribe(); + }); + + it('runs beforeFind and afterFind triggers', async () => { + let beforeFindCalled = false; + let afterFindCalled = false; + + Parse.Cloud.beforeFind('TestObject', () => { + beforeFindCalled = true; + }); + + Parse.Cloud.afterFind('TestObject', req => { + afterFindCalled = true; + return req.objects; + }); + + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create test object + const obj = new TestObject(); + obj.set('name', 'test'); + await obj.save(); + + // Subscribe + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + + // Wait for subscription to be ready + await new Promise(resolve => subscription.on('open', resolve)); + + // Set up result listener + const resultPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); + subscription.on('result', results => { + clearTimeout(timeout); + resolve(results); + }); + }); + + // Send query message + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + const message = { + op: 'query', + requestId: subscription.id, + }; + client.socket.send(JSON.stringify(message)); + + // Wait for results + await resultPromise; + + // Verify triggers were called + expect(beforeFindCalled).toBe(true); + expect(afterFindCalled).toBe(true); + + await subscription.unsubscribe(); + }); + + it('handles query with where constraints', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create multiple test objects + const obj1 = new TestObject(); + obj1.set('name', 'apple'); + await obj1.save(); + + const obj2 = new TestObject(); + obj2.set('name', 'banana'); + await obj2.save(); + + const obj3 = new TestObject(); + obj3.set('name', 'cherry'); + await obj3.save(); + + // Subscribe with where constraint + const query = new Parse.Query(TestObject); + query.equalTo('name', 'banana'); + const subscription = await query.subscribe(); + + // Wait for subscription to be ready + await new Promise(resolve => subscription.on('open', resolve)); + + // Set up result listener + const resultPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); + subscription.on('result', results => { + clearTimeout(timeout); + resolve(results); + }); + }); + + // Send query message + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + const message = { + op: 'query', + requestId: subscription.id, + }; + client.socket.send(JSON.stringify(message)); + + // Wait for and verify results - should only get banana + const results = await resultPromise; + expect(results.length).toBe(1); + expect(results[0].name).toBe('banana'); + + await subscription.unsubscribe(); + }); + + it('handles errors gracefully', async () => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + // Create an object + const obj = new TestObject(); + obj.set('name', 'test'); + await obj.save(); + + // Subscribe + const query = new Parse.Query(TestObject); + const subscription = await query.subscribe(); + await new Promise(resolve => subscription.on('open', resolve)); + + // Set up listeners for both result and error + let resultReceived = false; + let errorReceived = false; + + subscription.on('result', () => { + resultReceived = true; + }); + + subscription.on('error', () => { + errorReceived = true; + }); + + // Send query message + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + const message = { + op: 'query', + requestId: subscription.id, + }; + client.socket.send(JSON.stringify(message)); + + // Wait a bit for the response + await new Promise(resolve => setTimeout(resolve, 500)); + + // Should have received result (not error) since query is valid + expect(resultReceived).toBe(true); + expect(errorReceived).toBe(false); + + await subscription.unsubscribe(); + }); +}); diff --git a/src/LiveQuery/Client.js b/src/LiveQuery/Client.js index 0ce629bd4e..3b1d73b639 100644 --- a/src/LiveQuery/Client.js +++ b/src/LiveQuery/Client.js @@ -22,6 +22,7 @@ class Client { pushUpdate: Function; pushDelete: Function; pushLeave: Function; + pushResult: Function; constructor( id: number, @@ -45,6 +46,7 @@ class Client { this.pushUpdate = this._pushEvent('update'); this.pushDelete = this._pushEvent('delete'); this.pushLeave = this._pushEvent('leave'); + this.pushResult = this._pushQueryResult.bind(this); } static pushResponse(parseWebSocket: any, message: Message): void { @@ -126,6 +128,27 @@ class Client { } return limitedParseObject; } + + _pushQueryResult(subscriptionId: number, results: any[]): void { + const response: Message = { + op: 'result', + clientId: this.id, + installationId: this.installationId, + requestId: subscriptionId, + }; + + if (results && Array.isArray(results)) { + let keys; + if (this.subscriptionInfos.has(subscriptionId)) { + keys = this.subscriptionInfos.get(subscriptionId).keys; + } + response['results'] = results.map(obj => this._toJSONWithFields(obj, keys)); + } else { + response['results'] = []; + } + + Client.pushResponse(this.parseWebSocket, JSON.stringify(response)); + } } export { Client }; diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index 3e6048c345..4a1c9f9e05 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -19,7 +19,7 @@ import { toJSONwithObjects, } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; -import { getCacheController, getDatabaseController } from '../Controllers'; +import { getCacheController } from '../Controllers'; import { LRUCache as LRU } from 'lru-cache'; import UserRouter from '../Routers/UsersRouter'; import DatabaseController from '../Controllers/DatabaseController'; @@ -460,6 +460,9 @@ class ParseLiveQueryServer { case 'unsubscribe': this._handleUnsubscribe(parseWebsocket, request); break; + case 'query': + this._handleQuery(parseWebsocket, request); + break; default: Client.pushError(parseWebsocket, 3, 'Get unknown operation'); logger.error('Get unknown operation', request.op); @@ -1056,6 +1059,73 @@ class ParseLiveQueryServer { `Delete client: ${parseWebsocket.clientId} | subscription: ${request.requestId}` ); } + + async _handleQuery(parseWebsocket: any, request: any): Promise { + if (!Object.prototype.hasOwnProperty.call(parseWebsocket, 'clientId')) { + Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before querying'); + logger.error('Can not find this client, make sure you connect to server before querying'); + return; + } + + const client = this.clients.get(parseWebsocket.clientId); + if (!client) { + Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId); + logger.error('Can not find client ' + parseWebsocket.clientId); + return; + } + + const requestId = request.requestId; + const subscriptionInfo = client.getSubscriptionInfo(requestId); + if (!subscriptionInfo) { + Client.pushError(parseWebsocket, 2, 'Cannot find subscription with requestId ' + requestId); + logger.error('Can not find subscription with requestId ' + requestId); + return; + } + + const { subscription } = subscriptionInfo; + const { className, query } = subscription; + + try { + const sessionToken = subscriptionInfo.sessionToken || client.sessionToken; + const parseQuery = new Parse.Query(className); + + if (query && Object.keys(query).length > 0) { + parseQuery._where = query; + } + + if (subscriptionInfo.keys && subscriptionInfo.keys.length > 0) { + parseQuery.select(...subscriptionInfo.keys); + } + + const findOptions: any = {}; + if (sessionToken) { + findOptions.sessionToken = sessionToken; + } else if (client.hasMasterKey) { + findOptions.useMasterKey = true; + } + + const results = await parseQuery.find(findOptions); + const jsonResults = results.map(obj => obj.toJSON()); + client.pushResult(requestId, jsonResults); + + logger.verbose(`Executed query for client ${parseWebsocket.clientId} subscription ${requestId}`); + + runLiveQueryEventHandlers({ + client, + event: 'query', + clients: this.clients.size, + subscriptions: this.subscriptions.size, + sessionToken, + useMasterKey: client.hasMasterKey, + installationId: client.installationId, + }); + } catch (e) { + logger.error(`Exception in _handleQuery:`, e); + const error = resolveError(e); + Client.pushError(parseWebsocket, error.code, error.message, false, request.requestId); + logger.error(`Failed running query on ${className}: ${JSON.stringify(error)}`); + } + } } export { ParseLiveQueryServer }; diff --git a/src/LiveQuery/RequestSchema.js b/src/LiveQuery/RequestSchema.js index 6e0a0566b2..0b67953f9c 100644 --- a/src/LiveQuery/RequestSchema.js +++ b/src/LiveQuery/RequestSchema.js @@ -4,7 +4,7 @@ const general = { properties: { op: { type: 'string', - enum: ['connect', 'subscribe', 'unsubscribe', 'update'], + enum: ['connect', 'subscribe', 'unsubscribe', 'update', 'query'], }, }, required: ['op'], @@ -149,12 +149,26 @@ const unsubscribe = { additionalProperties: false, }; +const query = { + title: 'Query operation schema', + type: 'object', + properties: { + op: 'query', + requestId: { + type: 'number', + }, + }, + required: ['op', 'requestId'], + additionalProperties: false, +}; + const RequestSchema = { general: general, connect: connect, subscribe: subscribe, update: update, unsubscribe: unsubscribe, + query: query, }; export default RequestSchema; From aa91cd54d805b6d6c6269db475a57028bf386150 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Oct 2025 20:51:48 +1000 Subject: [PATCH 2/5] Update ParseLiveQueryServer.ts --- src/LiveQuery/ParseLiveQueryServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index 4a1c9f9e05..ad594078e6 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -19,7 +19,7 @@ import { toJSONwithObjects, } from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; -import { getCacheController } from '../Controllers'; +import { getCacheController, getDatabaseController } from '../Controllers'; import { LRUCache as LRU } from 'lru-cache'; import UserRouter from '../Routers/UsersRouter'; import DatabaseController from '../Controllers/DatabaseController'; From 1140484a25c1629f4e21f36c8068f1a21181e415 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Oct 2025 21:16:49 +1000 Subject: [PATCH 3/5] add subscription check --- src/LiveQuery/ParseLiveQueryServer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index ad594078e6..25065e7bbc 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -1083,6 +1083,12 @@ class ParseLiveQueryServer { } const { subscription } = subscriptionInfo; + if (!subscription) { + Client.pushError(parseWebsocket, 2, 'Subscription not found for requestId ' + requestId); + logger.error('Subscription not found for requestId ' + requestId); + return; + } + const { className, query } = subscription; try { From 7f3128597affe3462e440e0850279a07f513b93e Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Oct 2025 23:21:27 +1000 Subject: [PATCH 4/5] fix tests --- spec/ParseLiveQueryQuery.spec.js | 394 ++++++++++++-------------- src/LiveQuery/ParseLiveQueryServer.ts | 4 +- 2 files changed, 178 insertions(+), 220 deletions(-) diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js index c839a5457a..8c0672b123 100644 --- a/spec/ParseLiveQueryQuery.spec.js +++ b/spec/ParseLiveQueryQuery.spec.js @@ -1,28 +1,75 @@ 'use strict'; +const Parse = require('parse/node'); + describe('ParseLiveQuery query operation', function () { - beforeEach(() => { - Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); + beforeEach(function (done) { + // Mock ParseWebSocketServer + const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); + jasmine.mockLibrary( + '../lib/LiveQuery/ParseWebSocketServer', + 'ParseWebSocketServer', + mockParseWebSocketServer + ); + // Mock Client pushError + const Client = require('../lib/LiveQuery/Client').Client; + Client.pushError = jasmine.createSpy('pushError'); + done(); }); - afterEach(async () => { - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - if (client) { - await client.close(); - } + afterEach(function () { + jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer'); }); - it('can execute query on existing subscription and receive results', async () => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, + function addMockClient(parseLiveQueryServer, clientId) { + const Client = require('../lib/LiveQuery/Client').Client; + const client = new Client(clientId, {}); + client.pushResult = jasmine.createSpy('pushResult'); + parseLiveQueryServer.clients.set(clientId, client); + return client; + } + + function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query = {}) { + const Subscription = require('../lib/LiveQuery/Subscription').Subscription; + const subscription = new Subscription( + query.className || 'TestObject', + query.where || {}, + 'hash' + ); + + // Add to server subscriptions + if (!parseLiveQueryServer.subscriptions.has(subscription.className)) { + parseLiveQueryServer.subscriptions.set(subscription.className, new Map()); + } + const classSubscriptions = parseLiveQueryServer.subscriptions.get(subscription.className); + classSubscriptions.set('hash', subscription); + + // Add to client + const client = parseLiveQueryServer.clients.get(clientId); + const subscriptionInfo = { + subscription: subscription, + keys: query.keys, + }; + if (parseWebSocket.sessionToken) { + subscriptionInfo.sessionToken = parseWebSocket.sessionToken; + } + client.subscriptionInfos.set(requestId, subscriptionInfo); + + return subscription; + } + + it('can handle query command with existing subscription', async () => { + await reconfigureServer(); + + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({ + appId: 'test', + masterKey: 'test', + serverURL: 'http://localhost:1337/parse' }); // Create test objects + const TestObject = Parse.Object.extend('TestObject'); const obj1 = new TestObject(); obj1.set('name', 'object1'); await obj1.save(); @@ -31,257 +78,168 @@ describe('ParseLiveQuery query operation', function () { obj2.set('name', 'object2'); await obj2.save(); - // Subscribe to query - const query = new Parse.Query(TestObject); - const subscription = await query.subscribe(); - - // Wait for subscription to be ready - await new Promise(resolve => subscription.on('open', resolve)); - - // Set up result listener - const resultPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout waiting for result')), 5000); - subscription.on('result', results => { - clearTimeout(timeout); - resolve(results); - }); - }); + // Add mock client + const clientId = 1; + const client = addMockClient(parseLiveQueryServer, clientId); + client.hasMasterKey = true; + + // Add mock subscription + const parseWebSocket = { clientId: 1 }; + const requestId = 2; + const query = { + className: 'TestObject', + where: {}, + }; + addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); - // Get the LiveQuery client and send query message - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - const message = { + // Handle query command + const request = { op: 'query', - requestId: subscription.id, + requestId: requestId, }; - client.socket.send(JSON.stringify(message)); - // Wait for and verify results - const results = await resultPromise; + await parseLiveQueryServer._handleQuery(parseWebSocket, request); + + // Verify pushResult was called + expect(client.pushResult).toHaveBeenCalled(); + const results = client.pushResult.calls.mostRecent().args[1]; expect(Array.isArray(results)).toBe(true); expect(results.length).toBe(2); expect(results.some(r => r.name === 'object1')).toBe(true); expect(results.some(r => r.name === 'object2')).toBe(true); - - await subscription.unsubscribe(); }); - it('respects field filtering (keys) when executing query', async () => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); + it('can handle query command without clientId', async () => { + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const incompleteParseConn = {}; + await parseLiveQueryServer._handleQuery(incompleteParseConn, {}); - // Create test object with multiple fields - const obj = new TestObject(); - obj.set('name', 'test'); - obj.set('secret', 'confidential'); - obj.set('public', 'visible'); - await obj.save(); + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); - // Subscribe with field selection - const query = new Parse.Query(TestObject); - query.select('name', 'public'); // Only select these fields - const subscription = await query.subscribe(); - - // Wait for subscription to be ready - await new Promise(resolve => subscription.on('open', resolve)); - - // Set up result listener - const resultPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); - subscription.on('result', results => { - clearTimeout(timeout); - resolve(results); - }); - }); + it('can handle query command without subscription', async () => { + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const clientId = 1; + addMockClient(parseLiveQueryServer, clientId); - // Send query message - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - const message = { + const parseWebSocket = { clientId: 1 }; + const request = { op: 'query', - requestId: subscription.id, + requestId: 999, // Non-existent subscription }; - client.socket.send(JSON.stringify(message)); - // Wait for and verify results - const results = await resultPromise; - expect(results.length).toBe(1); - const result = results[0]; - expect(result.name).toBe('test'); - expect(result.public).toBe('visible'); - expect(result.secret).toBeUndefined(); // Should be filtered out + await parseLiveQueryServer._handleQuery(parseWebSocket, request); - await subscription.unsubscribe(); + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); }); - it('runs beforeFind and afterFind triggers', async () => { - let beforeFindCalled = false; - let afterFindCalled = false; - - Parse.Cloud.beforeFind('TestObject', () => { - beforeFindCalled = true; - }); - - Parse.Cloud.afterFind('TestObject', req => { - afterFindCalled = true; - return req.objects; - }); + it('respects field filtering (keys) when executing query', async () => { + await reconfigureServer(); - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({ + appId: 'test', + masterKey: 'test', + serverURL: 'http://localhost:1337/parse' }); - // Create test object + // Create test object with multiple fields + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); obj.set('name', 'test'); + obj.set('color', 'blue'); + obj.set('size', 'large'); await obj.save(); - // Subscribe - const query = new Parse.Query(TestObject); - const subscription = await query.subscribe(); - - // Wait for subscription to be ready - await new Promise(resolve => subscription.on('open', resolve)); - - // Set up result listener - const resultPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); - subscription.on('result', results => { - clearTimeout(timeout); - resolve(results); - }); - }); + // Add mock client + const clientId = 1; + const client = addMockClient(parseLiveQueryServer, clientId); + client.hasMasterKey = true; + + // Add mock subscription with keys + const parseWebSocket = { clientId: 1 }; + const requestId = 2; + const query = { + className: 'TestObject', + where: {}, + keys: ['name', 'color'], // Only these fields + }; + addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); - // Send query message - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - const message = { + // Handle query command + const request = { op: 'query', - requestId: subscription.id, + requestId: requestId, }; - client.socket.send(JSON.stringify(message)); - // Wait for results - await resultPromise; + await parseLiveQueryServer._handleQuery(parseWebSocket, request); - // Verify triggers were called - expect(beforeFindCalled).toBe(true); - expect(afterFindCalled).toBe(true); + // Verify results + expect(client.pushResult).toHaveBeenCalled(); + const results = client.pushResult.calls.mostRecent().args[1]; + expect(results.length).toBe(1); + + // Results should include selected fields + expect(results[0].name).toBe('test'); + expect(results[0].color).toBe('blue'); - await subscription.unsubscribe(); + // Results should NOT include size + expect(results[0].size).toBeUndefined(); }); it('handles query with where constraints', async () => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, + await reconfigureServer(); + + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({ + appId: 'test', + masterKey: 'test', + serverURL: 'http://localhost:1337/parse' }); - // Create multiple test objects + // Create test objects + const TestObject = Parse.Object.extend('TestObject'); const obj1 = new TestObject(); - obj1.set('name', 'apple'); + obj1.set('name', 'match'); + obj1.set('status', 'active'); await obj1.save(); const obj2 = new TestObject(); - obj2.set('name', 'banana'); + obj2.set('name', 'nomatch'); + obj2.set('status', 'inactive'); await obj2.save(); - const obj3 = new TestObject(); - obj3.set('name', 'cherry'); - await obj3.save(); - - // Subscribe with where constraint - const query = new Parse.Query(TestObject); - query.equalTo('name', 'banana'); - const subscription = await query.subscribe(); - - // Wait for subscription to be ready - await new Promise(resolve => subscription.on('open', resolve)); - - // Set up result listener - const resultPromise = new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); - subscription.on('result', results => { - clearTimeout(timeout); - resolve(results); - }); - }); - - // Send query message - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - const message = { - op: 'query', - requestId: subscription.id, + // Add mock client + const clientId = 1; + const client = addMockClient(parseLiveQueryServer, clientId); + client.hasMasterKey = true; + + // Add mock subscription with where clause + const parseWebSocket = { clientId: 1 }; + const requestId = 2; + const query = { + className: 'TestObject', + where: { status: 'active' }, // Only active objects }; - client.socket.send(JSON.stringify(message)); - - // Wait for and verify results - should only get banana - const results = await resultPromise; - expect(results.length).toBe(1); - expect(results[0].name).toBe('banana'); - - await subscription.unsubscribe(); - }); + addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); - it('handles errors gracefully', async () => { - await reconfigureServer({ - liveQuery: { - classNames: ['TestObject'], - }, - startLiveQueryServer: true, - verbose: false, - silent: true, - }); - - // Create an object - const obj = new TestObject(); - obj.set('name', 'test'); - await obj.save(); - - // Subscribe - const query = new Parse.Query(TestObject); - const subscription = await query.subscribe(); - await new Promise(resolve => subscription.on('open', resolve)); - - // Set up listeners for both result and error - let resultReceived = false; - let errorReceived = false; - - subscription.on('result', () => { - resultReceived = true; - }); - - subscription.on('error', () => { - errorReceived = true; - }); - - // Send query message - const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); - const message = { + // Handle query command + const request = { op: 'query', - requestId: subscription.id, + requestId: requestId, }; - client.socket.send(JSON.stringify(message)); - - // Wait a bit for the response - await new Promise(resolve => setTimeout(resolve, 500)); - // Should have received result (not error) since query is valid - expect(resultReceived).toBe(true); - expect(errorReceived).toBe(false); + await parseLiveQueryServer._handleQuery(parseWebSocket, request); - await subscription.unsubscribe(); + // Verify results + expect(client.pushResult).toHaveBeenCalled(); + const results = client.pushResult.calls.mostRecent().args[1]; + expect(results.length).toBe(1); + expect(results[0].name).toBe('match'); + expect(results[0].status).toBe('active'); }); }); diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index 25065e7bbc..5b71dc9032 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -1095,11 +1095,11 @@ class ParseLiveQueryServer { const sessionToken = subscriptionInfo.sessionToken || client.sessionToken; const parseQuery = new Parse.Query(className); - if (query && Object.keys(query).length > 0) { + if (query && typeof query === 'object' && query !== null && Object.keys(query).length > 0) { parseQuery._where = query; } - if (subscriptionInfo.keys && subscriptionInfo.keys.length > 0) { + if (subscriptionInfo.keys && Array.isArray(subscriptionInfo.keys) && subscriptionInfo.keys.length > 0) { parseQuery.select(...subscriptionInfo.keys); } From e3a169ca62ab50cb7ba6fba3173d54f6ff93ee23 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Oct 2025 23:55:34 +1000 Subject: [PATCH 5/5] fix tests --- spec/ParseLiveQueryQuery.spec.js | 34 ++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js index 8c0672b123..52e50c529c 100644 --- a/spec/ParseLiveQueryQuery.spec.js +++ b/spec/ParseLiveQueryQuery.spec.js @@ -4,6 +4,7 @@ const Parse = require('parse/node'); describe('ParseLiveQuery query operation', function () { beforeEach(function (done) { + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); // Mock ParseWebSocketServer const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); jasmine.mockLibrary( @@ -17,7 +18,11 @@ describe('ParseLiveQuery query operation', function () { done(); }); - afterEach(function () { + afterEach(async function () { + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (client) { + await client.close(); + } jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer'); }); @@ -59,7 +64,14 @@ describe('ParseLiveQuery query operation', function () { } it('can handle query command with existing subscription', async () => { - await reconfigureServer(); + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); const parseLiveQueryServer = new ParseLiveQueryServer({ @@ -138,7 +150,14 @@ describe('ParseLiveQuery query operation', function () { }); it('respects field filtering (keys) when executing query', async () => { - await reconfigureServer(); + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); const parseLiveQueryServer = new ParseLiveQueryServer({ @@ -192,7 +211,14 @@ describe('ParseLiveQuery query operation', function () { }); it('handles query with where constraints', async () => { - await reconfigureServer(); + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); const parseLiveQueryServer = new ParseLiveQueryServer({