Validation Handler Update (#6968)

* Initial Commit

* Update FunctionsRouter.js

* Update FunctionsRouter.js

* Change params to fields

* Changes requested

* Fix failing tests

* More tests

* More tests

* Remove existing functionality

* Remove legacy tests

* fix array typo

* Update triggers.js

* Docs

* Allow requireUserKeys to be object

* validateMasterKey

* Improve documentation

Co-authored-by: Diamond Lewis <findlewis@gmail.com>
This commit is contained in:
dblythy
2020-10-26 04:36:54 +11:00
committed by GitHub
parent e89cf25bc2
commit c2f2281e6d
8 changed files with 1752 additions and 176 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -113,6 +113,21 @@ describe('Cloud Code', () => {
);
});
it('returns an empty error', done => {
Parse.Cloud.define('cloudCodeWithError', () => {
throw null;
});
Parse.Cloud.run('cloudCodeWithError').then(
() => done.fail('should not succeed'),
e => {
expect(e.code).toEqual(141);
expect(e.message).toEqual('Script failed.');
done();
}
);
});
it('beforeSave rejection with custom error code', function (done) {
Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () {
throw new Parse.Error(999, 'Nope');
@@ -2675,6 +2690,34 @@ describe('beforeLogin hook', () => {
expect(result).toBe(file);
});
it('throw custom error from beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail');
});
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
fail('error should have thrown');
} catch (e) {
expect(e.code).toBe(Parse.Error.SCRIPT_FAILED);
done();
}
});
it('throw empty error from beforeSaveFile', async done => {
Parse.Cloud.beforeSaveFile(() => {
throw null;
});
try {
const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain');
await file.save({ useMasterKey: true });
fail('error should have thrown');
} catch (e) {
expect(e.code).toBe(130);
done();
}
});
it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();

View File

