From 9213bb9299ade800e8482f4904a8620c47d27ad2 Mon Sep 17 00:00:00 2001 From: AJ Ancheta <7781450+ancheetah@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:43:11 -0400 Subject: [PATCH] fix(davinci-client): add validation props to collectors --- .changeset/fine-windows-search.md | 5 ++ .../davinci-client/src/lib/client.store.ts | 17 +++-- .../davinci-client/src/lib/collector.types.ts | 12 ++- .../src/lib/collector.utils.test.ts | 76 ++++++++++++++++--- .../davinci-client/src/lib/collector.utils.ts | 74 +++++++++++++----- .../src/lib/node.reducer.test.ts | 4 +- 6 files changed, 152 insertions(+), 36 deletions(-) create mode 100644 .changeset/fine-windows-search.md diff --git a/.changeset/fine-windows-search.md b/.changeset/fine-windows-search.md new file mode 100644 index 000000000..c12a3ae1c --- /dev/null +++ b/.changeset/fine-windows-search.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': patch +--- + +Exposes `required` and `validatePhoneNumber` properties on collectors diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 1fae89106..d436777b8 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -33,6 +33,7 @@ import type { ObjectValueCollectors, PhoneNumberInputValue, AutoCollectors, + MultiValueCollectors, } from './collector.types.js'; import type { InitFlow, @@ -290,11 +291,13 @@ export async function davinci({ /** * @method validate - Method for validating the value against validation rules - * @param {SingleValueCollector} collector - the collector to validate + * @param {SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors} collector - the collector to validate * @returns {function} - a function to call for validating collector value - * @throws {Error} - if the collector is not a SingleValueCollector + * @throws {Error} - if the collector cannot be validated */ - validate: (collector: SingleValueCollectors): Validator => { + validate: ( + collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors, + ): Validator => { if (!collector.id) { return handleUpdateValidateError( 'Argument for `collector` has no ID', @@ -317,9 +320,13 @@ export async function davinci({ return handleUpdateValidateError('Collector not found', 'state_error', log.error); } - if (collectorToUpdate.category !== 'ValidatedSingleValueCollector') { + if ( + collectorToUpdate.category !== 'ValidatedSingleValueCollector' && + collectorToUpdate.category !== 'ObjectValueCollector' && + collectorToUpdate.category !== 'MultiValueCollector' + ) { return handleUpdateValidateError( - 'Collector is not a SingleValueCollector and cannot be validated', + 'Collector is not a SingleValueCollector, ObjectValueCollector, or MultiValueCollector and cannot be validated', 'state_error', log.error, ); diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index eca12e769..45a5df400 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -37,6 +37,12 @@ interface ValidationRegex { rule: string; } +interface ValidationPhoneNumber { + type: 'validatePhoneNumber'; + message: string; + rule: boolean; +} + export interface SingleValueCollectorWithValue { category: 'SingleValueCollector'; error: string | null; @@ -196,6 +202,7 @@ export interface MultiValueCollectorWithValue key: string; value: string[]; type: string; + validation?: ValidationRequired[]; }; output: { key: string; label: string; type: string; - value: string[]; options: SelectorOption[]; }; } @@ -308,6 +315,7 @@ export interface ObjectOptionsCollectorWithStringValue< key: string; value: V; type: string; + validation?: ValidationRequired[]; }; output: { key: string; @@ -331,6 +339,7 @@ export interface ObjectOptionsCollectorWithObjectValue< key: string; value: V; type: string; + validation?: ValidationRequired[]; }; output: { key: string; @@ -355,6 +364,7 @@ export interface ObjectValueCollectorWithObjectValue< key: string; value: IV; type: string; + validation?: (ValidationRequired | ValidationPhoneNumber)[]; }; output: { key: string; diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index 7fa13d562..1e8f70d5f 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -30,7 +30,12 @@ import type { RedirectField, StandardField, } from './davinci.types.js'; -import { PhoneNumberOutputValue, ValidatedTextCollector } from './collector.types.js'; +import { + MultiSelectCollector, + PhoneNumberCollector, + PhoneNumberOutputValue, + ValidatedTextCollector, +} from './collector.types.js'; describe('Action Collectors', () => { describe('returnFlowCollector', () => { @@ -410,6 +415,7 @@ describe('Multi-Value Collectors', () => { const result = returnMultiSelectCollector(comboField, 1, []); expect(result.type).toBe('MultiSelectCollector'); expect(result.output).toHaveProperty('value', []); + expect(result.input).toHaveProperty('validation'); }); }); }); @@ -438,7 +444,7 @@ describe('Object value collectors', () => { description: 'device2-value', }, ], - required: true, + required: false, }; const transformedDevices = mockField.options.map((device) => ({ @@ -524,6 +530,13 @@ describe('Object value collectors', () => { key: mockField.key, value: '', type: mockField.type, + validation: [ + { + message: 'Value cannot be empty', + rule: true, + type: 'required', + }, + ], }, output: { key: mockField.key, @@ -561,6 +574,18 @@ describe('returnPhoneNumberCollector', () => { phoneNumber: '', }, type: mockField.type, + validation: [ + { + message: 'Value cannot be empty', + rule: true, + type: 'required', + }, + { + message: 'Phone number should be validated', + rule: true, + type: 'validatePhoneNumber', + }, + ], }, output: { key: mockField.key, @@ -580,8 +605,8 @@ describe('returnPhoneNumberCollector', () => { defaultCountryCode: 'US', label: 'Phone Number', type: 'PHONE_NUMBER', - required: true, - validatePhoneNumber: true, + required: false, + validatePhoneNumber: false, }; const result = returnObjectValueCollector(mockField, 1, {}); expect(result).toEqual({ @@ -616,8 +641,8 @@ describe('returnPhoneNumberCollector', () => { defaultCountryCode: 'US', label: 'Phone Number', type: 'PHONE_NUMBER', - required: true, - validatePhoneNumber: true, + required: false, + validatePhoneNumber: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -657,8 +682,8 @@ describe('returnPhoneNumberCollector', () => { defaultCountryCode: null, label: 'Phone Number', type: 'PHONE_NUMBER', - required: true, - validatePhoneNumber: true, + required: false, + validatePhoneNumber: false, }; const prefillMock: PhoneNumberOutputValue = { phoneNumber: '1234567890', @@ -696,8 +721,8 @@ describe('returnPhoneNumberCollector', () => { defaultCountryCode: 'US', label: 'Phone Number', type: 'PHONE_NUMBER', - required: true, - validatePhoneNumber: true, + required: false, + validatePhoneNumber: false, }; const prefillMock: PhoneNumberOutputValue = { countryCode: 'CA', @@ -778,17 +803,40 @@ describe('Return collector validator', () => { const validatedTextCollector = { input: { validation: [ - { type: 'required', message: 'This field is required' }, + { type: 'required', message: 'This field is required', rule: true }, { type: 'regex', message: 'Invalid format', rule: '^[a-zA-Z0-9]+$' }, ], }, } as ValidatedTextCollector; + const objectValueCollector = { + input: { + validation: [ + { type: 'required', message: 'This field is required', rule: true }, + { type: 'validatePhoneNumber', message: 'Phone number should be validated', rule: true }, + ], + }, + } as PhoneNumberCollector; + + const multiValueCollector = { + input: { + validation: [{ type: 'required', message: 'This field is required', rule: true }], + }, + } as MultiSelectCollector; + const validator = returnValidator(validatedTextCollector); + const objValidator = returnValidator(objectValueCollector); + const multiValueValidator = returnValidator(multiValueCollector); it('should return an error message for required validation when value is empty', () => { const result = validator(''); expect(result).toContain('This field is required'); + + const objResult = objValidator({}); + expect(objResult).toContain('This field is required'); + + const multiValueResult = multiValueValidator({}); + expect(multiValueResult).toContain('This field is required'); }); it('should return an error message for regex validation when value does not match the pattern', () => { @@ -799,6 +847,12 @@ describe('Return collector validator', () => { it('should return no error messages when value passes all validations', () => { const result = validator('validValue123'); expect(result).toEqual([]); + + const objResult = objValidator({ countryCode: 'US', phoneNumber: '1234567890' }); + expect(objResult).toEqual([]); + + const multiValueResult = multiValueValidator(['a', 'b', 'c']); + expect(multiValueResult).toEqual([]); }); it('should handle invalid regex patterns gracefully', () => { diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index 1a542c4c2..b8485bfad 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -25,6 +25,8 @@ import type { UnknownCollector, InferAutoCollectorType, PhoneNumberOutputValue, + MultiValueCollectors, + ObjectValueCollectors, } from './collector.types.js'; import type { DeviceAuthenticationField, @@ -208,7 +210,7 @@ export function returnSingleValueCollector< rule: field.validation?.regex || '', }); } - if ('required' in field) { + if ('required' in field && field.required === true) { validationArray.push({ type: 'required', message: 'Value cannot be empty', @@ -382,6 +384,15 @@ export function returnMultiValueCollector< error = `${error}Options are not found in the field object. `; } + const validationArray = []; + if ('required' in field && field.required === true) { + validationArray.push({ + type: 'required', + message: 'Value cannot be empty', + rule: true, + }); + } + return { category: 'MultiValueCollector', error: error || null, @@ -392,6 +403,7 @@ export function returnMultiValueCollector< key: field.key, value: data || [], type: field.type, + validation: validationArray.length ? validationArray : undefined, }, output: { key: field.key, @@ -435,6 +447,15 @@ export function returnObjectCollector< error = `${error}Type is not found in the field object. `; } + const validationArray = []; + if ('required' in field && field.required === true) { + validationArray.push({ + type: 'required', + message: 'Value cannot be empty', + rule: true, + }); + } + let options; let defaultValue; @@ -482,6 +503,14 @@ export function returnObjectCollector< key: `${device.type}-${idx}`, })); } else if (field.type === 'PHONE_NUMBER') { + if ('validatePhoneNumber' in field && field.validatePhoneNumber === true) { + validationArray.push({ + type: 'validatePhoneNumber', + message: 'Phone number should be validated', + rule: true, + }); + } + const prefilledCountryCode = prefillData?.countryCode; const prefilledPhone = prefillData?.phoneNumber; defaultValue = { @@ -500,6 +529,7 @@ export function returnObjectCollector< key: field.key, value: defaultValue, type: field.type, + validation: validationArray.length ? validationArray : undefined, }, output: { key: field.key, @@ -583,28 +613,38 @@ export function returnReadOnlyCollector(field: ReadOnlyField, idx: number) { /** * @function returnValidator - Creates a validator function based on the provided collector - * @param collector {ValidatedTextCollector} - The collector to which the value will be validated + * @param {ValidatedTextCollector | ObjectValueCollectors | MultiValueCollectors} collector - The collector to which the value will be validated * @returns {function} - A "validator" function that validates the input value */ -export function returnValidator(collector: ValidatedTextCollector) { +export function returnValidator( + collector: ValidatedTextCollector | ObjectValueCollectors | MultiValueCollectors, +) { const rules = collector.input.validation; - return (value: string) => { - return rules.reduce((acc, next) => { - if (next.type === 'required' && !value) { - acc.push(next.message); - } else if (next.type === 'regex') { - try { - const result = new RegExp(next.rule).test(value); - if (!result) { + return (value: string | string[] | Record) => { + return ( + rules?.reduce((acc, next) => { + if (next.type === 'required') { + if ( + !value || + (Array.isArray(value) && !value.length) || + (typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) + ) { acc.push(next.message); } - } catch (err) { - const error = err as Error; - acc.push(error.message); + } else if (next.type === 'regex' && typeof value === 'string') { + try { + const result = new RegExp(next.rule).test(value); + if (!result) { + acc.push(next.message); + } + } catch (err) { + const error = err as Error; + acc.push(error.message); + } } - } - return acc; - }, [] as string[]); + return acc; + }, [] as string[]) ?? [] + ); }; } diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index dc6ac83a9..b9196b5a1 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -710,7 +710,7 @@ describe('The phone number collector reducer', () => { defaultCountryCode: null, label: 'Phone Number', type: 'PHONE_NUMBER', - required: true, + required: false, }, ], }, @@ -753,7 +753,7 @@ describe('The phone number collector reducer', () => { defaultCountryCode: 'US', label: 'Phone Number', type: 'PHONE_NUMBER', - required: true, + required: false, }, ], },