Skip to content

Commit

Permalink
Merge pull request #59 from formio/FIO-7488
Browse files Browse the repository at this point in the history
FIO 7488: improve error handling
  • Loading branch information
AlexeyNikipelau authored and lane-formio committed May 14, 2024
1 parent dcab19f commit d0e1718
Show file tree
Hide file tree
Showing 21 changed files with 262 additions and 190 deletions.
1 change: 0 additions & 1 deletion src/error/DereferenceError.ts

This file was deleted.

10 changes: 10 additions & 0 deletions src/error/ProcessorError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ProcessorContext } from "types";
export class ProcessorError extends Error {
context: Omit<ProcessorContext<any>, 'scope'>;
constructor(message: string, context: ProcessorContext<any>, processor: string = 'unknown') {
super(message);
this.message = `${message}\nin ${processor} at ${context.path}`;
const { component, path, data, row } = context;
this.context = {component, path, data, row};
}
};
1 change: 0 additions & 1 deletion src/error/ValidatorError.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/error/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './FieldError';
export * from './ValidatorError';
export * from './DereferenceError';
export * from './ProcessorError';
6 changes: 3 additions & 3 deletions src/process/dereference/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DereferenceError } from "error";
import { ProcessorError } from "error";
import {
ProcessorFn,
ProcessorScope,
Expand Down Expand Up @@ -37,7 +37,7 @@ export const dereferenceProcess: ProcessorFn<DereferenceScope> = async (context)
return;
}
if (!config?.database) {
throw new DereferenceError('Cannot dereference resource value without a database config object');
throw new ProcessorError('Cannot dereference resource value without a database config object', context, 'dereference');
}

try {
Expand All @@ -49,7 +49,7 @@ export const dereferenceProcess: ProcessorFn<DereferenceScope> = async (context)
component.components = vmCompatibleComponents;
}
catch (err: any) {
throw new DereferenceError(err.message || err);
throw new ProcessorError(err.message || err, context, 'dereference');
}
}

Expand Down
160 changes: 95 additions & 65 deletions src/process/validation/rules/validateAvailableItems.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import isEmpty from 'lodash/isEmpty';
import { FieldError, ValidatorError } from 'error';
import { FieldError, ProcessorError } from 'error';
import { Evaluator } from 'utils';
import { RadioComponent, SelectComponent, RuleFn, RuleFnSync, ValidationContext } from 'types';
import { isObject, isPromise } from '../util';
Expand Down Expand Up @@ -36,30 +36,36 @@ function mapStaticValues(values: { label: string; value: string }[]) {
return values.map((obj) => obj.value);
}

async function getAvailableSelectValues(component: SelectComponent) {
async function getAvailableSelectValues(component: SelectComponent, context: ValidationContext) {
switch (component.dataSrc) {
case 'values':
if (Array.isArray(component.data.values)) {
return mapStaticValues(component.data.values);
}
throw new ValidatorError(
throw new ProcessorError(
`Failed to validate available values in static values select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
case 'json': {
if (typeof component.data.json === 'string') {
try {
return mapDynamicValues(component, JSON.parse(component.data.json));
} catch (err) {
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': ${err}`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': ${err}`,
context,
'validate:validateAvailableItems'
);
}
} else if (Array.isArray(component.data.json)) {
// TODO: need to retype this
return mapDynamicValues(component, component.data.json as Record<string, any>[]);
} else {
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
}
}
Expand All @@ -76,48 +82,60 @@ async function getAvailableSelectValues(component: SelectComponent) {
if (Array.isArray(resolvedCustomItems)) {
return resolvedCustomItems;
}
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
}
if (Array.isArray(customItems)) {
return customItems;
} else {
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
}
default:
throw new ValidatorError(
`Failed to validate available values in select component '${component.key}': data source ${component.dataSrc} is not valid}`
throw new ProcessorError(
`Failed to validate available values in select component '${component.key}': data source ${component.dataSrc} is not valid}`,
context,
'validate:validateAvailableItems'
);
}
}