@@ -24,7 +24,7 @@ const headers = {
};
describe_only_db('mongo')('miscellaneous', () => {
it('test rest_create_app', function(done) {
it('test rest_create_app', function (done) {
let appId;
Parse._request('POST', 'rest_create_app')
.then(res => {
@@ -57,19 +57,19 @@ describe_only_db('mongo')('miscellaneous', () => {
});
});
describe('miscellaneous', function() {
it('create a GameScore object', function(done) {
describe('miscellaneous', function () {
it('create a GameScore object', function (done) {
const obj = new Parse.Object('GameScore');
obj.set('score', 1337);
obj.save().then(function(obj) {
obj.save().then(function (obj) {
expect(typeof obj.id).toBe('string');
expect(typeof obj.createdAt.toGMTString()).toBe('string');
done();
}, done.fail);
});
it('get a TestObject', function(done) {
create({ bloop: 'blarg' }, async function(obj) {
it('get a TestObject', function (done) {
create({ bloop: 'blarg' }, async function (obj) {
const t2 = new TestObject({ objectId: obj.id });
const obj2 = await t2.fetch();
expect(obj2.get('bloop')).toEqual('blarg');
@@ -79,8 +79,8 @@ describe('miscellaneous', function() {
});
});
it('create a valid parse user', function(done) {
createTestUser().then(function(data) {
it('create a valid parse user', function (done) {
createTestUser().then(function (data) {
expect(data.id).not.toBeUndefined();
expect(data.getSessionToken()).not.toBeUndefined();
expect(data.get('password')).toBeUndefined();
@@ -297,8 +297,8 @@ describe('miscellaneous', function() {
});
});
it('succeed in logging in', function(done) {
createTestUser().then(async function(u) {
it('succeed in logging in', function (done) {
createTestUser().then(async function (u) {
expect(typeof u.id).toEqual('string');
const user = await Parse.User.logIn('test', 'moon-y');
@@ -310,7 +310,7 @@ describe('miscellaneous', function() {
}, fail);
});
it('increment with a user object', function(done) {
it('increment with a user object', function (done) {
createTestUser()
.then(user => {
user.increment('foo');
@@ -338,7 +338,7 @@ describe('miscellaneous', function() {
);
});
it('save various data types', function(done) {
it('save various data types', function (done) {
const obj = new TestObject();
obj.set('date', new Date());
obj.set('array', [1, 2, 3]);
@@ -358,7 +358,7 @@ describe('miscellaneous', function() {
});
});
it('query with limit', function(done) {
it('query with limit', function (done) {
const baz = new TestObject({ foo: 'baz' });
const qux = new TestObject({ foo: 'qux' });
baz
@@ -383,7 +383,7 @@ describe('miscellaneous', function() {
);
});
it('query without limit get default 100 records', function(done) {
it('query without limit get default 100 records', function (done) {
const objects = [];
for (let i = 0; i < 150; i++) {
objects.push(new TestObject({ name: 'name' + i }));
@@ -404,7 +404,7 @@ describe('miscellaneous', function() {
);
});
it('basic saveAll', function(done) {
it('basic saveAll', function (done) {
const alpha = new TestObject({ letter: 'alpha' });
const beta = new TestObject({ letter: 'beta' });
Parse.Object.saveAll([alpha, beta])
@@ -425,26 +425,26 @@ describe('miscellaneous', function() {
);
});
it('test beforeSave set object acl success', function(done) {
it('test beforeSave set object acl success', function (done) {
const acl = new Parse.ACL({
'*': { read: true, write: false },
});
Parse.Cloud.beforeSave('BeforeSaveAddACL', function(req) {
Parse.Cloud.beforeSave('BeforeSaveAddACL', function (req) {
req.object.setACL(acl);
});
const obj = new Parse.Object('BeforeSaveAddACL');
obj.set('lol', true);
obj.save().then(
function() {
function () {
const query = new Parse.Query('BeforeSaveAddACL');
query.get(obj.id).then(
function(objAgain) {
function (objAgain) {
expect(objAgain.get('lol')).toBeTruthy();
expect(objAgain.getACL().equals(acl));
done();
},
function(error) {
function (error) {
fail(error);
done();
}
@@ -667,10 +667,10 @@ describe('miscellaneous', function() {
});
});
it('test afterSave get full object on create and update', function(done) {
it('test afterSave get full object on create and update', function (done) {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req) {
Parse.Cloud.afterSave('GameScore', function (req) {
const object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.id).not.toBeUndefined();
@@ -694,29 +694,29 @@ describe('miscellaneous', function() {
obj.set('fooAgain', 'barAgain');
obj
.save()
.then(function() {
.then(function () {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
})
.then(
function() {
function () {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
},
function(error) {
function (error) {
fail(error);
done();
}
);
});
it('test afterSave get original object on update', function(done) {
it('test afterSave get original object on update', function (done) {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req) {
Parse.Cloud.afterSave('GameScore', function (req) {
const object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
expect(object.get('fooAgain')).toEqual('barAgain');
@@ -750,18 +750,18 @@ describe('miscellaneous', function() {
obj.set('fooAgain', 'barAgain');
obj
.save()
.then(function() {
.then(function () {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
})
.then(
function() {
function () {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
},
function(error) {
function (error) {
jfail(error);
done();
}
@@ -771,7 +771,7 @@ describe('miscellaneous', function() {
it('test afterSave get full original object even req auth can not query it', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req) {
Parse.Cloud.afterSave('GameScore', function (req) {
const object = req.object;
const originalObject = req.original;
if (triggerTime == 0) {
@@ -802,18 +802,18 @@ describe('miscellaneous', function() {
obj.setACL(acl);
obj
.save()
.then(function() {
.then(function () {
// We only update foo
obj.set('foo', 'baz');
return obj.save();
})
.then(
function() {
function () {
// Make sure the checking has been triggered
expect(triggerTime).toBe(2);
done();
},
function(error) {
function (error) {
jfail(error);
done();
}
@@ -823,7 +823,7 @@ describe('miscellaneous', function() {
it('afterSave flattens custom operations', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req) {
Parse.Cloud.afterSave('GameScore', function (req) {
const object = req.object;
expect(object instanceof Parse.Object).toBeTruthy();
const originalObject = req.original;
@@ -865,7 +865,7 @@ describe('miscellaneous', function() {
it('beforeSave receives ACL', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.beforeSave('GameScore', function(req) {
Parse.Cloud.beforeSave('GameScore', function (req) {
const object = req.object;
if (triggerTime == 0) {
const acl = object.getACL();
@@ -909,7 +909,7 @@ describe('miscellaneous', function() {
it('afterSave receives ACL', done => {
let triggerTime = 0;
// Register a mock beforeSave hook
Parse.Cloud.afterSave('GameScore', function(req) {
Parse.Cloud.afterSave('GameScore', function (req) {
const object = req.object;
if (triggerTime == 0) {
const acl = object.getACL();
@@ -1057,14 +1057,14 @@ describe('miscellaneous', function() {
);
});
it('test beforeSave/afterSave get installationId', function(done) {
it('test beforeSave/afterSave get installationId', function (done) {
let triggerTime = 0;
Parse.Cloud.beforeSave('GameScore', function(req) {
Parse.Cloud.beforeSave('GameScore', function (req) {
triggerTime++;
expect(triggerTime).toEqual(1);
expect(req.installationId).toEqual('yolo');
});
Parse.Cloud.afterSave('GameScore', function(req) {
Parse.Cloud.afterSave('GameScore', function (req) {
triggerTime++;
expect(triggerTime).toEqual(2);
expect(req.installationId).toEqual('yolo');
@@ -1087,14 +1087,14 @@ describe('miscellaneous', function() {
});
});
it('test beforeDelete/afterDelete get installationId', function(done) {
it('test beforeDelete/afterDelete get installationId', function (done) {
let triggerTime = 0;
Parse.Cloud.beforeDelete('GameScore', function(req) {
Parse.Cloud.beforeDelete('GameScore', function (req) {
triggerTime++;
expect(triggerTime).toEqual(1);
expect(req.installationId).toEqual('yolo');
});
Parse.Cloud.afterDelete('GameScore', function(req) {
Parse.Cloud.afterDelete('GameScore', function (req) {
triggerTime++;
expect(triggerTime).toEqual(2);
expect(req.installationId).toEqual('yolo');
@@ -1170,33 +1170,6 @@ describe('miscellaneous', function() {
});
});
it('test cloud function parameter validation', done => {
// Register a function with validation
Parse.Cloud.define(
'functionWithParameterValidationFailure',
() => {
return 'noway';
},
request => {
return request.params.success === 100;
}
);
Parse.Cloud.run('functionWithParameterValidationFailure', {
success: 500,
}).then(
() => {
fail('Validation should not have succeeded');
done();
},
e => {
expect(e.code).toEqual(142);
expect(e.message).toEqual('Validation failed.');
done();
}
);
});
it('can handle null params in cloud functions (regression test for #1742)', done => {
Parse.Cloud.define('func', request => {
expect(request.params.nullParam).toEqual(null);
@@ -1715,10 +1688,7 @@ describe('miscellaneous', function() {
it('purge empty class', done => {
const testSchema = new Parse.Schema('UnknownClass');
testSchema
.purge()
.then(done)
.catch(done.fail);
testSchema.purge().then(done).catch(done.fail);
});
it('should not update schema beforeSave #2672', done => {

View File

@@ -2,6 +2,10 @@
const UserController = require('../lib/Controllers/UserController')
.UserController;
const Config = require('../lib/Config');
const validatorFail = () => {
throw 'you are not authorized';
};
describe('ParseLiveQuery', function () {
it('can subscribe to query', async done => {
await reconfigureServer({
@@ -231,6 +235,7 @@ describe('ParseLiveQuery', function () {
object.set({ foo: 'bar' });
await object.save();
});
it('can handle afterEvent throw', async done => {
await reconfigureServer({
liveQuery: {
@@ -300,6 +305,7 @@ describe('ParseLiveQuery', function () {
object.set({ foo: 'bar' });
await object.save();
});
it('expect afterEvent create', async done => {
await reconfigureServer({
liveQuery: {
@@ -551,6 +557,79 @@ describe('ParseLiveQuery', function () {
await object.save();
});
it('can handle beforeConnect validation function', async done => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const object = new TestObject();
await object.save();
Parse.Cloud.beforeConnect(() => {}, validatorFail);
let complete = false;
Parse.LiveQuery.on('error', error => {
if (complete) {
return;
}
complete = true;
expect(error).toBe('you are not authorized');
done();
});
const query = new Parse.Query(TestObject);
query.equalTo('objectId', object.id);
await query.subscribe();
});
it('can handle beforeSubscribe validation function', async done => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
const object = new TestObject();
await object.save();
Parse.Cloud.beforeSubscribe(TestObject, () => {}, validatorFail);
const query = new Parse.Query(TestObject);
query.equalTo('objectId', object.id);
const subscription = await query.subscribe();
subscription.on('error', error => {
expect(error).toBe('you are not authorized');
done();
});
});
it('can handle afterEvent validation function', async done => {
await reconfigureServer({
liveQuery: {
classNames: ['TestObject'],
},
startLiveQueryServer: true,
verbose: false,
silent: true,
});
Parse.Cloud.afterLiveQueryEvent('TestObject', () => {}, validatorFail);
const query = new Parse.Query(TestObject);
const subscription = await query.subscribe();
subscription.on('error', error => {
expect(error).toBe('you are not authorized');
done();
});
const object = new TestObject();
object.set('foo', 'bar');
await object.save();
});
it('can handle beforeConnect error', async done => {
await reconfigureServer({
liveQuery: {

View File

@@ -33,15 +33,6 @@ const addFileDataIfNeeded = async file => {
return file;
};
const errorMessageFromError = e => {
if (typeof e === 'string') {
return e;
} else if (e && e.message) {
return e.message;
}
return undefined;
};
export class FilesRouter {
expressRouter({ maxUploadSize = '20Mb' } = {}) {
var router = express.Router();
@@ -192,10 +183,11 @@ export class FilesRouter {
res.json(saveResult);
} catch (e) {
logger.error('Error creating a file: ', e);
const errorMessage =
errorMessageFromError(e) ||
`Could not store file: ${fileObject.file._name}.`;
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, errorMessage));
const error = triggers.resolveError(e, {
code: Parse.Error.FILE_SAVE_ERROR,
message: `Could not store file: ${fileObject.file._name}.`,
});
next(error);
}
}
@@ -227,8 +219,11 @@ export class FilesRouter {
res.end();
} catch (e) {
logger.error('Error deleting a file: ', e);
const errorMessage = errorMessageFromError(e) || `Could not delete file.`;
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, errorMessage));
const error = triggers.resolveError(e, {
code: Parse.Error.FILE_DELETE_ERROR,
message: 'Could not delete file.',
});
next(error);
}
}

View File

@@ -109,37 +109,17 @@ export class FunctionsRouter extends PromiseRouter {
});
},
error: function (message) {
// parse error, process away
if (message instanceof Parse.Error) {
return reject(message);
}
const code = Parse.Error.SCRIPT_FAILED;
// If it's an error, mark it as a script failed
if (typeof message === 'string') {
return reject(new Parse.Error(code, message));
}
const error = new Parse.Error(
code,
(message && message.message) || message
);
if (message instanceof Error) {
error.stack = message.stack;
}
const error = triggers.resolveError(message);
reject(error);
},
message: message,
};
}
static handleCloudFunction(req) {
const functionName = req.params.functionName;
const applicationId = req.config.applicationId;
const theFunction = triggers.getFunction(functionName, applicationId);
const theValidator = triggers.getValidator(
req.params.functionName,
applicationId
);
if (!theFunction) {
throw new Parse.Error(
Parse.Error.SCRIPT_FAILED,
@@ -160,16 +140,6 @@ export class FunctionsRouter extends PromiseRouter {
context: req.info.context,
};
if (theValidator && typeof theValidator === 'function') {
var result = theValidator(request);
if (!result) {
throw new Parse.Error(
Parse.Error.VALIDATION_ERROR,
'Validation failed.'
);
}
}
return new Promise(function (resolve, reject) {
const userString =
req.auth && req.auth.user ? req.auth.user.id : undefined;
@@ -212,6 +182,9 @@ export class FunctionsRouter extends PromiseRouter {
}
);
return Promise.resolve()
.then(() => {
return triggers.maybeRunValidator(request, functionName);
})
.then(() => {
return theFunction(request, { message });
})

View File

@@ -32,11 +32,24 @@ var ParseCloud = {};
* Defines a Cloud Function.
*
* **Available in Cloud Code only.**
*
* ```
* Parse.Cloud.define('functionName', (request) => {
* // code here
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.define('functionName', (request) => {
* // code here
* }, { ...validationObject });
* ```
*
* @static
* @memberof Parse.Cloud
* @param {String} name The name of the Cloud Function
* @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}.
* @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.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.define = function (functionName, handler, validationHandler) {
triggers.addFunction(
@@ -73,25 +86,29 @@ ParseCloud.job = function (functionName, handler) {
* ```
* Parse.Cloud.beforeSave('MyCustomClass', (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeSave(Parse.User, (request) => {
* // code here
* })
* }, { ...validationObject })
* ```
*
* @method beforeSave
* @name Parse.Cloud.beforeSave
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run before a save. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest};
* @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) {
ParseCloud.beforeSave = function (parseClass, handler, validationHandler) {
var className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.beforeSave,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -104,25 +121,29 @@ ParseCloud.beforeSave = function (parseClass, handler) {
* ```
* Parse.Cloud.beforeDelete('MyCustomClass', (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeDelete(Parse.User, (request) => {
* // code here
* })
* }, { ...validationObject })
*```
*
* @method beforeDelete
* @name Parse.Cloud.beforeDelete
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before delete function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run before a delete. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}.
* @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) {
ParseCloud.beforeDelete = function (parseClass, handler, validationHandler) {
var className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.beforeDelete,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -177,8 +198,7 @@ ParseCloud.beforeLogin = function (handler) {
* ```
* Parse.Cloud.afterLogin((request) => {
* // code here
* })
*
* });
* ```
*
* @method afterLogin
@@ -212,8 +232,7 @@ ParseCloud.afterLogin = function (handler) {
* ```
* Parse.Cloud.afterLogout((request) => {
* // code here
* })
*
* });
* ```
*
* @method afterLogout
@@ -246,25 +265,29 @@ ParseCloud.afterLogout = function (handler) {
* ```
* Parse.Cloud.afterSave('MyCustomClass', async function(request) {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterSave(Parse.User, async function(request) {
* // code here
* })
* }, { ...validationObject });
* ```
*
* @method afterSave
* @name Parse.Cloud.afterSave
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after save function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run after a save. This function can be an async function and should take just one parameter, {@link Parse.Cloud.TriggerRequest}.
* @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) {
ParseCloud.afterSave = function (parseClass, handler, validationHandler) {
var className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.afterSave,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -277,25 +300,29 @@ ParseCloud.afterSave = function (parseClass, handler) {
* ```
* Parse.Cloud.afterDelete('MyCustomClass', async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterDelete(Parse.User, async (request) => {
* // code here
* })
* }, { ...validationObject });
*```
*
* @method afterDelete
* @name Parse.Cloud.afterDelete
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after delete function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run after a delete. This function can be async and should take just one parameter, {@link Parse.Cloud.TriggerRequest}.
* @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) {
ParseCloud.afterDelete = function (parseClass, handler, validationHandler) {
var className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.afterDelete,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -308,25 +335,29 @@ ParseCloud.afterDelete = function (parseClass, handler) {
* ```
* Parse.Cloud.beforeFind('MyCustomClass', async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeFind(Parse.User, async (request) => {
* // code here
* })
* }, { ...validationObject });
*```
*
* @method beforeFind
* @name Parse.Cloud.beforeFind
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before find function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.BeforeFindRequest}.
* @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) {
ParseCloud.beforeFind = function (parseClass, handler, validationHandler) {
var className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.beforeFind,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -339,25 +370,29 @@ ParseCloud.beforeFind = function (parseClass, handler) {
* ```
* Parse.Cloud.afterFind('MyCustomClass', async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterFind(Parse.User, async (request) => {
* // code here
* })
* }, { ...validationObject });
*```
*
* @method afterFind
* @name Parse.Cloud.afterFind
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after find function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run before a find. This function can be async and should take just one parameter, {@link Parse.Cloud.AfterFindRequest}.
* @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) {
ParseCloud.afterFind = function (parseClass, handler, validationHandler) {
const className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.afterFind,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -369,18 +404,26 @@ ParseCloud.afterFind = function (parseClass, handler) {
* ```
* Parse.Cloud.beforeSaveFile(async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeSaveFile(async (request) => {
* // code here
* }, { ...validationObject });
*```
*
* @method beforeSaveFile
* @name Parse.Cloud.beforeSaveFile
* @param {Function} func The function to run before saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @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.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.beforeSaveFile = function (handler) {
ParseCloud.beforeSaveFile = function (handler, validationHandler) {
triggers.addFileTrigger(
triggers.Types.beforeSaveFile,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -392,18 +435,26 @@ ParseCloud.beforeSaveFile = function (handler) {
* ```
* Parse.Cloud.afterSaveFile(async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterSaveFile(async (request) => {
* // code here
* }, { ...validationObject });
*```
*
* @method afterSaveFile
* @name Parse.Cloud.afterSaveFile
* @param {Function} func The function to run after saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @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.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.afterSaveFile = function (handler) {
ParseCloud.afterSaveFile = function (handler, validationHandler) {
triggers.addFileTrigger(
triggers.Types.afterSaveFile,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -415,18 +466,26 @@ ParseCloud.afterSaveFile = function (handler) {
* ```
* Parse.Cloud.beforeDeleteFile(async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeDeleteFile(async (request) => {
* // code here
* }, { ...validationObject });
*```
*
* @method beforeDeleteFile
* @name Parse.Cloud.beforeDeleteFile
* @param {Function} func The function to run before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @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.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.beforeDeleteFile = function (handler) {
ParseCloud.beforeDeleteFile = function (handler, validationHandler) {
triggers.addFileTrigger(
triggers.Types.beforeDeleteFile,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -438,18 +497,26 @@ ParseCloud.beforeDeleteFile = function (handler) {
* ```
* Parse.Cloud.afterDeleteFile(async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterDeleteFile(async (request) => {
* // code here
* }, { ...validationObject });
*```
*
* @method afterDeleteFile
* @name Parse.Cloud.afterDeleteFile
* @param {Function} func The function to after before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}.
* @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.FileTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.afterDeleteFile = function (handler) {
ParseCloud.afterDeleteFile = function (handler, validationHandler) {
triggers.addFileTrigger(
triggers.Types.afterDeleteFile,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -461,18 +528,26 @@ ParseCloud.afterDeleteFile = function (handler) {
* ```
* Parse.Cloud.beforeConnect(async (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeConnect(async (request) => {
* // code here
* }, { ...validationObject });
*```
*
* @method beforeConnect
* @name Parse.Cloud.beforeConnect
* @param {Function} func The function to before connection is made. This function can be async and should take just one parameter, {@link Parse.Cloud.ConnectTriggerRequest}.
* @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.ConnectTriggerRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.beforeConnect = function (handler) {
ParseCloud.beforeConnect = function (handler, validationHandler) {
triggers.addConnectTrigger(
triggers.Types.beforeConnect,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -485,25 +560,29 @@ ParseCloud.beforeConnect = function (handler) {
* ```
* Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeSubscribe(Parse.User, (request) => {
* // code here
* })
* }, { ...validationObject });
*```
*
* @method beforeSubscribe
* @name Parse.Cloud.beforeSubscribe
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the before subscription function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run before a subscription. This function can be async and should take one parameter, a {@link Parse.Cloud.TriggerRequest}.
* @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.beforeSubscribe = function (parseClass, handler) {
ParseCloud.beforeSubscribe = function (parseClass, handler, validationHandler) {
var className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.beforeSubscribe,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -519,21 +598,33 @@ ParseCloud.onLiveQueryEvent = function (handler) {
* ```
* Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => {
* // code here
* })
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => {
* // code here
* }, { ...validationObject });
*```
*
* @method afterLiveQueryEvent
* @name Parse.Cloud.afterLiveQueryEvent
* @param {(String|Parse.Object)} arg1 The Parse.Object subclass to register the after live query event function for. This can instead be a String that is the className of the subclass.
* @param {Function} func The function to run after a live query event. This function can be async and should take one parameter, a {@link Parse.Cloud.LiveQueryEventTrigger}.
* @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) {
ParseCloud.afterLiveQueryEvent = function (
parseClass,
handler,
validationHandler
) {
const className = getClassName(parseClass);
triggers.addTrigger(
triggers.Types.afterEvent,
className,
handler,
Parse.applicationId
Parse.applicationId,
validationHandler
);
};
@@ -642,3 +733,23 @@ module.exports = ParseCloud;
* @property {Object} params The params passed to the background job.
* @property {function} message If message is called with a string argument, will update the current message to be stored in the job status.
*/
/**
* @interface Parse.Cloud.ValidatorObject
* @property {Boolean} requireUser whether the cloud trigger requires a user.
* @property {Boolean} requireMaster whether the cloud trigger requires a master key.
* @property {Boolean} validateMasterKey whether the validator should run if masterKey is provided. Defaults to false.
*
* @property {Array<String>|Object} requireUserKeys If set, keys required on request.user to make the request.
* @property {String} requireUserKeys.field If requireUserKeys is an object, name of field to validate on request user
* @property {Array|function|Any} requireUserKeys.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid.
* @property {String} requireUserKeys.field.error custom error message if field is invalid.
*
* @property {Object|Array<String>} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, fields to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`.
* @property {String} fields.field name of field to validate.
* @property {String} fields.field.type expected type of data for field.
* @property {Boolean} fields.field.constant whether the field can be modified on the object.
* @property {Any} fields.field.default default value if field is `null`, or initial value `constant` is `true`.
* @property {Array|function|Any} fields.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid.
* @property {String} fields.field.error custom error message if field is invalid.
*/

View File

@@ -25,7 +25,10 @@ const FileClassName = '@File';
const ConnectClassName = '@Connect';
const baseStore = function () {
const Validators = {};
const Validators = Object.keys(Types).reduce(function (base, key) {
base[key] = {};
return base;
}, {});
const Functions = {};
const Jobs = {};
const LiveQuery = [];
@@ -132,17 +135,51 @@ export function addJob(jobName, handler, applicationId) {
add(Category.Jobs, jobName, handler, applicationId);
}
export function addTrigger(type, className, handler, applicationId) {
export function addTrigger(
type,
className,
handler,
applicationId,
validationHandler
) {
validateClassNameForTriggers(className, type);
add(Category.Triggers, `${type}.${className}`, handler, applicationId);
add(
Category.Validators,
`${type}.${className}`,
validationHandler,
applicationId
);
}
export function addFileTrigger(type, handler, applicationId) {
export function addFileTrigger(
type,
handler,
applicationId,
validationHandler
) {
add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId);
add(
Category.Validators,
`${type}.${FileClassName}`,
validationHandler,
applicationId
);
}
export function addConnectTrigger(type, handler, applicationId) {
export function addConnectTrigger(
type,
handler,
applicationId,
validationHandler
) {
add(Category.Triggers, `${type}.${ConnectClassName}`, handler, applicationId);
add(
Category.Validators,
`${type}.${ConnectClassName}`,
validationHandler,
applicationId
);
}
export function addLiveQueryEventHandler(handler, applicationId) {
@@ -455,6 +492,9 @@ export function maybeRunAfterFindTrigger(
return Parse.Object.fromJSON(object);
});
return Promise.resolve()
.then(() => {
return maybeRunValidator(request, `${triggerType}.${className}`);
})
.then(() => {
const response = trigger(request);
if (response && typeof response.then === 'function') {
@@ -514,6 +554,9 @@ export function maybeRunQueryTrigger(
isGet
);
return Promise.resolve()
.then(() => {
return maybeRunValidator(requestObject, `${triggerType}.${className}`);
})
.then(() => {
return trigger(requestObject);
})
@@ -588,6 +631,184 @@ export function maybeRunQueryTrigger(
);
}
export function resolveError(message, defaultOpts) {
if (!defaultOpts) {
defaultOpts = {};
}
if (!message) {
return new Parse.Error(
defaultOpts.code || Parse.Error.SCRIPT_FAILED,
defaultOpts.message || 'Script failed.'
);
}
if (message instanceof Parse.Error) {
return message;
}
const code = defaultOpts.code || Parse.Error.SCRIPT_FAILED;
// If it's an error, mark it as a script failed
if (typeof message === 'string') {
return new Parse.Error(code, message);
}
const error = new Parse.Error(code, message.message || message);
if (message instanceof Error) {
error.stack = message.stack;
}
return error;
}
export function maybeRunValidator(request, functionName) {
const theValidator = getValidator(functionName, Parse.applicationId);
if (!theValidator) {
return;
}
return new Promise((resolve, reject) => {
return Promise.resolve()
.then(() => {
return typeof theValidator === 'object'
? builtInTriggerValidator(theValidator, request)
: theValidator(request);
})
.then(() => {
resolve();
})
.catch(e => {
const error = resolveError(e, {
code: Parse.Error.VALIDATION_ERROR,
message: 'Validation failed.',
});
reject(error);
});
});
}
function builtInTriggerValidator(options, request) {
if (request.master && !options.validateMasterKey) {
return;
}
let reqUser = request.user;
if (
!reqUser &&
request.object &&
request.object.className === '_User' &&
!request.object.existed()
) {
reqUser = request.object;
}
if (options.requireUser && !reqUser) {
throw 'Validation failed. Please login to continue.';
}
if (options.requireMaster && !request.master) {
throw 'Validation failed. Master key is required to complete this request.';
}
let params = request.params || {};
if (request.object) {
params = request.object.toJSON();
}
const requiredParam = key => {
const value = params[key];
if (value == null) {
throw `Validation failed. Please specify data for ${key}.`;
}
};
const validateOptions = (opt, key, val) => {
let opts = opt.options;
if (typeof opts === 'function') {
try {
const result = opts(val);
if (!result && result != null) {
throw opt.error || `Validation failed. Invalid value for ${key}.`;
}
} catch (e) {
if (!e) {
throw opt.error || `Validation failed. Invalid value for ${key}.`;
}
throw opt.error || e.message || e;
}
return;
}
if (!Array.isArray(opts)) {
opts = [opt.options];
}
if (!opts.includes(val)) {
throw (
opt.error ||
`Validation failed. Invalid option for ${key}. Expected: ${opts.join(
', '
)}`
);
}
};
const getType = fn => {
const match = fn && fn.toString().match(/^\s*function (\w+)/);
return (match ? match[1] : '').toLowerCase();
};
if (Array.isArray(options.fields)) {
for (const key of options.fields) {
requiredParam(key);
}
} else {
for (const key in options.fields) {
const opt = options.fields[key];
let val = params[key];
if (typeof opt === 'string') {
requiredParam(opt);
}
if (typeof opt === 'object') {
if (opt.default != null && val == null) {
val = opt.default;
params[key] = val;
if (request.object) {
request.object.set(key, val);
}
}
if (opt.constant && request.object) {
if (request.original) {
request.object.set(key, request.original.get(key));
} else if (opt.default != null) {
request.object.set(key, opt.default);
}
}
if (opt.required) {
requiredParam(key);
}
if (opt.type) {
const type = getType(opt.type);
if (type == 'array' && !Array.isArray(val)) {
throw `Validation failed. Invalid type for ${key}. Expected: array`;
} else if (typeof val !== type) {
throw `Validation failed. Invalid type for ${key}. Expected: ${type}`;
}
}
if (opt.options) {
validateOptions(opt, key, val);
}
}
}
}
const userKeys = options.requireUserKeys || [];
if (Array.isArray(userKeys)) {
for (const key of userKeys) {
if (!reqUser) {
throw 'Please login to make this request.';
}
if (reqUser.get(key) == null) {
throw `Validation failed. Please set data for ${key} on your account.`;
}
}
} else if (typeof userKeys === 'object') {
for (const key in options.requireUserKeys) {
const opt = options.requireUserKeys[key];
if (opt.options) {
validateOptions(opt, key, reqUser.get(key));
}
}
}
}
// To be used as part of the promise chain when saving/deleting an object
// Will resolve successfully if no trigger is configured
// Resolves to an object, empty or containing an object key. A beforeSave
@@ -657,6 +878,12 @@ export function maybeRunTrigger(
// If triggers do not return a promise, they can run async code parallel
// to the RestWrite.execute() call.
return Promise.resolve()
.then(() => {
return maybeRunValidator(
request,
`${triggerType}.${parseObject.className}`
);
})
.then(() => {
const promise = trigger(request);
if (
@@ -755,6 +982,7 @@ export async function maybeRunFileTrigger(
fileObject,
config
);
await maybeRunValidator(request, `${triggerType}.${FileClassName}`);
const result = await fileTrigger(request);
logTriggerSuccessBeforeHook(
triggerType,
@@ -788,6 +1016,7 @@ export async function maybeRunConnectTrigger(triggerType, request) {
return;
}
request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${ConnectClassName}`);
return trigger(request);
}
@@ -804,6 +1033,7 @@ export async function maybeRunSubscribeTrigger(
parseQuery.withJSON(request.query);
request.query = parseQuery;
request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${className}`);
await trigger(request);
const query = request.query.toJSON();
if (query.keys) {
@@ -828,6 +1058,7 @@ export async function maybeRunAfterEventTrigger(
request.original = Parse.Object.fromJSON(request.original);
}
request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${className}`);
return trigger(request);
}