fix: allow LiveQuery on Parse.Session (#7554)
This commit is contained in:
@@ -157,6 +157,7 @@ ___
|
|||||||
- Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496)
|
- 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)
|
- 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)
|
- 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
|
## 4.10.4
|
||||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.3...4.10.4)
|
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.10.3...4.10.4)
|
||||||
|
|||||||
@@ -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 => {
|
it('handle invalid websocket payload length', async done => {
|
||||||
await reconfigureServer({
|
await reconfigureServer({
|
||||||
liveQuery: {
|
liveQuery: {
|
||||||
@@ -754,7 +806,7 @@ describe('ParseLiveQuery', function () {
|
|||||||
|
|
||||||
await reconfigureServer({
|
await reconfigureServer({
|
||||||
liveQuery: {
|
liveQuery: {
|
||||||
classNames: ['_User'],
|
classNames: [Parse.User],
|
||||||
},
|
},
|
||||||
startLiveQueryServer: true,
|
startLiveQueryServer: true,
|
||||||
verbose: false,
|
verbose: false,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
|
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
|
||||||
import { LiveQueryOptions } from '../Options';
|
import { LiveQueryOptions } from '../Options';
|
||||||
|
import { getClassName } from './../triggers';
|
||||||
export class LiveQueryController {
|
export class LiveQueryController {
|
||||||
classNames: any;
|
classNames: any;
|
||||||
liveQueryPublisher: any;
|
liveQueryPublisher: any;
|
||||||
@@ -9,7 +10,10 @@ export class LiveQueryController {
|
|||||||
if (!config || !config.classNames) {
|
if (!config || !config.classNames) {
|
||||||
this.classNames = new Set();
|
this.classNames = new Set();
|
||||||
} else if (config.classNames instanceof Array) {
|
} 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);
|
this.classNames = new Set(classNames);
|
||||||
} else {
|
} else {
|
||||||
throw 'liveQuery.classes should be an array of string';
|
throw 'liveQuery.classes should be an array of string';
|
||||||
|
|||||||
@@ -729,10 +729,12 @@ class ParseLiveQueryServer {
|
|||||||
}
|
}
|
||||||
const client = this.clients.get(parseWebsocket.clientId);
|
const client = this.clients.get(parseWebsocket.clientId);
|
||||||
const className = request.query.className;
|
const className = request.query.className;
|
||||||
|
let authCalled = false;
|
||||||
try {
|
try {
|
||||||
const trigger = getTrigger(className, 'beforeSubscribe', Parse.applicationId);
|
const trigger = getTrigger(className, 'beforeSubscribe', Parse.applicationId);
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
const auth = await this.getAuthFromClient(client, request.requestId, request.sessionToken);
|
const auth = await this.getAuthFromClient(client, request.requestId, request.sessionToken);
|
||||||
|
authCalled = true;
|
||||||
if (auth && auth.user) {
|
if (auth && auth.user) {
|
||||||
request.user = auth.user;
|
request.user = auth.user;
|
||||||
}
|
}
|
||||||
@@ -749,6 +751,30 @@ class ParseLiveQueryServer {
|
|||||||
request.query = query;
|
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
|
// Get subscription from subscriptions, create one if necessary
|
||||||
const subscriptionHash = queryHash(request.query);
|
const subscriptionHash = queryHash(request.query);
|
||||||
// Add className to subscriptions if necessary
|
// Add className to subscriptions if necessary
|
||||||
|
|||||||
@@ -1591,11 +1591,22 @@ RestWrite.prototype.sanitizedData = function () {
|
|||||||
|
|
||||||
// Returns an updated copy of the object
|
// Returns an updated copy of the object
|
||||||
RestWrite.prototype.buildUpdatedObject = function (extraData) {
|
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);
|
const updatedObject = triggers.inflate(extraData, this.originalData);
|
||||||
Object.keys(this.data).reduce(function (data, key) {
|
Object.keys(this.data).reduce(function (data, key) {
|
||||||
if (key.indexOf('.') > 0) {
|
if (key.indexOf('.') > 0) {
|
||||||
if (typeof data[key].__op === 'string') {
|
if (typeof data[key].__op === 'string') {
|
||||||
updatedObject.set(key, data[key]);
|
if (!readOnlyAttributes.includes(key)) {
|
||||||
|
updatedObject.set(key, data[key]);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// subdocument key with dot notation { 'x.y': v } => { 'x': { 'y' : v } })
|
// subdocument key with dot notation { 'x.y': v } => { 'x': { 'y' : v } })
|
||||||
const splittedKey = key.split('.');
|
const splittedKey = key.split('.');
|
||||||
@@ -1612,7 +1623,11 @@ RestWrite.prototype.buildUpdatedObject = function (extraData) {
|
|||||||
return data;
|
return data;
|
||||||
}, deepcopy(this.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;
|
return updatedObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ function isParseObjectConstructor(object) {
|
|||||||
return typeof object === 'function' && Object.prototype.hasOwnProperty.call(object, 'className');
|
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) {
|
function validateValidator(validator) {
|
||||||
if (!validator || typeof validator === 'function') {
|
if (!validator || typeof validator === 'function') {
|
||||||
return;
|
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}.
|
* @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) {
|
ParseCloud.beforeSave = function (parseClass, handler, validationHandler) {
|
||||||
var className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.beforeSave,
|
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}.
|
* @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) {
|
ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) {
|
||||||
var className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.beforeDelete,
|
triggers.Types.beforeDelete,
|
||||||
@@ -236,7 +229,7 @@ ParseCloud.beforeLogin = function (handler) {
|
|||||||
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
||||||
// validation will occur downstream, this is to maintain internal
|
// validation will occur downstream, this is to maintain internal
|
||||||
// code consistency with the other hook types.
|
// code consistency with the other hook types.
|
||||||
className = getClassName(handler);
|
className = triggers.getClassName(handler);
|
||||||
handler = arguments[1];
|
handler = arguments[1];
|
||||||
}
|
}
|
||||||
triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId);
|
triggers.addTrigger(triggers.Types.beforeLogin, className, handler, Parse.applicationId);
|
||||||
@@ -266,7 +259,7 @@ ParseCloud.afterLogin = function (handler) {
|
|||||||
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
||||||
// validation will occur downstream, this is to maintain internal
|
// validation will occur downstream, this is to maintain internal
|
||||||
// code consistency with the other hook types.
|
// code consistency with the other hook types.
|
||||||
className = getClassName(handler);
|
className = triggers.getClassName(handler);
|
||||||
handler = arguments[1];
|
handler = arguments[1];
|
||||||
}
|
}
|
||||||
triggers.addTrigger(triggers.Types.afterLogin, className, handler, Parse.applicationId);
|
triggers.addTrigger(triggers.Types.afterLogin, className, handler, Parse.applicationId);
|
||||||
@@ -295,7 +288,7 @@ ParseCloud.afterLogout = function (handler) {
|
|||||||
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
if (typeof handler === 'string' || isParseObjectConstructor(handler)) {
|
||||||
// validation will occur downstream, this is to maintain internal
|
// validation will occur downstream, this is to maintain internal
|
||||||
// code consistency with the other hook types.
|
// code consistency with the other hook types.
|
||||||
className = getClassName(handler);
|
className = triggers.getClassName(handler);
|
||||||
handler = arguments[1];
|
handler = arguments[1];
|
||||||
}
|
}
|
||||||
triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId);
|
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}.
|
* @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) {
|
ParseCloud.afterSave = function (parseClass, handler, validationHandler) {
|
||||||
var className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.afterSave,
|
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}.
|
* @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) {
|
ParseCloud.afterDelete = function (parseClass, handler, validationHandler) {
|
||||||
var className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.afterDelete,
|
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}.
|
* @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) {
|
ParseCloud.beforeFind = function (parseClass, handler, validationHandler) {
|
||||||
var className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.beforeFind,
|
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}.
|
* @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) {
|
ParseCloud.afterFind = function (parseClass, handler, validationHandler) {
|
||||||
const className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.afterFind,
|
triggers.Types.afterFind,
|
||||||
@@ -663,7 +656,7 @@ ParseCloud.sendEmail = function (data) {
|
|||||||
*/
|
*/
|
||||||
ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) {
|
ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) {
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
var className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.beforeSubscribe,
|
triggers.Types.beforeSubscribe,
|
||||||
className,
|
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}.
|
* @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) {
|
ParseCloud.afterLiveQueryEvent = function (parseClass, handler, validationHandler) {
|
||||||
const className = getClassName(parseClass);
|
const className = triggers.getClassName(parseClass);
|
||||||
validateValidator(validationHandler);
|
validateValidator(validationHandler);
|
||||||
triggers.addTrigger(
|
triggers.addTrigger(
|
||||||
triggers.Types.afterEvent,
|
triggers.Types.afterEvent,
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ const baseStore = function () {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getClassName(parseClass) {
|
||||||
|
if (parseClass && parseClass.className) {
|
||||||
|
return parseClass.className;
|
||||||
|
}
|
||||||
|
return parseClass;
|
||||||
|
}
|
||||||
|
|
||||||
function validateClassNameForTriggers(className, type) {
|
function validateClassNameForTriggers(className, type) {
|
||||||
if (type == Types.beforeSave && className === '_PushStatus') {
|
if (type == Types.beforeSave && className === '_PushStatus') {
|
||||||
// _PushStatus uses undocumented nested key increment ops
|
// _PushStatus uses undocumented nested key increment ops
|
||||||
|
|||||||
Reference in New Issue
Block a user