Live query CLP (#4387)
* Auth module refactoring in order to be reusable * Ensure cache controller is properly forwarded from helpers * Nits * Adds support for static validation * Adds support for CLP in Live query (no support for roles yet) * Adds e2e test to validate liveQuery hooks is properly called * Adds tests over LiveQueryController to ensure data is correctly transmitted * nits * Fixes for flow types * Removes usage of Parse.Promise * Use the Auth module for authentication and caches * Cleaner implementation of getting auth * Adds authCache that stores auth promises * Proper testing of the caching * nits
This commit is contained in:
@@ -1335,7 +1335,7 @@ class DatabaseController {
|
||||
}
|
||||
|
||||
addPointerPermissions(
|
||||
schema: any,
|
||||
schema: SchemaController.SchemaController,
|
||||
className: string,
|
||||
operation: string,
|
||||
query: any,
|
||||
@@ -1343,10 +1343,10 @@ class DatabaseController {
|
||||
) {
|
||||
// Check if class has public permission for operation
|
||||
// If the BaseCLP pass, let go through
|
||||
if (schema.testBaseCLP(className, aclGroup, operation)) {
|
||||
if (schema.testPermissionsForClassName(className, aclGroup, operation)) {
|
||||
return query;
|
||||
}
|
||||
const perms = schema.schemaData[className].classLevelPermissions;
|
||||
const perms = schema.getClassLevelPermissions(className);
|
||||
const field =
|
||||
['get', 'find'].indexOf(operation) > -1
|
||||
? 'readUserFields'
|
||||
|
||||
@@ -16,19 +16,37 @@ export class LiveQueryController {
|
||||
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
|
||||
}
|
||||
|
||||
onAfterSave(className: string, currentObject: any, originalObject: any) {
|
||||
onAfterSave(
|
||||
className: string,
|
||||
currentObject: any,
|
||||
originalObject: any,
|
||||
classLevelPermissions: ?any
|
||||
) {
|
||||
if (!this.hasLiveQuery(className)) {
|
||||
return;
|
||||
}
|
||||
const req = this._makePublisherRequest(currentObject, originalObject);
|
||||
const req = this._makePublisherRequest(
|
||||
currentObject,
|
||||
originalObject,
|
||||
classLevelPermissions
|
||||
);
|
||||
this.liveQueryPublisher.onCloudCodeAfterSave(req);
|
||||
}
|
||||
|
||||
onAfterDelete(className: string, currentObject: any, originalObject: any) {
|
||||
onAfterDelete(
|
||||
className: string,
|
||||
currentObject: any,
|
||||
originalObject: any,
|
||||
classLevelPermissions: any
|
||||
) {
|
||||
if (!this.hasLiveQuery(className)) {
|
||||
return;
|
||||
}
|
||||
const req = this._makePublisherRequest(currentObject, originalObject);
|
||||
const req = this._makePublisherRequest(
|
||||
currentObject,
|
||||
originalObject,
|
||||
classLevelPermissions
|
||||
);
|
||||
this.liveQueryPublisher.onCloudCodeAfterDelete(req);
|
||||
}
|
||||
|
||||
@@ -36,13 +54,20 @@ export class LiveQueryController {
|
||||
return this.classNames.has(className);
|
||||
}
|
||||
|
||||
_makePublisherRequest(currentObject: any, originalObject: any): any {
|
||||
_makePublisherRequest(
|
||||
currentObject: any,
|
||||
originalObject: any,
|
||||
classLevelPermissions: ?any
|
||||
): any {
|
||||
const req = {
|
||||
object: currentObject,
|
||||
};
|
||||
if (currentObject) {
|
||||
req.original = originalObject;
|
||||
}
|
||||
if (classLevelPermissions) {
|
||||
req.classLevelPermissions = classLevelPermissions;
|
||||
}
|
||||
return req;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,18 +1122,28 @@ export default class SchemaController {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
// Validates the base CLP for an operation
|
||||
testBaseCLP(className: string, aclGroup: string[], operation: string) {
|
||||
const classSchema = this.schemaData[className];
|
||||
if (
|
||||
!classSchema ||
|
||||
!classSchema.classLevelPermissions ||
|
||||
!classSchema.classLevelPermissions[operation]
|
||||
) {
|
||||
testPermissionsForClassName(
|
||||
className: string,
|
||||
aclGroup: string[],
|
||||
operation: string
|
||||
) {
|
||||
return SchemaController.testPermissions(
|
||||
this.getClassLevelPermissions(className),
|
||||
aclGroup,
|
||||
operation
|
||||
);
|
||||
}
|
||||
|
||||
// Tests that the class level permission let pass the operation for a given aclGroup
|
||||
static testPermissions(
|
||||
classPermissions: ?any,
|
||||
aclGroup: string[],
|
||||
operation: string
|
||||
): boolean {
|
||||
if (!classPermissions || !classPermissions[operation]) {
|
||||
return true;
|
||||
}
|
||||
const perms = classSchema.classLevelPermissions[operation];
|
||||
// Handle the public scenario quickly
|
||||
const perms = classPermissions[operation];
|
||||
if (perms['*']) {
|
||||
return true;
|
||||
}
|
||||
@@ -1149,21 +1159,22 @@ export default class SchemaController {
|
||||
}
|
||||
|
||||
// Validates an operation passes class-level-permissions set in the schema
|
||||
validatePermission(className: string, aclGroup: string[], operation: string) {
|
||||
if (this.testBaseCLP(className, aclGroup, operation)) {
|
||||
static validatePermission(
|
||||
classPermissions: ?any,
|
||||
className: string,
|
||||
aclGroup: string[],
|
||||
operation: string
|
||||
) {
|
||||
if (
|
||||
SchemaController.testPermissions(classPermissions, aclGroup, operation)
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const classSchema = this.schemaData[className];
|
||||
if (
|
||||
!classSchema ||
|
||||
!classSchema.classLevelPermissions ||
|
||||
!classSchema.classLevelPermissions[operation]
|
||||
) {
|
||||
|
||||
if (!classPermissions || !classPermissions[operation]) {
|
||||
return true;
|
||||
}
|
||||
const classPerms = classSchema.classLevelPermissions;
|
||||
const perms = classSchema.classLevelPermissions[operation];
|
||||
|
||||
const perms = classPermissions[operation];
|
||||
// If only for authenticated users
|
||||
// make sure we have an aclGroup
|
||||
if (perms['requiresAuthentication']) {
|
||||
@@ -1201,8 +1212,8 @@ export default class SchemaController {
|
||||
|
||||
// Process the readUserFields later
|
||||
if (
|
||||
Array.isArray(classPerms[permissionField]) &&
|
||||
classPerms[permissionField].length > 0
|
||||
Array.isArray(classPermissions[permissionField]) &&
|
||||
classPermissions[permissionField].length > 0
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -1212,6 +1223,23 @@ export default class SchemaController {
|
||||
);
|
||||
}
|
||||
|
||||
// Validates an operation passes class-level-permissions set in the schema
|
||||
validatePermission(className: string, aclGroup: string[], operation: string) {
|
||||
return SchemaController.validatePermission(
|
||||
this.getClassLevelPermissions(className),
|
||||
className,
|
||||
aclGroup,
|
||||
operation
|
||||
);
|
||||
}
|
||||
|
||||
getClassLevelPermissions(className: string): any {
|
||||
return (
|
||||
this.schemaData[className] &&
|
||||
this.schemaData[className].classLevelPermissions
|
||||
);
|
||||
}
|
||||
|
||||
// Returns the expected type for a className+key combination
|
||||
// or undefined if the schema is not set
|
||||
getExpectedType(
|
||||
|
||||
@@ -7,10 +7,13 @@ import logger from '../logger';
|
||||
import RequestSchema from './RequestSchema';
|
||||
import { matchesQuery, queryHash } from './QueryTools';
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import { SessionTokenCache } from './SessionTokenCache';
|
||||
import SchemaController from '../Controllers/SchemaController';
|
||||
import _ from 'lodash';
|
||||
import uuid from 'uuid';
|
||||
import { runLiveQueryEventHandlers } from '../triggers';
|
||||
import { getAuthForSessionToken, Auth } from '../Auth';
|
||||
import { getCacheController } from '../Controllers';
|
||||
import LRU from 'lru-cache';
|
||||
|
||||
class ParseLiveQueryServer {
|
||||
clients: Map;
|
||||
@@ -21,12 +24,13 @@ class ParseLiveQueryServer {
|
||||
// The subscriber we use to get object update from publisher
|
||||
subscriber: Object;
|
||||
|
||||
constructor(server: any, config: any) {
|
||||
constructor(server: any, config: any = {}) {
|
||||
this.server = server;
|
||||
this.clients = new Map();
|
||||
this.subscriptions = new Map();
|
||||
|
||||
config = config || {};
|
||||
config.appId = config.appId || Parse.applicationId;
|
||||
config.masterKey = config.masterKey || Parse.masterKey;
|
||||
|
||||
// Store keys, convert obj to map
|
||||
const keyPairs = config.keyPairs || {};
|
||||
@@ -38,14 +42,20 @@ class ParseLiveQueryServer {
|
||||
|
||||
// Initialize Parse
|
||||
Parse.Object.disableSingleInstance();
|
||||
|
||||
const serverURL = config.serverURL || Parse.serverURL;
|
||||
Parse.serverURL = serverURL;
|
||||
const appId = config.appId || Parse.applicationId;
|
||||
const javascriptKey = Parse.javaScriptKey;
|
||||
const masterKey = config.masterKey || Parse.masterKey;
|
||||
Parse.initialize(appId, javascriptKey, masterKey);
|
||||
Parse.initialize(config.appId, Parse.javaScriptKey, config.masterKey);
|
||||
|
||||
// The cache controller is a proper cache controller
|
||||
// with access to User and Roles
|
||||
this.cacheController = getCacheController(config);
|
||||
|
||||
// This auth cache stores the promises for each auth resolution.
|
||||
// The main benefit is to be able to reuse the same user / session token resolution.
|
||||
this.authCache = new LRU({
|
||||
max: 500, // 500 concurrent
|
||||
maxAge: 60 * 60 * 1000, // 1h
|
||||
});
|
||||
// Initialize websocket server
|
||||
this.parseWebSocketServer = new ParseWebSocketServer(
|
||||
server,
|
||||
@@ -81,9 +91,6 @@ class ParseLiveQueryServer {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize sessionToken cache
|
||||
this.sessionTokenCache = new SessionTokenCache(config.cacheTimeout);
|
||||
}
|
||||
|
||||
// Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes.
|
||||
@@ -111,6 +118,7 @@ class ParseLiveQueryServer {
|
||||
logger.verbose(Parse.applicationId + 'afterDelete is triggered');
|
||||
|
||||
const deletedParseObject = message.currentParseObject.toJSON();
|
||||
const classLevelPermissions = message.classLevelPermissions;
|
||||
const className = deletedParseObject.className;
|
||||
logger.verbose(
|
||||
'ClassName: %j | ObjectId: %s',
|
||||
@@ -141,18 +149,28 @@ class ParseLiveQueryServer {
|
||||
}
|
||||
for (const requestId of requestIds) {
|
||||
const acl = message.currentParseObject.getACL();
|
||||
// Check ACL
|
||||
this._matchesACL(acl, client, requestId).then(
|
||||
isMatched => {
|
||||
// Check CLP
|
||||
const op = this._getCLPOperation(subscription.query);
|
||||
this._matchesCLP(
|
||||
classLevelPermissions,
|
||||
message.currentParseObject,
|
||||
client,
|
||||
requestId,
|
||||
op
|
||||
)
|
||||
.then(() => {
|
||||
// Check ACL
|
||||
return this._matchesACL(acl, client, requestId);
|
||||
})
|
||||
.then(isMatched => {
|
||||
if (!isMatched) {
|
||||
return null;
|
||||
}
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
},
|
||||
error => {
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +185,7 @@ class ParseLiveQueryServer {
|
||||
if (message.originalParseObject) {
|
||||
originalParseObject = message.originalParseObject.toJSON();
|
||||
}
|
||||
const classLevelPermissions = message.classLevelPermissions;
|
||||
const currentParseObject = message.currentParseObject.toJSON();
|
||||
const className = currentParseObject.className;
|
||||
logger.verbose(
|
||||
@@ -227,45 +246,55 @@ class ParseLiveQueryServer {
|
||||
requestId
|
||||
);
|
||||
}
|
||||
const op = this._getCLPOperation(subscription.query);
|
||||
this._matchesCLP(
|
||||
classLevelPermissions,
|
||||
message.currentParseObject,
|
||||
client,
|
||||
requestId,
|
||||
op
|
||||
)
|
||||
.then(() => {
|
||||
return Promise.all([
|
||||
originalACLCheckingPromise,
|
||||
currentACLCheckingPromise,
|
||||
]);
|
||||
})
|
||||
.then(
|
||||
([isOriginalMatched, isCurrentMatched]) => {
|
||||
logger.verbose(
|
||||
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
||||
originalParseObject,
|
||||
currentParseObject,
|
||||
isOriginalSubscriptionMatched,
|
||||
isCurrentSubscriptionMatched,
|
||||
isOriginalMatched,
|
||||
isCurrentMatched,
|
||||
subscription.hash
|
||||
);
|
||||
|
||||
Promise.all([
|
||||
originalACLCheckingPromise,
|
||||
currentACLCheckingPromise,
|
||||
]).then(
|
||||
([isOriginalMatched, isCurrentMatched]) => {
|
||||
logger.verbose(
|
||||
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
||||
originalParseObject,
|
||||
currentParseObject,
|
||||
isOriginalSubscriptionMatched,
|
||||
isCurrentSubscriptionMatched,
|
||||
isOriginalMatched,
|
||||
isCurrentMatched,
|
||||
subscription.hash
|
||||
);
|
||||
|
||||
// Decide event type
|
||||
let type;
|
||||
if (isOriginalMatched && isCurrentMatched) {
|
||||
type = 'Update';
|
||||
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||
type = 'Leave';
|
||||
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||
if (originalParseObject) {
|
||||
type = 'Enter';
|
||||
// Decide event type
|
||||
let type;
|
||||
if (isOriginalMatched && isCurrentMatched) {
|
||||
type = 'Update';
|
||||
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||
type = 'Leave';
|
||||
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||
if (originalParseObject) {
|
||||
type = 'Enter';
|
||||
} else {
|
||||
type = 'Create';
|
||||
}
|
||||
} else {
|
||||
type = 'Create';
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
const functionName = 'push' + type;
|
||||
client[functionName](requestId, currentParseObject);
|
||||
},
|
||||
error => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
}
|
||||
const functionName = 'push' + type;
|
||||
client[functionName](requestId, currentParseObject);
|
||||
},
|
||||
error => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
}
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,98 +403,149 @@ class ParseLiveQueryServer {
|
||||
return matchesQuery(parseObject, subscription.query);
|
||||
}
|
||||
|
||||
_matchesACL(acl: any, client: any, requestId: number): any {
|
||||
getAuthForSessionToken(
|
||||
sessionToken: ?string
|
||||
): Promise<{ auth: ?Auth, userId: ?string }> {
|
||||
if (!sessionToken) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
const fromCache = this.authCache.get(sessionToken);
|
||||
if (fromCache) {
|
||||
return fromCache;
|
||||
}
|
||||
const authPromise = getAuthForSessionToken({
|
||||
cacheController: this.cacheController,
|
||||
sessionToken: sessionToken,
|
||||
})
|
||||
.then(auth => {
|
||||
return { auth, userId: auth && auth.user && auth.user.id };
|
||||
})
|
||||
.catch(() => {
|
||||
// If you can't continue, let's just wrap it up and delete it.
|
||||
// Next time, one will try again
|
||||
this.authCache.del(sessionToken);
|
||||
return {};
|
||||
});
|
||||
this.authCache.set(sessionToken, authPromise);
|
||||
return authPromise;
|
||||
}
|
||||
|
||||
async _matchesCLP(
|
||||
classLevelPermissions: ?any,
|
||||
object: any,
|
||||
client: any,
|
||||
requestId: number,
|
||||
op: string
|
||||
): any {
|
||||
// try to match on user first, less expensive than with roles
|
||||
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
const aclGroup = ['*'];
|
||||
let userId;
|
||||
if (typeof subscriptionInfo !== 'undefined') {
|
||||
const { userId } = await this.getAuthForSessionToken(
|
||||
subscriptionInfo.sessionToken
|
||||
);
|
||||
if (userId) {
|
||||
aclGroup.push(userId);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await SchemaController.validatePermission(
|
||||
classLevelPermissions,
|
||||
object.className,
|
||||
aclGroup,
|
||||
op
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.verbose(`Failed matching CLP for ${object.id} ${userId} ${e}`);
|
||||
return false;
|
||||
}
|
||||
// TODO: handle roles permissions
|
||||
// Object.keys(classLevelPermissions).forEach((key) => {
|
||||
// const perm = classLevelPermissions[key];
|
||||
// Object.keys(perm).forEach((key) => {
|
||||
// if (key.indexOf('role'))
|
||||
// });
|
||||
// })
|
||||
// // it's rejected here, check the roles
|
||||
// var rolesQuery = new Parse.Query(Parse.Role);
|
||||
// rolesQuery.equalTo("users", user);
|
||||
// return rolesQuery.find({useMasterKey:true});
|
||||
}
|
||||
|
||||
_getCLPOperation(query: any) {
|
||||
return typeof query === 'object' &&
|
||||
Object.keys(query).length == 1 &&
|
||||
typeof query.objectId === 'string'
|
||||
? 'get'
|
||||
: 'find';
|
||||
}
|
||||
|
||||
async _matchesACL(
|
||||
acl: any,
|
||||
client: any,
|
||||
requestId: number
|
||||
): Promise<boolean> {
|
||||
// Return true directly if ACL isn't present, ACL is public read, or client has master key
|
||||
if (!acl || acl.getPublicReadAccess() || client.hasMasterKey) {
|
||||
return Promise.resolve(true);
|
||||
return true;
|
||||
}
|
||||
// Check subscription sessionToken matches ACL first
|
||||
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
if (typeof subscriptionInfo === 'undefined') {
|
||||
return Promise.resolve(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const subscriptionSessionToken = subscriptionInfo.sessionToken;
|
||||
return this.sessionTokenCache
|
||||
.getUserId(subscriptionSessionToken)
|
||||
.then(userId => {
|
||||
return acl.getReadAccess(userId);
|
||||
})
|
||||
.then(isSubscriptionSessionTokenMatched => {
|
||||
if (isSubscriptionSessionTokenMatched) {
|
||||
return Promise.resolve(true);
|
||||
// TODO: get auth there and de-duplicate code below to work with the same Auth obj.
|
||||
const { auth, userId } = await this.getAuthForSessionToken(
|
||||
subscriptionInfo.sessionToken
|
||||
);
|
||||
const isSubscriptionSessionTokenMatched = acl.getReadAccess(userId);
|
||||
if (isSubscriptionSessionTokenMatched) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the user has any roles that match the ACL
|
||||
return Promise.resolve()
|
||||
.then(async () => {
|
||||
// Resolve false right away if the acl doesn't have any roles
|
||||
const acl_has_roles = Object.keys(acl.permissionsById).some(key =>
|
||||
key.startsWith('role:')
|
||||
);
|
||||
if (!acl_has_roles) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the user has any roles that match the ACL
|
||||
return new Promise((resolve, reject) => {
|
||||
// Resolve false right away if the acl doesn't have any roles
|
||||
const acl_has_roles = Object.keys(acl.permissionsById).some(key =>
|
||||
key.startsWith('role:')
|
||||
);
|
||||
if (!acl_has_roles) {
|
||||
return resolve(false);
|
||||
const roleNames = await auth.getUserRoles();
|
||||
// Finally, see if any of the user's roles allow them read access
|
||||
for (const role of roleNames) {
|
||||
// We use getReadAccess as `role` is in the form `role:roleName`
|
||||
if (acl.getReadAccess(role)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
this.sessionTokenCache
|
||||
.getUserId(subscriptionSessionToken)
|
||||
.then(userId => {
|
||||
// Pass along a null if there is no user id
|
||||
if (!userId) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Prepare a user object to query for roles
|
||||
// To eliminate a query for the user, create one locally with the id
|
||||
var user = new Parse.User();
|
||||
user.id = userId;
|
||||
return user;
|
||||
})
|
||||
.then(user => {
|
||||
// Pass along an empty array (of roles) if no user
|
||||
if (!user) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// Then get the user's roles
|
||||
var rolesQuery = new Parse.Query(Parse.Role);
|
||||
rolesQuery.equalTo('users', user);
|
||||
return rolesQuery.find({ useMasterKey: true });
|
||||
})
|
||||
.then(roles => {
|
||||
// Finally, see if any of the user's roles allow them read access
|
||||
for (const role of roles) {
|
||||
if (acl.getRoleReadAccess(role)) {
|
||||
return resolve(true);
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.then(isRoleMatched => {
|
||||
.then(async isRoleMatched => {
|
||||
if (isRoleMatched) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// Check client sessionToken matches ACL
|
||||
const clientSessionToken = client.sessionToken;
|
||||
return this.sessionTokenCache
|
||||
.getUserId(clientSessionToken)
|
||||
.then(userId => {
|
||||
return acl.getReadAccess(userId);
|
||||
});
|
||||
})
|
||||
.then(
|
||||
isMatched => {
|
||||
return Promise.resolve(isMatched);
|
||||
},
|
||||
() => {
|
||||
return Promise.resolve(false);
|
||||
if (clientSessionToken) {
|
||||
const { userId } = await this.getAuthForSessionToken(
|
||||
clientSessionToken
|
||||
);
|
||||
return acl.getReadAccess(userId);
|
||||
} else {
|
||||
return isRoleMatched;
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
_handleConnect(parseWebsocket: any, request: any): any {
|
||||
|
||||
@@ -1440,12 +1440,18 @@ RestWrite.prototype.runAfterTrigger = function() {
|
||||
this.response.status || 200
|
||||
);
|
||||
|
||||
// Notifiy LiveQueryServer if possible
|
||||
this.config.liveQueryController.onAfterSave(
|
||||
updatedObject.className,
|
||||
updatedObject,
|
||||
originalObject
|
||||
);
|
||||
this.config.database.loadSchema().then(schemaController => {
|
||||
// Notifiy LiveQueryServer if possible
|
||||
const perms = schemaController.getClassLevelPermissions(
|
||||
updatedObject.className
|
||||
);
|
||||
this.config.liveQueryController.onAfterSave(
|
||||
updatedObject.className,
|
||||
updatedObject,
|
||||
originalObject,
|
||||
perms
|
||||
);
|
||||
});
|
||||
|
||||
// Run afterSave trigger
|
||||
return triggers
|
||||
|
||||
@@ -361,7 +361,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
// be used to enumerate valid emails
|
||||
return Promise.resolve({
|
||||
response: {},
|
||||
})
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
14
src/rest.js
14
src/rest.js
@@ -101,7 +101,7 @@ function del(config, auth, className, objectId) {
|
||||
|
||||
enforceRoleSecurity('delete', className, auth);
|
||||
|
||||
var inflatedObject;
|
||||
let inflatedObject;
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
@@ -113,7 +113,7 @@ function del(config, auth, className, objectId) {
|
||||
if (hasTriggers || hasLiveQuery || className == '_Session') {
|
||||
return new RestQuery(config, auth, className, { objectId })
|
||||
.forWrite()
|
||||
.execute()
|
||||
.execute({ op: 'delete' })
|
||||
.then(response => {
|
||||
if (response && response.results && response.results.length) {
|
||||
const firstResult = response.results[0];
|
||||
@@ -172,7 +172,15 @@ function del(config, auth, className, objectId) {
|
||||
})
|
||||
.then(() => {
|
||||
// Notify LiveQuery server if possible
|
||||
config.liveQueryController.onAfterDelete(className, inflatedObject);
|
||||
config.database.loadSchema().then(schemaController => {
|
||||
const perms = schemaController.getClassLevelPermissions(className);
|
||||
config.liveQueryController.onAfterDelete(
|
||||
className,
|
||||
inflatedObject,
|
||||
null,
|
||||
perms
|
||||
);
|
||||
});
|
||||
return triggers.maybeRunTrigger(
|
||||
triggers.Types.afterDelete,
|
||||
auth,
|
||||
|
||||
@@ -269,7 +269,7 @@ export function getResponseObject(request, resolve, reject) {
|
||||
if (error instanceof Parse.Error) {
|
||||
reject(error);
|
||||
} else if (error instanceof Error) {
|
||||
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error.message))
|
||||
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error.message));
|
||||
} else {
|
||||
reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user