Skip to content

Commit 639f67a

Browse files
feat: Added the validation for minDigits and maxDigits in a valid number (#1059)
2 parents 91ae4ee + d5cbfc0 commit 639f67a

File tree

9 files changed

+354
-8
lines changed

9 files changed

+354
-8
lines changed

apps/api/src/app/column/dtos/column-request.dto.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Type } from 'class-transformer';
1515

1616
import { ValidationTypesEnum } from '@impler/client';
1717
import { IsValidRegex } from '@shared/framework/is-valid-regex.validator';
18+
import { IsValidDigitsConstraint } from '@shared/framework/is-valid-digits.validator';
1819
import { IsNumberOrString } from '@shared/framework/number-or-string.validator';
1920
import { ColumnDelimiterEnum, ColumnTypesEnum, Defaults } from '@impler/shared';
2021

@@ -36,17 +37,25 @@ export class ValidationDto {
3637
errorMessage?: string;
3738

3839
@ApiPropertyOptional({
39-
description: 'Minimum value',
40+
description: 'Minimum value for digit validation',
4041
})
4142
@IsNumber()
4243
@IsOptional()
44+
@ValidateIf((object) => object.validate === ValidationTypesEnum.DIGITS)
45+
@Validate(IsValidDigitsConstraint, {
46+
message: 'Invalid number of digits',
47+
})
4348
min?: number;
4449

4550
@ApiPropertyOptional({
46-
description: 'Maximum value',
51+
description: 'Maximum value for digit validation',
4752
})
4853
@IsNumber()
4954
@IsOptional()
55+
@ValidateIf((object) => object.validate === ValidationTypesEnum.DIGITS)
56+
@Validate(IsValidDigitsConstraint, {
57+
message: 'Invalid number of digits',
58+
})
5059
max?: number;
5160

5261
@ApiPropertyOptional({

apps/api/src/app/review/usecases/do-review/base-review.usecase.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable multiline-comment-style */
12
import * as fs from 'fs';
23
import * as dayjs from 'dayjs';
34
import * as Papa from 'papaparse';
@@ -10,7 +11,7 @@ import Ajv, { AnySchemaObject, ErrorObject, ValidateFunction } from 'ajv';
1011
import { ValidationErrorMessages } from '@shared/types/review.types';
1112
import { ColumnTypesEnum, Defaults, ITemplateSchemaItem } from '@impler/shared';
1213
import { SManager, BATCH_LIMIT, MAIN_CODE, ExecuteIsolateResult } from '@shared/services/sandbox';
13-
import { ValidationTypesEnum, LengthValidationType, RangeValidationType } from '@impler/client';
14+
import { ValidationTypesEnum, LengthValidationType, RangeValidationType, DigitsValidationType } from '@impler/client';
1415

1516
dayjs.extend(customParseFormat);
1617

@@ -112,6 +113,9 @@ export class BaseReview {
112113
const lengthValidation = column.validations?.find(
113114
(validation) => validation.validate === ValidationTypesEnum.LENGTH
114115
) as LengthValidationType;
116+
const digitsValidation = column.validations?.find(
117+
(validation) => validation.validate === ValidationTypesEnum.DIGITS
118+
) as DigitsValidationType;
115119

116120
switch (column.type) {
117121
case ColumnTypesEnum.STRING:
@@ -122,15 +126,31 @@ export class BaseReview {
122126
};
123127
break;
124128
case ColumnTypesEnum.NUMBER:
125-
case ColumnTypesEnum.DOUBLE:
129+
case ColumnTypesEnum.DOUBLE: {
130+
const isInteger = column.type === ColumnTypesEnum.NUMBER;
131+
126132
property = {
127-
...(column.type === ColumnTypesEnum.NUMBER && { multipleOf: 1 }),
128133
type: ['number', 'null'],
129134
...(!column.isRequired && { default: null }),
135+
136+
// only enforce integer for NUMBER
137+
//...(isInteger && { multipleOf: 1 }),
138+
139+
// normal range validation (applies to both)
130140
...(typeof rangeValidation?.min === 'number' && { minimum: rangeValidation?.min }),
131141
...(typeof rangeValidation?.max === 'number' && { maximum: rangeValidation?.max }),
142+
143+
// digit validation (only for NUMBER)
144+
...(isInteger &&
145+
digitsValidation && {
146+
digitCount: {
147+
...(typeof digitsValidation.min === 'number' && { min: digitsValidation.min }),
148+
...(typeof digitsValidation.max === 'number' && { max: digitsValidation.max }),
149+
},
150+
}),
132151
};
133152
break;
153+
}
134154
case ColumnTypesEnum.SELECT:
135155
case ColumnTypesEnum.IMAGE:
136156
const selectValues =
@@ -263,6 +283,30 @@ export class BaseReview {
263283
validationErrorMessages?.[field]?.[ValidationTypesEnum.RANGE] ||
264284
`${String(data)} must be greater than or equal to ${error.params.limit}`;
265285
break;
286+
case error.keyword === 'digitCount': {
287+
const customMessage = validationErrorMessages?.[field]?.[ValidationTypesEnum.DIGITS];
288+
if (customMessage) {
289+
message = customMessage;
290+
} else {
291+
const minDigits = error.parentSchema.digitCount.min;
292+
const maxDigits = error.parentSchema.digitCount.max;
293+
294+
if (minDigits !== undefined && maxDigits !== undefined) {
295+
if (minDigits === maxDigits) {
296+
message = `Must have exactly ${minDigits} digits`;
297+
} else {
298+
message = `Must have between ${minDigits} and ${maxDigits} digits`;
299+
}
300+
} else if (minDigits !== undefined) {
301+
message = `Must have at least ${minDigits} digits`;
302+
} else if (maxDigits !== undefined) {
303+
message = `Must have at most ${maxDigits} digits`;
304+
} else {
305+
message = 'Invalid number of digits';
306+
}
307+
}
308+
break;
309+
}
266310
// empty string case
267311
case error.keyword === 'emptyCheck':
268312
case error.keyword === 'required':
@@ -548,6 +592,25 @@ export class BaseReview {
548592
},
549593
});
550594

595+
ajv.addKeyword({
596+
keyword: 'digitCount',
597+
type: 'number',
598+
schemaType: 'object',
599+
// schema = { min: number, max: number }
600+
validate: (schema: { min?: number; max?: number }, data: number) => {
601+
if (data === null || data === undefined) return true;
602+
603+
// Count digits (ignore sign and decimal part)
604+
const digits = Math.floor(Math.log10(Math.abs(data))) + 1;
605+
606+
if (schema.min !== undefined && digits < schema.min) return false;
607+
if (schema.max !== undefined && digits > schema.max) return false;
608+
609+
return true;
610+
},
611+
errors: true,
612+
});
613+
551614
const valuesMap = new Map();
552615
Object.keys(uniqueCombinations).forEach((keyword) => {
553616
valuesMap.set(keyword, new Set());
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/* eslint-disable multiline-comment-style */
2+
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
3+
4+
@ValidatorConstraint({ name: 'isValidDigits', async: false })
5+
export class IsValidDigitsConstraint implements ValidatorConstraintInterface {
6+
validate(value: any, args: ValidationArguments) {
7+
if (value === undefined || value === null || value === '') {
8+
return true;
9+
}
10+
11+
const strValue = String(value).replace(/[^0-9]/g, '');
12+
13+
if (strValue === '' || isNaN(Number(strValue))) {
14+
return false;
15+
}
16+
17+
const validation = args.object as any;
18+
const numDigits = strValue.length;
19+
20+
// if (validation.min !== undefined && numDigits < validation.min) {
21+
// console.log(`Validation failed: Number has fewer than ${validation.min} digits`);
22+
// return false;
23+
// }
24+
25+
if (validation.max !== undefined && numDigits > validation.max) {
26+
return false;
27+
}
28+
29+
return true;
30+
}
31+
32+
defaultMessage(args: ValidationArguments) {
33+
const validation = args.object as any;
34+
35+
if (validation.min !== undefined && validation.max !== undefined) {
36+
return `Number must have between ${validation.min} and ${validation.max} digits`;
37+
} else if (validation.min !== undefined) {
38+
return `Number must have at least ${validation.min} digits`;
39+
} else if (validation.max !== undefined) {
40+
return `Number must have at most ${validation.max} digits`;
41+
}
42+
43+
return 'Invalid number of digits';
44+
}
45+
}

apps/web/components/imports/forms/ColumnForm.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,41 @@ export function ColumnForm({ onSubmit, data, isLoading }: ColumnFormProps) {
4848
useEffect(() => {
4949
const rangeValidationIndex = fields.findIndex((field) => field.validate === ValidationTypesEnum.RANGE);
5050
const lengthValidationIndex = fields.findIndex((field) => field.validate === ValidationTypesEnum.LENGTH);
51+
const digitsValidationIndex = fields.findIndex((field) => field.validate === ValidationTypesEnum.DIGITS);
52+
5153
switch (typeValue) {
5254
case ColumnTypesEnum.STRING:
5355
if (rangeValidationIndex > -1) {
5456
remove(rangeValidationIndex);
5557
}
58+
if (digitsValidationIndex > -1) {
59+
remove(digitsValidationIndex);
60+
}
5661
break;
5762
case ColumnTypesEnum.DOUBLE:
5863
case ColumnTypesEnum.NUMBER:
5964
if (lengthValidationIndex > -1) {
6065
remove(lengthValidationIndex);
6166
}
6267
break;
68+
case ColumnTypesEnum.DOUBLE:
69+
if (lengthValidationIndex > -1) {
70+
remove(lengthValidationIndex);
71+
}
72+
if (digitsValidationIndex > -1) {
73+
remove(digitsValidationIndex);
74+
}
75+
break;
6376
default:
6477
if (rangeValidationIndex > -1) {
6578
remove(rangeValidationIndex);
6679
}
6780
if (lengthValidationIndex > -1) {
6881
remove(lengthValidationIndex);
6982
}
83+
if (digitsValidationIndex > -1) {
84+
remove(digitsValidationIndex);
85+
}
7086
break;
7187
}
7288
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -317,6 +333,43 @@ export function ColumnForm({ onSubmit, data, isLoading }: ColumnFormProps) {
317333
}}
318334
/>
319335
</AutoHeightComponent>
336+
<AutoHeightComponent isVisible={typeValue === ColumnTypesEnum.NUMBER}>
337+
<Validation
338+
errors={errors}
339+
control={control}
340+
min={1}
341+
max={10}
342+
minPlaceholder="Min digits (e.g. 1)"
343+
maxPlaceholder="Max digits (e.g. 10)"
344+
label="Number of Digits Validation"
345+
type={ValidationTypesEnum.DIGITS}
346+
unavailable={advancedValidationsUnavailable}
347+
link={DOCUMENTATION_REFERENCE_LINKS.lengthValidator}
348+
description="Set min/max digit count for valid numbers"
349+
errorMessagePlaceholder='Number must have between "Min" and "Max" digits'
350+
index={fields.findIndex((field) => field.validate === ValidationTypesEnum.DIGITS)}
351+
onCheckToggle={(status) => {
352+
if (status) {
353+
// Only add DIGITS validation if it doesn't already exist
354+
const exists = fields.some((field) => field.validate === ValidationTypesEnum.DIGITS);
355+
if (!exists) {
356+
append({
357+
validate: ValidationTypesEnum.DIGITS,
358+
min: 1,
359+
max: 10,
360+
errorMessage: 'Number must have between 1 and 10 digits',
361+
});
362+
}
363+
} else {
364+
// Remove the DIGITS validation using the correct index
365+
const digitsIndex = fields.findIndex((field) => field.validate === ValidationTypesEnum.DIGITS);
366+
if (digitsIndex > -1) {
367+
remove(digitsIndex);
368+
}
369+
}
370+
}}
371+
/>
372+
</AutoHeightComponent>
320373
<AutoHeightComponent isVisible={typeValue === ColumnTypesEnum.STRING}>
321374
<Validation
322375
min={0}

apps/web/components/imports/schema/ValidationsGroup.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,31 @@ function MinMaxValidationBadge({ max, min }: IMinMaxValidationBadgeProps) {
3838
return null;
3939
}
4040

41+
function DigitsValidationBadge({ min, max }: IMinMaxValidationBadgeProps) {
42+
if (typeof min === 'number' && typeof max === 'number')
43+
return (
44+
<Badge key={ValidationTypesEnum.DIGITS} color="pink" variant="filled">
45+
Digits: {min}-{max}
46+
</Badge>
47+
);
48+
49+
if (typeof min === 'number')
50+
return (
51+
<Badge key={ValidationTypesEnum.DIGITS} color="pink" variant="filled">
52+
Min digits: {min}
53+
</Badge>
54+
);
55+
56+
if (typeof max === 'number')
57+
return (
58+
<Badge key={ValidationTypesEnum.DIGITS} color="pink" variant="filled">
59+
Max digits: {max}
60+
</Badge>
61+
);
62+
63+
return null;
64+
}
65+
4166
export function ValidationsGroup({ item }: IValidationsGroupProps) {
4267
return (
4368
<Group spacing={5}>
@@ -63,6 +88,8 @@ export function ValidationsGroup({ item }: IValidationsGroupProps) {
6388
return <MinMaxValidationBadge min={validation.min} max={validation.max} key={ValidationTypesEnum.RANGE} />;
6489
} else if (validation.validate === ValidationTypesEnum.LENGTH) {
6590
return <MinMaxValidationBadge min={validation.min} max={validation.max} key={ValidationTypesEnum.LENGTH} />;
91+
} else if (validation.validate === ValidationTypesEnum.DIGITS) {
92+
return <DigitsValidationBadge min={validation.min} max={validation.max} key={ValidationTypesEnum.DIGITS} />;
6693
}
6794
}) || []}
6895
</Group>

0 commit comments

Comments
 (0)