Before Connect + Before Subscribe help required (#6793)

* Before Connect + Before Subscribe #1

* Cleanup and Documentation

* Add E2E tests

* Bump parse to 2.15.0

Co-authored-by: Diamond Lewis <findlewis@gmail.com>
This commit is contained in:
dblythy
2020-07-17 11:36:38 +10:00
committed by GitHub
parent 93a88c5cde
commit 44015c3e35
8 changed files with 529 additions and 229 deletions

View File

@@ -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,
})
);
}

View File

@@ -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
try {
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
);
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)
);
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
);
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 {

View File

@@ -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.

View File

@@ -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;
}