diff --git a/src/process/validation/rules/__tests__/validateAvailableItems.test.ts b/src/process/validation/rules/__tests__/validateAvailableItems.test.ts index 9f0251c6..cc5beaba 100644 --- a/src/process/validation/rules/__tests__/validateAvailableItems.test.ts +++ b/src/process/validation/rules/__tests__/validateAvailableItems.test.ts @@ -457,3 +457,105 @@ it('Validating a simple JSON select component (nested actual JSON with valueProp const result = await validateAvailableItems(context); expect(result).to.equal(null); }); + +it('Validating a simple radio component with url data source with the available items validation parameter will return null if the item is valid', async () => { + const component: RadioComponent = { + ...simpleRadioField, + dataSrc: 'url', + data: { + url: 'http://localhost:8080/numbers', + headers: [], + }, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: '2', + }; + + const context = generateProcessorContext(component, data); + context.fetch = (url: string, options?: RequestInit | undefined) => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(['1', '2', '3']) + }); + }; + const result = await validateAvailableItems(context); + expect(result).to.equal(null); +}); + +it('Validating a simple radio component with url data source with the available items validation parameter will return FieldError if the item is invalid', async () => { + const component: RadioComponent = { + ...simpleRadioField, + dataSrc: 'url', + data: { + url: 'http://localhost:8080/numbers', + headers: [], + }, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: '4', + }; + + const context = generateProcessorContext(component, data); + context.fetch = (url: string, options?: RequestInit | undefined) => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(['1', '2', '3']) + }); + }; + const result = await validateAvailableItems(context); + expect(result).to.be.instanceOf(FieldError); + expect(result?.errorKeyOrMessage).to.equal('invalidOption'); +}); + +it('Validating a simple select component with url data source with the available items validation parameter will return null if the item is valid', async () => { + const component: SelectComponent = { + ...simpleSelectOptions, + dataSrc: 'url', + data: { + url: 'http://localhost:8080/numbers', + headers: [], + }, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: {'id': 'opt_1', 'value': 1}, + }; + + const context = generateProcessorContext(component, data); + context.fetch = (url: string, options?: RequestInit | undefined) => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve([{'id': 'opt_1', 'value': 1}, {'id': 'opt_2', 'value': 2}]) + }); + }; + const result = await validateAvailableItems(context); + expect(result).to.equal(null); +}); + +it('Validating a simple select component with url data source with the available items validation parameter will return FieldError if the item is invalid', async () => { + const component: SelectComponent = { + ...simpleSelectOptions, + dataSrc: 'url', + data: { + url: 'http://localhost:8080/numbers', + headers: [], + }, + validate: { onlyAvailableItems: true }, + }; + const data = { + component: {'id': 'opt_3', 'value': 3}, + }; + + const context = generateProcessorContext(component, data); + context.fetch = (url: string, options?: RequestInit | undefined) => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve([{'id': 'opt_1', 'value': 1}, {'id': 'opt_2', 'value': 2}]) + }); + }; + const result = await validateAvailableItems(context); + expect(result).to.be.instanceOf(FieldError); + expect(result?.errorKeyOrMessage).to.equal('invalidOption'); +}); \ No newline at end of file diff --git a/src/process/validation/rules/asynchronousRules.ts b/src/process/validation/rules/asynchronousRules.ts index 4c0703d4..4b2fc2c8 100644 --- a/src/process/validation/rules/asynchronousRules.ts +++ b/src/process/validation/rules/asynchronousRules.ts @@ -1,7 +1,9 @@ import { ValidationRuleInfo } from "types"; import { validateUrlSelectValueInfo } from "./validateUrlSelectValue"; +import { validateAvailableItemsInfo } from "./validateAvailableItems"; // These are the validations that are asynchronouse (e.g. require fetch export const asynchronousRules: ValidationRuleInfo[] = [ validateUrlSelectValueInfo, + validateAvailableItemsInfo ]; diff --git a/src/process/validation/rules/validateAvailableItems.ts b/src/process/validation/rules/validateAvailableItems.ts index 5cad15c3..64c361b9 100644 --- a/src/process/validation/rules/validateAvailableItems.ts +++ b/src/process/validation/rules/validateAvailableItems.ts @@ -1,16 +1,16 @@ import { isEmpty, isUndefined} from 'lodash'; import { FieldError, ProcessorError } from 'error'; import { Evaluator } from 'utils'; -import { RadioComponent, SelectComponent, RuleFn, RuleFnSync, ValidationContext } from 'types'; +import { RadioComponent, SelectComponent, RuleFn, RuleFnSync, ValidationContext, FetchFn, SelectBoxesComponent } from 'types'; import { isObject, isPromise } from '../util'; import { ProcessorInfo } from 'types/process/ProcessorInfo'; +import { getErrorMessage } from 'utils/error'; function isValidatableRadioComponent(component: any): component is RadioComponent { return ( component && component.type === 'radio' && - !!component.validate?.onlyAvailableItems && - component.dataSrc === 'values' + !!component.validate?.onlyAvailableItems ); } @@ -23,6 +23,15 @@ function isValidateableSelectComponent(component: any): component is SelectCompo ); } +function isValidateableSelectBoxesComponent(component: any): component is SelectBoxesComponent { + return ( + component && + !!component.validate?.onlyAvailableItems && + component.type === 'selectboxes' && + component.dataSrc === 'url' + ); +} + function mapDynamicValues>(component: SelectComponent, values: T[]) { return values.map((value) => { if (component.valueProperty) { @@ -36,6 +45,29 @@ function mapStaticValues(values: { label: string; value: string }[]) { return values.map((obj) => obj.value); } +const getAvailableDynamicValues = async (component: any, context: ValidationContext) => { + let _fetch: FetchFn | null = null; + try { + _fetch = context.fetch ? context.fetch : fetch; + } + catch (err) { + _fetch = null; + } + try { + if (!_fetch) { + console.log('You must provide a fetch interface to the fetch processor.'); + return null; + } + const response = await _fetch((component as any).data.url, { method: 'GET' }); + const data = await response.json(); + return data ? mapDynamicValues(component, data) : null; + } + catch (err) { + console.error(getErrorMessage(err)); + return null; + } +} + async function getAvailableSelectValues(component: SelectComponent, context: ValidationContext) { if (isUndefined(component.dataSrc) && component.data.hasOwnProperty('values')) { component.dataSrc = 'values'; @@ -100,6 +132,8 @@ async function getAvailableSelectValues(component: SelectComponent, context: Val 'validate:validateAvailableItems' ); } + case 'url': + return await getAvailableDynamicValues(component, context); default: throw new ProcessorError( `Failed to validate available values in select component '${component.key}': data source ${component.dataSrc} is not valid}`, @@ -196,11 +230,15 @@ export const validateAvailableItems: RuleFn = async (context: ValidationContext) return null; } - const values = component.values; + const values = component.dataSrc === 'url' ? await getAvailableDynamicValues(component, context) : component.values; if (values) { - return values.findIndex(({ value: optionValue }) => optionValue === value) !== -1 - ? null - : error; + if (isObject(value)) { + return values.find((optionValue) => compareComplexValues(optionValue, value, context)) !== + undefined + ? null + : error; + } + return values.find((optionValue) => optionValue === value) !== undefined ? null : error; } return null; @@ -217,6 +255,21 @@ export const validateAvailableItems: RuleFn = async (context: ValidationContext) : error; } + return values.find((optionValue) => optionValue === value) !== undefined ? null : error; + } + } else if (isValidateableSelectBoxesComponent(component)) { + if (value == null || isEmpty(value)) { + return null; + } + const values = await getAvailableDynamicValues(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; } } @@ -237,6 +290,9 @@ export const shouldValidate = (context: any) => { if (isValidateableSelectComponent(component)) { return true; } + if (isValidateableSelectBoxesComponent(component)) { + return true; + } return false; } @@ -247,7 +303,7 @@ export const validateAvailableItemsSync: RuleFnSync = (context: ValidationContex if (!shouldValidate(context)) { return null; } - if (isValidatableRadioComponent(component)) { + if (isValidatableRadioComponent(component) && component.dataSrc !== 'url') { const values = component.values; if (values) { return values.findIndex(({ value: optionValue }) => optionValue === value) !== -1