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) { it('beforeSave rejection with custom error code', function (done) {
Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () {
throw new Parse.Error(999, 'Nope'); throw new Parse.Error(999, 'Nope');
@@ -2675,6 +2690,34 @@ describe('beforeLogin hook', () => {
expect(result).toBe(file); 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 () => { it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => {
await reconfigureServer({ filesAdapter: mockAdapter }); await reconfigureServer({ filesAdapter: mockAdapter });
const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough();

View File

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

View File

@@ -2,6 +2,10 @@
const UserController = require('../lib/Controllers/UserController') const UserController = require('../lib/Controllers/UserController')
.UserController; .UserController;
const Config = require('../lib/Config'); const Config = require('../lib/Config');
const validatorFail = () => {
throw 'you are not authorized';
};
describe('ParseLiveQuery', function () { describe('ParseLiveQuery', function () {
it('can subscribe to query', async done => { it('can subscribe to query', async done => {
await reconfigureServer({ await reconfigureServer({
@@ -231,6 +235,7 @@ describe('ParseLiveQuery', function () {
object.set({ foo: 'bar' }); object.set({ foo: 'bar' });
await object.save(); await object.save();
}); });
it('can handle afterEvent throw', async done => { it('can handle afterEvent throw', async done => {
await reconfigureServer({ await reconfigureServer({
liveQuery: { liveQuery: {
@@ -300,6 +305,7 @@ describe('ParseLiveQuery', function () {
object.set({ foo: 'bar' }); object.set({ foo: 'bar' });
await object.save(); await object.save();
}); });
it('expect afterEvent create', async done => { it('expect afterEvent create', async done => {
await reconfigureServer({ await reconfigureServer({
liveQuery: { liveQuery: {
@@ -551,6 +557,79 @@ describe('ParseLiveQuery', function () {
await object.save(); 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 => { it('can handle beforeConnect error', async done => {
await reconfigureServer({ await reconfigureServer({
liveQuery: { liveQuery: {

View File

@@ -33,15 +33,6 @@ const addFileDataIfNeeded = async file => {
return 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 { export class FilesRouter {
expressRouter({ maxUploadSize = '20Mb' } = {}) { expressRouter({ maxUploadSize = '20Mb' } = {}) {
var router = express.Router(); var router = express.Router();
@@ -192,10 +183,11 @@ export class FilesRouter {
res.json(saveResult); res.json(saveResult);
} catch (e) { } catch (e) {
logger.error('Error creating a file: ', e); logger.error('Error creating a file: ', e);
const errorMessage = const error = triggers.resolveError(e, {
errorMessageFromError(e) || code: Parse.Error.FILE_SAVE_ERROR,
`Could not store file: ${fileObject.file._name}.`; message: `Could not store file: ${fileObject.file._name}.`,
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, errorMessage)); });
next(error);
} }
} }
@@ -227,8 +219,11 @@ export class FilesRouter {
res.end(); res.end();
} catch (e) { } catch (e) {
logger.error('Error deleting a file: ', e); logger.error('Error deleting a file: ', e);
const errorMessage = errorMessageFromError(e) || `Could not delete file.`; const error = triggers.resolveError(e, {
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, errorMessage)); 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) { error: function (message) {
// parse error, process away const error = triggers.resolveError(message);
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;
}
reject(error); reject(error);
}, },
message: message, message: message,
}; };
} }
static handleCloudFunction(req) { static handleCloudFunction(req) {
const functionName = req.params.functionName; const functionName = req.params.functionName;
const applicationId = req.config.applicationId; const applicationId = req.config.applicationId;
const theFunction = triggers.getFunction(functionName, applicationId); const theFunction = triggers.getFunction(functionName, applicationId);
const theValidator = triggers.getValidator(
req.params.functionName,
applicationId
);
if (!theFunction) { if (!theFunction) {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.SCRIPT_FAILED, Parse.Error.SCRIPT_FAILED,
@@ -160,16 +140,6 @@ export class FunctionsRouter extends PromiseRouter {
context: req.info.context, 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) { return new Promise(function (resolve, reject) {
const userString = const userString =
req.auth && req.auth.user ? req.auth.user.id : undefined; req.auth && req.auth.user ? req.auth.user.id : undefined;
@@ -212,6 +182,9 @@ export class FunctionsRouter extends PromiseRouter {
} }
); );
return Promise.resolve() return Promise.resolve()
.then(() => {
return triggers.maybeRunValidator(request, functionName);
})
.then(() => { .then(() => {
return theFunction(request, { message }); return theFunction(request, { message });
}) })

View File

@@ -32,11 +32,24 @@ var ParseCloud = {};
* Defines a Cloud Function. * Defines a Cloud Function.
* *
* **Available in Cloud Code only.** * **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 * @static
* @memberof Parse.Cloud * @memberof Parse.Cloud
* @param {String} name The name of the Cloud Function * @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 {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) { ParseCloud.define = function (functionName, handler, validationHandler) {
triggers.addFunction( triggers.addFunction(
@@ -73,25 +86,29 @@ ParseCloud.job = function (functionName, handler) {
* ``` * ```
* Parse.Cloud.beforeSave('MyCustomClass', (request) => { * Parse.Cloud.beforeSave('MyCustomClass', (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.beforeSave(Parse.User, (request) => { * Parse.Cloud.beforeSave(Parse.User, (request) => {
* // code here * // code here
* }) * }, { ...validationObject })
* ``` * ```
* *
* @method beforeSave * @method beforeSave
* @name Parse.Cloud.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 {(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 {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); var className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.beforeSave, triggers.Types.beforeSave,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -104,25 +121,29 @@ ParseCloud.beforeSave = function (parseClass, handler) {
* ``` * ```
* Parse.Cloud.beforeDelete('MyCustomClass', (request) => { * Parse.Cloud.beforeDelete('MyCustomClass', (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.beforeDelete(Parse.User, (request) => { * Parse.Cloud.beforeDelete(Parse.User, (request) => {
* // code here * // code here
* }) * }, { ...validationObject })
*``` *```
* *
* @method beforeDelete * @method beforeDelete
* @name Parse.Cloud.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 {(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 {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); var className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.beforeDelete, triggers.Types.beforeDelete,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -177,8 +198,7 @@ ParseCloud.beforeLogin = function (handler) {
* ``` * ```
* Parse.Cloud.afterLogin((request) => { * Parse.Cloud.afterLogin((request) => {
* // code here * // code here
* }) * });
*
* ``` * ```
* *
* @method afterLogin * @method afterLogin
@@ -212,8 +232,7 @@ ParseCloud.afterLogin = function (handler) {
* ``` * ```
* Parse.Cloud.afterLogout((request) => { * Parse.Cloud.afterLogout((request) => {
* // code here * // code here
* }) * });
*
* ``` * ```
* *
* @method afterLogout * @method afterLogout
@@ -246,25 +265,29 @@ ParseCloud.afterLogout = function (handler) {
* ``` * ```
* Parse.Cloud.afterSave('MyCustomClass', async function(request) { * Parse.Cloud.afterSave('MyCustomClass', async function(request) {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.afterSave(Parse.User, async function(request) { * Parse.Cloud.afterSave(Parse.User, async function(request) {
* // code here * // code here
* }) * }, { ...validationObject });
* ``` * ```
* *
* @method afterSave * @method afterSave
* @name Parse.Cloud.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 {(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 {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); var className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.afterSave, triggers.Types.afterSave,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -277,25 +300,29 @@ ParseCloud.afterSave = function (parseClass, handler) {
* ``` * ```
* Parse.Cloud.afterDelete('MyCustomClass', async (request) => { * Parse.Cloud.afterDelete('MyCustomClass', async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.afterDelete(Parse.User, async (request) => { * Parse.Cloud.afterDelete(Parse.User, async (request) => {
* // code here * // code here
* }) * }, { ...validationObject });
*``` *```
* *
* @method afterDelete * @method afterDelete
* @name Parse.Cloud.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 {(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 {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); var className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.afterDelete, triggers.Types.afterDelete,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -308,25 +335,29 @@ ParseCloud.afterDelete = function (parseClass, handler) {
* ``` * ```
* Parse.Cloud.beforeFind('MyCustomClass', async (request) => { * Parse.Cloud.beforeFind('MyCustomClass', async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.beforeFind(Parse.User, async (request) => { * Parse.Cloud.beforeFind(Parse.User, async (request) => {
* // code here * // code here
* }) * }, { ...validationObject });
*``` *```
* *
* @method beforeFind * @method beforeFind
* @name Parse.Cloud.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 {(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 {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); var className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.beforeFind, triggers.Types.beforeFind,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -339,25 +370,29 @@ ParseCloud.beforeFind = function (parseClass, handler) {
* ``` * ```
* Parse.Cloud.afterFind('MyCustomClass', async (request) => { * Parse.Cloud.afterFind('MyCustomClass', async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.afterFind(Parse.User, async (request) => { * Parse.Cloud.afterFind(Parse.User, async (request) => {
* // code here * // code here
* }) * }, { ...validationObject });
*``` *```
* *
* @method afterFind * @method afterFind
* @name Parse.Cloud.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 {(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 {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); const className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.afterFind, triggers.Types.afterFind,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -369,18 +404,26 @@ ParseCloud.afterFind = function (parseClass, handler) {
* ``` * ```
* Parse.Cloud.beforeSaveFile(async (request) => { * Parse.Cloud.beforeSaveFile(async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeSaveFile(async (request) => {
* // code here
* }, { ...validationObject });
*``` *```
* *
* @method beforeSaveFile * @method beforeSaveFile
* @name Parse.Cloud.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 {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.addFileTrigger(
triggers.Types.beforeSaveFile, triggers.Types.beforeSaveFile,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -392,18 +435,26 @@ ParseCloud.beforeSaveFile = function (handler) {
* ``` * ```
* Parse.Cloud.afterSaveFile(async (request) => { * Parse.Cloud.afterSaveFile(async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterSaveFile(async (request) => {
* // code here
* }, { ...validationObject });
*``` *```
* *
* @method afterSaveFile * @method afterSaveFile
* @name Parse.Cloud.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 {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.addFileTrigger(
triggers.Types.afterSaveFile, triggers.Types.afterSaveFile,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -415,18 +466,26 @@ ParseCloud.afterSaveFile = function (handler) {
* ``` * ```
* Parse.Cloud.beforeDeleteFile(async (request) => { * Parse.Cloud.beforeDeleteFile(async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeDeleteFile(async (request) => {
* // code here
* }, { ...validationObject });
*``` *```
* *
* @method beforeDeleteFile * @method beforeDeleteFile
* @name Parse.Cloud.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 {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.addFileTrigger(
triggers.Types.beforeDeleteFile, triggers.Types.beforeDeleteFile,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -438,18 +497,26 @@ ParseCloud.beforeDeleteFile = function (handler) {
* ``` * ```
* Parse.Cloud.afterDeleteFile(async (request) => { * Parse.Cloud.afterDeleteFile(async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterDeleteFile(async (request) => {
* // code here
* }, { ...validationObject });
*``` *```
* *
* @method afterDeleteFile * @method afterDeleteFile
* @name Parse.Cloud.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 {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.addFileTrigger(
triggers.Types.afterDeleteFile, triggers.Types.afterDeleteFile,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -461,18 +528,26 @@ ParseCloud.afterDeleteFile = function (handler) {
* ``` * ```
* Parse.Cloud.beforeConnect(async (request) => { * Parse.Cloud.beforeConnect(async (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.beforeConnect(async (request) => {
* // code here
* }, { ...validationObject });
*``` *```
* *
* @method beforeConnect * @method beforeConnect
* @name Parse.Cloud.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 {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.addConnectTrigger(
triggers.Types.beforeConnect, triggers.Types.beforeConnect,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -485,25 +560,29 @@ ParseCloud.beforeConnect = function (handler) {
* ``` * ```
* Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => { * Parse.Cloud.beforeSubscribe('MyCustomClass', (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
* *
* Parse.Cloud.beforeSubscribe(Parse.User, (request) => { * Parse.Cloud.beforeSubscribe(Parse.User, (request) => {
* // code here * // code here
* }) * }, { ...validationObject });
*``` *```
* *
* @method beforeSubscribe * @method beforeSubscribe
* @name Parse.Cloud.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 {(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 {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); var className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.beforeSubscribe, triggers.Types.beforeSubscribe,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -519,21 +598,33 @@ ParseCloud.onLiveQueryEvent = function (handler) {
* ``` * ```
* Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => { * Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => {
* // code here * // code here
* }) * }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.afterLiveQueryEvent('MyCustomClass', (request) => {
* // code here
* }, { ...validationObject });
*``` *```
* *
* @method afterLiveQueryEvent * @method afterLiveQueryEvent
* @name Parse.Cloud.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 {(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 {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); const className = getClassName(parseClass);
triggers.addTrigger( triggers.addTrigger(
triggers.Types.afterEvent, triggers.Types.afterEvent,
className, className,
handler, handler,
Parse.applicationId Parse.applicationId,
validationHandler
); );
}; };
@@ -642,3 +733,23 @@ module.exports = ParseCloud;
* @property {Object} params The params passed to the background job. * @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. * @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 ConnectClassName = '@Connect';
const baseStore = function () { const baseStore = function () {
const Validators = {}; const Validators = Object.keys(Types).reduce(function (base, key) {
base[key] = {};
return base;
}, {});
const Functions = {}; const Functions = {};
const Jobs = {}; const Jobs = {};
const LiveQuery = []; const LiveQuery = [];
@@ -132,17 +135,51 @@ export function addJob(jobName, handler, applicationId) {
add(Category.Jobs, 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); validateClassNameForTriggers(className, type);
add(Category.Triggers, `${type}.${className}`, handler, applicationId); 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.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.Triggers, `${type}.${ConnectClassName}`, handler, applicationId);
add(
Category.Validators,
`${type}.${ConnectClassName}`,
validationHandler,
applicationId
);
} }
export function addLiveQueryEventHandler(handler, applicationId) { export function addLiveQueryEventHandler(handler, applicationId) {
@@ -455,6 +492,9 @@ export function maybeRunAfterFindTrigger(
return Parse.Object.fromJSON(object); return Parse.Object.fromJSON(object);
}); });
return Promise.resolve() return Promise.resolve()
.then(() => {
return maybeRunValidator(request, `${triggerType}.${className}`);
})
.then(() => { .then(() => {
const response = trigger(request); const response = trigger(request);
if (response && typeof response.then === 'function') { if (response && typeof response.then === 'function') {
@@ -514,6 +554,9 @@ export function maybeRunQueryTrigger(
isGet isGet
); );
return Promise.resolve() return Promise.resolve()
.then(() => {
return maybeRunValidator(requestObject, `${triggerType}.${className}`);
})
.then(() => { .then(() => {
return trigger(requestObject); 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 // To be used as part of the promise chain when saving/deleting an object
// Will resolve successfully if no trigger is configured // Will resolve successfully if no trigger is configured
// Resolves to an object, empty or containing an object key. A beforeSave // 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 // If triggers do not return a promise, they can run async code parallel
// to the RestWrite.execute() call. // to the RestWrite.execute() call.
return Promise.resolve() return Promise.resolve()
.then(() => {
return maybeRunValidator(
request,
`${triggerType}.${parseObject.className}`
);
})
.then(() => { .then(() => {
const promise = trigger(request); const promise = trigger(request);
if ( if (
@@ -755,6 +982,7 @@ export async function maybeRunFileTrigger(
fileObject, fileObject,
config config
); );
await maybeRunValidator(request, `${triggerType}.${FileClassName}`);
const result = await fileTrigger(request); const result = await fileTrigger(request);
logTriggerSuccessBeforeHook( logTriggerSuccessBeforeHook(
triggerType, triggerType,
@@ -788,6 +1016,7 @@ export async function maybeRunConnectTrigger(triggerType, request) {
return; return;
} }
request.user = await userForSessionToken(request.sessionToken); request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${ConnectClassName}`);
return trigger(request); return trigger(request);
} }
@@ -804,6 +1033,7 @@ export async function maybeRunSubscribeTrigger(
parseQuery.withJSON(request.query); parseQuery.withJSON(request.query);
request.query = parseQuery; request.query = parseQuery;
request.user = await userForSessionToken(request.sessionToken); request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${className}`);
await trigger(request); await trigger(request);
const query = request.query.toJSON(); const query = request.query.toJSON();
if (query.keys) { if (query.keys) {
@@ -828,6 +1058,7 @@ export async function maybeRunAfterEventTrigger(
request.original = Parse.Object.fromJSON(request.original); request.original = Parse.Object.fromJSON(request.original);
} }
request.user = await userForSessionToken(request.sessionToken); request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${className}`);
return trigger(request); return trigger(request);
} }