@theshelf/validation 0.0.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/README.md +102 -0
- package/dist/definitions/ValidationResult.d.ts +6 -0
- package/dist/definitions/ValidationResult.js +10 -0
- package/dist/definitions/constants.d.ts +13 -0
- package/dist/definitions/constants.js +14 -0
- package/dist/definitions/interfaces.d.ts +5 -0
- package/dist/definitions/interfaces.js +1 -0
- package/dist/definitions/types.d.ts +42 -0
- package/dist/definitions/types.js +1 -0
- package/dist/errors/UnknownImplementation.d.ts +4 -0
- package/dist/errors/UnknownImplementation.js +6 -0
- package/dist/errors/UnknownValidator.d.ts +4 -0
- package/dist/errors/UnknownValidator.js +6 -0
- package/dist/errors/ValidationError.d.ts +3 -0
- package/dist/errors/ValidationError.js +5 -0
- package/dist/implementation.d.ts +3 -0
- package/dist/implementation.js +12 -0
- package/dist/implementations/zod/Zod.d.ts +8 -0
- package/dist/implementations/zod/Zod.js +128 -0
- package/dist/implementations/zod/create.d.ts +2 -0
- package/dist/implementations/zod/create.js +4 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/package.json +24 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
# Validation | The Shelf
|
|
3
|
+
|
|
4
|
+
The validation package provides a universal interaction layer with an actual data validation solution.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @theshelf/validation
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Implementations
|
|
13
|
+
|
|
14
|
+
Currently, there is only one implementation:
|
|
15
|
+
|
|
16
|
+
* **Zod** - implementation for the currently popular Zod library.
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
The used implementation needs to be configured in the `.env` file.
|
|
21
|
+
|
|
22
|
+
```env
|
|
23
|
+
VALIDATION_IMPLEMENTATION="zod"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How to use
|
|
27
|
+
|
|
28
|
+
An instance of the configured validator implementation can be imported for performing validation operations.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import validator from '@theshelf/validation';
|
|
32
|
+
|
|
33
|
+
// Perform operations with the validator instance
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Operations
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import validator, { ValidationSchema, ValidationResult } from '@theshelf/validation';
|
|
40
|
+
|
|
41
|
+
const data = {
|
|
42
|
+
name: 'John Doe',
|
|
43
|
+
age: '42'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const schema: ValidationSchema = {
|
|
47
|
+
name: { message: 'Invalid name', STRING: { required: true, minLength: 4, maxLength: 40 } },
|
|
48
|
+
nickname: { message: 'Invalid nickname', STRING: { required: false, , pattern: '^[a-z]+$' } },
|
|
49
|
+
age: { message: 'Invalid age', NUMBER: { required: true, minValue: 18, maxValue: 99 } }
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Validate data
|
|
53
|
+
const result: ValidationResult = validator.validate(data, schema);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Validation scheme options
|
|
57
|
+
|
|
58
|
+
A basic validation scheme has the following structure.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const schema: ValidationSchema = {
|
|
62
|
+
fieldName1: { TYPE: { /* type options */ } },
|
|
63
|
+
fieldName2: { TYPE: { /* type options */ } },
|
|
64
|
+
...
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Note** that a custom validation error `message` can optionally be set per field.
|
|
69
|
+
|
|
70
|
+
The following types are supported:
|
|
71
|
+
|
|
72
|
+
* **STRING**
|
|
73
|
+
* `required: boolean`
|
|
74
|
+
* `minLength?: number`
|
|
75
|
+
* `maxLength?: number`
|
|
76
|
+
* `pattern?: string`
|
|
77
|
+
* **NUMBER**
|
|
78
|
+
* `required: boolean`
|
|
79
|
+
* `minValue?: number`
|
|
80
|
+
* `maxValue?: number`
|
|
81
|
+
* **ARRAY**
|
|
82
|
+
* `required: boolean`
|
|
83
|
+
* `minLength?: number`
|
|
84
|
+
* `maxLength?: number`
|
|
85
|
+
* `validations?: Partial<Validation>`
|
|
86
|
+
* **BOOLEAN**
|
|
87
|
+
* `required: boolean`
|
|
88
|
+
* **DATE**
|
|
89
|
+
* `required: boolean`
|
|
90
|
+
* **UUID**
|
|
91
|
+
* `required: boolean`
|
|
92
|
+
* **EMAIL**
|
|
93
|
+
* `required: boolean`
|
|
94
|
+
* **URL**
|
|
95
|
+
* `required: boolean`
|
|
96
|
+
|
|
97
|
+
### Validation result structure
|
|
98
|
+
|
|
99
|
+
The validation result has two fields:
|
|
100
|
+
|
|
101
|
+
* **invalid** - boolean indicating if at least one of the fields is invalid.
|
|
102
|
+
* **messages** - map containing the validation error messages per field.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default class ValidationResult {
|
|
2
|
+
#invalid;
|
|
3
|
+
#messages;
|
|
4
|
+
constructor(invalid, messages = new Map()) {
|
|
5
|
+
this.#invalid = invalid;
|
|
6
|
+
this.#messages = messages;
|
|
7
|
+
}
|
|
8
|
+
get invalid() { return this.#invalid; }
|
|
9
|
+
get messages() { return this.#messages; }
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
declare const FieldTypes: {
|
|
2
|
+
STRING: string;
|
|
3
|
+
NUMBER: string;
|
|
4
|
+
BOOLEAN: string;
|
|
5
|
+
DATE: string;
|
|
6
|
+
UUID: string;
|
|
7
|
+
EMAIL: string;
|
|
8
|
+
ARRAY: string;
|
|
9
|
+
URL: string;
|
|
10
|
+
};
|
|
11
|
+
declare const MAX_EMAIL_LENGTH = 320;
|
|
12
|
+
declare const MAX_URL_LENGTH = 2083;
|
|
13
|
+
export { FieldTypes, MAX_EMAIL_LENGTH, MAX_URL_LENGTH };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const FieldTypes = {
|
|
2
|
+
STRING: 'string',
|
|
3
|
+
NUMBER: 'number',
|
|
4
|
+
BOOLEAN: 'boolean',
|
|
5
|
+
DATE: 'date',
|
|
6
|
+
UUID: 'uuid',
|
|
7
|
+
EMAIL: 'email',
|
|
8
|
+
ARRAY: 'array',
|
|
9
|
+
URL: 'url'
|
|
10
|
+
};
|
|
11
|
+
Object.freeze(FieldTypes);
|
|
12
|
+
const MAX_EMAIL_LENGTH = 320;
|
|
13
|
+
const MAX_URL_LENGTH = 2083;
|
|
14
|
+
export { FieldTypes, MAX_EMAIL_LENGTH, MAX_URL_LENGTH };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FieldTypes } from './constants';
|
|
2
|
+
export type ValidationType = keyof typeof FieldTypes;
|
|
3
|
+
type DefaultProperties = {
|
|
4
|
+
required: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type StringProperties = DefaultProperties & {
|
|
7
|
+
minLength?: number;
|
|
8
|
+
maxLength?: number;
|
|
9
|
+
pattern?: string;
|
|
10
|
+
};
|
|
11
|
+
export type NumberProperties = DefaultProperties & {
|
|
12
|
+
minValue?: number;
|
|
13
|
+
maxValue?: number;
|
|
14
|
+
};
|
|
15
|
+
export type ArrayProperties = DefaultProperties & {
|
|
16
|
+
minLength?: number;
|
|
17
|
+
maxLength?: number;
|
|
18
|
+
validations?: Partial<Validation>;
|
|
19
|
+
};
|
|
20
|
+
export type BooleanProperties = DefaultProperties;
|
|
21
|
+
export type DateProperties = DefaultProperties;
|
|
22
|
+
export type UUIDProperties = DefaultProperties;
|
|
23
|
+
export type EmailProperties = DefaultProperties;
|
|
24
|
+
export type URLProperties = DefaultProperties & {
|
|
25
|
+
protocols?: string[];
|
|
26
|
+
};
|
|
27
|
+
export type Message = {
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
export type ValidationTypes = {
|
|
31
|
+
STRING: StringProperties;
|
|
32
|
+
NUMBER: NumberProperties;
|
|
33
|
+
BOOLEAN: BooleanProperties;
|
|
34
|
+
DATE: DateProperties;
|
|
35
|
+
UUID: UUIDProperties;
|
|
36
|
+
EMAIL: EmailProperties;
|
|
37
|
+
ARRAY: ArrayProperties;
|
|
38
|
+
URL: URLProperties;
|
|
39
|
+
};
|
|
40
|
+
export type Validation = Partial<ValidationTypes | Message>;
|
|
41
|
+
export type ValidationSchema = Record<string, Validation>;
|
|
42
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import UnknownImplementation from './errors/UnknownImplementation';
|
|
2
|
+
import createZod from './implementations/zod/create';
|
|
3
|
+
const implementations = new Map([
|
|
4
|
+
['zod', createZod]
|
|
5
|
+
]);
|
|
6
|
+
const DEFAULT_VALIDATION_IMPLEMENTATION = 'zod';
|
|
7
|
+
const implementationName = process.env.VALIDATION_IMPLEMENTATION ?? DEFAULT_VALIDATION_IMPLEMENTATION;
|
|
8
|
+
const creator = implementations.get(implementationName.toLowerCase());
|
|
9
|
+
if (creator === undefined) {
|
|
10
|
+
throw new UnknownImplementation(implementationName);
|
|
11
|
+
}
|
|
12
|
+
export default creator();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import ValidationResult from '../../definitions/ValidationResult';
|
|
2
|
+
import type { Validator } from '../../definitions/interfaces';
|
|
3
|
+
import type { ValidationSchema } from '../../definitions/types';
|
|
4
|
+
export default class Zod implements Validator {
|
|
5
|
+
#private;
|
|
6
|
+
constructor();
|
|
7
|
+
validate(data: unknown, schema: ValidationSchema): ValidationResult;
|
|
8
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import ValidationResult from '../../definitions/ValidationResult';
|
|
3
|
+
import { FieldTypes, MAX_EMAIL_LENGTH, MAX_URL_LENGTH } from '../../definitions/constants';
|
|
4
|
+
import UnknownValidator from '../../errors/UnknownValidator';
|
|
5
|
+
// Zod is so type heavy that we've chosen for inferred types to be used.
|
|
6
|
+
// This is a trade-off between readability and verbosity.
|
|
7
|
+
export default class Zod {
|
|
8
|
+
#validations = new Map();
|
|
9
|
+
constructor() {
|
|
10
|
+
this.#validations.set(FieldTypes.STRING, (value) => this.#validateString(value));
|
|
11
|
+
this.#validations.set(FieldTypes.NUMBER, (value) => this.#validateNumber(value));
|
|
12
|
+
this.#validations.set(FieldTypes.BOOLEAN, (value) => this.#validateBoolean(value));
|
|
13
|
+
this.#validations.set(FieldTypes.DATE, (value) => this.#validateDate(value));
|
|
14
|
+
this.#validations.set(FieldTypes.UUID, (value) => this.#validateUuid(value));
|
|
15
|
+
this.#validations.set(FieldTypes.EMAIL, (value) => this.#validateEmail(value));
|
|
16
|
+
this.#validations.set(FieldTypes.ARRAY, (value) => this.#validateArray(value));
|
|
17
|
+
this.#validations.set(FieldTypes.URL, (value) => this.#validateUrl(value));
|
|
18
|
+
}
|
|
19
|
+
validate(data, schema) {
|
|
20
|
+
const validator = this.#buildValidator(schema);
|
|
21
|
+
const result = validator.safeParse(data);
|
|
22
|
+
if (result.success === false) {
|
|
23
|
+
const issues = result.error.issues;
|
|
24
|
+
const messages = this.#getMessages(issues, schema);
|
|
25
|
+
return new ValidationResult(true, messages);
|
|
26
|
+
}
|
|
27
|
+
return new ValidationResult(false);
|
|
28
|
+
}
|
|
29
|
+
#buildValidator(schema) {
|
|
30
|
+
return Object.entries(schema)
|
|
31
|
+
.reduce((partialSchema, [key, value]) => {
|
|
32
|
+
const fieldValidation = this.#getValidation(value);
|
|
33
|
+
return partialSchema.extend({ [key]: fieldValidation });
|
|
34
|
+
}, z.object({})).strict();
|
|
35
|
+
}
|
|
36
|
+
#getValidation(schema) {
|
|
37
|
+
for (const [key, validation] of Object.entries(schema)) {
|
|
38
|
+
if (key === 'message')
|
|
39
|
+
continue;
|
|
40
|
+
const validator = this.#validations.get(key.toLocaleLowerCase());
|
|
41
|
+
if (validator === undefined) {
|
|
42
|
+
throw new UnknownValidator(key);
|
|
43
|
+
}
|
|
44
|
+
return validator(validation);
|
|
45
|
+
}
|
|
46
|
+
return z.never();
|
|
47
|
+
}
|
|
48
|
+
#validateString(value) {
|
|
49
|
+
let validation = z.string();
|
|
50
|
+
if (value.minLength !== undefined)
|
|
51
|
+
validation = validation.min(value.minLength);
|
|
52
|
+
if (value.maxLength !== undefined)
|
|
53
|
+
validation = validation.max(value.maxLength);
|
|
54
|
+
if (value.pattern !== undefined)
|
|
55
|
+
validation = validation.regex(new RegExp(value.pattern));
|
|
56
|
+
return this.#checkRequired(value, validation);
|
|
57
|
+
}
|
|
58
|
+
#validateNumber(value) {
|
|
59
|
+
let validation = z.number();
|
|
60
|
+
if (value.minValue !== undefined)
|
|
61
|
+
validation = validation.min(value.minValue);
|
|
62
|
+
if (value.maxValue !== undefined)
|
|
63
|
+
validation = validation.max(value.maxValue);
|
|
64
|
+
return this.#checkRequired(value, validation);
|
|
65
|
+
}
|
|
66
|
+
#validateBoolean(value) {
|
|
67
|
+
const validation = z.boolean();
|
|
68
|
+
return this.#checkRequired(value, validation);
|
|
69
|
+
}
|
|
70
|
+
#validateDate(value) {
|
|
71
|
+
const validation = z.iso.datetime();
|
|
72
|
+
return this.#checkRequired(value, validation);
|
|
73
|
+
}
|
|
74
|
+
#validateUuid(value) {
|
|
75
|
+
const validation = z.uuid();
|
|
76
|
+
return this.#checkRequired(value, validation);
|
|
77
|
+
}
|
|
78
|
+
#validateEmail(value) {
|
|
79
|
+
const validation = z.email().max(MAX_EMAIL_LENGTH);
|
|
80
|
+
return this.#checkRequired(value, validation);
|
|
81
|
+
}
|
|
82
|
+
#validateArray(value) {
|
|
83
|
+
let validation = value.validations === undefined
|
|
84
|
+
? z.array(z.unknown())
|
|
85
|
+
: z.array(this.#getValidation(value.validations));
|
|
86
|
+
if (value.minLength !== undefined)
|
|
87
|
+
validation = validation.min(value.minLength);
|
|
88
|
+
if (value.maxLength !== undefined)
|
|
89
|
+
validation = validation.max(value.maxLength);
|
|
90
|
+
return this.#checkRequired(value, validation);
|
|
91
|
+
}
|
|
92
|
+
#validateUrl(value) {
|
|
93
|
+
let validation = z.url().max(MAX_URL_LENGTH);
|
|
94
|
+
if (value.protocols !== undefined) {
|
|
95
|
+
const expression = value.protocols.join('|');
|
|
96
|
+
validation = validation.regex(new RegExp(`^(${expression}):.*`));
|
|
97
|
+
}
|
|
98
|
+
return this.#checkRequired(value, validation);
|
|
99
|
+
}
|
|
100
|
+
#checkRequired(value, validation) {
|
|
101
|
+
return value.required
|
|
102
|
+
? validation
|
|
103
|
+
: validation.optional();
|
|
104
|
+
}
|
|
105
|
+
#getMessages(issues, scheme) {
|
|
106
|
+
const messages = new Map();
|
|
107
|
+
for (const issue of issues) {
|
|
108
|
+
if (issue.code === 'unrecognized_keys') {
|
|
109
|
+
this.#mapUnrecognizedKeys(issue, scheme, messages);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const field = String(issue.path[0]);
|
|
113
|
+
const message = this.#getMessageByField(field, scheme);
|
|
114
|
+
messages.set(field, message);
|
|
115
|
+
}
|
|
116
|
+
return messages;
|
|
117
|
+
}
|
|
118
|
+
#mapUnrecognizedKeys(issue, scheme, messages) {
|
|
119
|
+
for (const key of issue.keys) {
|
|
120
|
+
const message = this.#getMessageByField(key, scheme);
|
|
121
|
+
messages.set(key, message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
#getMessageByField(path, scheme) {
|
|
125
|
+
const field = scheme[path];
|
|
126
|
+
return field?.message ?? 'Invalid field';
|
|
127
|
+
}
|
|
128
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './definitions/constants';
|
|
2
|
+
export * from './definitions/types';
|
|
3
|
+
export { default as UnknownImplementation } from './errors/UnknownImplementation';
|
|
4
|
+
export { default as UnknownValidator } from './errors/UnknownValidator';
|
|
5
|
+
export { default as ValidationError } from './errors/ValidationError';
|
|
6
|
+
export { default } from './implementation';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './definitions/constants';
|
|
2
|
+
export * from './definitions/types';
|
|
3
|
+
export { default as UnknownImplementation } from './errors/UnknownImplementation';
|
|
4
|
+
export { default as UnknownValidator } from './errors/UnknownValidator';
|
|
5
|
+
export { default as ValidationError } from './errors/ValidationError';
|
|
6
|
+
export { default } from './implementation';
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theshelf/validation",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"clean": "rimraf dist",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test-coverage": "vitest run --coverage",
|
|
11
|
+
"lint": "eslint",
|
|
12
|
+
"review": "npm run build && npm run lint && npm run test",
|
|
13
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"README.md",
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": "./dist/index.js",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"zod": "4.1.12"
|
|
23
|
+
}
|
|
24
|
+
}
|