Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow LiveQuery on Parse.Session #7554

Merged
merged 6 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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)
Expand Down
54 changes: 53 additions & 1 deletion spec/ParseLiveQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -754,7 +806,7 @@ describe('ParseLiveQuery', function () {

await reconfigureServer({
liveQuery: {
classNames: ['_User'],
classNames: [Parse.User],
},
startLiveQueryServer: true,
verbose: false,
Expand Down
6 changes: 5 additions & 1 deletion src/Controllers/LiveQueryController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ParseCloudCodePublisher } from '../LiveQuery/ParseCloudCodePublisher';
import { LiveQueryOptions } from '../Options';
import { getClassName } from './../triggers';
export class LiveQueryController {
classNames: any;
liveQueryPublisher: any;
Expand All @@ -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';
Expand Down
26 changes: 26 additions & 0 deletions src/LiveQuery/ParseLiveQueryServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -729,10 +729,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;
}
Expand All @@ -749,6 +751,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
Expand Down
19 changes: 17 additions & 2 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -1591,11 +1591,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('.');
Expand All @@ -1612,7 +1623,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;
};

Expand Down
29 changes: 11 additions & 18 deletions src/cloud-code/Parse.Cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
const className = triggers.getClassName(parseClass);
validateValidator(validationHandler);
triggers.addTrigger(
triggers.Types.beforeSave,
Expand Down Expand Up @@ -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);
const className = triggers.getClassName(parseClass);
validateValidator(validationHandler);
triggers.addTrigger(
triggers.Types.beforeDelete,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
const className = triggers.getClassName(parseClass);
validateValidator(validationHandler);
triggers.addTrigger(
triggers.Types.afterSave,
Expand Down Expand Up @@ -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);
const className = triggers.getClassName(parseClass);
validateValidator(validationHandler);
triggers.addTrigger(
triggers.Types.afterDelete,
Expand Down Expand Up @@ -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);
const className = triggers.getClassName(parseClass);
validateValidator(validationHandler);
triggers.addTrigger(
triggers.Types.beforeFind,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -663,7 +656,7 @@ ParseCloud.sendEmail = function (data) {
*/
ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) {
validateValidator(validationHandler);
var className = getClassName(parseClass);
const className = triggers.getClassName(parseClass);
triggers.addTrigger(
triggers.Types.beforeSubscribe,
className,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/triggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down