diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index 63ac0a0505..85b69cac97 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -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', }); @@ -316,7 +316,7 @@ describe('ParseLiveQueryServer', function() { 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 +338,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 +363,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 +382,7 @@ describe('ParseLiveQueryServer', function() { }, fields: ['test'], }; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -401,7 +401,7 @@ describe('ParseLiveQueryServer', function() { fields: ['testAgain'], }; const requestIdAgain = 1; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientIdAgain, requestIdAgain, @@ -447,7 +447,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 +462,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 +472,7 @@ describe('ParseLiveQueryServer', function() { clientId: 1, }; const requestId = 2; - const subscription = addMockSubscription( + const subscription = await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -696,7 +696,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,7 +714,7 @@ 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() { @@ -729,7 +729,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,7 +746,7 @@ 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() { @@ -765,7 +765,7 @@ describe('ParseLiveQueryServer', function() { }, 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 +773,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,7 +782,7 @@ 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() { return false; @@ -804,7 +804,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,7 +813,7 @@ 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 @@ -841,7 +841,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,7 +850,7 @@ 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) { if (!parseObject) { @@ -874,7 +874,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,7 +883,7 @@ 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 @@ -911,7 +911,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,7 +930,7 @@ describe('ParseLiveQueryServer', function() { // Add mock subscription const requestId = 2; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -961,7 +961,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,7 +970,7 @@ 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) { if (!parseObject) { @@ -994,7 +994,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 +1019,7 @@ describe('ParseLiveQueryServer', function() { }, fields: ['test'], }; - addMockSubscription( + await addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -1842,7 +1842,7 @@ describe('ParseLiveQueryServer', function() { return client; } - function addMockSubscription( + async function addMockSubscription( parseLiveQueryServer, clientId, requestId, @@ -1870,7 +1870,7 @@ describe('ParseLiveQueryServer', function() { requestId: requestId, sessionToken: 'sessionToken', }; - parseLiveQueryServer._handleSubscribe(parseWebSocket, request); + await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make mock subscription const subscription = parseLiveQueryServer.subscriptions diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index fc3d7e3902..c1111a97c7 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -11,6 +11,8 @@ import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import uuid from 'uuid'; import { runLiveQueryEventHandlers } from '../triggers'; +import {maybeRunConnectTrigger} from '../triggers'; +import {maybeRunSubscribeTrigger} from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; import { getCacheController } from '../Controllers'; import LRU from 'lru-cache'; @@ -574,7 +576,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,11 +591,7 @@ 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({ + const req = { client, event: 'connect', clients: this.clients.size, @@ -601,7 +599,18 @@ class ParseLiveQueryServer { sessionToken: request.sessionToken, useMasterKey: client.hasMasterKey, installationId: request.installationId, - }); + } + try { + 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(error); + } } _hasMasterKey(request: any, validKeyPairs: any): boolean { @@ -636,7 +645,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 +659,64 @@ 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 + try { + request.query = await maybeRunSubscribeTrigger('beforeSubscribe', className, request) + // Get subscription from subscriptions, create one if necessary + const subscriptionHash = queryHash(request.query); + // Add className to subscriptions if necessary + 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); + } + // 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); + + // Add clientId to subscription + subscription.addClientSubscription( + parseWebsocket.clientId, + request.requestId ); - 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; + 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); + logger.error(e); } - client.addSubscriptionInfo(request.requestId, subscriptionInfo); - - // 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, - }); } _handleUpdateSubscription(parseWebsocket: any, request: any): any { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index c4bd380b1e..d12c861558 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -453,6 +453,24 @@ ParseCloud.afterDeleteFile = function(handler) { ); }; +ParseCloud.beforeConnect = function(handler) { + triggers.addConnectTrigger( + triggers.Types.beforeConnect, + handler, + Parse.applicationId, + ); +}; + +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); }; diff --git a/src/triggers.js b/src/triggers.js index 9ffa0fd7e6..313749ff18 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -16,9 +16,12 @@ export const Types = { afterSaveFile: 'afterSaveFile', beforeDeleteFile: 'beforeDeleteFile', afterDeleteFile: 'afterDeleteFile', + beforeConnect:'beforeConnect', + beforeSubscribe:'beforeSubscribe' }; const FileClassName = '@File'; +const ConnectClassName = '@Connect'; const baseStore = function() { const Validators = {}; @@ -131,7 +134,9 @@ export function addTrigger(type, className, handler, applicationId) { 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(); @@ -744,3 +749,54 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) } return fileObject; } +export async function maybeRunConnectTrigger(triggerType, request) { + const trigger = getTrigger(ConnectClassName, triggerType, Parse.applicationId); + if (!trigger) { + return; + } + if (request.sessionToken) { + try { + const user = await userForSessionToken(request.sessionToken); + request.user = user; + } catch(e) { + delete request.sessionToken; + } + } + await trigger(request) +} +export async function maybeRunSubscribeTrigger(triggerType, className, request) { + const trigger = getTrigger(className, triggerType, Parse.applicationId); + if (!trigger) { + return request.query; + } + const parseQuery = new Parse.Query(className); + parseQuery.withJSON(request.query); + request.query = parseQuery; + if (request.sessionToken) { + try { + const user = await userForSessionToken(request.sessionToken); + request.user = user; + } catch(e) { + delete request.sessionToken; + } + } + const result = await trigger(request); + if (result) { + request.query = result; + } + return request.query.toJSON(); +} +async function userForSessionToken(sessionToken) { + const q = new Parse.Query('_Session'); + q.equalTo('sessionToken', sessionToken); + const session = await q.first({ useMasterKey: true }) + if (!session) { + throw 'No session found for session token'; + } + const user = session.get('user'); + if (!user) { + throw 'No session found for session token'; + } + await user.fetch({useMasterKey:true}); + return user; +}