feat: Allow returning objects in Parse.Cloud.beforeFind without invoking database query (#9770)
This commit is contained in:
@@ -202,6 +202,346 @@ describe('Cloud Code', () => {
|
||||
}
|
||||
});
|
||||
|
||||
describe('beforeFind without DB operations', () => {
|
||||
let findSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
const config = Config.get('test');
|
||||
const databaseAdapter = config.database.adapter;
|
||||
findSpy = spyOn(databaseAdapter, 'find').and.callThrough();
|
||||
});
|
||||
|
||||
it('beforeFind can return object without DB operation', async () => {
|
||||
Parse.Cloud.beforeFind('TestObject', () => {
|
||||
return new Parse.Object('TestObject', { foo: 'bar' });
|
||||
});
|
||||
Parse.Cloud.afterFind('TestObject', req => {
|
||||
expect(req.objects).toBeDefined();
|
||||
expect(req.objects[0].get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
const newObj = await new Parse.Query('TestObject').first();
|
||||
expect(newObj.className).toBe('TestObject');
|
||||
expect(newObj.toJSON()).toEqual({ foo: 'bar' });
|
||||
expect(findSpy).not.toHaveBeenCalled();
|
||||
await newObj.save();
|
||||
});
|
||||
|
||||
it('beforeFind can return array of objects without DB operation', async () => {
|
||||
Parse.Cloud.beforeFind('TestObject', () => {
|
||||
return [new Parse.Object('TestObject', { foo: 'bar' })];
|
||||
});
|
||||
Parse.Cloud.afterFind('TestObject', req => {
|
||||
expect(req.objects).toBeDefined();
|
||||
expect(req.objects[0].get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
const newObj = await new Parse.Query('TestObject').first();
|
||||
expect(newObj.className).toBe('TestObject');
|
||||
expect(newObj.toJSON()).toEqual({ foo: 'bar' });
|
||||
expect(findSpy).not.toHaveBeenCalled();
|
||||
await newObj.save();
|
||||
});
|
||||
|
||||
it('beforeFind can return object for get query without DB operation', async () => {
|
||||
Parse.Cloud.beforeFind('TestObject', () => {
|
||||
return [new Parse.Object('TestObject', { foo: 'bar' })];
|
||||
});
|
||||
Parse.Cloud.afterFind('TestObject', req => {
|
||||
expect(req.objects).toBeDefined();
|
||||
expect(req.objects[0].get('foo')).toBe('bar');
|
||||
});
|
||||
|
||||
const testObj = new Parse.Object('TestObject');
|
||||
await testObj.save();
|
||||
findSpy.calls.reset();
|
||||
|
||||
const newObj = await new Parse.Query('TestObject').get(testObj.id);
|
||||
expect(newObj.className).toBe('TestObject');
|
||||
expect(newObj.toJSON()).toEqual({ foo: 'bar' });
|
||||
expect(findSpy).not.toHaveBeenCalled();
|
||||
await newObj.save();
|
||||
});
|
||||
|
||||
it('beforeFind can return empty array without DB operation', async () => {
|
||||
Parse.Cloud.beforeFind('TestObject', () => {
|
||||
return [];
|
||||
});
|
||||
Parse.Cloud.afterFind('TestObject', req => {
|
||||
expect(req.objects.length).toBe(0);
|
||||
});
|
||||
|
||||
const obj = new Parse.Object('TestObject');
|
||||
await obj.save();
|
||||
findSpy.calls.reset();
|
||||
|
||||
const newObj = await new Parse.Query('TestObject').first();
|
||||
expect(newObj).toBeUndefined();
|
||||
expect(findSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('beforeFind security with returned objects', () => {
|
||||
let userA;
|
||||
let userB;
|
||||
let secret;
|
||||
|
||||
beforeEach(async () => {
|
||||
userA = new Parse.User();
|
||||
userA.setUsername('userA_' + Date.now());
|
||||
userA.setPassword('passA');
|
||||
await userA.signUp();
|
||||
|
||||
userB = new Parse.User();
|
||||
userB.setUsername('userB_' + Date.now());
|
||||
userB.setPassword('passB');
|
||||
await userB.signUp();
|
||||
|
||||
// Create an object readable only by userB
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(false);
|
||||
acl.setPublicWriteAccess(false);
|
||||
acl.setReadAccess(userB.id, true);
|
||||
acl.setWriteAccess(userB.id, true);
|
||||
|
||||
secret = new Parse.Object('SecretDoc');
|
||||
secret.set('title', 'top');
|
||||
secret.set('content', 'classified');
|
||||
secret.setACL(acl);
|
||||
await secret.save(null, { sessionToken: userB.getSessionToken() });
|
||||
|
||||
Parse.Cloud.beforeFind('SecretDoc', () => {
|
||||
return [secret];
|
||||
});
|
||||
});
|
||||
|
||||
it('should not expose objects not readable by current user', async () => {
|
||||
const q = new Parse.Query('SecretDoc');
|
||||
const results = await q.find({ sessionToken: userA.getSessionToken() });
|
||||
expect(results.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow authorized user to see their objects', async () => {
|
||||
const q = new Parse.Query('SecretDoc');
|
||||
const results = await q.find({ sessionToken: userB.getSessionToken() });
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].id).toBe(secret.id);
|
||||
expect(results[0].get('title')).toBe('top');
|
||||
expect(results[0].get('content')).toBe('classified');
|
||||
});
|
||||
|
||||
it('should return OBJECT_NOT_FOUND on get() for unauthorized user', async () => {
|
||||
const q = new Parse.Query('SecretDoc');
|
||||
await expectAsync(
|
||||
q.get(secret.id, { sessionToken: userA.getSessionToken() })
|
||||
).toBeRejectedWith(jasmine.objectContaining({ code: Parse.Error.OBJECT_NOT_FOUND }));
|
||||
});
|
||||
|
||||
it('should allow master key to bypass ACL filtering when returning objects', async () => {
|
||||
const q = new Parse.Query('SecretDoc');
|
||||
const results = await q.find({ useMasterKey: true });
|
||||
expect(results.length).toBe(1);
|
||||
expect(results[0].id).toBe(secret.id);
|
||||
});
|
||||
|
||||
it('should apply protectedFields masking after re-filtering', async () => {
|
||||
// Configure protectedFields for SecretMask: mask `secretField` for everyone
|
||||
const protectedFields = { SecretMask: { '*': ['secretField'] } };
|
||||
await reconfigureServer({ protectedFields });
|
||||
|
||||
const user = new Parse.User();
|
||||
user.setUsername('pfUser');
|
||||
user.setPassword('pfPass');
|
||||
await user.signUp();
|
||||
|
||||
// Object is publicly readable but has a protected field
|
||||
const doc = new Parse.Object('SecretMask');
|
||||
doc.set('name', 'visible');
|
||||
doc.set('secretField', 'hiddenValue');
|
||||
await doc.save(null, { useMasterKey: true });
|
||||
|
||||
Parse.Cloud.beforeFind('SecretMask', () => {
|
||||
return [doc];
|
||||
});
|
||||
|
||||
// Query as normal user; after re-filtering, secretField should be removed
|
||||
const res = await new Parse.Query('SecretMask').first({ sessionToken: user.getSessionToken() });
|
||||
expect(res).toBeDefined();
|
||||
expect(res.get('name')).toBe('visible');
|
||||
expect(res.get('secretField')).toBeUndefined();
|
||||
const json = res.toJSON();
|
||||
expect(Object.prototype.hasOwnProperty.call(json, 'secretField')).toBeFalse();
|
||||
});
|
||||
});
|
||||
const { maybeRunAfterFindTrigger } = require('../lib/triggers');
|
||||
|
||||
describe('maybeRunAfterFindTrigger - direct function tests', () => {
|
||||
const testConfig = {
|
||||
applicationId: 'test',
|
||||
logLevels: { triggerBeforeSuccess: 'info', triggerAfter: 'info' },
|
||||
};
|
||||
|
||||
it('should convert Parse.Object instances to JSON when no trigger defined', async () => {
|
||||
const className = 'TestParseObjectDirect_' + Date.now();
|
||||
|
||||
const parseObj1 = new Parse.Object(className);
|
||||
parseObj1.set('name', 'test1');
|
||||
parseObj1.id = 'obj1';
|
||||
|
||||
const parseObj2 = new Parse.Object(className);
|
||||
parseObj2.set('name', 'test2');
|
||||
parseObj2.id = 'obj2';
|
||||
|
||||
const result = await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
[parseObj1, parseObj2],
|
||||
testConfig,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].name).toBe('test1');
|
||||
expect(result[1].name).toBe('test2');
|
||||
});
|
||||
|
||||
it('should handle null/undefined objectsInput when no trigger', async () => {
|
||||
const className = 'TestNullDirect_' + Date.now();
|
||||
|
||||
const resultNull = await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
null,
|
||||
testConfig,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
expect(resultNull).toEqual([]);
|
||||
|
||||
const resultUndefined = await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
undefined,
|
||||
testConfig,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
expect(resultUndefined).toEqual([]);
|
||||
|
||||
const resultEmpty = await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
[],
|
||||
testConfig,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
expect(resultEmpty).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle plain object query with where clause', async () => {
|
||||
const className = 'TestQueryWhereDirect_' + Date.now();
|
||||
let receivedQuery = null;
|
||||
|
||||
Parse.Cloud.afterFind(className, req => {
|
||||
receivedQuery = req.query;
|
||||
return req.objects;
|
||||
});
|
||||
|
||||
const mockObject = { id: 'test123', className: className, name: 'test' };
|
||||
|
||||
const result = await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
[mockObject],
|
||||
testConfig,
|
||||
{ where: { name: 'test' }, limit: 10 },
|
||||
{}
|
||||
);
|
||||
|
||||
expect(receivedQuery).toBeInstanceOf(Parse.Query);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should handle plain object query without where clause', async () => {
|
||||
const className = 'TestQueryNoWhereDirect_' + Date.now();
|
||||
let receivedQuery = null;
|
||||
|
||||
Parse.Cloud.afterFind(className, req => {
|
||||
receivedQuery = req.query;
|
||||
return req.objects;
|
||||
});
|
||||
|
||||
const mockObject = { id: 'test456', className: className, name: 'test' };
|
||||
const pq = new Parse.Query(className).withJSON({ limit: 5, skip: 1 });
|
||||
|
||||
const result = await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
[mockObject],
|
||||
testConfig,
|
||||
pq,
|
||||
{}
|
||||
);
|
||||
|
||||
expect(receivedQuery).toBeInstanceOf(Parse.Query);
|
||||
const qJSON = receivedQuery.toJSON();
|
||||
expect(qJSON.limit).toBe(5);
|
||||
expect(qJSON.skip).toBe(1);
|
||||
expect(qJSON.where).toEqual({});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create default query for invalid query parameter', async () => {
|
||||
const className = 'TestInvalidQueryDirect_' + Date.now();
|
||||
let receivedQuery = null;
|
||||
|
||||
Parse.Cloud.afterFind(className, req => {
|
||||
receivedQuery = req.query;
|
||||
return req.objects;
|
||||
});
|
||||
|
||||
const mockObject = { id: 'test789', className: className, name: 'test' };
|
||||
|
||||
await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
[mockObject],
|
||||
testConfig,
|
||||
'invalid_query_string',
|
||||
{}
|
||||
);
|
||||
|
||||
expect(receivedQuery).toBeInstanceOf(Parse.Query);
|
||||
expect(receivedQuery.className).toBe(className);
|
||||
|
||||
receivedQuery = null;
|
||||
|
||||
await maybeRunAfterFindTrigger(
|
||||
'afterFind',
|
||||
null,
|
||||
className,
|
||||
[mockObject],
|
||||
testConfig,
|
||||
null,
|
||||
{}
|
||||
);
|
||||
|
||||
expect(receivedQuery).toBeInstanceOf(Parse.Query);
|
||||
expect(receivedQuery.className).toBe(className);
|
||||
});
|
||||
});
|
||||
|
||||
it('beforeSave rejection with custom error code', function (done) {
|
||||
Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () {
|
||||
throw new Parse.Error(999, 'Nope');
|
||||
|
||||
@@ -50,6 +50,7 @@ async function RestQuery({
|
||||
if (![RestQuery.Method.find, RestQuery.Method.get].includes(method)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'bad query type');
|
||||
}
|
||||
const isGet = method === RestQuery.Method.get;
|
||||
enforceRoleSecurity(method, className, auth);
|
||||
const result = runBeforeFind
|
||||
? await triggers.maybeRunQueryTrigger(
|
||||
@@ -60,7 +61,7 @@ async function RestQuery({
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
method === RestQuery.Method.get
|
||||
isGet
|
||||
)
|
||||
: Promise.resolve({ restWhere, restOptions });
|
||||
|
||||
@@ -72,7 +73,8 @@ async function RestQuery({
|
||||
result.restOptions || restOptions,
|
||||
clientSDK,
|
||||
runAfterFind,
|
||||
context
|
||||
context,
|
||||
isGet
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,7 +103,8 @@ function _UnsafeRestQuery(
|
||||
restOptions = {},
|
||||
clientSDK,
|
||||
runAfterFind = true,
|
||||
context
|
||||
context,
|
||||
isGet
|
||||
) {
|
||||
this.config = config;
|
||||
this.auth = auth;
|
||||
@@ -113,6 +116,7 @@ function _UnsafeRestQuery(
|
||||
this.response = null;
|
||||
this.findOptions = {};
|
||||
this.context = context || {};
|
||||
this.isGet = isGet;
|
||||
if (!this.auth.isMaster) {
|
||||
if (this.className == '_Session') {
|
||||
if (!this.auth.user) {
|
||||
@@ -914,7 +918,8 @@ _UnsafeRestQuery.prototype.runAfterFindTrigger = function () {
|
||||
this.response.results,
|
||||
this.config,
|
||||
parseQuery,
|
||||
this.context
|
||||
this.context,
|
||||
this.isGet
|
||||
)
|
||||
.then(results => {
|
||||
// Ensure we properly set the className back
|
||||
|
||||
114
src/rest.js
114
src/rest.js
@@ -23,11 +23,91 @@ function checkTriggers(className, config, types) {
|
||||
function checkLiveQuery(className, config) {
|
||||
return config.liveQueryController && config.liveQueryController.hasLiveQuery(className);
|
||||
}
|
||||
async function runFindTriggers(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
options = {}
|
||||
) {
|
||||
const { isGet } = options;
|
||||
|
||||
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
|
||||
// Run beforeFind trigger - may modify query or return objects directly
|
||||
const result = await triggers.maybeRunQueryTrigger(
|
||||
triggers.Types.beforeFind,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
config,
|
||||
auth,
|
||||
context,
|
||||
isGet
|
||||
);
|
||||
|
||||
restWhere = result.restWhere || restWhere;
|
||||
restOptions = result.restOptions || restOptions;
|
||||
|
||||
// Short-circuit path: beforeFind returned objects directly
|
||||
// Security risk: These objects may have been fetched with master privileges
|
||||
if (result?.objects) {
|
||||
const objectsFromBeforeFind = result.objects;
|
||||
|
||||
let objectsForAfterFind = objectsFromBeforeFind;
|
||||
|
||||
// Security check: Re-filter objects if not master to ensure ACL/CLP compliance
|
||||
if (!auth?.isMaster && !auth?.isMaintenance) {
|
||||
const ids = (Array.isArray(objectsFromBeforeFind) ? objectsFromBeforeFind : [objectsFromBeforeFind])
|
||||
.map(o => (o && (o.id || o.objectId)) || null)
|
||||
.filter(Boolean);
|
||||
|
||||
// Objects without IDs are(normally) unsaved objects
|
||||
// For unsaved objects, the ACL security does not apply, so no need to redo the query.
|
||||
// For saved objects, we need to re-query to ensure proper ACL/CLP enforcement
|
||||
if (ids.length > 0) {
|
||||
const refilterWhere = isGet ? { objectId: ids[0] } : { objectId: { $in: ids } };
|
||||
|
||||
// Re-query with proper security: no triggers to avoid infinite loops
|
||||
const refilterQuery = await RestQuery({
|
||||
method: isGet ? RestQuery.Method.get : RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere: refilterWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
runBeforeFind: false,
|
||||
runAfterFind: false,
|
||||
});
|
||||
|
||||
const refiltered = await refilterQuery.execute();
|
||||
objectsForAfterFind = (refiltered && refiltered.results) || [];
|
||||
}
|
||||
}
|
||||
|
||||
// Run afterFind trigger on security-filtered objects
|
||||
const afterFindProcessedObjects = await triggers.maybeRunAfterFindTrigger(
|
||||
triggers.Types.afterFind,
|
||||
auth,
|
||||
className,
|
||||
objectsForAfterFind,
|
||||
config,
|
||||
new Parse.Query(className).withJSON({ where: restWhere, ...restOptions }),
|
||||
context,
|
||||
isGet
|
||||
);
|
||||
|
||||
return {
|
||||
results: afterFindProcessedObjects,
|
||||
};
|
||||
}
|
||||
|
||||
// Normal path: execute database query with modified conditions
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
method: isGet ? RestQuery.Method.get : RestQuery.Method.find,
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
@@ -35,24 +115,40 @@ const find = async (config, auth, className, restWhere, restOptions, clientSDK,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
runBeforeFind: false,
|
||||
});
|
||||
|
||||
return query.execute();
|
||||
}
|
||||
|
||||
// Returns a promise for an object with optional keys 'results' and 'count'.
|
||||
const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => {
|
||||
enforceRoleSecurity('find', className, auth);
|
||||
return runFindTriggers(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
{ isGet: false }
|
||||
);
|
||||
};
|
||||
|
||||
// get is just like find but only queries an objectId.
|
||||
const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => {
|
||||
var restWhere = { objectId };
|
||||
const query = await RestQuery({
|
||||
method: RestQuery.Method.get,
|
||||
enforceRoleSecurity('get', className, auth);
|
||||
return runFindTriggers(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere,
|
||||
{ objectId },
|
||||
restOptions,
|
||||
clientSDK,
|
||||
context,
|
||||
});
|
||||
return query.execute();
|
||||
{ isGet: true }
|
||||
);
|
||||
};
|
||||
|
||||
// Returns a promise that doesn't resolve to any useful value.
|
||||
|
||||
@@ -182,8 +182,11 @@ export function toJSONwithObjects(object, className) {
|
||||
}
|
||||
toJSON[key] = val._toFullJSON();
|
||||
}
|
||||
// Preserve original object's className if no override className is provided
|
||||
if (className) {
|
||||
toJSON.className = className;
|
||||
} else if (object.className && !toJSON.className) {
|
||||
toJSON.className = object.className;
|
||||
}
|
||||
return toJSON;
|
||||
}
|
||||
@@ -257,7 +260,8 @@ export function getRequestObject(
|
||||
parseObject,
|
||||
originalParseObject,
|
||||
config,
|
||||
context
|
||||
context,
|
||||
isGet
|
||||
) {
|
||||
const request = {
|
||||
triggerName: triggerType,
|
||||
@@ -268,6 +272,10 @@ export function getRequestObject(
|
||||
ip: config.ip,
|
||||
};
|
||||
|
||||
if (isGet !== undefined) {
|
||||
request.isGet = !!isGet;
|
||||
}
|
||||
|
||||
if (originalParseObject) {
|
||||
request.original = originalParseObject;
|
||||
}
|
||||
@@ -437,69 +445,93 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, l
|
||||
export function maybeRunAfterFindTrigger(
|
||||
triggerType,
|
||||
auth,
|
||||
className,
|
||||
objects,
|
||||
classNameQuery,
|
||||
objectsInput,
|
||||
config,
|
||||
query,
|
||||
context
|
||||
context,
|
||||
isGet
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const trigger = getTrigger(className, triggerType, config.applicationId);
|
||||
const trigger = getTrigger(classNameQuery, triggerType, config.applicationId);
|
||||
|
||||
if (!trigger) {
|
||||
return resolve();
|
||||
if (objectsInput && objectsInput.length > 0 && objectsInput[0] instanceof Parse.Object) {
|
||||
return resolve(objectsInput.map(obj => toJSONwithObjects(obj)));
|
||||
}
|
||||
return resolve(objectsInput || []);
|
||||
}
|
||||
const request = getRequestObject(triggerType, auth, null, null, config, context);
|
||||
if (query) {
|
||||
|
||||
const request = getRequestObject(triggerType, auth, null, null, config, context, isGet);
|
||||
// Convert query parameter to Parse.Query instance
|
||||
if (query instanceof Parse.Query) {
|
||||
request.query = query;
|
||||
} else if (typeof query === 'object' && query !== null) {
|
||||
const parseQueryInstance = new Parse.Query(classNameQuery);
|
||||
if (query.where) {
|
||||
parseQueryInstance.withJSON(query);
|
||||
}
|
||||
request.query = parseQueryInstance;
|
||||
} else {
|
||||
request.query = new Parse.Query(classNameQuery);
|
||||
}
|
||||
|
||||
const { success, error } = getResponseObject(
|
||||
request,
|
||||
object => {
|
||||
resolve(object);
|
||||
processedObjectsJSON => {
|
||||
resolve(processedObjectsJSON);
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
errorData => {
|
||||
reject(errorData);
|
||||
}
|
||||
);
|
||||
logTriggerSuccessBeforeHook(
|
||||
triggerType,
|
||||
className,
|
||||
'AfterFind',
|
||||
JSON.stringify(objects),
|
||||
classNameQuery,
|
||||
'AfterFind Input (Pre-Transform)',
|
||||
JSON.stringify(
|
||||
objectsInput.map(o => (o instanceof Parse.Object ? o.id + ':' + o.className : o))
|
||||
),
|
||||
auth,
|
||||
config.logLevels.triggerBeforeSuccess
|
||||
);
|
||||
request.objects = objects.map(object => {
|
||||
//setting the class name to transform into parse object
|
||||
object.className = className;
|
||||
return Parse.Object.fromJSON(object);
|
||||
|
||||
// Convert plain objects to Parse.Object instances for trigger
|
||||
request.objects = objectsInput.map(currentObject => {
|
||||
if (currentObject instanceof Parse.Object) {
|
||||
return currentObject;
|
||||
}
|
||||
// Preserve the original className if it exists, otherwise use the query className
|
||||
const originalClassName = currentObject.className || classNameQuery;
|
||||
const tempObjectWithClassName = { ...currentObject, className: originalClassName };
|
||||
return Parse.Object.fromJSON(tempObjectWithClassName);
|
||||
});
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return maybeRunValidator(request, `${triggerType}.${className}`, auth);
|
||||
return maybeRunValidator(request, `${triggerType}.${classNameQuery}`, auth);
|
||||
})
|
||||
.then(() => {
|
||||
if (request.skipWithMasterKey) {
|
||||
return request.objects;
|
||||
}
|
||||
const response = trigger(request);
|
||||
if (response && typeof response.then === 'function') {
|
||||
return response.then(results => {
|
||||
const responseFromTrigger = trigger(request);
|
||||
if (responseFromTrigger && typeof responseFromTrigger.then === 'function') {
|
||||
return responseFromTrigger.then(results => {
|
||||
return results;
|
||||
});
|
||||
}
|
||||
return response;
|
||||
return responseFromTrigger;
|
||||
})
|
||||
.then(success, error);
|
||||
}).then(results => {
|
||||
}).then(resultsAsJSON => {
|
||||
logTriggerAfterHook(
|
||||
triggerType,
|
||||
className,
|
||||
JSON.stringify(results),
|
||||
classNameQuery,
|
||||
JSON.stringify(resultsAsJSON),
|
||||
auth,
|
||||
config.logLevels.triggerAfter
|
||||
);
|
||||
return results;
|
||||
return resultsAsJSON;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -607,9 +639,19 @@ export function maybeRunQueryTrigger(
|
||||
restOptions = restOptions || {};
|
||||
restOptions.subqueryReadPreference = requestObject.subqueryReadPreference;
|
||||
}
|
||||
let objects = undefined;
|
||||
if (result instanceof Parse.Object) {
|
||||
objects = [result];
|
||||
} else if (
|
||||
Array.isArray(result) &&
|
||||
(!result.length || result.every(obj => obj instanceof Parse.Object))
|
||||
) {
|
||||
objects = result;
|
||||
}
|
||||
return {
|
||||
restWhere,
|
||||
restOptions,
|
||||
objects,
|
||||
};
|
||||
},
|
||||
err => {
|
||||
|
||||
Reference in New Issue
Block a user