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:
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