From 45312d56cbff2e291f57a9d847099d3cd91d4f31 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 8 Sep 2021 00:23:34 +1000 Subject: [PATCH 1/3] new: allow LiveQuery on Parse.Session --- spec/ParseLiveQuery.spec.js | 54 +++++++++++++++++++++++++- src/Controllers/LiveQueryController.js | 6 ++- src/LiveQuery/ParseLiveQueryServer.js | 26 +++++++++++++ src/RestWrite.js | 19 ++++++++- src/cloud-code/Parse.Cloud.js | 29 ++++++-------- src/triggers.js | 7 ++++ 6 files changed, 119 insertions(+), 22 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index 65d1836c5f..39476aeef8 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -708,6 +708,58 @@ describe('ParseLiveQuery', function () { } }); + it('liveQuery on Session class', async done => { + await reconfigureServer({ + liveQuery: { classNames: [Parse.Session] }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + const user = new Parse.User(); + user.setUsername('username'); + user.setPassword('password'); + await user.signUp(); + + const query = new Parse.Query(Parse.Session); + const subscription = await query.subscribe(); + + subscription.on('create', async obj => { + await new Promise(resolve => setTimeout(resolve, 200)); + expect(obj.get('user').id).toBe(user.id); + expect(obj.get('createdWith')).toEqual({ action: 'login', authProvider: 'password' }); + expect(obj.get('expiresAt')).toBeInstanceOf(Date); + expect(obj.get('installationId')).toBeDefined(); + expect(obj.get('createdAt')).toBeInstanceOf(Date); + expect(obj.get('updatedAt')).toBeInstanceOf(Date); + done(); + }); + + await Parse.User.logIn('username', 'password'); + }); + + it('prevent liveQuery on Session class when not logged in', async done => { + await reconfigureServer({ + liveQuery: { + classNames: [Parse.Session], + }, + startLiveQueryServer: true, + verbose: false, + silent: true, + }); + + Parse.LiveQuery.on('error', error => { + expect(error).toBe('Invalid session token'); + }); + const query = new Parse.Query(Parse.Session); + const subscription = await query.subscribe(); + subscription.on('error', error => { + Parse.LiveQuery.removeAllListeners('error'); + expect(error).toBe('Invalid session token'); + done(); + }); + }); + it('handle invalid websocket payload length', async done => { await reconfigureServer({ liveQuery: { @@ -754,7 +806,7 @@ describe('ParseLiveQuery', function () { await reconfigureServer({ liveQuery: { - classNames: ['_User'], + classNames: [Parse.User], }, startLiveQueryServer: true, verbose: false, diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 34cf28893f..7b77a8e07d 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -1,5 +1,6 @@ import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher'; import { LiveQueryOptions } from '../Options'; +import { getClassName } from './../triggers'; export class LiveQueryController { classNames: any; liveQueryPublisher: any; @@ -9,7 +10,10 @@ export class LiveQueryController { if (!config || !config.classNames) { this.classNames = new Set(); } else if (config.classNames instanceof Array) { - const classNames = config.classNames.map(name => new RegExp('^' + name + '$')); + const classNames = config.classNames.map(name => { + const _name = getClassName(name); + return new RegExp('^' + _name + '$'); + }); this.classNames = new Set(classNames); } else { throw 'liveQuery.classes should be an array of string'; diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index 5a44ae5c8b..943ef31f0f 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -711,10 +711,12 @@ class ParseLiveQueryServer { } const client = this.clients.get(parseWebsocket.clientId); const className = request.query.className; + let authCalled = false; try { const trigger = getTrigger(className, 'beforeSubscribe', Parse.applicationId); if (trigger) { const auth = await this.getAuthFromClient(client, request.requestId, request.sessionToken); + authCalled = true; if (auth && auth.user) { request.user = auth.user; } @@ -731,6 +733,30 @@ class ParseLiveQueryServer { request.query = query; } + if (className === '_Session') { + if (!authCalled) { + const auth = await this.getAuthFromClient( + client, + request.requestId, + request.sessionToken + ); + if (auth && auth.user) { + request.user = auth.user; + } + } + if (request.user) { + request.query.where.user = request.user.toPointer(); + } else if (!request.master) { + Client.pushError( + parseWebsocket, + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token', + false, + request.requestId + ); + return; + } + } // Get subscription from subscriptions, create one if necessary const subscriptionHash = queryHash(request.query); // Add className to subscriptions if necessary diff --git a/src/RestWrite.js b/src/RestWrite.js index 360c7e0dc0..72101eaedd 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1589,11 +1589,22 @@ RestWrite.prototype.sanitizedData = function () { // Returns an updated copy of the object RestWrite.prototype.buildUpdatedObject = function (extraData) { + const className = Parse.Object.fromJSON(extraData); + const readOnlyAttributes = className.constructor.readOnlyAttributes + ? className.constructor.readOnlyAttributes() + : []; + if (!this.originalData) { + for (const attribute of readOnlyAttributes) { + extraData[attribute] = this.data[attribute]; + } + } const updatedObject = triggers.inflate(extraData, this.originalData); Object.keys(this.data).reduce(function (data, key) { if (key.indexOf('.') > 0) { if (typeof data[key].__op === 'string') { - updatedObject.set(key, data[key]); + if (!readOnlyAttributes.includes(key)) { + updatedObject.set(key, data[key]); + } } else { // subdocument key with dot notation { 'x.y': v } => { 'x': { 'y' : v } }) const splittedKey = key.split('.'); @@ -1610,7 +1621,11 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) { return data; }, deepcopy(this.data)); - updatedObject.set(this.sanitizedData()); + const sanitized = this.sanitizedData(); + for (const attribute of readOnlyAttributes) { + delete sanitized[attribute]; + } + updatedObject.set(sanitized); return updatedObject; }; diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index d16fe28ad4..ec7028d975 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -6,13 +6,6 @@ function isParseObjectConstructor(object) { return typeof object === 'function' && Object.prototype.hasOwnProperty.call(object, 'className'); } -function getClassName(parseClass) { - if (parseClass && parseClass.className) { - return parseClass.className; - } - return parseClass; -} - function validateValidator(validator) { if (!validator || typeof validator === 'function') { return; @@ -161,7 +154,7 @@ ParseCloud.job = function (functionName, handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + var className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeSave, @@ -197,7 +190,7 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + var className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeDelete, @@ -236,7 +229,7 @@ ParseCloud.beforeLogin = function (handler) { if (typeof handler === 'string' || isParseObjectConstructor(handler)) { // validation will occur downstream, this is to maintain internal // code consistency with the other hook types. - className = getClassName(handler); + className = triggers.getClassName(handler); handler = arguments[1]; } triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId); @@ -266,7 +259,7 @@ ParseCloud.afterLogin = function (handler) { if (typeof handler === 'string' || isParseObjectConstructor(handler)) { // validation will occur downstream, this is to maintain internal // code consistency with the other hook types. - className = getClassName(handler); + className = triggers.getClassName(handler); handler = arguments[1]; } triggers.addTrigger(triggers.Types.afterLogin, className, handler, Parse.applicationId); @@ -295,7 +288,7 @@ ParseCloud.afterLogout = function (handler) { if (typeof handler === 'string' || isParseObjectConstructor(handler)) { // validation will occur downstream, this is to maintain internal // code consistency with the other hook types. - className = getClassName(handler); + className = triggers.getClassName(handler); handler = arguments[1]; } triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId); @@ -327,7 +320,7 @@ ParseCloud.afterLogout = function (handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + var className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterSave, @@ -363,7 +356,7 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + var className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterDelete, @@ -399,7 +392,7 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.BeforeFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { - var className = getClassName(parseClass); + var className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeFind, @@ -435,7 +428,7 @@ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.AfterFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterFind = function (parseClass, handler, validationHandler) { - const className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterFind, @@ -663,7 +656,7 @@ ParseCloud.sendEmail = function (data) { */ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { validateValidator(validationHandler); - var className = getClassName(parseClass); + var className = triggers.getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeSubscribe, className, @@ -701,7 +694,7 @@ ParseCloud.onLiveQueryEvent = function (handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.LiveQueryEventTrigger}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterLiveQueryEvent = function (parseClass, handler, validationHandler) { - const className = getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterEvent, diff --git a/src/triggers.js b/src/triggers.js index cdd4353128..4d1cb5fba9 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -46,6 +46,13 @@ const baseStore = function () { }); }; +export function getClassName(parseClass) { + if (parseClass && parseClass.className) { + return parseClass.className; + } + return parseClass; +} + function validateClassNameForTriggers(className, type) { if (type == Types.beforeSave && className === '_PushStatus') { // _PushStatus uses undocumented nested key increment ops From f621b8cecf8073f0fd5da022d947ef6905d14e8a Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 8 Sep 2021 00:31:18 +1000 Subject: [PATCH 2/3] better syntax --- src/Controllers/LiveQueryController.js | 2 +- src/cloud-code/Parse.Cloud.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 7b77a8e07d..064084caa4 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -12,7 +12,7 @@ export class LiveQueryController { } else if (config.classNames instanceof Array) { const classNames = config.classNames.map(name => { const _name = getClassName(name); - return new RegExp('^' + _name + '$'); + return new RegExp(`^${_name}$`); }); this.classNames = new Set(classNames); } else { diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index ec7028d975..5329a3eda2 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -154,7 +154,7 @@ ParseCloud.job = function (functionName, handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { - var className = triggers.getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeSave, @@ -190,7 +190,7 @@ ParseCloud.beforeSave = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) { - var className = triggers.getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeDelete, @@ -320,7 +320,7 @@ ParseCloud.afterLogout = function (handler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { - var className = triggers.getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterSave, @@ -356,7 +356,7 @@ ParseCloud.afterSave = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.TriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { - var className = triggers.getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.afterDelete, @@ -392,7 +392,7 @@ ParseCloud.afterDelete = function (parseClass, handler, validationHandler) { * @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.BeforeFindRequest}, or a {@link Parse.Cloud.ValidatorObject}. */ ParseCloud.beforeFind = function (parseClass, handler, validationHandler) { - var className = triggers.getClassName(parseClass); + const className = triggers.getClassName(parseClass); validateValidator(validationHandler); triggers.addTrigger( triggers.Types.beforeFind, @@ -656,7 +656,7 @@ ParseCloud.sendEmail = function (data) { */ ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) { validateValidator(validationHandler); - var className = triggers.getClassName(parseClass); + const className = triggers.getClassName(parseClass); triggers.addTrigger( triggers.Types.beforeSubscribe, className, From 13a7d855ef3f2d9e1b2e715b013454933e3280e2 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 7 Oct 2021 00:02:17 +1100 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 635777d7f1..6b560000fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -156,6 +156,7 @@ ___ - Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496) - Allow cloud string for ES modules (Daniel Blyth) [#7560](https://github.com/parse-community/parse-server/pull/7560) - docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562) +- Allow liveQuery on Session class (Daniel Blyth) [#7554](https://github.com/parse-community/parse-server/pull/7554) ## 4.10.4 [Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.3...4.10.4)