diff --git a/src/error/DereferenceError.ts b/src/error/DereferenceError.ts deleted file mode 100644 index 27f2097c..00000000 --- a/src/error/DereferenceError.ts +++ /dev/null @@ -1 +0,0 @@ -export class DereferenceError extends Error {}; diff --git a/src/error/ProcessorError.ts b/src/error/ProcessorError.ts new file mode 100644 index 00000000..7f3a882a --- /dev/null +++ b/src/error/ProcessorError.ts @@ -0,0 +1,10 @@ +import { ProcessorContext } from "types"; +export class ProcessorError extends Error { + context: Omit, 'scope'>; + constructor(message: string, context: ProcessorContext, 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}; + } +}; diff --git a/src/error/ValidatorError.ts b/src/error/ValidatorError.ts deleted file mode 100644 index 0af16886..00000000 --- a/src/error/ValidatorError.ts +++ /dev/null @@ -1 +0,0 @@ -export class ValidatorError extends Error {} diff --git a/src/error/index.ts b/src/error/index.ts index 4e2f6a33..ca6076db 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,3 +1,2 @@ export * from './FieldError'; -export * from './ValidatorError'; -export * from './DereferenceError'; +export * from './ProcessorError'; diff --git a/src/process/dereference/index.ts b/src/process/dereference/index.ts index a3a2be14..b0850c90 100644 --- a/src/process/dereference/index.ts +++ b/src/process/dereference/index.ts @@ -1,4 +1,4 @@ -import { DereferenceError } from "error"; +import { ProcessorError } from "error"; import { ProcessorFn, ProcessorScope, @@ -37,7 +37,7 @@ export const dereferenceProcess: ProcessorFn = 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 { @@ -49,7 +49,7 @@ export const dereferenceProcess: ProcessorFn = async (context) component.components = vmCompatibleComponents; } catch (err: any) { - throw new DereferenceError(err.message || err); + throw new ProcessorError(err.message || err, context, 'dereference'); } } diff --git a/src/process/validation/rules/validateAvailableItems.ts b/src/process/validation/rules/validateAvailableItems.ts index df780e5f..069311e4 100644 --- a/src/process/validation/rules/validateAvailableItems.ts +++ b/src/process/validation/rules/validateAvailableItems.ts @@ -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'; @@ -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[]); } 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' ); } } @@ -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[]); } 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' ); } } @@ -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; } @@ -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; }; @@ -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; }; diff --git a/src/process/validation/rules/validateCaptcha.ts b/src/process/validation/rules/validateCaptcha.ts index 5de789fe..b9f39cea 100644 --- a/src/process/validation/rules/validateCaptcha.ts +++ b/src/process/validation/rules/validateCaptcha.ts @@ -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) => { @@ -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) { @@ -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'); } }; diff --git a/src/process/validation/rules/validateCustom.ts b/src/process/validation/rules/validateCustom.ts index fac8322e..52490a7e 100644 --- a/src/process/validation/rules/validateCustom.ts +++ b/src/process/validation/rules/validateCustom.ts @@ -1,16 +1,14 @@ import { isEmpty } from 'lodash'; -import { RuleFn, RuleFnSync } from 'types/RuleFn'; -import { FieldError } from 'error/FieldError'; +import { RuleFn, RuleFnSync, ProcessorInfo, ValidationContext } from 'types'; +import { FieldError, ProcessorError } from 'error'; import { Evaluator } from 'utils'; -import { ValidationContext } from 'types'; -import { ProcessorInfo } from 'types/process/ProcessorInfo'; export const validateCustom: RuleFn = async (context: ValidationContext) => { return validateCustomSync(context); }; export const shouldValidate = (context: ValidationContext) => { - const { component, value } = context; + const { component } = context; const customValidation = component.validate?.custom; if (!customValidation) { return false; @@ -21,39 +19,43 @@ export const shouldValidate = (context: ValidationContext) => { export const validateCustomSync: RuleFnSync = (context: ValidationContext) => { const { component, data, row, value, index, instance, evalContext } = context; const customValidation = component.validate?.custom; - if (!shouldValidate(context)) { - return null; - } + try { + if (!shouldValidate(context)) { + return null; + } - const evalContextValue = { - ...(instance?.evalContext ? instance.evalContext() : (evalContext ? evalContext(context) : context)), - component, - data, - row, - rowIndex: index, - instance, - valid: true, - input: value, - } + const evalContextValue = { + ...(instance?.evalContext ? instance.evalContext() : (evalContext ? evalContext(context) : context)), + component, + data, + row, + rowIndex: index, + instance, + valid: true, + input: value, + } - const isValid = Evaluator.evaluate( - customValidation, - evalContextValue, - 'valid', - true, - {}, - {} - ); + const isValid = Evaluator.evaluate( + customValidation, + evalContextValue, + 'valid', + true, + {}, + {} + ); - if (isValid === null || isValid === true) { - return null; - } + if (isValid === null || isValid === true) { + return null; + } - return new FieldError(typeof isValid === 'string' ? isValid : 'custom', { - ...context, - hasLabel: false, - setting: customValidation, - }, 'custom'); + return new FieldError(typeof isValid === 'string' ? isValid : 'custom', { + ...context, + hasLabel: false, + setting: customValidation + }, 'custom'); + } catch (err: any) { + throw new ProcessorError(err.message || err, context, 'validate:validateCustom'); + } }; diff --git a/src/process/validation/rules/validateMaximumDay.ts b/src/process/validation/rules/validateMaximumDay.ts index 7fda9288..bc55280d 100644 --- a/src/process/validation/rules/validateMaximumDay.ts +++ b/src/process/validation/rules/validateMaximumDay.ts @@ -1,4 +1,4 @@ -import { ValidatorError, FieldError } from 'error'; +import { ProcessorError, FieldError } from 'error'; import { DayComponent, RuleFn, RuleFnSync, ValidationContext } from 'types'; import { dayjs, isPartialDay, getDateValidationFormat, getDateSetting } from 'utils/date'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; @@ -31,7 +31,7 @@ export const validateMaximumDaySync: RuleFnSync = (context: ValidationContext) = return null; } if (typeof value !== 'string') { - throw new ValidatorError(`Cannot validate day value ${value} because it is not a string`); + throw new ProcessorError(`Cannot validate day value ${value} because it is not a string`, context, 'validate:validateMaximumDay'); } // TODO: this validation probably goes for dates and days const format = getDateValidationFormat(component as DayComponent); @@ -52,4 +52,4 @@ export const validateMaximumDayInfo: ProcessorInfo { return true; }; -function validateValue(value: DataObject[any]): asserts value is Record { +function validateValue(value: DataObject[any], context: ValidationContext): asserts value is Record { if (value == null || typeof value !== 'object') { - throw new ValidatorError( - `Cannot validate maximum selected count for value ${value} as it is not an object` + throw new ProcessorError( + `Cannot validate maximum selected count for value ${value} as it is not an object`, + context, + 'validate:validateMaximumSelectedCount' ); } const subValues = Object.values(value); if (!subValues.every((value) => typeof value === 'boolean')) { - throw new ValidatorError( - `Cannot validate maximum selected count for value ${value} because it has non-boolean members` + throw new ProcessorError( + `Cannot validate maximum selected count for value ${value} because it has non-boolean members`, + context, + 'validate:validateMaximumSelectedCount' ); } } @@ -48,27 +52,32 @@ export const validateMaximumSelectedCount: RuleFn = async (context: ValidationCo export const validateMaximumSelectedCountSync: RuleFnSync = (context: ValidationContext) => { const { component, value } = context; - if (!shouldValidate(context)) { - return null; - } - validateValue(value); - const max = getValidationSetting(component as SelectBoxesComponent); - if (!max) { - return null; - } - const count = Object.keys(value).reduce((sum, key) => (value[key] ? ++sum : sum), 0); + try { + + if (!shouldValidate(context)) { + return null; + } + validateValue(value, context); + const max = getValidationSetting(component as SelectBoxesComponent); + if (!max) { + return null; + } + const count = Object.keys(value).reduce((sum, key) => (value[key] ? ++sum : sum), 0); - // Should not be triggered if there is no options selected at all - if (count <= 0) { - return null; + // Should not be triggered if there is no options selected at all + if (count <= 0) { + return null; + } + return count > max + ? new FieldError((component as SelectBoxesComponent).maxSelectedCountMessage || 'maxSelectedCount', { + ...context, + maxCount: String(max), + setting: String(max), + }) + : null; + } catch (err: any) { + throw new ProcessorError(err.message || err, context, 'validate:validateMaximumSelectedCount'); } - return count > max - ? new FieldError((component as SelectBoxesComponent).maxSelectedCountMessage || 'maxSelectedCount', { - ...context, - maxCount: String(max), - setting: String(max), - }) - : null; } export const validateMaximumSelectedCountInfo: ProcessorInfo = { diff --git a/src/process/validation/rules/validateMaximumValue.ts b/src/process/validation/rules/validateMaximumValue.ts index 9a13d031..8fc279c7 100644 --- a/src/process/validation/rules/validateMaximumValue.ts +++ b/src/process/validation/rules/validateMaximumValue.ts @@ -1,4 +1,4 @@ -import { FieldError, ValidatorError } from 'error'; +import { FieldError, ProcessorError } from 'error'; import { NumberComponent, RuleFn, RuleFnSync, ValidationContext } from 'types'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; @@ -43,8 +43,10 @@ export const validateMaximumValueSync: RuleFnSync = (context: ValidationContext) } const parsedValue = typeof value === 'string' ? parseFloat(value) : Number(value); if (Number.isNaN(parsedValue)) { - throw new ValidatorError( - `Cannot validate value ${parsedValue} because it is invalid` + throw new ProcessorError( + `Cannot validate value ${parsedValue} because it is invalid`, + context, + 'validate:validateMaximumValue' ); } @@ -58,4 +60,4 @@ export const validateMaximumValueInfo: ProcessorInfo { return true; }; -function validateValue(value: DataObject[any]): asserts value is Record { +function validateValue(value: DataObject[any], context: ValidationContext): asserts value is Record { if (value == null || typeof value !== 'object') { - throw new ValidatorError( - `Cannot validate maximum selected count for value ${value} as it is not an object` + throw new ProcessorError( + `Cannot validate maximum selected count for value ${value} as it is not an object`, + context, + 'validate:validateMinimumSelectedCount' ); } const subValues = Object.values(value); if (!subValues.every((value) => typeof value === 'boolean')) { - throw new ValidatorError( - `Cannot validate maximum selected count for value ${value} because it has non-boolean members` + throw new ProcessorError( + `Cannot validate maximum selected count for value ${value} because it has non-boolean members`, + context, + 'validate:validateMinimumSelectedCount' ); } } @@ -48,27 +52,32 @@ export const validateMinimumSelectedCount: RuleFn = async (context: ValidationCo export const validateMinimumSelectedCountSync: RuleFnSync = (context: ValidationContext) => { const { component, value } = context; - if (!shouldValidate(context)) { - return null; - } - validateValue(value); - const min = getValidationSetting((component as SelectBoxesComponent)); - if (!min) { - return null; - } - const count = Object.keys(value).reduce((sum, key) => (value[key] ? ++sum : sum), 0); + try { + + if (!shouldValidate(context)) { + return null; + } + validateValue(value, context); + const min = getValidationSetting((component as SelectBoxesComponent)); + if (!min) { + return null; + } + const count = Object.keys(value).reduce((sum, key) => (value[key] ? ++sum : sum), 0); - // Should not be triggered if there are no options selected at all - if (count <= 0) { - return null; + // Should not be triggered if there are no options selected at all + if (count <= 0) { + return null; + } + return count < min + ? new FieldError((component as SelectBoxesComponent).minSelectedCountMessage || 'minSelectedCount', { + ...context, + minCount: String(min), + setting: String(min), + }, 'minSelectedCount') + : null; + } catch (err: any) { + throw new ProcessorError(err.message || err, context, 'validate:validateMinimumSelectedCount'); } - return count < min - ? new FieldError((component as SelectBoxesComponent).minSelectedCountMessage || 'minSelectedCount', { - ...context, - minCount: String(min), - setting: String(min), - }, 'minSelectedCount') - : null; }; export const validateMinimumSelectedCountInfo: ProcessorInfo = { diff --git a/src/process/validation/rules/validateMinimumValue.ts b/src/process/validation/rules/validateMinimumValue.ts index 648373fc..79d41323 100644 --- a/src/process/validation/rules/validateMinimumValue.ts +++ b/src/process/validation/rules/validateMinimumValue.ts @@ -1,4 +1,4 @@ -import { FieldError, ValidatorError } from 'error'; +import { FieldError, ProcessorError } from 'error'; import { NumberComponent, RuleFn, RuleFnSync, ValidationContext } from 'types'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; @@ -43,8 +43,10 @@ export const validateMinimumValueSync: RuleFnSync = (context: ValidationContext) } const parsedValue = typeof value === 'string' ? parseFloat(value) : Number(value); if (Number.isNaN(parsedValue)) { - throw new ValidatorError( + throw new ProcessorError( `Cannot validate value ${parsedValue} because it is invalid`, + context, + 'validate:validateMinimumValue' ); } @@ -58,4 +60,4 @@ export const validateMinimumValueInfo: ProcessorInfo { return isValid ? null : new FieldError('time', context); } catch (err) { - throw new ValidatorError(`Could not validate time component ${component.key} with value ${value}`); + throw new ProcessorError(`Could not validate time component ${component.key} with value ${value}`, context, 'validate:validateTime'); } } diff --git a/src/process/validation/rules/validateUnique.ts b/src/process/validation/rules/validateUnique.ts index bedb0fbf..7cfe2b4f 100644 --- a/src/process/validation/rules/validateUnique.ts +++ b/src/process/validation/rules/validateUnique.ts @@ -1,7 +1,7 @@ import { FieldError } from '../../../error/FieldError'; import { RuleFn, ValidationContext } from '../../../types/index'; import { isEmptyObject } from '../util'; -import { ValidatorError } from 'error'; +import { ProcessorError} from 'error'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; export const shouldValidate = (context: ValidationContext) => { @@ -23,7 +23,7 @@ export const validateUnique: RuleFn = async (context: ValidationContext) => { } if (!config || !config.database) { - throw new ValidatorError("Can't test for unique value without a database config object"); + throw new ProcessorError("Can't test for unique value without a database config object", context, 'validate:validateUnique'); } try { const isUnique = await config.database?.isUnique(context, value); @@ -36,7 +36,7 @@ export const validateUnique: RuleFn = async (context: ValidationContext) => { return (isUnique === true) ? null : new FieldError('unique', context); } catch (err: any) { - throw new ValidatorError(err.message || err); + throw new ProcessorError(err.message || err, context, 'validate:validateUnique'); } };