Skip to content

Commit bf3670a

Browse files
Vojtěch Václav Portešvojtechportes
authored andcommitted
feat: Added support for rule-agnostic and common validation rules
1 parent 61189d8 commit bf3670a

25 files changed

Lines changed: 1438 additions & 565 deletions

README.md

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,38 +209,72 @@ const fields: IBuilderFieldProps[] = [
209209
type: 'TEXT',
210210
operators: ['EQUAL', 'CONTAINS'],
211211
validation: {
212-
required: true,
213-
minLength: 2,
214-
maxLength: 50,
212+
common: {
213+
required: true,
214+
},
215+
rules: [
216+
{
217+
operators: ['EQUAL', 'CONTAINS'],
218+
minLength: 2,
219+
maxLength: 50,
220+
},
221+
],
215222
},
216223
},
217224
{
218225
field: 'PRICE',
219226
label: 'Price',
220227
type: 'NUMBER',
221-
operators: ['EQUAL', 'BETWEEN'],
228+
operators: ['EQUAL', 'BETWEEN', 'NOT_BETWEEN'],
222229
validation: {
223-
min: 0,
224-
range: {
225-
requireAscending: true,
226-
allowEqual: false,
230+
common: {
231+
required: true,
227232
},
233+
rules: [
234+
{
235+
operators: ['EQUAL'],
236+
min: 0,
237+
max: 100,
238+
},
239+
{
240+
operators: ['BETWEEN', 'NOT_BETWEEN'],
241+
range: {
242+
common: {
243+
min: 0,
244+
max: 100,
245+
},
246+
start: {
247+
max: 80,
248+
},
249+
end: {
250+
min: 10,
251+
},
252+
requireAscending: true,
253+
allowEqual: false,
254+
},
255+
},
256+
],
228257
},
229258
},
230259
];
231260
```
232261

233262
Built-in validation supports:
234263

264+
- operator-agnostic validation in `validation.common`
265+
- operator-aware validation in `validation.rules`
235266
- shared rules like `required`, `oneOf`, and `custom`
236267
- text rules like `minLength`, `maxLength`, and `matches`
237268
- number rules like `min`, `max`, `integer`, `positive`, and `negative`
238269
- date rules like `minDate` and `maxDate`
239270
- multi-list rules like `minItems` and `maxItems`
240271
- range validation for `BETWEEN` and `NOT_BETWEEN`
241272

242-
If you use `range`, you can validate both the range shape and the relationship between values:
273+
If you use `range`, you can validate both values together and individually:
243274

275+
- `common`
276+
- `start`
277+
- `end`
244278
- `requireAscending`
245279
- `allowEqual`
246280
- `validate`

src/builder.tsx

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ import { Text } from './text';
4646
import { IButtonProps } from './button';
4747
import { createGroupNode } from './utils/create-group-node.util';
4848
import { createId } from './utils/create-id.util';
49-
import { createBuilderValidationResult } from './utils/create-builder-validation-result.util';
5049
import { emitQuery } from './utils/emit-query.util';
5150
import { ingestQuery } from './utils/ingest-query.util';
5251
import { isPromiseLike } from './utils/is-promise-like.util';
5352
import { isSameQuery } from './utils/is-same-query.util';
5453
import { moveQueryNode } from './utils/move-query-node.util';
55-
import { validateBuilderQuery } from './utils/validate-builder-query.util';
54+
import { createBuilderValidationResult } from './utils/validation/create-builder-validation-result.util';
55+
import { validateBuilderQuery } from './utils/validation/validate-builder-query.util';
5656
import {
5757
DenormalizedQuery,
5858
INormalizedRuleNode,
@@ -111,16 +111,26 @@ export interface IBuilderValidationMessageContext {
111111
operator?: BuilderFieldOperator;
112112
value?: BuilderFieldValue;
113113
ruleId?: string;
114+
rangeBoundary?: 'start' | 'end';
114115
}
115116

116117
export type BuilderValidationMessage =
117118
| string
118119
| ((context: IBuilderValidationMessageContext) => string);
119120

120-
export interface IBuilderRangeValidation<TValue = string | number> {
121+
export interface IBuilderRangeValidation<
122+
TValueValidation = unknown,
123+
TRangeValue = string | number
124+
> {
125+
common?: Partial<TValueValidation>;
126+
start?: Partial<TValueValidation>;
127+
end?: Partial<TValueValidation>;
121128
allowEqual?: boolean;
122129
requireAscending?: boolean;
123-
validate?: (range: [TValue, TValue]) => boolean | Promise<boolean>;
130+
validate?: (
131+
range: [TRangeValue, TRangeValue],
132+
context: IBuilderValidationMessageContext
133+
) => boolean | Promise<boolean>;
124134
message?: BuilderValidationMessage;
125135
}
126136

@@ -134,48 +144,100 @@ export interface IBuilderFieldValidationBase<TValue = unknown> {
134144
customMessage?: BuilderValidationMessage;
135145
}
136146

137-
export interface ITextFieldValidation
147+
export interface ITextValueValidationRule
138148
extends IBuilderFieldValidationBase<string | string[]> {
139149
minLength?: number;
140150
maxLength?: number;
141151
matches?: RegExp;
142-
range?: IBuilderRangeValidation<string>;
143152
}
144153

145-
export interface INumberFieldValidation
154+
export interface INumberValueValidationRule
146155
extends IBuilderFieldValidationBase<number | number[]> {
147156
min?: number;
148157
max?: number;
149158
integer?: boolean;
150159
positive?: boolean;
151160
negative?: boolean;
152-
range?: IBuilderRangeValidation<number>;
153161
}
154162

155-
export interface IDateFieldValidation
163+
export interface IDateValueValidationRule
156164
extends IBuilderFieldValidationBase<string | string[]> {
157165
minDate?: string | Date;
158166
maxDate?: string | Date;
159-
range?: IBuilderRangeValidation<string>;
160167
}
161168

162-
export type IBooleanFieldValidation = IBuilderFieldValidationBase<boolean>;
169+
export type IBooleanValueValidationRule = IBuilderFieldValidationBase<boolean>;
163170

164-
export type IListFieldValidation =
171+
export type IListValueValidationRule =
165172
IBuilderFieldValidationBase<string | number>;
166173

167-
export interface IMultiListFieldValidation
174+
export interface IMultiListValueValidationRule
168175
extends IBuilderFieldValidationBase<Array<string | number>> {
169176
minItems?: number;
170177
maxItems?: number;
171178
}
172179

173-
export type IStatementFieldValidation = IBuilderFieldValidationBase<string> & {
180+
export type IStatementValueValidationRule =
181+
IBuilderFieldValidationBase<string> & {
174182
minLength?: number;
175183
maxLength?: number;
176184
matches?: RegExp;
177185
};
178186

187+
export type IBuilderOperatorValidationRule<TRule> = Partial<TRule> & {
188+
operators: BuilderFieldOperator[];
189+
};
190+
191+
export interface IBuilderValidationConfig<TRule> {
192+
common?: Partial<TRule>;
193+
rules?: Array<IBuilderOperatorValidationRule<TRule>>;
194+
}
195+
196+
export interface ITextFieldValidationRule extends ITextValueValidationRule {
197+
range?: IBuilderRangeValidation<ITextValueValidationRule, string>;
198+
}
199+
200+
export interface INumberFieldValidationRule extends INumberValueValidationRule {
201+
range?: IBuilderRangeValidation<INumberValueValidationRule, number>;
202+
}
203+
204+
export interface IDateFieldValidationRule extends IDateValueValidationRule {
205+
range?: IBuilderRangeValidation<IDateValueValidationRule, string>;
206+
}
207+
208+
export type IStatementFieldValidationRule = IStatementValueValidationRule;
209+
export type IBooleanFieldValidationRule = IBooleanValueValidationRule;
210+
export type IListFieldValidationRule = IListValueValidationRule;
211+
export type IMultiListFieldValidationRule = IMultiListValueValidationRule;
212+
213+
export type ITextFieldValidation =
214+
| Partial<ITextFieldValidationRule>
215+
| IBuilderValidationConfig<ITextFieldValidationRule>;
216+
217+
export type INumberFieldValidation =
218+
| Partial<INumberFieldValidationRule>
219+
| IBuilderValidationConfig<INumberFieldValidationRule>;
220+
221+
export type IDateFieldValidation =
222+
| Partial<IDateFieldValidationRule>
223+
| IBuilderValidationConfig<IDateFieldValidationRule>;
224+
225+
export type IBooleanFieldValidation =
226+
| Partial<IBooleanFieldValidationRule>
227+
| IBuilderValidationConfig<IBooleanFieldValidationRule>;
228+
229+
export type IListFieldValidation =
230+
| Partial<IListFieldValidationRule>
231+
| IBuilderValidationConfig<IListFieldValidationRule>;
232+
233+
export type IMultiListFieldValidation =
234+
| Partial<IMultiListFieldValidationRule>
235+
| IBuilderValidationConfig<IMultiListFieldValidationRule>;
236+
237+
export type IStatementFieldValidation =
238+
| Partial<IStatementFieldValidationRule>
239+
| IBuilderValidationConfig<IStatementFieldValidationRule>;
240+
179241
interface IBuilderFieldBase<
180242
TType extends BuilderFieldType,
181243
TValue extends BuilderFieldValue | undefined,

src/index.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export type {
1212
IBuilderComponentsProps,
1313
IBuilderFieldProps,
1414
IBuilderFieldValidationBase,
15+
IBuilderOperatorValidationRule,
1516
IBuilderStateChange,
17+
IBuilderValidationConfig,
1618
IBuilderValidationContext,
1719
IBuilderValidationIssue,
1820
IBuilderValidationMessageContext,
@@ -23,17 +25,31 @@ export type {
2325
IResolvedBuilderComponentsProps,
2426
IDateFieldProps,
2527
IDateFieldValidation,
28+
IDateFieldValidationRule,
29+
IDateValueValidationRule,
2630
IGroupFieldProps,
2731
IListFieldProps,
2832
IListFieldValidation,
33+
IListFieldValidationRule,
34+
IListValueValidationRule,
2935
IMultiListFieldProps,
3036
IMultiListFieldValidation,
37+
IMultiListFieldValidationRule,
38+
IMultiListValueValidationRule,
3139
INumberFieldProps,
3240
INumberFieldValidation,
41+
INumberFieldValidationRule,
42+
INumberValueValidationRule,
3343
IStatementFieldProps,
3444
IStatementFieldValidation,
45+
IStatementFieldValidationRule,
46+
IStatementValueValidationRule,
3547
ITextFieldProps,
3648
ITextFieldValidation,
49+
ITextFieldValidationRule,
50+
ITextValueValidationRule,
51+
IBooleanFieldValidationRule,
52+
IBooleanValueValidationRule,
3753
} from './builder';
3854
export { ThemeProvider } from './theme-provider/theme-provider'
3955
export type { IThemeProviderProps } from './theme-provider/theme-provider'

src/utils/validate-builder-query.util.test.ts

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
IBuilderValidationContext,
44
} from '../builder';
55
import { strings } from '../constants/strings';
6-
import { validateBuilderQuery } from './validate-builder-query.util';
6+
import { validateBuilderQuery } from './validation/validate-builder-query.util';
77

88
describe('validateBuilderQuery', () => {
99
const context: IBuilderValidationContext = {
@@ -22,13 +22,35 @@ describe('validateBuilderQuery', () => {
2222
field: 'AMOUNT',
2323
label: 'Amount',
2424
type: 'NUMBER',
25-
operators: ['BETWEEN'],
25+
operators: ['LARGER', 'BETWEEN', 'NOT_BETWEEN'],
2626
validation: {
27-
required: true,
28-
range: {
29-
requireAscending: true,
30-
allowEqual: false,
27+
common: {
28+
required: true,
3129
},
30+
rules: [
31+
{
32+
operators: ['LARGER'],
33+
min: 10,
34+
max: 20,
35+
},
36+
{
37+
operators: ['BETWEEN', 'NOT_BETWEEN'],
38+
range: {
39+
common: {
40+
min: 10,
41+
max: 20,
42+
},
43+
start: {
44+
max: 15,
45+
},
46+
end: {
47+
min: 12,
48+
},
49+
requireAscending: true,
50+
allowEqual: false,
51+
},
52+
},
53+
],
3254
},
3355
},
3456
] as IBuilderFieldProps[],
@@ -93,7 +115,59 @@ describe('validateBuilderQuery', () => {
93115
);
94116

95117
expect(result.isValid).toEqual(false);
96-
expect(result.issuesByRuleId['rule-2'][0].code).toEqual('range_order');
118+
expect(result.issuesByRuleId['rule-2'].map((issue) => issue.code)).toEqual(
119+
expect.arrayContaining(['range_order'])
120+
);
121+
});
122+
123+
it('Applies operator-specific scalar validation for single-value operators', async () => {
124+
const result = await validateBuilderQuery(
125+
[
126+
{
127+
type: 'GROUP',
128+
value: 'AND',
129+
isNegated: false,
130+
children: [
131+
{
132+
id: 'rule-3',
133+
field: 'AMOUNT',
134+
operator: 'LARGER',
135+
value: 5,
136+
},
137+
],
138+
},
139+
],
140+
context
141+
);
142+
143+
expect(result.isValid).toEqual(false);
144+
expect(result.issuesByRuleId['rule-3'][0].code).toEqual('min');
145+
});
146+
147+
it('Applies common and boundary-specific validation for range operators', async () => {
148+
const result = await validateBuilderQuery(
149+
[
150+
{
151+
type: 'GROUP',
152+
value: 'AND',
153+
isNegated: false,
154+
children: [
155+
{
156+
id: 'rule-4',
157+
field: 'AMOUNT',
158+
operator: 'BETWEEN',
159+
value: [16, 11],
160+
},
161+
],
162+
},
163+
],
164+
context
165+
);
166+
167+
expect(result.isValid).toEqual(false);
168+
expect(result.issuesByRuleId['rule-4'].map((issue) => issue.code)).toEqual(
169+
expect.arrayContaining(['max', 'min', 'range_order'])
170+
);
97171
});
98172

99173
it('Uses localized validation messages from strings', async () => {

0 commit comments

Comments
 (0)