From 9642207392af5c76cf50ac3a2f62e052d4d1e290 Mon Sep 17 00:00:00 2001 From: Corvus400 <13657682+Corvus400@users.noreply.github.com> Date: Wed, 20 May 2026 12:45:15 +0900 Subject: [PATCH 1/5] feat: add LiveQuery query operation --- spec/Client.spec.js | 70 +++++++ spec/ParseLiveQueryQuery.spec.js | 270 ++++++++++++++++++++++++++ src/LiveQuery/Client.js | 23 +++ src/LiveQuery/ParseLiveQueryServer.ts | 76 ++++++++ src/LiveQuery/RequestSchema.js | 16 +- 5 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 spec/ParseLiveQueryQuery.spec.js diff --git a/spec/Client.spec.js b/spec/Client.spec.js index 0de226204a..ecd6acb14e 100644 --- a/spec/Client.spec.js +++ b/spec/Client.spec.js @@ -190,6 +190,76 @@ describe('Client', function () { expect(messageJSON.requestId).toBe(2); }); + it('can push query result response', function () { + const parseObjectJSON = { + key: 'value', + className: 'test', + objectId: 'test', + updatedAt: '2015-12-07T21:27:13.746Z', + createdAt: '2015-12-07T21:27:13.746Z', + ACL: 'test', + test: 'test', + }; + const parseWebSocket = { + send: jasmine.createSpy('send'), + }; + const client = new Client(1, parseWebSocket, false, undefined, 'installationId'); + client.pushResult(2, [parseObjectJSON]); + + const lastCall = parseWebSocket.send.calls.first(); + const messageJSON = JSON.parse(lastCall.args[0]); + expect(messageJSON.op).toBe('result'); + expect(messageJSON.clientId).toBe(1); + expect(messageJSON.installationId).toBe('installationId'); + expect(messageJSON.requestId).toBe(2); + expect(messageJSON.results).toEqual([parseObjectJSON]); + }); + + it('can push query result response with selected fields', function () { + const parseObjectJSON = { + key: 'value', + className: 'test', + objectId: 'test', + updatedAt: '2015-12-07T21:27:13.746Z', + createdAt: '2015-12-07T21:27:13.746Z', + ACL: 'test', + test: 'test', + }; + const parseWebSocket = { + send: jasmine.createSpy('send'), + }; + const client = new Client(1, parseWebSocket); + client.addSubscriptionInfo(2, { keys: ['test'] }); + client.pushResult(2, [parseObjectJSON]); + + const lastCall = parseWebSocket.send.calls.first(); + const messageJSON = JSON.parse(lastCall.args[0]); + expect(messageJSON.results).toEqual([ + { + className: 'test', + objectId: 'test', + updatedAt: '2015-12-07T21:27:13.746Z', + createdAt: '2015-12-07T21:27:13.746Z', + ACL: 'test', + test: 'test', + }, + ]); + }); + + it('can push empty query result response when results are missing', function () { + const parseWebSocket = { + send: jasmine.createSpy('send'), + }; + const client = new Client(1, parseWebSocket); + client.pushResult(2); + + const lastCall = parseWebSocket.send.calls.first(); + const messageJSON = JSON.parse(lastCall.args[0]); + expect(messageJSON.op).toBe('result'); + expect(messageJSON.requestId).toBe(2); + expect(messageJSON.results).toEqual([]); + }); + it('can push create response', function () { const parseObjectJSON = { key: 'value', diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js new file mode 100644 index 0000000000..baebebe822 --- /dev/null +++ b/spec/ParseLiveQueryQuery.spec.js @@ -0,0 +1,270 @@ +'use strict'; + +const Parse = require('parse/node'); + +describe('ParseLiveQuery query operation', function () { + beforeEach(function () { + Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null); + // Mock ParseWebSocketServer + const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); + jasmine.mockLibrary( + '../lib/LiveQuery/ParseWebSocketServer', + 'ParseWebSocketServer', + mockParseWebSocketServer + ); + // Mock Client pushError + const Client = require('../lib/LiveQuery/Client').Client; + spyOn(Client, 'pushError'); + }); + + afterEach(async function () { + const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (client) { + await client.close(); + } + jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer'); + }); + + 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({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({ + appId: 'test', + masterKey: 'test', + serverURL: Parse.serverURL + }); + + // Create test objects + const TestObject = Parse.Object.extend('TestObject'); + const obj1 = new TestObject(); + obj1.set('name', 'object1'); + await obj1.save(); + + const obj2 = new TestObject(); + obj2.set('name', 'object2'); + await obj2.save(); + + // 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); + + // Handle query command + const request = { + op: 'query', + requestId: requestId, + }; + + 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); + }); + + it('can handle query command without clientId', async () => { + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const incompleteParseConn = {}; + await parseLiveQueryServer._handleQuery(incompleteParseConn, {}); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); + + it('can handle query command without subscription', async () => { + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const clientId = 1; + addMockClient(parseLiveQueryServer, clientId); + + const parseWebSocket = { clientId: 1 }; + const request = { + op: 'query', + requestId: 999, // Non-existent subscription + }; + + await parseLiveQueryServer._handleQuery(parseWebSocket, request); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); + + it('respects field filtering (keys) when executing query', async () => { + 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: Parse.serverURL + }); + + // 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(); + + // 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); + + // Handle query command + const request = { + op: 'query', + requestId: requestId, + }; + + await parseLiveQueryServer._handleQuery(parseWebSocket, request); + + // 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'); + + // 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, + }); + + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + const parseLiveQueryServer = new ParseLiveQueryServer({ + appId: 'test', + masterKey: 'test', + serverURL: Parse.serverURL + }); + + // Create test objects + const TestObject = Parse.Object.extend('TestObject'); + const obj1 = new TestObject(); + obj1.set('name', 'match'); + obj1.set('status', 'active'); + await obj1.save(); + + const obj2 = new TestObject(); + obj2.set('name', 'nomatch'); + obj2.set('status', 'inactive'); + await obj2.save(); + + // 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 + }; + addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); + + // Handle query command + const request = { + op: 'query', + requestId: requestId, + }; + + await parseLiveQueryServer._handleQuery(parseWebSocket, request); + + // 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/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 ef98cf7f8e..7b6cf71333 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -521,6 +521,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); @@ -1375,6 +1378,79 @@ 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; + 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 { + const sessionToken = subscriptionInfo.sessionToken || client.sessionToken; + const parseQuery = new Parse.Query(className); + + if (query && typeof query === 'object' && query !== null && Object.keys(query).length > 0) { + parseQuery._where = query; + } + + if (subscriptionInfo.keys && Array.isArray(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 50bbc3def284171fd71d1e176bc761393167ba45 Mon Sep 17 00:00:00 2001 From: Corvus400 <13657682+Corvus400@users.noreply.github.com> Date: Wed, 20 May 2026 13:30:54 +0900 Subject: [PATCH 2/5] Use JSON for LiveQuery query build --- src/LiveQuery/ParseLiveQueryServer.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index 7b6cf71333..6f7af79780 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -1413,10 +1413,10 @@ class ParseLiveQueryServer { try { const sessionToken = subscriptionInfo.sessionToken || client.sessionToken; const parseQuery = new Parse.Query(className); - - if (query && typeof query === 'object' && query !== null && Object.keys(query).length > 0) { - parseQuery._where = query; - } + parseQuery.withJSON({ + className, + where: query || {}, + }); if (subscriptionInfo.keys && Array.isArray(subscriptionInfo.keys) && subscriptionInfo.keys.length > 0) { parseQuery.select(...subscriptionInfo.keys); From d2d0a58ad9222c07d44e4e859316d2f39995dc9d Mon Sep 17 00:00:00 2001 From: Corvus400 <13657682+Corvus400@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:35:07 +0900 Subject: [PATCH 3/5] fix: run LiveQuery query with stored where --- spec/ParseLiveQueryQuery.spec.js | 42 +++++++++++++++------------ src/LiveQuery/ParseLiveQueryServer.ts | 1 - 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js index baebebe822..f7a59e244e 100644 --- a/spec/ParseLiveQueryQuery.spec.js +++ b/spec/ParseLiveQueryQuery.spec.js @@ -57,11 +57,24 @@ describe('ParseLiveQuery query operation', function () { if (parseWebSocket.sessionToken) { subscriptionInfo.sessionToken = parseWebSocket.sessionToken; } - client.subscriptionInfos.set(requestId, subscriptionInfo); + client.addSubscriptionInfo(requestId, subscriptionInfo); + subscription.addClientSubscription(clientId, requestId); return subscription; } + function createParseLiveQueryServer() { + const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); + return new ParseLiveQueryServer( + {}, + { + appId: 'test', + masterKey: 'test', + serverURL: Parse.serverURL, + } + ); + } + it('can handle query command with existing subscription', async () => { await reconfigureServer({ liveQuery: { @@ -72,12 +85,7 @@ describe('ParseLiveQuery query operation', function () { silent: true, }); - const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); - const parseLiveQueryServer = new ParseLiveQueryServer({ - appId: 'test', - masterKey: 'test', - serverURL: Parse.serverURL - }); + const parseLiveQueryServer = createParseLiveQueryServer(); // Create test objects const TestObject = Parse.Object.extend('TestObject'); @@ -112,6 +120,8 @@ describe('ParseLiveQuery query operation', function () { await parseLiveQueryServer._handleQuery(parseWebSocket, request); // Verify pushResult was called + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError.calls.allArgs()).toEqual([]); expect(client.pushResult).toHaveBeenCalled(); const results = client.pushResult.calls.mostRecent().args[1]; expect(Array.isArray(results)).toBe(true); @@ -158,12 +168,7 @@ describe('ParseLiveQuery query operation', function () { silent: true, }); - const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); - const parseLiveQueryServer = new ParseLiveQueryServer({ - appId: 'test', - masterKey: 'test', - serverURL: Parse.serverURL - }); + const parseLiveQueryServer = createParseLiveQueryServer(); // Create test object with multiple fields const TestObject = Parse.Object.extend('TestObject'); @@ -197,6 +202,8 @@ describe('ParseLiveQuery query operation', function () { await parseLiveQueryServer._handleQuery(parseWebSocket, request); // Verify results + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError.calls.allArgs()).toEqual([]); expect(client.pushResult).toHaveBeenCalled(); const results = client.pushResult.calls.mostRecent().args[1]; expect(results.length).toBe(1); @@ -219,12 +226,7 @@ describe('ParseLiveQuery query operation', function () { silent: true, }); - const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); - const parseLiveQueryServer = new ParseLiveQueryServer({ - appId: 'test', - masterKey: 'test', - serverURL: Parse.serverURL - }); + const parseLiveQueryServer = createParseLiveQueryServer(); // Create test objects const TestObject = Parse.Object.extend('TestObject'); @@ -261,6 +263,8 @@ describe('ParseLiveQuery query operation', function () { await parseLiveQueryServer._handleQuery(parseWebSocket, request); // Verify results + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError.calls.allArgs()).toEqual([]); expect(client.pushResult).toHaveBeenCalled(); const results = client.pushResult.calls.mostRecent().args[1]; expect(results.length).toBe(1); diff --git a/src/LiveQuery/ParseLiveQueryServer.ts b/src/LiveQuery/ParseLiveQueryServer.ts index 6f7af79780..245b426847 100644 --- a/src/LiveQuery/ParseLiveQueryServer.ts +++ b/src/LiveQuery/ParseLiveQueryServer.ts @@ -1414,7 +1414,6 @@ class ParseLiveQueryServer { const sessionToken = subscriptionInfo.sessionToken || client.sessionToken; const parseQuery = new Parse.Query(className); parseQuery.withJSON({ - className, where: query || {}, }); From 22c066a8061ada83c85a51b837cedc32a4657486 Mon Sep 17 00:00:00 2001 From: Corvus400 <13657682+Corvus400@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:05:38 +0900 Subject: [PATCH 4/5] test: cover LiveQuery query operation --- spec/ParseLiveQueryQuery.spec.js | 140 +++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js index f7a59e244e..6f83fd43fb 100644 --- a/spec/ParseLiveQueryQuery.spec.js +++ b/spec/ParseLiveQueryQuery.spec.js @@ -75,6 +75,24 @@ describe('ParseLiveQuery query operation', function () { ); } + it('dispatches query command messages to the query handler', function () { + const parseLiveQueryServer = createParseLiveQueryServer(); + parseLiveQueryServer._handleQuery = jasmine.createSpy('_handleQuery'); + const EventEmitter = require('events'); + const parseWebSocket = new EventEmitter(); + parseLiveQueryServer._onConnect(parseWebSocket); + + const request = JSON.stringify({ + op: 'query', + requestId: 1, + }); + parseWebSocket.emit('message', request); + + const args = parseLiveQueryServer._handleQuery.calls.mostRecent().args; + expect(args[0]).toBe(parseWebSocket); + expect(JSON.stringify(args[1])).toBe(request); + }); + it('can handle query command with existing subscription', async () => { await reconfigureServer({ liveQuery: { @@ -140,6 +158,23 @@ describe('ParseLiveQuery query operation', function () { expect(Client.pushError).toHaveBeenCalled(); }); + it('can handle query command without a registered client', async () => { + const parseLiveQueryServer = createParseLiveQueryServer(); + const parseWebSocket = { clientId: 1 }; + + await parseLiveQueryServer._handleQuery(parseWebSocket, { + op: 'query', + requestId: 2, + }); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalledWith( + parseWebSocket, + 2, + 'Cannot find client with clientId 1' + ); + }); + it('can handle query command without subscription', async () => { const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer'); const parseLiveQueryServer = new ParseLiveQueryServer({}); @@ -158,6 +193,111 @@ describe('ParseLiveQuery query operation', function () { expect(Client.pushError).toHaveBeenCalled(); }); + it('can handle query command without a subscription object', async () => { + const parseLiveQueryServer = createParseLiveQueryServer(); + const clientId = 1; + const requestId = 2; + const client = addMockClient(parseLiveQueryServer, clientId); + client.addSubscriptionInfo(requestId, {}); + const parseWebSocket = { clientId }; + + await parseLiveQueryServer._handleQuery(parseWebSocket, { + op: 'query', + requestId, + }); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalledWith( + parseWebSocket, + 2, + 'Subscription not found for requestId 2' + ); + expect(client.pushResult).not.toHaveBeenCalled(); + }); + + it('uses the subscription session token when executing query', async () => { + const parseLiveQueryServer = createParseLiveQueryServer(); + const clientId = 1; + const requestId = 2; + const client = addMockClient(parseLiveQueryServer, clientId); + client.hasMasterKey = true; + const parseWebSocket = { clientId, sessionToken: 'session-token' }; + addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, { + className: 'TestObject', + where: {}, + }); + const find = spyOn(Parse.Query.prototype, 'find').and.resolveTo([]); + + await parseLiveQueryServer._handleQuery(parseWebSocket, { + op: 'query', + requestId, + }); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError.calls.allArgs()).toEqual([]); + expect(find).toHaveBeenCalledWith({ sessionToken: 'session-token' }); + expect(client.pushResult).toHaveBeenCalledWith(requestId, []); + }); + + it('uses an empty where clause when the stored query is missing', async () => { + const parseLiveQueryServer = createParseLiveQueryServer(); + const clientId = 1; + const requestId = 2; + const client = addMockClient(parseLiveQueryServer, clientId); + client.hasMasterKey = true; + const parseWebSocket = { clientId }; + const subscription = addMockSubscription( + parseLiveQueryServer, + clientId, + requestId, + parseWebSocket, + { className: 'TestObject' } + ); + subscription.query = undefined; + const withJSON = spyOn(Parse.Query.prototype, 'withJSON').and.callThrough(); + spyOn(Parse.Query.prototype, 'find').and.resolveTo([]); + + await parseLiveQueryServer._handleQuery(parseWebSocket, { + op: 'query', + requestId, + }); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError.calls.allArgs()).toEqual([]); + expect(withJSON).toHaveBeenCalledWith({ where: {} }); + expect(client.pushResult).toHaveBeenCalledWith(requestId, []); + }); + + it('pushes an error when executing query fails', async () => { + const parseLiveQueryServer = createParseLiveQueryServer(); + const clientId = 1; + const requestId = 2; + const client = addMockClient(parseLiveQueryServer, clientId); + client.hasMasterKey = true; + const parseWebSocket = { clientId }; + addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, { + className: 'TestObject', + where: {}, + }); + const error = new Parse.Error(Parse.Error.INVALID_QUERY, 'query failed'); + spyOn(Parse.Query.prototype, 'find').and.rejectWith(error); + + await parseLiveQueryServer._handleQuery(parseWebSocket, { + op: 'query', + requestId, + }); + + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalledWith( + parseWebSocket, + error.code, + error.message, + false, + requestId + ); + expect(client.pushResult).not.toHaveBeenCalled(); + }); + it('respects field filtering (keys) when executing query', async () => { await reconfigureServer({ liveQuery: { From 86c7a179bffe423d40172c7e70323ce7c4317f10 Mon Sep 17 00:00:00 2001 From: Corvus400 <13657682+Corvus400@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:14:54 +0900 Subject: [PATCH 5/5] test: verify LiveQuery master key query --- spec/ParseLiveQueryQuery.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/ParseLiveQueryQuery.spec.js b/spec/ParseLiveQueryQuery.spec.js index 6f83fd43fb..456e9a74c7 100644 --- a/spec/ParseLiveQueryQuery.spec.js +++ b/spec/ParseLiveQueryQuery.spec.js @@ -255,7 +255,7 @@ describe('ParseLiveQuery query operation', function () { ); subscription.query = undefined; const withJSON = spyOn(Parse.Query.prototype, 'withJSON').and.callThrough(); - spyOn(Parse.Query.prototype, 'find').and.resolveTo([]); + const find = spyOn(Parse.Query.prototype, 'find').and.resolveTo([]); await parseLiveQueryServer._handleQuery(parseWebSocket, { op: 'query', @@ -265,6 +265,7 @@ describe('ParseLiveQuery query operation', function () { const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError.calls.allArgs()).toEqual([]); expect(withJSON).toHaveBeenCalledWith({ where: {} }); + expect(find).toHaveBeenCalledWith({ useMasterKey: true }); expect(client.pushResult).toHaveBeenCalledWith(requestId, []); });