Skip to content

Commit

Permalink
Merge pull request #52 from formio/FIO-7964-add-resource-validation
Browse files Browse the repository at this point in the history
FIO-7964: add resource-based select component validation
  • Loading branch information
AlexeyNikipelau authored Apr 19, 2024
2 parents d311d92 + 5c599a9 commit 10df483
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { DataObject, SelectComponent } from 'types';
import { FieldError } from 'error';
import { simpleSelectOptions, simpleTextField } from './fixtures/components';
import { generateProcessorContext } from './fixtures/util';
import { validateRemoteSelectValue, generateUrl } from '../validateRemoteSelectValue';
import { validateUrlSelectValue, generateUrl } from '../validateUrlSelectValue';

it('Validating a component without the remote value validation parameter will return null', async () => {
const component = simpleTextField;
const data = {
component: 'Hello, world!',
};
const context = generateProcessorContext(component, data);
const result = await validateRemoteSelectValue(context);
const result = await validateUrlSelectValue(context);
expect(result).to.equal(null);
});

Expand All @@ -32,7 +32,7 @@ it('Validating a select component without the remote value validation parameter
},
};
const context = generateProcessorContext(component, data);
const result = await validateRemoteSelectValue(context);
const result = await validateUrlSelectValue(context);
expect(result).to.equal(null);
});

Expand Down Expand Up @@ -84,7 +84,7 @@ it('Validating a select component with the remote validation parameter will retu
},
};
const context = generateProcessorContext(component, data);
const result = await validateRemoteSelectValue(context);
const result = await validateUrlSelectValue(context);
expect(result).to.be.instanceOf(FieldError);
expect(result?.errorKeyOrMessage).to.equal('select');
});
Expand All @@ -108,7 +108,7 @@ it('Validating a select component with the remote validation parameter will retu
},
};
const context = generateProcessorContext(component, data);
const result = await validateRemoteSelectValue(context);
const result = await validateUrlSelectValue(context);
expect(result).to.be.instanceOf(FieldError);
expect(result?.errorKeyOrMessage).to.equal('select');
});
Expand Down Expand Up @@ -139,6 +139,6 @@ it('Validating a select component with the remote validation parameter will retu
json: () => Promise.resolve([{ id: 'b', value: 2 }])
});
};
const result = await validateRemoteSelectValue(context);
const result = await validateUrlSelectValue(context);
expect(result).to.equal(null);
});
4 changes: 2 additions & 2 deletions src/process/validation/rules/asynchronousRules.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ValidationRuleInfo } from "types";
import { validateRemoteSelectValueInfo } from "./validateRemoteSelectValue";
import { validateUrlSelectValueInfo } from "./validateUrlSelectValue";

// These are the validations that are asynchronouse (e.g. require fetch
export const asynchronousRules: ValidationRuleInfo[] = [
validateRemoteSelectValueInfo,
validateUrlSelectValueInfo,
];
4 changes: 3 additions & 1 deletion src/process/validation/rules/databaseRules.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ValidationRuleInfo } from "types";
import { validateUniqueInfo } from "./validateUnique";
import { validateCaptchaInfo } from "./validateCaptcha";
import { validateResourceSelectValueInfo } from "./validateResourceSelectValue";

// These are the validations that require a database connection.
export const databaseRules: ValidationRuleInfo[] = [
validateUniqueInfo,
validateCaptchaInfo
validateCaptchaInfo,
validateResourceSelectValueInfo
];
89 changes: 89 additions & 0 deletions src/process/validation/rules/validateResourceSelectValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { FieldError, ProcessorError } from 'error';
import { SelectComponent, RuleFn, ValidationContext } from 'types';
import { Evaluator } from 'utils';
import { isEmptyObject, toBoolean } from '../util';
import { getErrorMessage } from 'utils/error';
import { ProcessorInfo } from 'types/process/ProcessorInfo';

const isValidatableSelectComponent = (component: any): component is SelectComponent => {
return (
component &&
component.type === 'select' &&
toBoolean(component.dataSrc === 'resource') &&
toBoolean(component.validate?.select)
);
};

export const generateUrl = (baseUrl: URL, component: SelectComponent, value: any) => {
const url = baseUrl;
const query = url.searchParams;
if (component.searchField) {
let searchValue = value;
if (component.valueProperty) {
searchValue = value[component.valueProperty];
} else {
searchValue = value;
}
query.set(component.searchField, typeof searchValue === 'string' ? searchValue : JSON.stringify(searchValue))
}
if (component.selectFields) {
query.set('select', component.selectFields);
}
if (component.sort) {
query.set('sort', component.sort);
}
if (component.filter) {
const filterQueryStrings = new URLSearchParams(component.filter);
filterQueryStrings.forEach((value, key) => query.set(key, value));
}
return url;
};

export const shouldValidate = (context: ValidationContext) => {
const { component, value, data, config } = context;
// Only run this validation if server-side
if (!config?.server) {
return false;
}
if (!isValidatableSelectComponent(component)) {
return false;
}
if (
!value ||
isEmptyObject(value) ||
(Array.isArray(value) && (value as Array<Record<string, any>>).length === 0)
) {
return false;
}

// If given an invalid configuration, do not validate the remote value
if (component.dataSrc !== 'resource' || !component.data?.resource) {
return false;
}

return true;
};

export const validateResourceSelectValue: RuleFn = async (context: ValidationContext) => {
const { value, config, component } = context;
if (!shouldValidate(context)) {
return null;
}

if (!config || !config.database) {
throw new ProcessorError("Can't validate for resource value without a database config object", context, 'validate:validateResourceSelectValue');
}
try {
const resourceSelectValueResult: boolean = await config.database?.validateResourceSelectValue(context, value);
return (resourceSelectValueResult === true) ? null : new FieldError('select', context);
}
catch (err: any) {
throw new ProcessorError(err.message || err, context, 'validate:validateResourceSelectValue');
}
};

export const validateResourceSelectValueInfo: ProcessorInfo<ValidationContext, FieldError | null> = {
name: 'validateResourceSelectValue',
process: validateResourceSelectValue,
shouldProcess: shouldValidate,
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const shouldValidate = (context: ValidationContext) => {
return true;
};

export const validateRemoteSelectValue: RuleFn = async (context: ValidationContext) => {
export const validateUrlSelectValue: RuleFn = async (context: ValidationContext) => {
const { component, value, data, config } = context;
let _fetch: FetchFn | null = null;
try {
Expand Down Expand Up @@ -129,8 +129,8 @@ export const validateRemoteSelectValue: RuleFn = async (context: ValidationConte
}
};

export const validateRemoteSelectValueInfo: ProcessorInfo<ValidationContext, FieldError | null> = {
name: 'validateRemoteSelectValue',
process: validateRemoteSelectValue,
export const validateUrlSelectValueInfo: ProcessorInfo<ValidationContext, FieldError | null> = {
name: 'validateUrlSelectValue',
process: validateUrlSelectValue,
shouldProcess: shouldValidate,
}

0 comments on commit 10df483

Please sign in to comment.