@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 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,6 @@
1
+ export default class ValidationResult {
2
+ #private;
3
+ constructor(invalid: boolean, messages?: Map<string, string>);
4
+ get invalid(): boolean;
5
+ get messages(): Map<string, string>;
6
+ }
@@ -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,5 @@
1
+ import type ValidationResult from './ValidationResult';
2
+ import type { ValidationSchema } from './types';
3
+ export interface Validator {
4
+ validate(data: unknown, schema: ValidationSchema): ValidationResult;
5
+ }
@@ -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,4 @@
1
+ import ValidationError from './ValidationError';
2
+ export default class UnknownImplementation extends ValidationError {
3
+ constructor(name: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import ValidationError from './ValidationError';
2
+ export default class UnknownImplementation extends ValidationError {
3
+ constructor(name) {
4
+ super(`Unknown validation implementation: ${name}`);
5
+ }
6
+ }
@@ -0,0 +1,4 @@
1
+ import ValidationError from './ValidationError';
2
+ export default class UnknownValidator extends ValidationError {
3
+ constructor(name: string);
4
+ }
@@ -0,0 +1,6 @@
1
+ import ValidationError from './ValidationError';
2
+ export default class UnknownValidator extends ValidationError {
3
+ constructor(name) {
4
+ super(`Unknown validator: ${name}`);
5
+ }
6
+ }
@@ -0,0 +1,3 @@
1
+ export default class ValidationError extends Error {
2
+ constructor(message?: string);
3
+ }
@@ -0,0 +1,5 @@
1
+ export default class ValidationError extends Error {
2
+ constructor(message) {
3
+ super(message ?? 'Validation error');
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ import type { Validator } from './definitions/interfaces';
2
+ declare const _default: Validator;
3
+ export default _default;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import Zod from './Zod';
2
+ export default function create(): Zod;
@@ -0,0 +1,4 @@
1
+ import Zod from './Zod';
2
+ export default function create() {
3
+ return new Zod();
4
+ }
@@ -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
+ }