Skip to content

Commit 9433a2b

Browse files
committed
feat: add LiveQuery query operation
1 parent 828d0e0 commit 9433a2b

5 files changed

Lines changed: 454 additions & 1 deletion

File tree

spec/Client.spec.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,76 @@ describe('Client', function () {
190190
expect(messageJSON.requestId).toBe(2);
191191
});
192192

193+
it('can push query result response', function () {
194+
const parseObjectJSON = {
195+
key: 'value',
196+
className: 'test',
197+
objectId: 'test',
198+
updatedAt: '2015-12-07T21:27:13.746Z',
199+
createdAt: '2015-12-07T21:27:13.746Z',
200+
ACL: 'test',
201+
test: 'test',
202+
};
203+
const parseWebSocket = {
204+
send: jasmine.createSpy('send'),
205+
};
206+
const client = new Client(1, parseWebSocket, false, undefined, 'installationId');
207+
client.pushResult(2, [parseObjectJSON]);
208+
209+
const lastCall = parseWebSocket.send.calls.first();
210+
const messageJSON = JSON.parse(lastCall.args[0]);
211+
expect(messageJSON.op).toBe('result');
212+
expect(messageJSON.clientId).toBe(1);
213+
expect(messageJSON.installationId).toBe('installationId');
214+
expect(messageJSON.requestId).toBe(2);
215+
expect(messageJSON.results).toEqual([parseObjectJSON]);
216+
});
217+
218+
it('can push query result response with selected fields', function () {
219+
const parseObjectJSON = {
220+
key: 'value',
221+
className: 'test',
222+
objectId: 'test',
223+
updatedAt: '2015-12-07T21:27:13.746Z',
224+
createdAt: '2015-12-07T21:27:13.746Z',
225+
ACL: 'test',
226+
test: 'test',
227+
};
228+
const parseWebSocket = {
229+
send: jasmine.createSpy('send'),
230+
};
231+
const client = new Client(1, parseWebSocket);
232+
client.addSubscriptionInfo(2, { keys: ['test'] });
233+
client.pushResult(2, [parseObjectJSON]);
234+
235+
const lastCall = parseWebSocket.send.calls.first();
236+
const messageJSON = JSON.parse(lastCall.args[0]);
237+
expect(messageJSON.results).toEqual([
238+
{
239+
className: 'test',
240+
objectId: 'test',
241+
updatedAt: '2015-12-07T21:27:13.746Z',
242+
createdAt: '2015-12-07T21:27:13.746Z',
243+
ACL: 'test',
244+
test: 'test',
245+
},
246+
]);
247+
});
248+
249+
it('can push empty query result response when results are missing', function () {
250+
const parseWebSocket = {
251+
send: jasmine.createSpy('send'),
252+
};
253+
const client = new Client(1, parseWebSocket);
254+
client.pushResult(2);
255+
256+
const lastCall = parseWebSocket.send.calls.first();
257+
const messageJSON = JSON.parse(lastCall.args[0]);
258+
expect(messageJSON.op).toBe('result');
259+
expect(messageJSON.requestId).toBe(2);
260+
expect(messageJSON.results).toEqual([]);
261+
});
262+
193263
it('can push create response', function () {
194264
const parseObjectJSON = {
195265
key: 'value',

spec/ParseLiveQueryQuery.spec.js

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
'use strict';
2+
3+
const Parse = require('parse/node');
4+
5+
describe('ParseLiveQuery query operation', function () {
6+
beforeEach(function () {
7+
Parse.CoreManager.getLiveQueryController().setDefaultLiveQueryClient(null);
8+
// Mock ParseWebSocketServer
9+
const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
10+
jasmine.mockLibrary(
11+
'../lib/LiveQuery/ParseWebSocketServer',
12+
'ParseWebSocketServer',
13+
mockParseWebSocketServer
14+
);
15+
// Mock Client pushError
16+
const Client = require('../lib/LiveQuery/Client').Client;
17+
spyOn(Client, 'pushError');
18+
});
19+
20+
afterEach(async function () {
21+
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
22+
if (client) {
23+
await client.close();
24+
}
25+
jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
26+
});
27+
28+
function addMockClient(parseLiveQueryServer, clientId) {
29+
const Client = require('../lib/LiveQuery/Client').Client;
30+
const client = new Client(clientId, {});
31+
client.pushResult = jasmine.createSpy('pushResult');
32+
parseLiveQueryServer.clients.set(clientId, client);
33+
return client;
34+
}
35+
36+
function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query = {}) {
37+
const Subscription = require('../lib/LiveQuery/Subscription').Subscription;
38+
const subscription = new Subscription(
39+
query.className || 'TestObject',
40+
query.where || {},
41+
'hash'
42+
);
43+
44+
// Add to server subscriptions
45+
if (!parseLiveQueryServer.subscriptions.has(subscription.className)) {
46+
parseLiveQueryServer.subscriptions.set(subscription.className, new Map());
47+
}
48+
const classSubscriptions = parseLiveQueryServer.subscriptions.get(subscription.className);
49+
classSubscriptions.set('hash', subscription);
50+
51+
// Add to client
52+
const client = parseLiveQueryServer.clients.get(clientId);
53+
const subscriptionInfo = {
54+
subscription: subscription,
55+
keys: query.keys,
56+
};
57+
if (parseWebSocket.sessionToken) {
58+
subscriptionInfo.sessionToken = parseWebSocket.sessionToken;
59+
}
60+
client.subscriptionInfos.set(requestId, subscriptionInfo);
61+
62+
return subscription;
63+
}
64+
65+
it('can handle query command with existing subscription', async () => {
66+
await reconfigureServer({
67+
liveQuery: {
68+
classNames: ['TestObject'],
69+
},
70+
startLiveQueryServer: true,
71+
verbose: false,
72+
silent: true,
73+
});
74+
75+
const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer');
76+
const parseLiveQueryServer = new ParseLiveQueryServer({
77+
appId: 'test',
78+
masterKey: 'test',
79+
serverURL: Parse.serverURL
80+
});
81+
82+
// Create test objects
83+
const TestObject = Parse.Object.extend('TestObject');
84+
const obj1 = new TestObject();
85+
obj1.set('name', 'object1');
86+
await obj1.save();
87+
88+
const obj2 = new TestObject();
89+
obj2.set('name', 'object2');
90+
await obj2.save();
91+
92+
// Add mock client
93+
const clientId = 1;
94+
const client = addMockClient(parseLiveQueryServer, clientId);
95+
client.hasMasterKey = true;
96+
97+
// Add mock subscription
98+
const parseWebSocket = { clientId: 1 };
99+
const requestId = 2;
100+
const query = {
101+
className: 'TestObject',
102+
where: {},
103+
};
104+
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
105+
106+
// Handle query command
107+
const request = {
108+
op: 'query',
109+
requestId: requestId,
110+
};
111+
112+
await parseLiveQueryServer._handleQuery(parseWebSocket, request);
113+
114+
// Verify pushResult was called
115+
expect(client.pushResult).toHaveBeenCalled();
116+
const results = client.pushResult.calls.mostRecent().args[1];
117+
expect(Array.isArray(results)).toBe(true);
118+
expect(results.length).toBe(2);
119+
expect(results.some(r => r.name === 'object1')).toBe(true);
120+
expect(results.some(r => r.name === 'object2')).toBe(true);
121+
});
122+
123+
it('can handle query command without clientId', async () => {
124+
const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer');
125+
const parseLiveQueryServer = new ParseLiveQueryServer({});
126+
const incompleteParseConn = {};
127+
await parseLiveQueryServer._handleQuery(incompleteParseConn, {});
128+
129+
const Client = require('../lib/LiveQuery/Client').Client;
130+
expect(Client.pushError).toHaveBeenCalled();
131+
});
132+
133+
it('can handle query command without subscription', async () => {
134+
const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer');
135+
const parseLiveQueryServer = new ParseLiveQueryServer({});
136+
const clientId = 1;
137+
addMockClient(parseLiveQueryServer, clientId);
138+
139+
const parseWebSocket = { clientId: 1 };
140+
const request = {
141+
op: 'query',
142+
requestId: 999, // Non-existent subscription
143+
};
144+
145+
await parseLiveQueryServer._handleQuery(parseWebSocket, request);
146+
147+
const Client = require('../lib/LiveQuery/Client').Client;
148+
expect(Client.pushError).toHaveBeenCalled();
149+
});
150+
151+
it('respects field filtering (keys) when executing query', async () => {
152+
await reconfigureServer({
153+
liveQuery: {
154+
classNames: ['TestObject'],
155+
},
156+
startLiveQueryServer: true,
157+
verbose: false,
158+
silent: true,
159+
});
160+
161+
const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer');
162+
const parseLiveQueryServer = new ParseLiveQueryServer({
163+
appId: 'test',
164+
masterKey: 'test',
165+
serverURL: Parse.serverURL
166+
});
167+
168+
// Create test object with multiple fields
169+
const TestObject = Parse.Object.extend('TestObject');
170+
const obj = new TestObject();
171+
obj.set('name', 'test');
172+
obj.set('color', 'blue');
173+
obj.set('size', 'large');
174+
await obj.save();
175+
176+
// Add mock client
177+
const clientId = 1;
178+
const client = addMockClient(parseLiveQueryServer, clientId);
179+
client.hasMasterKey = true;
180+
181+
// Add mock subscription with keys
182+
const parseWebSocket = { clientId: 1 };
183+
const requestId = 2;
184+
const query = {
185+
className: 'TestObject',
186+
where: {},
187+
keys: ['name', 'color'], // Only these fields
188+
};
189+
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
190+
191+
// Handle query command
192+
const request = {
193+
op: 'query',
194+
requestId: requestId,
195+
};
196+
197+
await parseLiveQueryServer._handleQuery(parseWebSocket, request);
198+
199+
// Verify results
200+
expect(client.pushResult).toHaveBeenCalled();
201+
const results = client.pushResult.calls.mostRecent().args[1];
202+
expect(results.length).toBe(1);
203+
204+
// Results should include selected fields
205+
expect(results[0].name).toBe('test');
206+
expect(results[0].color).toBe('blue');
207+
208+
// Results should NOT include size
209+
expect(results[0].size).toBeUndefined();
210+
});
211+
212+
it('handles query with where constraints', async () => {
213+
await reconfigureServer({
214+
liveQuery: {
215+
classNames: ['TestObject'],
216+
},
217+
startLiveQueryServer: true,
218+
verbose: false,
219+
silent: true,
220+
});
221+
222+
const { ParseLiveQueryServer } = require('../lib/LiveQuery/ParseLiveQueryServer');
223+
const parseLiveQueryServer = new ParseLiveQueryServer({
224+
appId: 'test',
225+
masterKey: 'test',
226+
serverURL: Parse.serverURL
227+
});
228+
229+
// Create test objects
230+
const TestObject = Parse.Object.extend('TestObject');
231+
const obj1 = new TestObject();
232+
obj1.set('name', 'match');
233+
obj1.set('status', 'active');
234+
await obj1.save();
235+
236+
const obj2 = new TestObject();
237+
obj2.set('name', 'nomatch');
238+
obj2.set('status', 'inactive');
239+
await obj2.save();
240+
241+
// Add mock client
242+
const clientId = 1;
243+
const client = addMockClient(parseLiveQueryServer, clientId);
244+
client.hasMasterKey = true;
245+
246+
// Add mock subscription with where clause
247+
const parseWebSocket = { clientId: 1 };
248+
const requestId = 2;
249+
const query = {
250+
className: 'TestObject',
251+
where: { status: 'active' }, // Only active objects
252+
};
253+
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
254+
255+
// Handle query command
256+
const request = {
257+
op: 'query',
258+
requestId: requestId,
259+
};
260+
261+
await parseLiveQueryServer._handleQuery(parseWebSocket, request);
262+
263+
// Verify results
264+
expect(client.pushResult).toHaveBeenCalled();
265+
const results = client.pushResult.calls.mostRecent().args[1];
266+
expect(results.length).toBe(1);
267+
expect(results[0].name).toBe('match');
268+
expect(results[0].status).toBe('active');
269+
});
270+
});

src/LiveQuery/Client.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Client {
2222
pushUpdate: Function;
2323
pushDelete: Function;
2424
pushLeave: Function;
25+
pushResult: Function;
2526

2627
constructor(
2728
id: number,
@@ -45,6 +46,7 @@ class Client {
4546
this.pushUpdate = this._pushEvent('update');
4647
this.pushDelete = this._pushEvent('delete');
4748
this.pushLeave = this._pushEvent('leave');
49+
this.pushResult = this._pushQueryResult.bind(this);
4850
}
4951

5052
static pushResponse(parseWebSocket: any, message: Message): void {
@@ -126,6 +128,27 @@ class Client {
126128
}
127129
return limitedParseObject;
128130
}
131+
132+
_pushQueryResult(subscriptionId: number, results: any[]): void {
133+
const response: Message = {
134+
op: 'result',
135+
clientId: this.id,
136+
installationId: this.installationId,
137+
requestId: subscriptionId,
138+
};
139+
140+
if (results && Array.isArray(results)) {
141+
let keys;
142+
if (this.subscriptionInfos.has(subscriptionId)) {
143+
keys = this.subscriptionInfos.get(subscriptionId).keys;
144+
}
145+
response['results'] = results.map(obj => this._toJSONWithFields(obj, keys));
146+
} else {
147+
response['results'] = [];
148+
}
149+
150+
Client.pushResponse(this.parseWebSocket, JSON.stringify(response));
151+
}
129152
}
130153

131154
export { Client };

0 commit comments

Comments
 (0)