1 | import { ClientException, Exception } from './logger';
|
2 |
|
3 | type NumberContraints = { min?: number; max?: number };
|
4 | type StringContraints = { minLen?: number; maxLen?: number; regexp?: RegExp };
|
5 | type ArrayContraints = { minSize?: number; maxSize?: number };
|
6 | type DateContraints = { min?: Date | undefined; max?: Date | undefined };
|
7 |
|
8 | type NullableMethods<T> = {
|
9 | [P in Exclude<keyof T, 'nullable'>]: T[P] extends (value: infer Value, constraints: infer Constraints) => infer Return
|
10 | ? (value: Value | undefined, constraints: Constraints) => Return | undefined
|
11 | : never
|
12 | };
|
13 |
|
14 | class NullableAssert implements NullableMethods<Assert> {
|
15 | signedFloat(value: number | undefined, constraints?: NumberContraints) {
|
16 | return value === undefined ? undefined : this.assert.signedFloat(value, constraints);
|
17 | }
|
18 | uFloat(value: number | undefined, constraints?: NumberContraints) {
|
19 | return value === undefined ? undefined : this.assert.uFloat(value, constraints);
|
20 | }
|
21 | signedInt(value: number | undefined, constraints?: NumberContraints) {
|
22 | return value === undefined ? undefined : this.assert.signedInt(value, constraints);
|
23 | }
|
24 | uInt(value: number | undefined, constraints?: NumberContraints) {
|
25 | return value === undefined ? undefined : this.assert.uInt(value, constraints);
|
26 | }
|
27 | string(value: string | undefined, constraints?: StringContraints) {
|
28 | return value === undefined ? undefined : this.assert.string(value, constraints);
|
29 | }
|
30 | noEmptyString(value: string | undefined, constraints?: StringContraints) {
|
31 | return value === undefined ? undefined : this.assert.noEmptyString(value, constraints);
|
32 | }
|
33 | date(value: Date | undefined, constraints?: DateContraints) {
|
34 | return value === undefined ? undefined : this.assert.date(value, constraints);
|
35 | }
|
36 | array<T>(value: readonly T[] | undefined, constraints?: ArrayContraints) {
|
37 | return value === undefined ? undefined : this.assert.array(value, constraints);
|
38 | }
|
39 | noEmptyArray<T>(value: readonly T[] | undefined, constraints?: ArrayContraints) {
|
40 | return value === undefined ? undefined : this.assert.noEmptyArray(value, constraints);
|
41 | }
|
42 | bool(value: boolean | undefined) {
|
43 | return value === undefined ? undefined : this.assert.bool(value);
|
44 | }
|
45 | email(value: string | undefined) {
|
46 | return value === undefined ? undefined : this.assert.email(value);
|
47 | }
|
48 | object<T extends object>(value: T | undefined) {
|
49 | return value === undefined ? undefined : this.assert.object(value);
|
50 | }
|
51 | noEmptyObject<T extends object>(value: T | undefined) {
|
52 | return value === undefined ? undefined : this.assert.noEmptyObject(value);
|
53 | }
|
54 | union<T extends U, U>(value: T | undefined, union: readonly U[]) {
|
55 | return value === undefined ? undefined : this.assert.union(value, union);
|
56 | }
|
57 | enum(value: string | number | undefined, Enum: { [key: number]: string }) {
|
58 | return value === undefined ? undefined : this.assert.enum(value, Enum);
|
59 | }
|
60 | constructor(protected assert: Assert) {}
|
61 | }
|
62 |
|
63 | export class Assert {
|
64 | constructor(protected ErrorFactory: typeof ClientException) {}
|
65 |
|
66 | nullable = new NullableAssert(this);
|
67 |
|
68 | protected error(name: string, json: object) {
|
69 | throw new this.ErrorFactory(name, json);
|
70 | }
|
71 |
|
72 | protected validateNumber(value: number, constraints: NumberContraints | undefined) {
|
73 | if (typeof value !== 'number') this.error('Value is not a number', { value });
|
74 | if (Number.isNaN(value)) this.error('Value is NaN', { value });
|
75 | if (!Number.isFinite(value)) this.error('Value is not finite', { value });
|
76 | if (constraints !== undefined) {
|
77 | this.between(value, constraints.min, constraints.max, 'Value');
|
78 | }
|
79 | }
|
80 |
|
81 | protected between(value: number, min: number | undefined, max: number | undefined, error: string) {
|
82 | if (min !== undefined && min > value) this.error(error + ' should be >= ', { min, value });
|
83 | if (max !== undefined && max < value) this.error(error + ' should be <= ', { max, value });
|
84 | }
|
85 |
|
86 | protected validateUnsigned(value: number) {
|
87 | if (value < 0) this.error('Value should be 0 or positive', { value });
|
88 | }
|
89 |
|
90 | protected validateInt(value: number) {
|
91 | if (value !== Math.ceil(value)) this.error('Value should be integer', { value });
|
92 | }
|
93 |
|
94 | signedFloat(value: number, constraints?: NumberContraints) {
|
95 | this.validateNumber(value, constraints);
|
96 | return value;
|
97 | }
|
98 |
|
99 | uFloat(value: number, constraints?: NumberContraints) {
|
100 | this.signedFloat(value, constraints);
|
101 | this.validateUnsigned(value);
|
102 | return value;
|
103 | }
|
104 | signedInt(value: number, constraints?: NumberContraints) {
|
105 | this.validateNumber(value, constraints);
|
106 | this.validateInt(value);
|
107 | return value;
|
108 | }
|
109 | uInt(value: number, constraints?: NumberContraints) {
|
110 | this.signedInt(value, constraints);
|
111 | this.validateUnsigned(value);
|
112 | return value;
|
113 | }
|
114 | string(value: string, constraints?: StringContraints) {
|
115 | if (typeof value !== 'string') this.error('Value is not a string', { value });
|
116 | if (constraints !== undefined) {
|
117 | this.between(value.length, constraints.minLen, constraints.maxLen, 'String length');
|
118 | if (constraints.regexp !== undefined && constraints.regexp.test(value)) {
|
119 | this.error('String is not valueidated by regexp ', { regexp: constraints.regexp.toString(), value });
|
120 | }
|
121 | }
|
122 | return value;
|
123 | }
|
124 | noEmptyString(value: string, constraints?: StringContraints) {
|
125 | this.string(value, constraints);
|
126 | if (value === '') this.error('String should not be empty', { value });
|
127 | return value;
|
128 | }
|
129 | date(value: Date, constraints?: DateContraints) {
|
130 | if (!(value instanceof Date)) this.error('Value is not a date', { value });
|
131 | if (Number.isNaN(value.getTime())) this.error('Date is invalueid', { value });
|
132 | if (constraints !== undefined) {
|
133 | this.between(
|
134 | value.getTime(),
|
135 | constraints.min !== undefined ? constraints.min.getTime() : undefined,
|
136 | constraints.max !== undefined ? constraints.max.getTime() : undefined,
|
137 | 'Date',
|
138 | );
|
139 | }
|
140 | return value;
|
141 | }
|
142 | array<T>(value: readonly T[], constraints?: { minSize?: number; maxSize?: number }) {
|
143 | if (!Array.isArray(value)) this.error('Value is not an array', { value });
|
144 | if (constraints !== undefined) this.between(value.length, constraints.minSize, constraints.maxSize, 'Array.length');
|
145 | return value;
|
146 | }
|
147 | noEmptyArray<T>(value: readonly T[], constraints?: { minSize?: number; maxSize?: number }) {
|
148 | this.array(value, constraints);
|
149 | if (value.length === 0) this.error('Array should have elements', { value });
|
150 | return value;
|
151 | }
|
152 | bool(value: boolean) {
|
153 | if (typeof value !== 'boolean') this.error('Value is not a boolean', { value });
|
154 | return value;
|
155 | }
|
156 | email(value: string) {
|
157 | this.string(value);
|
158 | if (!/^\S+@\S+$/.test(value)) this.error('Value is an incorrect email', { value });
|
159 | return value;
|
160 | }
|
161 | object<T extends object>(value: T) {
|
162 | if (typeof value !== 'object' || value === null) this.error('Value is not an object', { value });
|
163 | return value;
|
164 | }
|
165 | noEmptyObject<T extends object>(value: T) {
|
166 | this.object(value);
|
167 | if (Object.keys(value).length === 0) this.error('Object should have elements', { value });
|
168 | return value;
|
169 | }
|
170 | union<T extends U, U>(value: T, union: readonly U[]): T {
|
171 | if (!union.includes(value)) this.error(`Union doesn't have specified element`, { value });
|
172 | return value;
|
173 | }
|
174 | enum(value: number | string, Enum: { [key: number]: string }) {
|
175 | if (Enum[value as number] === undefined) this.error(`Enum doesn't have specified element`, { value });
|
176 | return value;
|
177 | }
|
178 | }
|
179 |
|
180 | export const assert = new Assert(Exception);
|
181 | export const clientValidation = new Assert(ClientException);
|