@cleverbrush/schema 0.0.3

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.
@@ -0,0 +1,256 @@
1
+ import { deepExtend } from '@cleverbrush/deep';
2
+ import {
3
+ ISchemasProvider,
4
+ Schema,
5
+ ISchemaActions,
6
+ ObjectSchemaDefinitionParam,
7
+ ValidationResult,
8
+ NumberSchemaDefinition,
9
+ StringSchemaDefinition,
10
+ CompositeSchema,
11
+ ValidationResultRaw,
12
+ ArraySchemaDefinition,
13
+ ISchemaValidator,
14
+ DefaultSchemaType,
15
+ ObjectSchemaDefinition
16
+ } from './index';
17
+ import { validateNumber } from './validators/validateNumber';
18
+ import { validateString } from './validators/validateString';
19
+ import { validateArray } from './validators/validateArray';
20
+ import { validateObject } from './validators/validateObject';
21
+
22
+ const defaultSchemaNames = ['string', 'number', 'date', 'array'];
23
+
24
+ const defaultSchemas: { [key in DefaultSchemaType]?: Schema<any> } = {
25
+ number: {
26
+ type: 'number',
27
+ isRequired: true,
28
+ isNullable: false,
29
+ ensureNotNaN: true,
30
+ ensureIsFinite: true
31
+ },
32
+ string: {
33
+ type: 'string',
34
+ isNullable: false,
35
+ isRequired: true
36
+ },
37
+ array: {
38
+ type: 'array'
39
+ },
40
+ object: {
41
+ type: 'object',
42
+ isNullable: false,
43
+ isRequired: true
44
+ }
45
+ };
46
+
47
+ const defaultSchemasValidationStrategies = {
48
+ number: (
49
+ obj: any,
50
+ schema: NumberSchemaDefinition<any>,
51
+ validator: ISchemaValidator<any>
52
+ ): Promise<ValidationResult> => validateNumber(obj, schema, validator),
53
+ string: (
54
+ obj: any,
55
+ schema: StringSchemaDefinition<any>,
56
+ validator: ISchemaValidator<any>
57
+ ): Promise<ValidationResult> => validateString(obj, schema, validator),
58
+ array: (
59
+ obj: any,
60
+ schema: ArraySchemaDefinition<any>,
61
+ validator: ISchemaValidator<any>
62
+ ): Promise<ValidationResult> => validateArray(obj, schema, validator)
63
+ };
64
+
65
+ const isDefaultType = (name: string): boolean =>
66
+ defaultSchemaNames.indexOf(name) !== -1;
67
+
68
+ export default class SchemaValidator<T = Record<string, never>>
69
+ implements ISchemasProvider<T>, ISchemaValidator<T>
70
+ {
71
+ private _schemasMap = new Map<string, Schema<any>>();
72
+ private _schemasCache: { [K in keyof T]: ISchemaActions<T, K> } = null;
73
+
74
+ public addSchemaType<K, L extends ObjectSchemaDefinitionParam<M>, M = any>(
75
+ name: keyof K,
76
+ schema: L
77
+ ): SchemaValidator<T & { [key in keyof K]: L }> {
78
+ if (typeof name !== 'string' || !name)
79
+ throw new Error('Name is required');
80
+ if (typeof schema !== 'object') throw new Error('Object is required');
81
+ if (isDefaultType(name.toString()))
82
+ throw new Error(
83
+ `You can't add a schema named "${name}" because it's a name of a default schema, please consider another name to be used`
84
+ );
85
+ if (this._schemasMap.has(name.toString())) {
86
+ throw new Error(`Schema "${name}" already exists`);
87
+ }
88
+
89
+ this._schemasMap.set(name.toString(), {
90
+ ...schema,
91
+ type: 'object'
92
+ } as Schema<any>);
93
+ this._schemasCache = null;
94
+
95
+ return this as any as SchemaValidator<T & { [key in keyof K]: L }>;
96
+ }
97
+
98
+ public get schemas(): { [K in keyof T]: ISchemaActions<T, K> } {
99
+ if (this._schemasCache) return this._schemasCache;
100
+ const res = {};
101
+ for (const key of this._schemasMap.keys()) {
102
+ res[key] = {
103
+ validate: (value: any): Promise<any> =>
104
+ this.validate(this._schemasMap.get(key), value),
105
+ schema: this._schemasMap.get(key)
106
+ };
107
+ }
108
+ this._schemasCache = res as { [K in keyof T]: ISchemaActions<T, K> };
109
+ return this._schemasCache;
110
+ }
111
+
112
+ private async validateDefaultType(
113
+ name: DefaultSchemaType,
114
+ value: any,
115
+ mergeSchema?: Schema<any>
116
+ ): Promise<ValidationResult> {
117
+ const strategy = defaultSchemasValidationStrategies[name] as (
118
+ obj: any,
119
+ schema: Schema<any>,
120
+ validator: ISchemaValidator<any>
121
+ ) => ValidationResult;
122
+ let finalSchema = defaultSchemas[name] as CompositeSchema<
123
+ Record<string, never>
124
+ >;
125
+ if (strategy) {
126
+ if (typeof mergeSchema === 'object') {
127
+ finalSchema = deepExtend(finalSchema, mergeSchema);
128
+ }
129
+ if (typeof mergeSchema === 'number') {
130
+ finalSchema = {
131
+ ...finalSchema,
132
+ equals: mergeSchema
133
+ } as NumberSchemaDefinition<any>;
134
+ mergeSchema;
135
+ }
136
+
137
+ let preliminaryResult = await strategy(value, finalSchema, this);
138
+ if (!preliminaryResult.valid) return preliminaryResult;
139
+
140
+ preliminaryResult = await this.checkValidators(finalSchema, value);
141
+
142
+ return preliminaryResult;
143
+ }
144
+ throw new Error('not implemented');
145
+ }
146
+
147
+ private async checkValidators(
148
+ schema: CompositeSchema<Record<string, never>>,
149
+ value: any
150
+ ): Promise<ValidationResult> {
151
+ if (Array.isArray(schema.validators)) {
152
+ const validatorsResults = await Promise.allSettled(
153
+ schema.validators.map((v) => Promise.resolve(v(value)))
154
+ );
155
+ const rejections = validatorsResults
156
+ .filter((f) => f.status === 'rejected')
157
+ .map((f: PromiseRejectedResult) => f.reason);
158
+ const errors = validatorsResults
159
+ .filter(
160
+ (f) =>
161
+ f.status === 'fulfilled' &&
162
+ typeof f.value !== 'boolean' &&
163
+ f.value.valid === false
164
+ )
165
+ .map(
166
+ (f: PromiseFulfilledResult<ValidationResultRaw>) => f.value
167
+ )
168
+ .map((f: ValidationResultRaw) => f.errors);
169
+
170
+ if (rejections.length === 0 && errors.length === 0) {
171
+ return {
172
+ valid: true
173
+ };
174
+ }
175
+ return {
176
+ valid: false,
177
+ errors: [...rejections, ...errors]
178
+ };
179
+ }
180
+ return {
181
+ valid: true
182
+ };
183
+ }
184
+
185
+ public async validate(
186
+ schema: keyof T | DefaultSchemaType | Schema<any>,
187
+ obj: any
188
+ ): Promise<ValidationResult> {
189
+ if (!schema) throw new Error('schemaName is required');
190
+ if (typeof schema === 'string') {
191
+ if (isDefaultType(schema)) {
192
+ return await this.validateDefaultType(
193
+ schema as DefaultSchemaType,
194
+ obj
195
+ );
196
+ } else if (typeof this.schemas[schema as keyof T] !== 'undefined') {
197
+ const objSchema = deepExtend(
198
+ defaultSchemas.object,
199
+ this.schemas[schema as keyof T].schema
200
+ ) as ObjectSchemaDefinition<Record<string, never>>;
201
+ const res = await validateObject(obj, objSchema, this);
202
+ if (!res.valid) return res;
203
+ return await this.checkValidators(objSchema, obj);
204
+ } else {
205
+ return await this.validateDefaultType('string', obj, {
206
+ type: 'string',
207
+ equals: schema
208
+ });
209
+ }
210
+ }
211
+
212
+ if (Array.isArray(schema)) {
213
+ for (let i = 0; i < schema.length; i++) {
214
+ const res = await this.validate(schema[i], obj);
215
+ if (res.valid) {
216
+ return res;
217
+ }
218
+ }
219
+ return {
220
+ valid: false,
221
+ errors: ['object does not match any schema']
222
+ };
223
+ }
224
+
225
+ if (typeof schema === 'number') {
226
+ return await this.validateDefaultType('number', obj, schema);
227
+ }
228
+
229
+ if (typeof schema === 'object') {
230
+ if (typeof schema.type !== 'string')
231
+ throw new Error('Schema has no type');
232
+
233
+ if (isDefaultType(schema.type)) {
234
+ return await this.validateDefaultType(
235
+ schema.type as DefaultSchemaType,
236
+ obj,
237
+ schema
238
+ );
239
+ } else if (schema.type === 'object') {
240
+ const preliminaryResult = await validateObject(
241
+ obj,
242
+ schema,
243
+ this
244
+ );
245
+ if (!preliminaryResult.valid) return preliminaryResult;
246
+
247
+ return await this.checkValidators(
248
+ schema as CompositeSchema<Record<string, never>>,
249
+ obj
250
+ );
251
+ }
252
+ }
253
+
254
+ throw new Error("Coldn't understand the Schema provided");
255
+ }
256
+ }
@@ -0,0 +1,66 @@
1
+ import {
2
+ ValidationResult,
3
+ ArraySchemaDefinition,
4
+ ISchemaValidator
5
+ } from '../index';
6
+
7
+ export const validateArray = async (
8
+ obj: any,
9
+ schema: ArraySchemaDefinition<any>,
10
+ validator: ISchemaValidator<any>
11
+ ): Promise<ValidationResult> => {
12
+ if (
13
+ typeof obj === 'undefined' &&
14
+ typeof schema === 'object' &&
15
+ schema.isRequired === false
16
+ ) {
17
+ return {
18
+ valid: true
19
+ };
20
+ }
21
+ if (!Array.isArray(obj))
22
+ return {
23
+ valid: false,
24
+ errors: ['expected type array']
25
+ };
26
+
27
+ if (typeof schema.minLength === 'number' && obj.length < schema.minLength) {
28
+ return {
29
+ valid: false,
30
+ errors: [`expected to be at least ${schema.minLength} chars long`]
31
+ };
32
+ }
33
+
34
+ if (typeof schema.maxLength === 'number' && obj.length > schema.maxLength) {
35
+ return {
36
+ valid: false,
37
+ errors: [`expected to be at most ${schema.maxLength} chars long`]
38
+ };
39
+ }
40
+
41
+ if (typeof schema.ofType !== 'undefined') {
42
+ const results = await Promise.all(
43
+ obj.map((i) => validator.validate(schema.ofType, i))
44
+ );
45
+
46
+ const errors = results
47
+ .filter((r) => !r.valid)
48
+ .map((r) => r.errors)
49
+ .flat(Infinity) as Array<string>;
50
+
51
+ if (errors.length) {
52
+ return {
53
+ valid: false,
54
+ errors
55
+ };
56
+ }
57
+
58
+ return {
59
+ valid: true
60
+ };
61
+ }
62
+
63
+ return {
64
+ valid: true
65
+ };
66
+ };
@@ -0,0 +1,87 @@
1
+ import {
2
+ ValidationResult,
3
+ NumberSchemaDefinition,
4
+ ISchemaValidator
5
+ } from '../index';
6
+
7
+ export const validateNumber = async (
8
+ obj: any,
9
+ schema: NumberSchemaDefinition<any>,
10
+ validator: ISchemaValidator<any>
11
+ ): Promise<ValidationResult> => {
12
+ if (
13
+ typeof obj === 'undefined' &&
14
+ typeof schema === 'object' &&
15
+ schema.isRequired === false
16
+ ) {
17
+ return {
18
+ valid: true
19
+ };
20
+ }
21
+ if (typeof obj !== 'number')
22
+ return {
23
+ valid: false,
24
+ errors: [`expected type number, but saw ${typeof obj}`]
25
+ };
26
+
27
+ if (typeof schema === 'number') {
28
+ return obj === schema
29
+ ? { valid: true }
30
+ : {
31
+ valid: false,
32
+ errors: [`should be equal to ${schema}`]
33
+ };
34
+ }
35
+
36
+ const num = obj as number;
37
+
38
+ if (schema.ensureNotNaN && Number.isNaN(num)) {
39
+ return {
40
+ valid: false,
41
+ errors: ['is not expected to be NaN']
42
+ };
43
+ }
44
+
45
+ if (
46
+ schema.ensureIsFinite &&
47
+ !Number.isFinite(num) &&
48
+ schema.ensureNotNaN &&
49
+ !Number.isNaN(num)
50
+ ) {
51
+ return {
52
+ valid: false,
53
+ errors: ['is expected to be a finite number']
54
+ };
55
+ }
56
+
57
+ if (typeof schema.equals !== 'undefined' && num !== schema.equals) {
58
+ return {
59
+ valid: false,
60
+ errors: [`expected to be equal to ${schema.equals}`]
61
+ };
62
+ }
63
+
64
+ if (typeof schema.min !== 'undefined') {
65
+ if (typeof schema.min !== 'number')
66
+ throw new Error('min constraint should be a number');
67
+ if (num < schema.min)
68
+ return {
69
+ valid: false,
70
+ errors: [`expected to be at least ${schema.min}`]
71
+ };
72
+ }
73
+
74
+ if (typeof schema.max !== 'undefined') {
75
+ if (typeof schema.max !== 'number')
76
+ throw new Error('max constraint should be a number');
77
+ if (num > schema.max)
78
+ return {
79
+ valid: false,
80
+ errors: [`expected to be no more than ${schema.max}`]
81
+ };
82
+ }
83
+
84
+ return {
85
+ valid: true
86
+ };
87
+ };
@@ -0,0 +1,65 @@
1
+ import {
2
+ ValidationResult,
3
+ ObjectSchemaDefinition,
4
+ ISchemaValidator,
5
+ Schema
6
+ } from '../index';
7
+
8
+ export const validateObject = async (
9
+ obj: any,
10
+ schema: ObjectSchemaDefinition<any>,
11
+ validator: ISchemaValidator<any>
12
+ ): Promise<ValidationResult> => {
13
+ if (
14
+ typeof obj === 'undefined' &&
15
+ typeof schema === 'object' &&
16
+ schema.isRequired === false
17
+ ) {
18
+ return {
19
+ valid: true
20
+ };
21
+ }
22
+ if (typeof obj !== 'object') {
23
+ return {
24
+ valid: false,
25
+ errors: [
26
+ `expected to have type='object', but saw '${typeof obj}' instead`
27
+ ]
28
+ };
29
+ }
30
+
31
+ if (typeof schema.properties === 'object' && schema.properties) {
32
+ const errors = (
33
+ await Promise.all(
34
+ Object.entries(schema.properties).map(
35
+ async ([name, schema]: [string, Schema<any>]) => {
36
+ const result = await validator.validate(
37
+ schema,
38
+ obj[name]
39
+ );
40
+ if (result.valid) return result;
41
+ return {
42
+ valid: false,
43
+ errors: (result.errors || []).map(
44
+ (e) => `->${name} ${e}`
45
+ )
46
+ };
47
+ }
48
+ )
49
+ )
50
+ )
51
+ .filter((r) => !r.valid)
52
+ .map((r) => r.errors)
53
+ .flat(Infinity) as string[];
54
+ if (errors.length) {
55
+ return {
56
+ valid: false,
57
+ errors
58
+ };
59
+ }
60
+ }
61
+
62
+ return {
63
+ valid: true
64
+ };
65
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ ValidationResult,
3
+ StringSchemaDefinition,
4
+ ISchemaValidator
5
+ } from '../index';
6
+
7
+ export const validateString = async (
8
+ obj: any,
9
+ schema: StringSchemaDefinition<any>,
10
+ validator: ISchemaValidator<any>
11
+ ): Promise<ValidationResult> => {
12
+ if (
13
+ typeof obj === 'undefined' &&
14
+ typeof schema === 'object' &&
15
+ schema.isRequired === false
16
+ ) {
17
+ return {
18
+ valid: true
19
+ };
20
+ }
21
+ if (typeof obj !== 'string')
22
+ return {
23
+ valid: false,
24
+ errors: [`expected type string, but saw ${typeof obj}`]
25
+ };
26
+
27
+ if (typeof schema === 'string') {
28
+ return obj === schema
29
+ ? { valid: true }
30
+ : {
31
+ valid: false,
32
+ errors: [`should be equal to ${schema}`]
33
+ };
34
+ }
35
+
36
+ const str = obj as string;
37
+
38
+ if (typeof schema.equals === 'string' && str !== schema.equals) {
39
+ return {
40
+ valid: false,
41
+ errors: [
42
+ `expected to be equal to '${schema.equals}' but saw '${str}'`
43
+ ]
44
+ };
45
+ }
46
+
47
+ if (typeof schema.minLength === 'number' && str.length < schema.minLength) {
48
+ return {
49
+ valid: false,
50
+ errors: [`expected to be at least ${schema.minLength} chars long`]
51
+ };
52
+ }
53
+
54
+ if (typeof schema.maxLength === 'number' && str.length > schema.maxLength) {
55
+ return {
56
+ valid: false,
57
+ errors: [`expected to be at most ${schema.maxLength} chars long`]
58
+ };
59
+ }
60
+
61
+ return {
62
+ valid: true
63
+ };
64
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDir": "./src",
4
+ "outDir": "./dist",
5
+ "declaration": true,
6
+ "target": "esnext",
7
+ "module": "CommonJS",
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "moduleResolution": "Node"
11
+ },
12
+ "files": ["./src/index.ts"],
13
+ "watchOptions": {
14
+ "excludeDirectories": ["./dist"]
15
+ }
16
+ }