function getAvailableSelectValuesSync(component: SelectComponent) {
function getAvailableSelectValuesSync(component: SelectComponent, context: ValidationContext) {
switch (component.dataSrc) {
case 'values':
if (Array.isArray(component.data?.values)) {
return mapStaticValues(component.data.values);
}
throw new ValidatorError(
`Failed to validate available values in static values select component '${component.key}': the values are not an array`
throw new ProcessorError(
`Failed to validate available values in static values select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
case 'json': {
if (typeof component.data.json === 'string') {
try {
return mapDynamicValues(component, JSON.parse(component.data.json));
} catch (err) {
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': ${err}`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': ${err}`,
context,
'validate:validateAvailableItems'
);
}
} else if (Array.isArray(component.data.json)) {
// TODO: need to retype this
return mapDynamicValues(component, component.data.json as Record<string, any>[]);
} else {
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
}
}
Expand All @@ -132,18 +150,22 @@ function getAvailableSelectValuesSync(component: SelectComponent) {
if (Array.isArray(customItems)) {
return customItems;
} else {
throw new ValidatorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`
throw new ProcessorError(
`Failed to validate available values in JSON select component '${component.key}': the values are not an array`,
context,
'validate:validateAvailableItems'
);
}
default:
throw new ValidatorError(
`Failed to validate available values in select component '${component.key}': data source ${component.dataSrc} is not valid}`
throw new ProcessorError(
`Failed to validate available values in select component '${component.key}': data source ${component.dataSrc} is not valid}`,
context,
'validate:validateAvailableItems'
);
}
}

function compareComplexValues(valueA: unknown, valueB: unknown) {
function compareComplexValues(valueA: unknown, valueB: unknown, context: ValidationContext) {
if (!isObject(valueA) || !isObject(valueB)) {
return false;
}
Expand All @@ -153,41 +175,45 @@ function compareComplexValues(valueA: unknown, valueB: unknown) {
// this won't work
return JSON.stringify(valueA) === JSON.stringify(valueB);
} catch (err) {
throw new ValidatorError(`Error while comparing available values: ${err}`);
throw new ProcessorError(`Error while comparing available values: ${err}`, context, 'validate:validateAvailableItems');
}
}

export const validateAvailableItems: RuleFn = async (context: ValidationContext) => {
const { component, value } = context;
const error = new FieldError('invalidOption', context, 'onlyAvailableItems');
if (isValidatableRadioComponent(component)) {
if (value == null || isEmpty(value)) {
return null;
}

const values = component.values;
if (values) {
return values.findIndex(({ value: optionValue }) => optionValue === value) !== -1
? null
: error;
}
try {
if (isValidatableRadioComponent(component)) {
if (value == null || isEmpty(value)) {
return null;
}

return null;
} else if (isValidateableSelectComponent(component)) {
if (value == null || isEmpty(value)) {
return null;
}
const values = await getAvailableSelectValues(component);
if (values) {
if (isObject(value)) {
return values.find((optionValue) => compareComplexValues(optionValue, value)) !==
undefined
const values = component.values;
if (values) {
return values.findIndex(({ value: optionValue }) => optionValue === value) !== -1
? null
: error;
}

return values.find((optionValue) => optionValue === value) !== undefined ? null : error;
return null;
} else if (isValidateableSelectComponent(component)) {
if (value == null || isEmpty(value)) {
return null;
}
const values = await getAvailableSelectValues(component, context);
if (values) {
if (isObject(value)) {
return values.find((optionValue) => compareComplexValues(optionValue, value, context)) !==
undefined
? null
: error;
}

return values.find((optionValue) => optionValue === value) !== undefined ? null : error;
}
}
} catch (err: any) {
throw new ProcessorError(err.message || err, context, 'validate:validateAvailableItems');
}
return null;
};
Expand All @@ -209,28 +235,32 @@ export const shouldValidate = (context: any) => {
export const validateAvailableItemsSync: RuleFnSync = (context: ValidationContext) => {
const { component, value } = context;
const error = new FieldError('invalidOption', context, 'onlyAvailableItems');
if (!shouldValidate(context)) {
return null;
}
if (isValidatableRadioComponent(component)) {
const values = component.values;
if (values) {
return values.findIndex(({ value: optionValue }) => optionValue === value) !== -1
? null
: error;
try {
if (!shouldValidate(context)) {
return null;
}
return null;
} else if (isValidateableSelectComponent(component)) {
const values = getAvailableSelectValuesSync(component);
if (values) {
if (isObject(value)) {
return values.find((optionValue) => compareComplexValues(optionValue, value)) !==
undefined
if (isValidatableRadioComponent(component)) {
const values = component.values;
if (values) {
return values.findIndex(({ value: optionValue }) => optionValue === value) !== -1
? null
: error;
}
return values.find((optionValue) => optionValue === value) !== undefined ? null : error;
return null;
} else if (isValidateableSelectComponent(component)) {
const values = getAvailableSelectValuesSync(component, context);
if (values) {
if (isObject(value)) {
return values.find((optionValue) => compareComplexValues(optionValue, value, context)) !==
undefined
? null
: error;
}
return values.find((optionValue) => optionValue === value) !== undefined ? null : error;
}
}
} catch (err: any) {
throw new ProcessorError(err.message || err, context, 'validate:validateAvailableItems');
}
return null;
};
Expand Down
6 changes: 3 additions & 3 deletions src/process/validation/rules/validateCaptcha.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FieldError } from '../../../error/FieldError';
import { RuleFn, ValidationContext } from '../../../types/index';
import { ValidatorError } from 'error';
import { ProcessorError } from 'error';
import { ProcessorInfo } from 'types/process/ProcessorInfo';

export const shouldValidate = (context: ValidationContext) => {
Expand All @@ -18,7 +18,7 @@ export const validateCaptcha: RuleFn = async (context: ValidationContext) => {
}

if (!config || !config.database) {
throw new ValidatorError("Can't test for recaptcha success without a database config object");
throw new ProcessorError("Can't test for recaptcha success without a database config object", context, 'validate:validateCaptcha');
}
try {
if (!value || !value.token) {
Expand All @@ -31,7 +31,7 @@ export const validateCaptcha: RuleFn = async (context: ValidationContext) => {
return (captchaResult === true) ? null : new FieldError('captchaFailure', context, 'captcha');
}
catch (err: any) {
throw new ValidatorError(err.message || err);
throw new ProcessorError(err.message || err, context, 'validate:validateCaptcha');
}
};

Expand Down
Loading

0 comments on commit d0e1718

Please sign in to comment.