1 | import * as fs from 'fs'
|
2 | import {
|
3 | JsonSchema,
|
4 | JsonSchemaAnyBuilder,
|
5 | JsonSchemaBuilder,
|
6 | _filterNullishValues,
|
7 | _isObject,
|
8 | _substringBefore,
|
9 | CommonLogger,
|
10 | } from '@naturalcycles/js-lib'
|
11 | import Ajv, { ValidateFunction } from 'ajv'
|
12 | import { inspectAny, requireFileToExist } from '../../index'
|
13 | import { AjvValidationError } from './ajvValidationError'
|
14 | import { getAjv } from './getAjv'
|
15 |
|
16 | export interface AjvValidationOptions {
|
17 | objectName?: string
|
18 | objectId?: string
|
19 |
|
20 | |
21 |
|
22 |
|
23 | logErrors?: boolean
|
24 |
|
25 | |
26 |
|
27 |
|
28 |
|
29 |
|
30 | separator?: string
|
31 | }
|
32 |
|
33 | export interface AjvSchemaCfg {
|
34 | |
35 |
|
36 |
|
37 |
|
38 | ajv: Ajv
|
39 |
|
40 | |
41 |
|
42 |
|
43 |
|
44 | schemas?: (JsonSchema | JsonSchemaBuilder | AjvSchema)[]
|
45 |
|
46 | objectName?: string
|
47 |
|
48 | |
49 |
|
50 |
|
51 |
|
52 |
|
53 | separator: string
|
54 |
|
55 | |
56 |
|
57 |
|
58 | logErrors: boolean
|
59 |
|
60 | |
61 |
|
62 |
|
63 | logger: CommonLogger
|
64 |
|
65 | |
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | coerceTypes?: boolean
|
73 | }
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 | export class AjvSchema<T = unknown> {
|
82 | private constructor(public schema: JsonSchema<T>, cfg: Partial<AjvSchemaCfg> = {}) {
|
83 | this.cfg = {
|
84 | logErrors: true,
|
85 | logger: console,
|
86 | separator: '\n',
|
87 | ...cfg,
|
88 | ajv:
|
89 | cfg.ajv ||
|
90 | getAjv({
|
91 | schemas: cfg.schemas?.map(s => {
|
92 | if (s instanceof AjvSchema) return s.schema
|
93 | if (s instanceof JsonSchemaAnyBuilder) return s.build()
|
94 | return s as JsonSchema
|
95 | }),
|
96 | coerceTypes: cfg.coerceTypes || false,
|
97 |
|
98 | }),
|
99 |
|
100 | objectName: cfg.objectName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
101 | }
|
102 |
|
103 | this.validateFunction = this.cfg.ajv.compile<T>(schema)
|
104 | }
|
105 |
|
106 | |
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | static create<T>(
|
116 | schema: JsonSchemaBuilder<T> | JsonSchema<T> | AjvSchema<T>,
|
117 | cfg: Partial<AjvSchemaCfg> = {},
|
118 | ): AjvSchema<T> {
|
119 | if (schema instanceof AjvSchema) return schema
|
120 | if (schema instanceof JsonSchemaAnyBuilder) {
|
121 | return new AjvSchema<T>(schema.build(), cfg)
|
122 | }
|
123 | return new AjvSchema<T>(schema as JsonSchema<T>, cfg)
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 |
|
130 | static readJsonSync<T = unknown>(
|
131 | filePath: string,
|
132 | cfg: Partial<AjvSchemaCfg> = {},
|
133 | ): AjvSchema<T> {
|
134 | requireFileToExist(filePath)
|
135 | const schema = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
136 | return new AjvSchema<T>(schema, cfg)
|
137 | }
|
138 |
|
139 | readonly cfg: AjvSchemaCfg
|
140 | private readonly validateFunction: ValidateFunction<T>
|
141 |
|
142 | |
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | validate(obj: T, opt: AjvValidationOptions = {}): T {
|
151 | const err = this.getValidationError(obj, opt)
|
152 | if (err) throw err
|
153 | return obj
|
154 | }
|
155 |
|
156 | getValidationError(obj: T, opt: AjvValidationOptions = {}): AjvValidationError | undefined {
|
157 | if (this.isValid(obj)) return
|
158 |
|
159 | const errors = this.validateFunction.errors!
|
160 |
|
161 | const {
|
162 | objectId = _isObject(obj) ? (obj['id'] as string) : undefined,
|
163 | objectName = this.cfg.objectName,
|
164 | logErrors = this.cfg.logErrors,
|
165 | separator = this.cfg.separator,
|
166 | } = opt
|
167 | const name = [objectName || 'Object', objectId].filter(Boolean).join('.')
|
168 |
|
169 | let message = this.cfg.ajv.errorsText(errors, {
|
170 | dataVar: name,
|
171 | separator,
|
172 | })
|
173 |
|
174 | const strValue = inspectAny(obj, { maxLen: 1000 })
|
175 | message = [message, 'Input: ' + strValue].join(separator)
|
176 |
|
177 | if (logErrors) {
|
178 | this.cfg.logger.error(errors)
|
179 | }
|
180 |
|
181 | return new AjvValidationError(
|
182 | message,
|
183 | _filterNullishValues({
|
184 | errors,
|
185 | userFriendly: true,
|
186 | objectName,
|
187 | objectId,
|
188 | }),
|
189 | )
|
190 | }
|
191 |
|
192 | isValid(obj: T): boolean {
|
193 | return this.validateFunction(obj)
|
194 | }
|
195 | }
|