diff --git a/package-lock.json b/package-lock.json index cc58dd1a64..c94de248dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10028,31 +10028,31 @@ } }, "parse": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/parse/-/parse-2.14.0.tgz", - "integrity": "sha512-S4bbF80Aom/xDk4YNkzZG1xBHYbiFQGueJWyO4DpYlajfkEs3gp0oszFDnGadTARyCgoQGxNE4Qkege/QqNETA==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/parse/-/parse-2.15.0.tgz", + "integrity": "sha512-Aupg+qd6I4X5uTacpsxROg5GlhkVn2+qOHtyOhlGj/Woi75c5cPD8kn7qhhLKcVVpe2L+HoJ+yGkMdI8IjKBKA==", "requires": { - "@babel/runtime": "7.10.2", - "@babel/runtime-corejs3": "7.10.2", + "@babel/runtime": "7.10.3", + "@babel/runtime-corejs3": "7.10.3", "crypto-js": "4.0.0", "react-native-crypto-js": "1.0.0", - "uuid": "3.3.3", + "uuid": "3.4.0", "ws": "7.3.0", "xmlhttprequest": "1.8.0" }, "dependencies": { "@babel/runtime": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.2.tgz", - "integrity": "sha512-6sF3uQw2ivImfVIl62RZ7MXhO2tap69WeWK57vAaimT6AZbE4FbqjdEJIN1UqoD6wI6B+1n9UiagafH1sxjOtg==", + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz", + "integrity": "sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw==", "requires": { "regenerator-runtime": "^0.13.4" } }, "@babel/runtime-corejs3": { - "version": "7.10.2", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.2.tgz", - "integrity": "sha512-+a2M/u7r15o3dV1NEizr9bRi+KUVnrs/qYxF0Z06DAPx/4VCWaz1WA7EcbE+uqGgt39lp5akWGmHsTseIkHkHg==", + "version": "7.10.3", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz", + "integrity": "sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw==", "requires": { "core-js-pure": "^3.0.0", "regenerator-runtime": "^0.13.4" @@ -10064,9 +10064,9 @@ "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, diff --git a/package.json b/package.json index 55115e3baa..2fa08872e1 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "lru-cache": "5.1.1", "mime": "2.4.6", "mongodb": "3.5.9", - "parse": "2.14.0", + "parse": "2.15.0", "pg-promise": "10.5.7", "pluralize": "8.0.0", "redis": "3.0.2", @@ -104,6 +104,7 @@ "posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} mongodb-runner stop", "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=4.0.4} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=mmapv1} TESTING=1 nyc jasmine", "start": "node ./bin/parse-server", + "prettier": "prettier --write {src,spec}/**/*.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'" }, diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index fa83588b9d..80eebaa733 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1,6 +1,6 @@ 'use strict'; -describe('ParseLiveQuery', function() { +describe('ParseLiveQuery', function () { it('can subscribe to query', async done => { await reconfigureServer({ liveQuery: { @@ -24,6 +24,97 @@ describe('ParseLiveQuery', function() { await object.save(); }); + it('can handle beforeConnect / beforeSubscribe hooks', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeSubscribe('TestObject', req => { + expect(req.op).toBe('subscribe'); + expect(req.requestId).toBe(1); + expect(req.query).toBeDefined(); + expect(req.user).toBeUndefined(); + }); + + Parse.Cloud.beforeConnect(req => { + expect(req.event).toBe('connect'); + expect(req.clients).toBe(0); + expect(req.subscriptions).toBe(0); + expect(req.useMasterKey).toBe(false); + expect(req.installationId).toBeDefined(); + expect(req.user).toBeUndefined(); + expect(req.sessionToken).toBeUndefined(); + expect(req.client).toBeDefined(); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('update', async object => { + expect(object.get('foo')).toBe('bar'); + done(); + }); + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can handle beforeConnect error', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeConnect(() => { + throw new Error('You shall not pass!'); + }); + Parse.LiveQuery.on('error', error => { + expect(error).toBe('You shall not pass!'); + done(); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + await query.subscribe(); + }); + + it('can handle beforeSubscribe error', async done => { + await reconfigureServer({ + liveQuery: { + classNames: ['TestObject'], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + const object = new TestObject(); + await object.save(); + + Parse.Cloud.beforeSubscribe(TestObject, () => { + throw new Error('You shall not subscribe!'); + }); + Parse.LiveQuery.on('error', error => { + expect(error).toBe('You shall not subscribe!'); + }); + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + subscription.on('error', error => { + expect(error).toBe('You shall not subscribe!'); + done(); + }); + }); + it('handle invalid websocket payload length', async done => { await reconfigureServer({ liveQuery: { @@ -61,7 +152,7 @@ describe('ParseLiveQuery', function() { }, 1000); }); - afterEach(async function(done) { + afterEach(async function (done) { const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); client.close(); // Wait for live query client to disconnect diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 63ac0a0505..6c1b831a5d 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -11,8 +11,8 @@ const queryHashValue = 'hash'; const testUserId = 'userId'; const testClassName = 'TestObject'; -describe('ParseLiveQueryServer', function() { - beforeEach(function(done) { +describe('ParseLiveQueryServer', function () { + beforeEach(function (done) { // Mock ParseWebSocketServer const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); jasmine.mockLibrary( @@ -21,7 +21,7 @@ describe('ParseLiveQueryServer', function() { mockParseWebSocketServer ); // Mock Client - const mockClient = function(id, socket, hasMasterKey) { + const mockClient = function (id, socket, hasMasterKey) { this.pushConnect = jasmine.createSpy('pushConnect'); this.pushSubscribe = jasmine.createSpy('pushSubscribe'); this.pushUnsubscribe = jasmine.createSpy('pushUnsubscribe'); @@ -38,7 +38,7 @@ describe('ParseLiveQueryServer', function() { mockClient.pushError = jasmine.createSpy('pushError'); jasmine.mockLibrary('../lib/LiveQuery/Client', 'Client', mockClient); // Mock Subscription - const mockSubscriotion = function() { + const mockSubscriotion = function () { this.addClientSubscription = jasmine.createSpy('addClientSubscription'); this.deleteClientSubscription = jasmine.createSpy( 'deleteClientSubscription' @@ -69,13 +69,13 @@ describe('ParseLiveQueryServer', function() { ); // Mock ParsePubSub const mockParsePubSub = { - createPublisher: function() { + createPublisher: function () { return { publish: jasmine.createSpy('publish'), on: jasmine.createSpy('on'), }; }, - createSubscriber: function() { + createSubscriber: function () { return { subscribe: jasmine.createSpy('subscribe'), on: jasmine.createSpy('on'), @@ -114,7 +114,7 @@ describe('ParseLiveQueryServer', function() { done(); }); - it('can be initialized', function() { + it('can be initialized', function () { const httpServer = {}; const parseLiveQueryServer = new ParseLiveQueryServer(httpServer); @@ -123,7 +123,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.subscriptions.size).toBe(0); }); - it('can be initialized from ParseServer', function() { + it('can be initialized from ParseServer', function () { const httpServer = {}; const parseLiveQueryServer = ParseServer.createLiveQueryServer( httpServer, @@ -135,7 +135,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.subscriptions.size).toBe(0); }); - it('can be initialized from ParseServer without httpServer', function(done) { + it('can be initialized from ParseServer without httpServer', function (done) { const parseLiveQueryServer = ParseServer.createLiveQueryServer(undefined, { port: 22345, }); @@ -147,7 +147,7 @@ describe('ParseLiveQueryServer', function() { }); describe_only_db('mongo')('initialization', () => { - it('can be initialized through ParseServer without liveQueryServerOptions', function(done) { + it('can be initialized through ParseServer without liveQueryServerOptions', function (done) { const parseServer = ParseServer.start({ appId: 'hello', masterKey: 'world', @@ -166,7 +166,7 @@ describe('ParseLiveQueryServer', function() { }); }); - it('can be initialized through ParseServer with liveQueryServerOptions', function(done) { + it('can be initialized through ParseServer with liveQueryServerOptions', function (done) { const parseServer = ParseServer.start({ appId: 'hello', masterKey: 'world', @@ -192,7 +192,7 @@ describe('ParseLiveQueryServer', function() { }); }); - it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + it('properly passes the CLP to afterSave/afterDelete hook', function (done) { function setPermissionsOnClass(className, permissions, doPut) { const request = require('request'); let op = request.post; @@ -285,7 +285,7 @@ describe('ParseLiveQueryServer', function() { .catch(done.fail); }); - it('can handle connect command', function() { + it('can handle connect command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1, @@ -293,7 +293,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._validateKeys = jasmine .createSpy('validateKeys') .and.returnValue(true); - parseLiveQueryServer._handleConnect(parseWebSocket, { + await parseLiveQueryServer._handleConnect(parseWebSocket, { sessionToken: 'token', }); @@ -307,16 +307,62 @@ describe('ParseLiveQueryServer', function() { expect(client.pushConnect).toHaveBeenCalled(); }); - it('can handle subscribe command without clientId', function() { + it('basic beforeConnect rejection', async () => { + Parse.Cloud.beforeConnect(function () { + throw new Error('You shall not pass!'); + }); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const parseWebSocket = { + clientId: -1, + }; + await parseLiveQueryServer._handleConnect(parseWebSocket, { + sessionToken: 'token', + }); + expect(parseLiveQueryServer.clients.size).toBe(0); + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); + + it('basic beforeSubscribe rejection', async () => { + Parse.Cloud.beforeSubscribe('test', function () { + throw new Error('You shall not pass!'); + }); + const parseLiveQueryServer = new ParseLiveQueryServer({}); + const parseWebSocket = { + clientId: -1, + }; + await parseLiveQueryServer._handleConnect(parseWebSocket, { + sessionToken: 'token', + }); + const query = { + className: 'test', + where: { + key: 'value', + }, + fields: ['test'], + }; + const requestId = 2; + const request = { + query: query, + requestId: requestId, + sessionToken: 'sessionToken', + }; + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + expect(parseLiveQueryServer.clients.size).toBe(1); + const Client = require('../lib/LiveQuery/Client').Client; + expect(Client.pushError).toHaveBeenCalled(); + }); + + it('can handle subscribe command without clientId', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; - parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); + await parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle subscribe command with new query', function() { + it('can handle subscribe command with new query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; @@ -338,7 +384,7 @@ describe('ParseLiveQueryServer', function() { requestId: requestId, sessionToken: 'sessionToken', }; - parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make sure we add the subscription to the server const subscriptions = parseLiveQueryServer.subscriptions; @@ -363,7 +409,7 @@ describe('ParseLiveQueryServer', function() { expect(client.pushSubscribe).toHaveBeenCalledWith(requestId); }); - it('can handle subscribe command with existing query', function() { + it('can handle subscribe command with existing query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add two mock clients const clientId = 1; @@ -382,7 +428,7 @@ describe('ParseLiveQueryServer', function() { }, fields: ['test'], }; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -401,7 +447,7 @@ describe('ParseLiveQueryServer', function() { fields: ['testAgain'], }; const requestIdAgain = 1; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientIdAgain, requestIdAgain, @@ -427,7 +473,7 @@ describe('ParseLiveQueryServer', function() { expect(args[1].fields).toBe(queryAgain.fields); }); - it('can handle unsubscribe command without clientId', function() { + it('can handle unsubscribe command without clientId', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {}); @@ -436,7 +482,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle unsubscribe command without not existed client', function() { + it('can handle unsubscribe command without not existed client', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: 1, @@ -447,7 +493,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle unsubscribe command without not existed query', function() { + it('can handle unsubscribe command without not existed query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; @@ -462,7 +508,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can handle unsubscribe command', function() { + it('can handle unsubscribe command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; @@ -472,7 +518,7 @@ describe('ParseLiveQueryServer', function() { clientId: 1, }; const requestId = 2; - const subscription = addMockSubscription( + const subscription = await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -481,7 +527,7 @@ describe('ParseLiveQueryServer', function() { // Mock client.getSubscriptionInfo const subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent() .args[1]; - client.getSubscriptionInfo = function() { + client.getSubscriptionInfo = function () { return subscriptionInfo; }; // Handle unsubscribe command @@ -502,7 +548,7 @@ describe('ParseLiveQueryServer', function() { expect(subscriptions.size).toBe(0); }); - it('can set connect command message handler for a parseWebSocket', function() { + it('can set connect command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe'); @@ -525,7 +571,7 @@ describe('ParseLiveQueryServer', function() { expect(args[0]).toBe(parseWebSocket); }); - it('can set subscribe command message handler for a parseWebSocket', function() { + it('can set subscribe command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleSubscribe = jasmine.createSpy( @@ -551,7 +597,7 @@ describe('ParseLiveQueryServer', function() { expect(JSON.stringify(args[1])).toBe(subscribeRequest); }); - it('can set unsubscribe command message handler for a parseWebSocket', function() { + it('can set unsubscribe command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy( @@ -577,7 +623,7 @@ describe('ParseLiveQueryServer', function() { expect(JSON.stringify(args[1])).toBe(unsubscribeRequest); }); - it('can set update command message handler for a parseWebSocket', function() { + it('can set update command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough(); @@ -612,7 +658,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer._handleSubscribe).toHaveBeenCalled(); }); - it('can set missing command message handler for a parseWebSocket', function() { + it('can set missing command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); @@ -628,7 +674,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can set unknown command message handler for a parseWebSocket', function() { + it('can set unknown command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); @@ -644,7 +690,7 @@ describe('ParseLiveQueryServer', function() { expect(Client.pushError).toHaveBeenCalled(); }); - it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function() { + it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -657,7 +703,7 @@ describe('ParseLiveQueryServer', function() { parseWebSocket.emit('disconnect'); }); - it('can forward event to cloud code', function() { + it('can forward event to cloud code', function () { const cloudCodeHandler = { handler: () => {}, }; @@ -680,7 +726,7 @@ describe('ParseLiveQueryServer', function() { // TODO: Test server can set disconnect command message handler for a parseWebSocket - it('has no subscription and can handle object delete command', function() { + it('has no subscription and can handle object delete command', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); @@ -696,7 +742,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._onAfterDelete(message, {}); }); - it('can handle object delete command which does not match any subscription', function() { + it('can handle object delete command which does not match any subscription', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); @@ -714,13 +760,13 @@ describe('ParseLiveQueryServer', function() { addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); const client = parseLiveQueryServer.clients.get(clientId); // Mock _matchesSubscription to return not matching - parseLiveQueryServer._matchesSubscription = function() { + parseLiveQueryServer._matchesSubscription = function () { return false; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return true; }; parseLiveQueryServer._onAfterDelete(message); @@ -729,7 +775,7 @@ describe('ParseLiveQueryServer', function() { expect(client.pushDelete).not.toHaveBeenCalled(); }); - it('can handle object delete command which matches some subscriptions', function(done) { + it('can handle object delete command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); @@ -746,26 +792,26 @@ describe('ParseLiveQueryServer', function() { addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); const client = parseLiveQueryServer.clients.get(clientId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function() { + parseLiveQueryServer._matchesSubscription = function () { return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterDelete(message); // Make sure we send command to client, since _matchesACL is async, we have to // wait and check - setTimeout(function() { + setTimeout(function () { expect(client.pushDelete).toHaveBeenCalled(); done(); }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('has no subscription and can handle object save command', function() { + it('has no subscription and can handle object save command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); @@ -773,7 +819,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._onAfterSave(message); }); - it('can handle object save command which does not match any subscription', function(done) { + it('can handle object save command which does not match any subscription', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); @@ -782,19 +828,19 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return not matching - parseLiveQueryServer._matchesSubscription = function() { + parseLiveQueryServer._matchesSubscription = function () { return false; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; // Trigger onAfterSave parseLiveQueryServer._onAfterSave(message); // Make sure we do not send command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -804,7 +850,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object enter command which matches some subscriptions', function(done) { + it('can handle object enter command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); @@ -813,25 +859,25 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching // In order to mimic a enter, we need original match return false // and the current match return true let counter = 0; - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; return counter % 2 === 0; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send enter command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -841,7 +887,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object update command which matches some subscriptions', function(done) { + it('can handle object update command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); @@ -850,21 +896,21 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).toHaveBeenCalled(); @@ -874,7 +920,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object leave command which matches some subscriptions', function(done) { + it('can handle object leave command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); @@ -883,25 +929,25 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching // In order to mimic a leave, we need original match return true // and the current match return false let counter = 0; - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; return counter % 2 !== 0; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send leave command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -911,7 +957,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle update command with original object', function(done) { + it('can handle update command with original object', async done => { jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); const Client = require('../lib/LiveQuery/Client').Client; const parseLiveQueryServer = new ParseLiveQueryServer({}); @@ -930,27 +976,27 @@ describe('ParseLiveQueryServer', function() { // Add mock subscription const requestId = 2; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, parseWebSocket ); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushUpdate).toHaveBeenCalled(); const args = parseWebSocket.send.calls.mostRecent().args; const toSend = JSON.parse(args[0]); @@ -961,7 +1007,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle object create command which matches some subscriptions', function(done) { + it('can handle object create command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); @@ -970,21 +1016,21 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; - addMockSubscription(parseLiveQueryServer, clientId, requestId); + await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -994,7 +1040,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can handle create command with fields', function(done) { + it('can handle create command with fields', async done => { jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); const Client = require('../lib/LiveQuery/Client').Client; const parseLiveQueryServer = new ParseLiveQueryServer({}); @@ -1019,7 +1065,7 @@ describe('ParseLiveQueryServer', function() { }, fields: ['test'], }; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -1027,20 +1073,20 @@ describe('ParseLiveQueryServer', function() { query ); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject) { + parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; - parseLiveQueryServer._matchesACL = function() { + parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client - setTimeout(function() { + setTimeout(function () { expect(client.pushCreate).toHaveBeenCalled(); const args = parseWebSocket.send.calls.mostRecent().args; const toSend = JSON.parse(args[0]); @@ -1050,7 +1096,7 @@ describe('ParseLiveQueryServer', function() { }, jasmine.ASYNC_TEST_WAIT_TIME); }); - it('can match subscription for null or undefined parse object', function() { + it('can match subscription for null or undefined parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { @@ -1067,7 +1113,7 @@ describe('ParseLiveQueryServer', function() { expect(subscription.match).not.toHaveBeenCalled(); }); - it('can match subscription', function() { + it('can match subscription', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { @@ -1082,7 +1128,7 @@ describe('ParseLiveQueryServer', function() { expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query); }); - it('can inflate parse object', function() { + it('can inflate parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request const objectJSON = { @@ -1177,20 +1223,20 @@ describe('ParseLiveQueryServer', function() { expect(originalObject.updatedAt).not.toBeUndefined(); }); - it('can match undefined ACL', function(done) { + it('can match undefined ACL', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const client = {}; const requestId = 0; parseLiveQueryServer ._matchesACL(undefined, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with none exist requestId', function(done) { + it('can match ACL with none exist requestId', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); const client = { @@ -1202,13 +1248,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('can match ACL with public read access', function(done) { + it('can match ACL with public read access', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); @@ -1223,13 +1269,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with valid subscription sessionToken', function(done) { + it('can match ACL with valid subscription sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1244,13 +1290,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with valid client sessionToken', function(done) { + it('can match ACL with valid client sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1267,13 +1313,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it('can match ACL with invalid subscription and client sessionToken', function(done) { + it('can match ACL with invalid subscription and client sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1290,13 +1336,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('can match ACL with subscription sessionToken checking error', function(done) { + it('can match ACL with subscription sessionToken checking error', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1313,13 +1359,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('can match ACL with client sessionToken checking error', function(done) { + it('can match ACL with client sessionToken checking error', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); @@ -1336,13 +1382,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it("won't match ACL that doesn't have public read or any roles", function(done) { + it("won't match ACL that doesn't have public read or any roles", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1357,13 +1403,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it("won't match non-public ACL with role when there is no user", function(done) { + it("won't match non-public ACL with role when there is no user", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1377,14 +1423,14 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }) .catch(done.fail); }); - it("won't match ACL with role based read access set to false", function(done) { + it("won't match ACL with role based read access set to false", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1398,7 +1444,7 @@ describe('ParseLiveQueryServer', function() { }; const requestId = 0; - spyOn(Parse, 'Query').and.callFake(function() { + spyOn(Parse, 'Query').and.callFake(function () { let shouldReturn = false; return { equalTo() { @@ -1427,20 +1473,20 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); - it('will match ACL with role based read access set to true', function(done) { + it('will match ACL with role based read access set to true', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1454,7 +1500,7 @@ describe('ParseLiveQueryServer', function() { }; const requestId = 0; - spyOn(Parse, 'Query').and.callFake(function() { + spyOn(Parse, 'Query').and.callFake(function () { let shouldReturn = false; return { equalTo() { @@ -1493,7 +1539,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); @@ -1625,7 +1671,7 @@ describe('ParseLiveQueryServer', function() { }); }); - it('can validate key when valid key is provided', function() { + it('can validate key when valid key is provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1643,7 +1689,7 @@ describe('ParseLiveQueryServer', function() { ).toBeTruthy(); }); - it('can validate key when invalid key is provided', function() { + it('can validate key when invalid key is provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1661,7 +1707,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it('can validate key when key is not provided', function() { + it('can validate key when key is not provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1677,7 +1723,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it('can validate key when validKerPairs is empty', function() { + it('can validate key when validKerPairs is empty', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = {}; @@ -1686,7 +1732,7 @@ describe('ParseLiveQueryServer', function() { ).toBeTruthy(); }); - it('can validate client has master key when valid', function() { + it('can validate client has master key when valid', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1704,7 +1750,7 @@ describe('ParseLiveQueryServer', function() { ).toBeTruthy(); }); - it("can validate client doesn't have master key when invalid", function() { + it("can validate client doesn't have master key when invalid", function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1722,7 +1768,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it("can validate client doesn't have master key when not provided", function() { + it("can validate client doesn't have master key when not provided", function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { @@ -1737,7 +1783,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it("can validate client doesn't have master key when validKeyPairs is empty", function() { + it("can validate client doesn't have master key when validKeyPairs is empty", function () { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = { masterKey: 'test', @@ -1748,7 +1794,7 @@ describe('ParseLiveQueryServer', function() { ).not.toBeTruthy(); }); - it('will match non-public ACL when client has master key', function(done) { + it('will match non-public ACL when client has master key', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1762,13 +1808,13 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); - it("won't match non-public ACL when client has no master key", function(done) { + it("won't match non-public ACL when client has no master key", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); @@ -1782,7 +1828,7 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer ._matchesACL(acl, client, requestId) - .then(function(isMatched) { + .then(function (isMatched) { expect(isMatched).toBe(false); done(); }); @@ -1822,7 +1868,7 @@ describe('ParseLiveQueryServer', function() { expect(parseLiveQueryServer.authCache.get('invalid')).not.toBe(undefined); }); - afterEach(function() { + afterEach(function () { jasmine.restoreLibrary( '../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer' @@ -1842,7 +1888,7 @@ describe('ParseLiveQueryServer', function() { return client; } - function addMockSubscription( + async function addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -1870,13 +1916,13 @@ describe('ParseLiveQueryServer', function() { requestId: requestId, sessionToken: 'sessionToken', }; - parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make mock subscription const subscription = parseLiveQueryServer.subscriptions .get(query.className) .get(queryHashValue); - subscription.hasSubscribingClient = function() { + subscription.hasSubscribingClient = function () { return false; }; subscription.className = query.className; @@ -1915,7 +1961,7 @@ describe('ParseLiveQueryServer', function() { }); describe('LiveQueryController', () => { - it('properly passes the CLP to afterSave/afterDelete hook', function(done) { + it('properly passes the CLP to afterSave/afterDelete hook', function (done) { function setPermissionsOnClass(className, permissions, doPut) { const request = require('request'); let op = request.post; diff --git a/src/LiveQuery/Client.js b/src/LiveQuery/Client.js index 26cd999834..253234b9bb 100644 --- a/src/LiveQuery/Client.js +++ b/src/LiveQuery/Client.js @@ -62,15 +62,17 @@ class Client { parseWebSocket: any, code: number, error: string, - reconnect: boolean = true + reconnect: boolean = true, + requestId: number | void = null ): void { Client.pushResponse( parseWebSocket, JSON.stringify({ op: 'error', - error: error, - code: code, - reconnect: reconnect, + error, + code, + reconnect, + requestId, }) ); } diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index a19ff3962c..5d28367961 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -10,7 +10,11 @@ import { ParsePubSub } from './ParsePubSub'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { runLiveQueryEventHandlers } from '../triggers'; +import { + runLiveQueryEventHandlers, + maybeRunConnectTrigger, + maybeRunSubscribeTrigger, +} from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; import LRU from 'lru-cache'; @@ -574,7 +578,7 @@ class ParseLiveQueryServer { return false; } - _handleConnect(parseWebsocket: any, request: any): any { + async _handleConnect(parseWebsocket: any, request: any): any { if (!this._validateKeys(request, this.keyPairs)) { Client.pushError(parseWebsocket, 4, 'Key in request is not valid'); logger.error('Key in request is not valid'); @@ -589,19 +593,34 @@ class ParseLiveQueryServer { request.sessionToken, request.installationId ); - parseWebsocket.clientId = clientId; - this.clients.set(parseWebsocket.clientId, client); - logger.info(`Create new client: ${parseWebsocket.clientId}`); - client.pushConnect(); - runLiveQueryEventHandlers({ - client, - event: 'connect', - clients: this.clients.size, - subscriptions: this.subscriptions.size, - sessionToken: request.sessionToken, - useMasterKey: client.hasMasterKey, - installationId: request.installationId, - }); + try { + const req = { + client, + event: 'connect', + clients: this.clients.size, + subscriptions: this.subscriptions.size, + sessionToken: request.sessionToken, + useMasterKey: client.hasMasterKey, + installationId: request.installationId, + }; + await maybeRunConnectTrigger('beforeConnect', req); + parseWebsocket.clientId = clientId; + this.clients.set(parseWebsocket.clientId, client); + logger.info(`Create new client: ${parseWebsocket.clientId}`); + client.pushConnect(); + runLiveQueryEventHandlers(req); + } catch (error) { + Client.pushError( + parseWebsocket, + error.code || 101, + error.message || error, + false + ); + logger.error( + `Failed running beforeConnect for session ${request.sessionToken} with:\n Error: ` + + JSON.stringify(error) + ); + } } _hasMasterKey(request: any, validKeyPairs: any): boolean { @@ -636,7 +655,7 @@ class ParseLiveQueryServer { return isValid; } - _handleSubscribe(parseWebsocket: any, request: any): any { + async _handleSubscribe(parseWebsocket: any, request: any): any { // If we can not find this client, return error to client if (!Object.prototype.hasOwnProperty.call(parseWebsocket, 'clientId')) { Client.pushError( @@ -650,61 +669,77 @@ class ParseLiveQueryServer { return; } const client = this.clients.get(parseWebsocket.clientId); - - // Get subscription from subscriptions, create one if necessary - const subscriptionHash = queryHash(request.query); - // Add className to subscriptions if necessary const className = request.query.className; - if (!this.subscriptions.has(className)) { - this.subscriptions.set(className, new Map()); - } - const classSubscriptions = this.subscriptions.get(className); - let subscription; - if (classSubscriptions.has(subscriptionHash)) { - subscription = classSubscriptions.get(subscriptionHash); - } else { - subscription = new Subscription( - className, - request.query.where, - subscriptionHash - ); - classSubscriptions.set(subscriptionHash, subscription); - } + try { + await maybeRunSubscribeTrigger('beforeSubscribe', className, request); - // Add subscriptionInfo to client - const subscriptionInfo = { - subscription: subscription, - }; - // Add selected fields, sessionToken and installationId for this subscription if necessary - if (request.query.fields) { - subscriptionInfo.fields = request.query.fields; - } - if (request.sessionToken) { - subscriptionInfo.sessionToken = request.sessionToken; - } - client.addSubscriptionInfo(request.requestId, subscriptionInfo); + // Get subscription from subscriptions, create one if necessary + const subscriptionHash = queryHash(request.query); + // Add className to subscriptions if necessary - // Add clientId to subscription - subscription.addClientSubscription( - parseWebsocket.clientId, - request.requestId - ); + if (!this.subscriptions.has(className)) { + this.subscriptions.set(className, new Map()); + } + const classSubscriptions = this.subscriptions.get(className); + let subscription; + if (classSubscriptions.has(subscriptionHash)) { + subscription = classSubscriptions.get(subscriptionHash); + } else { + subscription = new Subscription( + className, + request.query.where, + subscriptionHash + ); + classSubscriptions.set(subscriptionHash, subscription); + } - client.pushSubscribe(request.requestId); + // Add subscriptionInfo to client + const subscriptionInfo = { + subscription: subscription, + }; + // Add selected fields, sessionToken and installationId for this subscription if necessary + if (request.query.fields) { + subscriptionInfo.fields = request.query.fields; + } + if (request.sessionToken) { + subscriptionInfo.sessionToken = request.sessionToken; + } + client.addSubscriptionInfo(request.requestId, subscriptionInfo); - logger.verbose( - `Create client ${parseWebsocket.clientId} new subscription: ${request.requestId}` - ); - logger.verbose('Current client number: %d', this.clients.size); - runLiveQueryEventHandlers({ - client, - event: 'subscribe', - clients: this.clients.size, - subscriptions: this.subscriptions.size, - sessionToken: request.sessionToken, - useMasterKey: client.hasMasterKey, - installationId: client.installationId, - }); + // Add clientId to subscription + subscription.addClientSubscription( + parseWebsocket.clientId, + request.requestId + ); + + client.pushSubscribe(request.requestId); + + logger.verbose( + `Create client ${parseWebsocket.clientId} new subscription: ${request.requestId}` + ); + logger.verbose('Current client number: %d', this.clients.size); + runLiveQueryEventHandlers({ + client, + event: 'subscribe', + clients: this.clients.size, + subscriptions: this.subscriptions.size, + sessionToken: request.sessionToken, + useMasterKey: client.hasMasterKey, + installationId: client.installationId, + }); + } catch (e) { + Client.pushError( + parseWebsocket, + e.code || 101, + e.message || e, + false, + request.requestId + ); + logger.error( + `Failed running beforeSubscribe on ${className} for session ${request.sessionToken} with:\n Error: ` + + JSON.stringify(e) + ); + } } _handleUpdateSubscription(parseWebsocket: any, request: any): any { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 861eb68b2b..088c4dc3c1 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -453,6 +453,60 @@ ParseCloud.afterDeleteFile = function (handler) { ); }; +/** + * Registers a before live query server connect function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.beforeConnect(async (request) => { + * // code here + * }) + *``` + * + * @method beforeConnect + * @name Parse.Cloud.beforeConnect + * @param {Function} func The function to before connection is made. This function can be async and should take just one parameter, {@link Parse.Cloud.ConnectTriggerRequest}. + */ +ParseCloud.beforeConnect = function (handler) { + triggers.addConnectTrigger( + triggers.Types.beforeConnect, + handler, + Parse.applicationId + ); +}; + +/** + * Registers a before live query subscription function. + * + * **Available in Cloud Code only.** + * + * If you want to use beforeSubscribe for a predefined class in the Parse JavaScript SDK (e.g. {@link Parse.User}), you should pass the class itself and not the String for arg1. + * ``` + * Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => { + * // code here + * }) + * + * Parse.Cloud.beforeSubscribe(Parse.User, (request) => { + * // code here + * }) + *``` + * + * @method beforeSubscribe + * @name Parse.Cloud.beforeSubscribe + * @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before subscription function for. This can instead be a String that is the className of the subclass. + * @param {Function} func The function to run before a subscription. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}. + */ +ParseCloud.beforeSubscribe = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger( + triggers.Types.beforeSubscribe, + className, + handler, + Parse.applicationId + ); +}; + ParseCloud.onLiveQueryEvent = function (handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; @@ -499,6 +553,16 @@ module.exports = ParseCloud; * @property {Object} log The current logger inside Parse Server. */ +/** + * @interface Parse.Cloud.ConnectTriggerRequest + * @property {String} installationId If set, the installationId triggering the request. + * @property {Boolean} useMasterKey If true, means the master key was used. + * @property {Parse.User} user If set, the user that made the request. + * @property {Integer} clients The number of clients connected. + * @property {Integer} subscriptions The number of subscriptions connected. + * @property {String} sessionToken If set, the session of the user that made the request. + */ + /** * @interface Parse.Cloud.BeforeFindRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/triggers.js b/src/triggers.js index 998e6a3cf6..96dcb65e47 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -16,16 +16,19 @@ export const Types = { afterSaveFile: 'afterSaveFile', beforeDeleteFile: 'beforeDeleteFile', afterDeleteFile: 'afterDeleteFile', + beforeConnect: 'beforeConnect', + beforeSubscribe: 'beforeSubscribe', }; const FileClassName = '@File'; +const ConnectClassName = '@Connect'; -const baseStore = function() { +const baseStore = function () { const Validators = {}; const Functions = {}; const Jobs = {}; const LiveQuery = []; - const Triggers = Object.keys(Types).reduce(function(base, key) { + const Triggers = Object.keys(Types).reduce(function (base, key) { base[key] = {}; return base; }, {}); @@ -132,6 +135,10 @@ export function addFileTrigger(type, handler, applicationId) { add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId); } +export function addConnectTrigger(type, handler, applicationId) { + add(Category.Triggers, `${type}.${ConnectClassName}`, handler, applicationId); +} + export function addLiveQueryEventHandler(handler, applicationId) { applicationId = applicationId || Parse.applicationId; _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); @@ -233,10 +240,12 @@ export function getRequestObject( request.original = originalParseObject; } - if (triggerType === Types.beforeSave || + if ( + triggerType === Types.beforeSave || triggerType === Types.afterSave || triggerType === Types.beforeDelete || - triggerType === Types.afterDelete) { + triggerType === Types.afterDelete + ) { // Set a copy of the context on the request object. request.context = Object.assign({}, context); } @@ -300,7 +309,7 @@ export function getRequestQueryObject( // Any changes made to the object in a beforeSave will be included. export function getResponseObject(request, resolve, reject) { return { - success: function(response) { + success: function (response) { if (request.triggerName === Types.afterFind) { if (!response) { response = request.objects; @@ -335,7 +344,7 @@ export function getResponseObject(request, resolve, reject) { } return resolve(response); }, - error: function(error) { + error: function (error) { if (error instanceof Parse.Error) { reject(error); } else if (error instanceof Error) { @@ -585,7 +594,7 @@ export function maybeRunTrigger( if (!parseObject) { return Promise.resolve({}); } - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { var trigger = getTrigger( parseObject.className, triggerType, @@ -721,7 +730,12 @@ export function getRequestFileObject(triggerType, auth, fileObject, config) { return request; } -export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) { +export async function maybeRunFileTrigger( + triggerType, + fileObject, + config, + auth +) { const fileTrigger = getFileTrigger(triggerType, config.applicationId); if (typeof fileTrigger === 'function') { try { @@ -737,8 +751,8 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) 'Parse.File', { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, result, - auth, - ) + auth + ); return result || fileObject; } catch (error) { logTriggerErrorBeforeHook( @@ -746,10 +760,57 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) 'Parse.File', { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, auth, - error, + error ); throw error; } } return fileObject; } + +export async function maybeRunConnectTrigger(triggerType, request) { + const trigger = getTrigger( + ConnectClassName, + triggerType, + Parse.applicationId + ); + if (!trigger) { + return; + } + request.user = await userForSessionToken(request.sessionToken); + return trigger(request); +} + +export async function maybeRunSubscribeTrigger( + triggerType, + className, + request +) { + const trigger = getTrigger(className, triggerType, Parse.applicationId); + if (!trigger) { + return; + } + const parseQuery = new Parse.Query(className); + parseQuery.withJSON(request.query); + request.query = parseQuery; + request.user = await userForSessionToken(request.sessionToken); + return trigger(request); +} + +async function userForSessionToken(sessionToken) { + if (!sessionToken) { + return; + } + const q = new Parse.Query('_Session'); + q.equalTo('sessionToken', sessionToken); + const session = await q.first({ useMasterKey: true }); + if (!session) { + return; + } + const user = session.get('user'); + if (!user) { + return; + } + await user.fetch({ useMasterKey: true }); + return user; +}