1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | import { _hb, _isObject, _truncateMiddle } from '@naturalcycles/js-lib'
|
10 | import { ValidationError, ValidationOptions } from 'joi'
|
11 | import { AnySchemaTyped } from './joi.model'
|
12 | import { JoiValidationError, JoiValidationErrorData } from './joi.validation.error'
|
13 |
|
14 |
|
15 | export interface JoiValidationResult<T = any> {
|
16 | value: T
|
17 | error?: JoiValidationError
|
18 | }
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | const stripColors = true
|
24 |
|
25 | const defaultOptions: ValidationOptions = {
|
26 | abortEarly: false,
|
27 | convert: true,
|
28 | allowUnknown: true,
|
29 | stripUnknown: {
|
30 | objects: true,
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 | arrays: false,
|
38 | },
|
39 | presence: 'required',
|
40 |
|
41 |
|
42 |
|
43 | }
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | export function validate<IN, OUT = IN>(
|
53 | value: IN,
|
54 | schema?: AnySchemaTyped<IN, OUT>,
|
55 | objectName?: string,
|
56 | options: ValidationOptions = {},
|
57 | ): OUT {
|
58 | const { value: returnValue, error } = getValidationResult<IN, OUT>(
|
59 | value,
|
60 | schema,
|
61 | objectName,
|
62 | options,
|
63 | )
|
64 |
|
65 | if (error) {
|
66 | throw error
|
67 | }
|
68 |
|
69 | return returnValue
|
70 | }
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | export function getValidationResult<IN, OUT = IN>(
|
80 | value: IN,
|
81 | schema?: AnySchemaTyped<IN, OUT>,
|
82 | objectName?: string,
|
83 | options: ValidationOptions = {},
|
84 | ): JoiValidationResult<OUT> {
|
85 | if (!schema) return { value } as any
|
86 |
|
87 | const { value: returnValue, error } = schema.validate(value, {
|
88 | ...defaultOptions,
|
89 | ...options,
|
90 | })
|
91 |
|
92 | const vr: JoiValidationResult<OUT> = {
|
93 | value: returnValue as OUT,
|
94 | }
|
95 |
|
96 | if (error) {
|
97 | vr.error = createError(value, error, objectName)
|
98 | }
|
99 |
|
100 | return vr
|
101 | }
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | export function isValid<IN, OUT = IN>(value: IN, schema?: AnySchemaTyped<IN, OUT>): boolean {
|
107 | if (!schema) return { value } as any
|
108 |
|
109 | const { error } = schema.validate(value, defaultOptions)
|
110 | return !error
|
111 | }
|
112 |
|
113 | export function undefinedIfInvalid<IN, OUT = IN>(
|
114 | value: IN,
|
115 | schema?: AnySchemaTyped<IN, OUT>,
|
116 | ): OUT | undefined {
|
117 | if (!schema) return { value } as any
|
118 |
|
119 | const { value: returnValue, error } = schema.validate(value, defaultOptions)
|
120 |
|
121 | return error ? undefined : returnValue
|
122 | }
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 | export function convert<IN, OUT = IN>(value: IN, schema?: AnySchemaTyped<IN, OUT>): OUT {
|
130 | if (!schema) return value as any
|
131 | const { value: returnValue } = schema.validate(value, defaultOptions)
|
132 | return returnValue
|
133 | }
|
134 |
|
135 | function createError(value: any, err: ValidationError, objectName?: string): JoiValidationError {
|
136 | if (!err) return undefined as any
|
137 | const tokens: string[] = []
|
138 |
|
139 | const objectId = _isObject(value) ? (value['id'] as string) : undefined
|
140 |
|
141 | if (objectId || objectName) {
|
142 | objectName = objectName || value?.constructor?.name
|
143 |
|
144 | tokens.push('Invalid ' + [objectName, objectId].filter(Boolean).join('.'))
|
145 | }
|
146 |
|
147 | const annotation = err.annotate(stripColors)
|
148 |
|
149 | if (annotation.length > 4000) {
|
150 |
|
151 |
|
152 | tokens.push(
|
153 | _truncateMiddle(annotation, 4000, `\n... ${_hb(annotation.length)} message truncated ...\n`),
|
154 | )
|
155 |
|
156 |
|
157 | tokens.push(...err.details.slice(0, 5).map(i => `${i.message} @ .${i.path.join('.')}`))
|
158 |
|
159 | if (err.details.length > 5) tokens.push(`... ${err.details.length} errors`)
|
160 | } else {
|
161 | tokens.push(annotation)
|
162 | }
|
163 |
|
164 | const msg = tokens.join('\n')
|
165 |
|
166 | const data: JoiValidationErrorData = {
|
167 | joiValidationErrorItems: err.details,
|
168 | ...(objectName && { joiValidationObjectName: objectName }),
|
169 | ...(objectId && { joiValidationObjectId: objectId }),
|
170 | }
|
171 |
|
172 |
|
173 |
|
174 | Object.defineProperty(data, 'annotation', {
|
175 | writable: true,
|
176 | configurable: true,
|
177 | enumerable: false,
|
178 | value: annotation,
|
179 | })
|
180 |
|
181 | return new JoiValidationError(msg, data)
|
182 | }
|