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

View File

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