@player-ui/common-types-plugin 0.0.1-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs.js +756 -0
- package/dist/index.d.ts +250 -0
- package/dist/index.esm.js +743 -0
- package/package.json +19 -0
- package/src/data-types/refs.ts +33 -0
- package/src/data-types/types.ts +113 -0
- package/src/formats/index.ts +425 -0
- package/src/formats/utils.ts +192 -0
- package/src/index.ts +27 -0
- package/src/validators/index.ts +366 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { FormatType } from '@player-ui/schema';
|
|
2
|
+
|
|
3
|
+
export const PLACEHOLDER = '#';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Remove any formatting characters in the 'mask' from 'value'
|
|
7
|
+
*
|
|
8
|
+
* @param value - The string to remove the control characters from
|
|
9
|
+
* @param mask - The mask to use and test against
|
|
10
|
+
* @param reserved - The reserved _slots_ (these are the chars that you expect new values to be subbed into)
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* removeFormatCharactersFromMaskedString('123-456', '###-###', ['#']) => '123456'
|
|
14
|
+
*/
|
|
15
|
+
export const removeFormatCharactersFromMaskedString = (
|
|
16
|
+
value: string,
|
|
17
|
+
mask: string,
|
|
18
|
+
reserved: string[] = [PLACEHOLDER]
|
|
19
|
+
): string => {
|
|
20
|
+
return value.split('').reduce((newString, nextChar, nextIndex) => {
|
|
21
|
+
const maskedVal = mask[nextIndex];
|
|
22
|
+
|
|
23
|
+
if (reserved.includes(maskedVal)) {
|
|
24
|
+
return newString + nextChar;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return newString;
|
|
28
|
+
}, '');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format the given string using one of the accepted values
|
|
33
|
+
* Optionally, the value can be choose to ignore case when formatting, or to autocomplete if only 1 option is viable
|
|
34
|
+
* If no such option is viable, undefined is returned
|
|
35
|
+
*/
|
|
36
|
+
export const formatAsEnum = (
|
|
37
|
+
value: string,
|
|
38
|
+
acceptedValues: string[],
|
|
39
|
+
options?: {
|
|
40
|
+
/** Ignore the case of the provided value when comparing to the acceptedValues */
|
|
41
|
+
ignoreCase?: boolean;
|
|
42
|
+
|
|
43
|
+
/** If only 1 option is viable, autocomplete the value to the accepted one */
|
|
44
|
+
autocomplete?: boolean;
|
|
45
|
+
}
|
|
46
|
+
): string | undefined => {
|
|
47
|
+
const autoCompletionsByOverlapCount = acceptedValues
|
|
48
|
+
.reduce<
|
|
49
|
+
Array<{
|
|
50
|
+
/** The size of the overlap (ranking) */
|
|
51
|
+
count: number;
|
|
52
|
+
|
|
53
|
+
/** One of the acceptedValues */
|
|
54
|
+
target: string;
|
|
55
|
+
}>
|
|
56
|
+
>((validCompletions, validValue) => {
|
|
57
|
+
let overlap = 0;
|
|
58
|
+
|
|
59
|
+
for (
|
|
60
|
+
let charIndex = 0;
|
|
61
|
+
charIndex < Math.min(validValue.length, value.length);
|
|
62
|
+
charIndex++
|
|
63
|
+
) {
|
|
64
|
+
const validChar = options?.ignoreCase
|
|
65
|
+
? validValue[charIndex].toLowerCase()
|
|
66
|
+
: validValue[charIndex];
|
|
67
|
+
const actualChar = options?.ignoreCase
|
|
68
|
+
? value[charIndex].toLowerCase()
|
|
69
|
+
: value[charIndex];
|
|
70
|
+
|
|
71
|
+
if (validChar !== actualChar) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
overlap += 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (overlap === 0) {
|
|
79
|
+
return validCompletions;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
...validCompletions,
|
|
84
|
+
{
|
|
85
|
+
count: overlap,
|
|
86
|
+
target: validValue,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
}, [])
|
|
90
|
+
.sort((e) => e.count);
|
|
91
|
+
|
|
92
|
+
if (autoCompletionsByOverlapCount.length === 0) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (autoCompletionsByOverlapCount.length === 1 && options?.autocomplete) {
|
|
97
|
+
return autoCompletionsByOverlapCount[0].target;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return autoCompletionsByOverlapCount[0].target.substr(
|
|
101
|
+
0,
|
|
102
|
+
autoCompletionsByOverlapCount[0].count
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Format the given value using the mask + match
|
|
108
|
+
*
|
|
109
|
+
* @param value - The string value to format
|
|
110
|
+
* @param valueCharMaskMatch - A regular expression that matches characters to substitute in the match. This is typically `/\d/g` or `/\w/g`
|
|
111
|
+
* @param mask - The mask to format against. Use # as a placeholder for
|
|
112
|
+
*/
|
|
113
|
+
export const formatAsMasked = (
|
|
114
|
+
value: string | number,
|
|
115
|
+
valueCharMaskMatch: RegExp,
|
|
116
|
+
mask: string
|
|
117
|
+
): string => {
|
|
118
|
+
const valStr = String(value);
|
|
119
|
+
let withMask = mask;
|
|
120
|
+
|
|
121
|
+
if (valStr.trim() === '') {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
valStr.replace(valueCharMaskMatch, (match) => {
|
|
126
|
+
withMask = withMask.replace(PLACEHOLDER, match);
|
|
127
|
+
|
|
128
|
+
return match;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return withMask.split(PLACEHOLDER)[0];
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Creates a format definition with the given mask
|
|
136
|
+
* Use the `#` char as a placeholder for a number
|
|
137
|
+
*/
|
|
138
|
+
export const createMaskedNumericFormatter = (
|
|
139
|
+
name: string,
|
|
140
|
+
mask: string
|
|
141
|
+
): FormatType<
|
|
142
|
+
string,
|
|
143
|
+
string,
|
|
144
|
+
{
|
|
145
|
+
/** An enum of values that are also acceptable, and don't fall under the mask */
|
|
146
|
+
exceptions?: Array<string>;
|
|
147
|
+
}
|
|
148
|
+
> => {
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
format: (value, options) => {
|
|
152
|
+
if (typeof value !== 'string') {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options?.exceptions && options.exceptions.length > 0) {
|
|
157
|
+
const formattedUsingExceptions = formatAsEnum(
|
|
158
|
+
value,
|
|
159
|
+
options.exceptions,
|
|
160
|
+
{
|
|
161
|
+
autocomplete: true,
|
|
162
|
+
ignoreCase: true,
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (formattedUsingExceptions !== undefined) {
|
|
167
|
+
return formattedUsingExceptions;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return formatAsMasked(value, /\d/g, mask);
|
|
172
|
+
},
|
|
173
|
+
deformat: (value, options) => {
|
|
174
|
+
if (typeof value !== 'string') {
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (options?.exceptions && options.exceptions.length > 0) {
|
|
179
|
+
const usingExceptions = formatAsEnum(value, options.exceptions, {
|
|
180
|
+
autocomplete: false,
|
|
181
|
+
ignoreCase: false,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (usingExceptions !== undefined) {
|
|
185
|
+
return usingExceptions;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return removeFormatCharactersFromMaskedString(value, mask);
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Player, PlayerPlugin } from '@player-ui/player';
|
|
2
|
+
import { TypesProviderPlugin } from '@player-ui/types-provider-plugin';
|
|
3
|
+
|
|
4
|
+
import * as validators from './validators';
|
|
5
|
+
import * as dataTypes from './data-types/types';
|
|
6
|
+
import * as dataRefs from './data-types/refs';
|
|
7
|
+
import * as formats from './formats';
|
|
8
|
+
|
|
9
|
+
export { validators, dataTypes, dataRefs, formats };
|
|
10
|
+
export * from './formats/utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Exposes a lot of common DataTypes, validations, and formats to Player instance.
|
|
14
|
+
*/
|
|
15
|
+
export class CommonTypesPlugin implements PlayerPlugin {
|
|
16
|
+
name = 'CommonTypes';
|
|
17
|
+
|
|
18
|
+
apply(player: Player) {
|
|
19
|
+
player.registerPlugin(
|
|
20
|
+
new TypesProviderPlugin({
|
|
21
|
+
types: Object.values(dataTypes),
|
|
22
|
+
formats: Object.values(formats),
|
|
23
|
+
validators: Object.entries(validators),
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type { Expression } from '@player-ui/types';
|
|
2
|
+
import type { ValidatorFunction } from '@player-ui/validator';
|
|
3
|
+
|
|
4
|
+
// Shamelessly lifted from Scott Gonzalez via the Bassistance Validation plugin http://projects.scottsplayground.com/email_address_validation/
|
|
5
|
+
|
|
6
|
+
const EMAIL_REGEX =
|
|
7
|
+
/^((([a-z]|\d|[!#$%&'*+\-/=?^_`{|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#$%&'*+-/=?^_`{|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i;
|
|
8
|
+
const PHONE_REGEX = /^\+?[1]?[- ]?\(?\d{3}[)\- ]?\s?\d{3}[ -]?\d{4}$/;
|
|
9
|
+
const ZIP_REGEX = /^\d{5}(-\d{4})?$/;
|
|
10
|
+
|
|
11
|
+
/** Skip any null or undefined value when running the validator */
|
|
12
|
+
function skipNullish<T>(
|
|
13
|
+
validationFn: ValidatorFunction<T>
|
|
14
|
+
): ValidatorFunction<T> {
|
|
15
|
+
return (context, value, options) => {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return validationFn(context, value, options);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Checks to see if the data-type is a string */
|
|
25
|
+
export const string: ValidatorFunction = skipNullish((context, value) => {
|
|
26
|
+
if (typeof value !== 'string') {
|
|
27
|
+
const message = context.constants.getConstants(
|
|
28
|
+
'validation.string',
|
|
29
|
+
'constants',
|
|
30
|
+
'Value must be a string'
|
|
31
|
+
) as string;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
message,
|
|
35
|
+
parameters: {
|
|
36
|
+
type: typeof value,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Validation for a non-mutable property */
|
|
43
|
+
export const readonly: ValidatorFunction = (context) => {
|
|
44
|
+
const message = context.constants.getConstants(
|
|
45
|
+
'validation.readonly',
|
|
46
|
+
'constants',
|
|
47
|
+
'Value cannot be modified'
|
|
48
|
+
) as string;
|
|
49
|
+
|
|
50
|
+
return { message };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Check to see if the value represents an array of items */
|
|
54
|
+
export const collection: ValidatorFunction = skipNullish((context, value) => {
|
|
55
|
+
if (!Array.isArray(value)) {
|
|
56
|
+
const message = context.constants.getConstants(
|
|
57
|
+
'validation.collection',
|
|
58
|
+
'constants',
|
|
59
|
+
'Cannot set collection to non-array'
|
|
60
|
+
) as string;
|
|
61
|
+
|
|
62
|
+
return { message };
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** Checks to see if the value is an integer */
|
|
67
|
+
export const integer: ValidatorFunction = skipNullish((context, value) => {
|
|
68
|
+
if (
|
|
69
|
+
value &&
|
|
70
|
+
(typeof value !== 'number' ||
|
|
71
|
+
Math.floor(value) !== value ||
|
|
72
|
+
Number(value) > Number.MAX_SAFE_INTEGER ||
|
|
73
|
+
Number(value) < Number.MIN_SAFE_INTEGER)
|
|
74
|
+
) {
|
|
75
|
+
const message = context.constants.getConstants(
|
|
76
|
+
'validation.integer',
|
|
77
|
+
'constants',
|
|
78
|
+
'Value must be an integer'
|
|
79
|
+
) as string;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
message,
|
|
83
|
+
parameters: {
|
|
84
|
+
type: typeof value,
|
|
85
|
+
flooredValue: Math.floor(value),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** An enum check to see if the value is in a provided list of acceptable options */
|
|
92
|
+
export const oneOf: ValidatorFunction<{
|
|
93
|
+
/** The enum values that are acceptable */
|
|
94
|
+
options: Array<unknown>;
|
|
95
|
+
}> = skipNullish((context, value, options) => {
|
|
96
|
+
if (options?.options === undefined || options.options?.includes(value)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const message = context.constants.getConstants(
|
|
101
|
+
'validation.oneOf',
|
|
102
|
+
'constants',
|
|
103
|
+
'Invalid entry'
|
|
104
|
+
) as string;
|
|
105
|
+
|
|
106
|
+
return { message };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** A validator that evaluates an expression for validation */
|
|
110
|
+
export const expression: ValidatorFunction<{
|
|
111
|
+
/**
|
|
112
|
+
* The expression to evaluate.
|
|
113
|
+
* Falsy values indicate an invalid response
|
|
114
|
+
*/
|
|
115
|
+
exp: Expression;
|
|
116
|
+
}> = (context, value, options?) => {
|
|
117
|
+
if (options?.exp === undefined) {
|
|
118
|
+
context.logger.warn('No expression defined for validation');
|
|
119
|
+
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = context.evaluate(options.exp);
|
|
124
|
+
|
|
125
|
+
if (!result) {
|
|
126
|
+
const message = context.constants.getConstants(
|
|
127
|
+
'validation.expression',
|
|
128
|
+
'constants',
|
|
129
|
+
'Expression evaluation failed'
|
|
130
|
+
) as string;
|
|
131
|
+
|
|
132
|
+
return { message };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/** A validator that requires a non-null value */
|
|
137
|
+
export const required: ValidatorFunction<{
|
|
138
|
+
/** An optional expression to limit the required check only if true */
|
|
139
|
+
if?: Expression;
|
|
140
|
+
|
|
141
|
+
/** An optional expression to limit the required check only if false */
|
|
142
|
+
ifNot?: Expression;
|
|
143
|
+
}> = (context, value, options) => {
|
|
144
|
+
if (
|
|
145
|
+
(options?.if && !context.evaluate(options.if)) ||
|
|
146
|
+
(options?.ifNot && context.evaluate(options.ifNot))
|
|
147
|
+
) {
|
|
148
|
+
// Skipping due to if check
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (value === undefined || value === null || value === '') {
|
|
153
|
+
const message = context.constants.getConstants(
|
|
154
|
+
'validation.required',
|
|
155
|
+
'constants',
|
|
156
|
+
'A value is required'
|
|
157
|
+
) as string;
|
|
158
|
+
|
|
159
|
+
return { message, severity: 'error' };
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** A validator that uses a regular expression */
|
|
164
|
+
export const regex: ValidatorFunction<{
|
|
165
|
+
/**
|
|
166
|
+
* The regular expression to test: /pattern/
|
|
167
|
+
* Can optionally include flags after the pattern: /pattern/flags
|
|
168
|
+
*/
|
|
169
|
+
regex: string;
|
|
170
|
+
}> = skipNullish((context, value, options) => {
|
|
171
|
+
if (
|
|
172
|
+
value === undefined ||
|
|
173
|
+
value === null ||
|
|
174
|
+
value === '' ||
|
|
175
|
+
typeof options?.regex !== 'string'
|
|
176
|
+
) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Split up /pattern/flags into [pattern, flags]
|
|
181
|
+
const patternMatch = options.regex.match(/^\/(.*)\/(\w)*$/);
|
|
182
|
+
|
|
183
|
+
const regexp = patternMatch
|
|
184
|
+
? new RegExp(patternMatch[1], patternMatch[2])
|
|
185
|
+
: new RegExp(options.regex);
|
|
186
|
+
|
|
187
|
+
if (!regexp.test(value)) {
|
|
188
|
+
const message = context.constants.getConstants(
|
|
189
|
+
'validation.regex',
|
|
190
|
+
'constants',
|
|
191
|
+
'Invalid entry'
|
|
192
|
+
) as string;
|
|
193
|
+
|
|
194
|
+
return { message };
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/** Checks the length of a value */
|
|
199
|
+
export const length: ValidatorFunction<
|
|
200
|
+
| {
|
|
201
|
+
/** The minimum length to check against */
|
|
202
|
+
min?: number;
|
|
203
|
+
|
|
204
|
+
/** The maximum length to check against */
|
|
205
|
+
max?: number;
|
|
206
|
+
}
|
|
207
|
+
| {
|
|
208
|
+
/** The exact length to match against */
|
|
209
|
+
exact: number;
|
|
210
|
+
}
|
|
211
|
+
> = skipNullish((context, value, options) => {
|
|
212
|
+
if (typeof options !== 'object') {
|
|
213
|
+
context.logger.warn('Missing comparison in length validation');
|
|
214
|
+
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let valLength: number | undefined;
|
|
219
|
+
let itemName = 'items';
|
|
220
|
+
|
|
221
|
+
if (typeof value === 'string') {
|
|
222
|
+
valLength = value.length;
|
|
223
|
+
itemName = 'characters';
|
|
224
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
225
|
+
valLength = Object.keys(value).length;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (valLength === undefined) {
|
|
229
|
+
context.logger.warn(
|
|
230
|
+
`Unable to determine a length for value of type: ${value}`
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if ('exact' in options) {
|
|
237
|
+
if (valLength !== options.exact) {
|
|
238
|
+
return {
|
|
239
|
+
message: `Must be exactly ${options.exact} ${itemName} long`,
|
|
240
|
+
parameters: {
|
|
241
|
+
validationLength: valLength,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (options.min !== undefined && valLength < options.min) {
|
|
250
|
+
const message = context.constants.getConstants(
|
|
251
|
+
'validation.length.minimum',
|
|
252
|
+
'constants',
|
|
253
|
+
`At least ${options.min} ${itemName} needed`
|
|
254
|
+
) as string;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
message,
|
|
258
|
+
parameters: {
|
|
259
|
+
validationLength: valLength,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (options.max !== undefined && valLength > options.max) {
|
|
265
|
+
const message = context.constants.getConstants(
|
|
266
|
+
'validation.length.maximum',
|
|
267
|
+
'constants',
|
|
268
|
+
`Up to ${options.max} ${itemName} allowed`
|
|
269
|
+
) as string;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
message,
|
|
273
|
+
parameters: {
|
|
274
|
+
validationLength: valLength,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Checks that the given value is at least the expected one
|
|
282
|
+
*/
|
|
283
|
+
export const min: ValidatorFunction<{
|
|
284
|
+
/** The minimum value */
|
|
285
|
+
value: number;
|
|
286
|
+
}> = skipNullish((context, value, options) => {
|
|
287
|
+
if (typeof value !== 'number' || options?.value === undefined) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (value < options.value) {
|
|
292
|
+
const message = context.constants.getConstants(
|
|
293
|
+
'validation.min',
|
|
294
|
+
'constants',
|
|
295
|
+
`Must be at least ${options.value}`
|
|
296
|
+
) as string;
|
|
297
|
+
|
|
298
|
+
return { message };
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Checks that the given value is at least the expected one
|
|
304
|
+
*/
|
|
305
|
+
export const max: ValidatorFunction<{
|
|
306
|
+
/** The minimum value */
|
|
307
|
+
value: number;
|
|
308
|
+
}> = skipNullish((context, value, options) => {
|
|
309
|
+
if (typeof value !== 'number' || options?.value === undefined) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (value > options.value) {
|
|
314
|
+
const message = context.constants.getConstants(
|
|
315
|
+
'validation.max',
|
|
316
|
+
'constants',
|
|
317
|
+
`Cannot exceed ${options.value}`
|
|
318
|
+
) as string;
|
|
319
|
+
|
|
320
|
+
return { message };
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
/** Create a validator using a regular expression */
|
|
325
|
+
const stringRegexValidator = (
|
|
326
|
+
test: RegExp,
|
|
327
|
+
messagePath: string,
|
|
328
|
+
invalidMessage: string
|
|
329
|
+
): ValidatorFunction => {
|
|
330
|
+
return skipNullish((context, value) => {
|
|
331
|
+
if (typeof value === 'string' && value === '') {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (typeof value !== 'string' || !test.test(value)) {
|
|
336
|
+
const message = context.constants.getConstants(
|
|
337
|
+
messagePath,
|
|
338
|
+
'constants',
|
|
339
|
+
invalidMessage
|
|
340
|
+
) as string;
|
|
341
|
+
|
|
342
|
+
return { message };
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Checks that the given value represents an email */
|
|
348
|
+
export const email = stringRegexValidator(
|
|
349
|
+
EMAIL_REGEX,
|
|
350
|
+
'validation.email',
|
|
351
|
+
'Improper email format'
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
/** Checks that the given value represents a phone number */
|
|
355
|
+
export const phone = stringRegexValidator(
|
|
356
|
+
PHONE_REGEX,
|
|
357
|
+
'validation.phone',
|
|
358
|
+
'Invalid phone number'
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
/** Checks that the given value represents a phone number */
|
|
362
|
+
export const zip = stringRegexValidator(
|
|
363
|
+
ZIP_REGEX,
|
|
364
|
+
'validation.regex',
|
|
365
|
+
'Invalid zip code'
|
|
366
|
+
);
|