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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
239
src/triggers.js
239
src/triggers.js
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user