@naturalcycles/nodejs-lib 15.93.0 → 15.95.0
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/dist/jwt/jwt.service.d.ts +4 -4
- package/dist/validation/ajv/from-data/generateJsonSchemaFromData.d.ts +1 -1
- package/dist/validation/ajv/getAjv.d.ts +1 -1
- package/dist/validation/ajv/index.d.ts +1 -1
- package/dist/validation/ajv/index.js +1 -1
- package/dist/validation/ajv/{ajvSchema.d.ts → jSchema.d.ts} +233 -272
- package/dist/validation/ajv/{ajvSchema.js → jSchema.js} +455 -461
- package/dist/validation/ajv/jsonSchemaBuilder.util.d.ts +1 -1
- package/package.json +1 -1
- package/src/jwt/jwt.service.ts +8 -4
- package/src/validation/ajv/from-data/generateJsonSchemaFromData.ts +2 -2
- package/src/validation/ajv/getAjv.ts +5 -5
- package/src/validation/ajv/index.ts +1 -1
- package/src/validation/ajv/{ajvSchema.ts → jSchema.ts} +1453 -1474
- package/src/validation/ajv/jsonSchemaBuilder.util.ts +1 -1
|
@@ -5,7 +5,6 @@ import type { ValidationFunction, ValidationFunctionResult } from '@naturalcycle
|
|
|
5
5
|
import {
|
|
6
6
|
_isObject,
|
|
7
7
|
_isUndefined,
|
|
8
|
-
_lazyValue,
|
|
9
8
|
_numberEnumValues,
|
|
10
9
|
_stringEnumValues,
|
|
11
10
|
getEnumType,
|
|
@@ -55,469 +54,27 @@ import {
|
|
|
55
54
|
mergeJsonSchemaObjects,
|
|
56
55
|
} from './jsonSchemaBuilder.util.js'
|
|
57
56
|
|
|
58
|
-
// ====
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* On creation - compiles ajv validation function.
|
|
62
|
-
* Provides convenient methods, error reporting, etc.
|
|
63
|
-
*/
|
|
64
|
-
export class AjvSchema<OUT> {
|
|
65
|
-
private constructor(
|
|
66
|
-
public schema: JsonSchema<OUT>,
|
|
67
|
-
cfg: Partial<AjvSchemaCfg> = {},
|
|
68
|
-
) {
|
|
69
|
-
this.cfg = {
|
|
70
|
-
lazy: false,
|
|
71
|
-
...cfg,
|
|
72
|
-
ajv: cfg.ajv || getAjv(),
|
|
73
|
-
// Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
|
|
74
|
-
inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!cfg.lazy) {
|
|
78
|
-
this.getAJVValidateFunction() // compile eagerly
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Shortcut for AjvSchema.create(schema, { lazy: true })
|
|
84
|
-
*/
|
|
85
|
-
static createLazy<OUT>(
|
|
86
|
-
schema: SchemaHandledByAjv<OUT>,
|
|
87
|
-
cfg?: Partial<AjvSchemaCfg>,
|
|
88
|
-
): AjvSchema<OUT> {
|
|
89
|
-
return AjvSchema.create(schema, {
|
|
90
|
-
lazy: true,
|
|
91
|
-
...cfg,
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Conveniently allows to pass either JsonSchema or JsonSchemaBuilder, or existing AjvSchema.
|
|
97
|
-
* If it's already an AjvSchema - it'll just return it without any processing.
|
|
98
|
-
* If it's a Builder - will call `build` before proceeding.
|
|
99
|
-
* Otherwise - will construct AjvSchema instance ready to be used.
|
|
100
|
-
*
|
|
101
|
-
* Implementation note: JsonSchemaBuilder goes first in the union type, otherwise TypeScript fails to infer <T> type
|
|
102
|
-
* correctly for some reason.
|
|
103
|
-
*/
|
|
104
|
-
static create<OUT>(schema: SchemaHandledByAjv<OUT>, cfg?: Partial<AjvSchemaCfg>): AjvSchema<OUT> {
|
|
105
|
-
if (schema instanceof AjvSchema) return schema
|
|
106
|
-
|
|
107
|
-
if (AjvSchema.isSchemaWithCachedAjvSchema<typeof schema, OUT>(schema)) {
|
|
108
|
-
return AjvSchema.requireCachedAjvSchema<typeof schema, OUT>(schema)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let jsonSchema: JsonSchema<OUT>
|
|
112
|
-
|
|
113
|
-
if (AjvSchema.isJsonSchemaBuilder(schema)) {
|
|
114
|
-
// oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
|
|
115
|
-
jsonSchema = (schema as JsonSchemaTerminal<OUT, any>).build()
|
|
116
|
-
AjvSchema.requireValidJsonSchema(jsonSchema)
|
|
117
|
-
} else {
|
|
118
|
-
jsonSchema = schema
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// This is our own helper which marks a schema as optional
|
|
122
|
-
// in case it is going to be used in an object schema,
|
|
123
|
-
// where we need to mark the given property as not-required.
|
|
124
|
-
// But once all compilation is done, the presence of this field
|
|
125
|
-
// really upsets Ajv.
|
|
126
|
-
delete jsonSchema.optionalField
|
|
127
|
-
|
|
128
|
-
const ajvSchema = new AjvSchema<OUT>(jsonSchema, cfg)
|
|
129
|
-
AjvSchema.cacheAjvSchema(schema, ajvSchema)
|
|
130
|
-
|
|
131
|
-
return ajvSchema
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
static isJsonSchemaBuilder<OUT>(schema: unknown): schema is JsonSchemaTerminal<OUT, any> {
|
|
135
|
-
return schema instanceof JsonSchemaTerminal
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
readonly cfg: AjvSchemaCfg
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* It returns the original object just for convenience.
|
|
142
|
-
* Reminder: Ajv will MUTATE your object under 2 circumstances:
|
|
143
|
-
* 1. `useDefaults` option (enabled by default!), which will set missing/empty values that have `default` set in the schema.
|
|
144
|
-
* 2. `coerceTypes` (false by default).
|
|
145
|
-
*
|
|
146
|
-
* Returned object is always the same object (`===`) that was passed, so it is returned just for convenience.
|
|
147
|
-
*/
|
|
148
|
-
validate(input: unknown, opt: AjvValidationOptions = {}): OUT {
|
|
149
|
-
const [err, output] = this.getValidationResult(input, opt)
|
|
150
|
-
if (err) throw err
|
|
151
|
-
return output
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
isValid(input: unknown, opt?: AjvValidationOptions): boolean {
|
|
155
|
-
// todo: we can make it both fast and non-mutating by using Ajv
|
|
156
|
-
// with "removeAdditional" and "useDefaults" disabled.
|
|
157
|
-
const [err] = this.getValidationResult(input, opt)
|
|
158
|
-
return !err
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
getValidationResult(
|
|
162
|
-
input: unknown,
|
|
163
|
-
opt: AjvValidationOptions = {},
|
|
164
|
-
): ValidationFunctionResult<OUT, AjvValidationError> {
|
|
165
|
-
const fn = this.getAJVValidateFunction()
|
|
166
|
-
|
|
167
|
-
const item =
|
|
168
|
-
opt.mutateInput !== false || typeof input !== 'object'
|
|
169
|
-
? input // mutate
|
|
170
|
-
: _deepCopy(input) // not mutate
|
|
171
|
-
|
|
172
|
-
let valid = fn(item) // mutates item, but not input
|
|
173
|
-
_typeCast<OUT>(item)
|
|
174
|
-
|
|
175
|
-
let output: OUT = item
|
|
176
|
-
if (valid && this.schema.postValidation) {
|
|
177
|
-
const [err, result] = _try(() => this.schema.postValidation!(output))
|
|
178
|
-
if (err) {
|
|
179
|
-
valid = false
|
|
180
|
-
;(fn as any).errors = [
|
|
181
|
-
{
|
|
182
|
-
instancePath: '',
|
|
183
|
-
message: err.message,
|
|
184
|
-
},
|
|
185
|
-
]
|
|
186
|
-
} else {
|
|
187
|
-
output = result
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (valid) return [null, output]
|
|
192
|
-
|
|
193
|
-
const errors = fn.errors!
|
|
194
|
-
|
|
195
|
-
const {
|
|
196
|
-
inputId = _isObject(input) ? (input as any)['id'] : undefined,
|
|
197
|
-
inputName = this.cfg.inputName || 'Object',
|
|
198
|
-
} = opt
|
|
199
|
-
const dataVar = [inputName, inputId].filter(Boolean).join('.')
|
|
200
|
-
|
|
201
|
-
this.applyImprovementsOnErrorMessages(errors)
|
|
202
|
-
|
|
203
|
-
let message = this.cfg.ajv.errorsText(errors, {
|
|
204
|
-
dataVar,
|
|
205
|
-
separator,
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
// Note: if we mutated the input already, e.g stripped unknown properties,
|
|
209
|
-
// the error message Input would contain already mutated object print, such as Input: {}
|
|
210
|
-
// Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
|
|
211
|
-
const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 })
|
|
212
|
-
message = [message, 'Input: ' + inputStringified].join(separator)
|
|
213
|
-
|
|
214
|
-
const err = new AjvValidationError(
|
|
215
|
-
message,
|
|
216
|
-
_filterNullishValues({
|
|
217
|
-
errors,
|
|
218
|
-
inputName,
|
|
219
|
-
inputId,
|
|
220
|
-
}),
|
|
221
|
-
)
|
|
222
|
-
return [err, output]
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
getValidationFunction(): ValidationFunction<OUT, AjvValidationError> {
|
|
226
|
-
return (input, opt) => {
|
|
227
|
-
return this.getValidationResult(input, {
|
|
228
|
-
mutateInput: opt?.mutateInput,
|
|
229
|
-
inputName: opt?.inputName,
|
|
230
|
-
inputId: opt?.inputId,
|
|
231
|
-
})
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
static isSchemaWithCachedAjvSchema<Base, OUT>(
|
|
236
|
-
schema: Base,
|
|
237
|
-
): schema is WithCachedAjvSchema<Base, OUT> {
|
|
238
|
-
return !!(schema as any)?.[HIDDEN_AJV_SCHEMA]
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
static cacheAjvSchema<Base extends AnyObject, OUT>(
|
|
242
|
-
schema: Base,
|
|
243
|
-
ajvSchema: AjvSchema<OUT>,
|
|
244
|
-
): WithCachedAjvSchema<Base, OUT> {
|
|
245
|
-
return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema })
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
static requireCachedAjvSchema<Base, OUT>(schema: WithCachedAjvSchema<Base, OUT>): AjvSchema<OUT> {
|
|
249
|
-
return schema[HIDDEN_AJV_SCHEMA]
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private getAJVValidateFunction = _lazyValue(() => this.cfg.ajv.compile(this.schema as any))
|
|
253
|
-
|
|
254
|
-
private static requireValidJsonSchema(schema: JsonSchema): void {
|
|
255
|
-
// For object schemas we require that it is type checked against an external type, e.g.:
|
|
256
|
-
// interface Foo { name: string }
|
|
257
|
-
// const schema = j.object({ name: j.string() }).ofType<Foo>()
|
|
258
|
-
_assert(
|
|
259
|
-
schema.type !== 'object' || schema.hasIsOfTypeCheck,
|
|
260
|
-
'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.',
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private applyImprovementsOnErrorMessages(
|
|
265
|
-
errors: ErrorObject<string, Record<string, any>, unknown>[] | null | undefined,
|
|
266
|
-
): void {
|
|
267
|
-
if (!errors) return
|
|
268
|
-
|
|
269
|
-
this.filterNullableAnyOfErrors(errors)
|
|
270
|
-
|
|
271
|
-
const { errorMessages } = this.schema
|
|
272
|
-
|
|
273
|
-
for (const error of errors) {
|
|
274
|
-
const errorMessage = this.getErrorMessageForInstancePath(
|
|
275
|
-
this.schema,
|
|
276
|
-
error.instancePath,
|
|
277
|
-
error.keyword,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
if (errorMessage) {
|
|
281
|
-
error.message = errorMessage
|
|
282
|
-
} else if (errorMessages?.[error.keyword]) {
|
|
283
|
-
error.message = errorMessages[error.keyword]
|
|
284
|
-
} else {
|
|
285
|
-
const unwrapped = unwrapNullableAnyOf(this.schema)
|
|
286
|
-
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
287
|
-
error.message = unwrapped.errorMessages[error.keyword]
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.')
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
297
|
-
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
298
|
-
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
299
|
-
* that are confusing. This method splices them out, keeping only the real errors.
|
|
300
|
-
*/
|
|
301
|
-
private filterNullableAnyOfErrors(
|
|
302
|
-
errors: ErrorObject<string, Record<string, any>, unknown>[],
|
|
303
|
-
): void {
|
|
304
|
-
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
305
|
-
const exactPaths: string[] = []
|
|
306
|
-
const nullBranchPrefixes: string[] = []
|
|
307
|
-
|
|
308
|
-
for (const error of errors) {
|
|
309
|
-
if (error.keyword !== 'anyOf') continue
|
|
310
|
-
|
|
311
|
-
const parentSchema = this.resolveSchemaPath(error.schemaPath)
|
|
312
|
-
if (!parentSchema) continue
|
|
313
|
-
|
|
314
|
-
const nullIndex = unwrapNullableAnyOfIndex(parentSchema)
|
|
315
|
-
if (nullIndex === -1) continue
|
|
316
|
-
|
|
317
|
-
exactPaths.push(error.schemaPath) // e.g. "#/anyOf"
|
|
318
|
-
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length)
|
|
319
|
-
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`) // e.g. "#/anyOf/1/"
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (!exactPaths.length) return
|
|
323
|
-
|
|
324
|
-
for (let i = errors.length - 1; i >= 0; i--) {
|
|
325
|
-
const sp = errors[i]!.schemaPath
|
|
326
|
-
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
327
|
-
errors.splice(i, 1)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
334
|
-
* and returns the parent schema containing the last keyword.
|
|
335
|
-
*/
|
|
336
|
-
private resolveSchemaPath(schemaPath: string): JsonSchema | undefined {
|
|
337
|
-
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
338
|
-
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
339
|
-
const segments = schemaPath.replace(/^#\//, '').split('/')
|
|
340
|
-
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
341
|
-
segments.pop()
|
|
342
|
-
|
|
343
|
-
let current: any = this.schema
|
|
344
|
-
for (const segment of segments) {
|
|
345
|
-
if (!current || typeof current !== 'object') return undefined
|
|
346
|
-
current = current[segment]
|
|
347
|
-
}
|
|
348
|
-
return current as JsonSchema | undefined
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private getErrorMessageForInstancePath(
|
|
352
|
-
schema: JsonSchema<OUT> | undefined,
|
|
353
|
-
instancePath: string,
|
|
354
|
-
keyword: string,
|
|
355
|
-
): string | undefined {
|
|
356
|
-
if (!schema || !instancePath) return undefined
|
|
357
|
-
|
|
358
|
-
const segments = instancePath.split('/').filter(Boolean)
|
|
359
|
-
return this.traverseSchemaPath(schema, segments, keyword)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
private traverseSchemaPath<T>(
|
|
363
|
-
schema: JsonSchema<T>,
|
|
364
|
-
segments: string[],
|
|
365
|
-
keyword: string,
|
|
366
|
-
): string | undefined {
|
|
367
|
-
if (!segments.length) return undefined
|
|
368
|
-
|
|
369
|
-
const [currentSegment, ...remainingSegments] = segments
|
|
370
|
-
|
|
371
|
-
const nextSchema = this.getChildSchema(schema, currentSegment)
|
|
372
|
-
if (!nextSchema) return undefined
|
|
373
|
-
|
|
374
|
-
if (nextSchema.errorMessages?.[keyword]) {
|
|
375
|
-
return nextSchema.errorMessages[keyword]
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Check through nullable wrapper
|
|
379
|
-
const unwrapped = unwrapNullableAnyOf(nextSchema)
|
|
380
|
-
if (unwrapped?.errorMessages?.[keyword]) {
|
|
381
|
-
return unwrapped.errorMessages[keyword]
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (remainingSegments.length) {
|
|
385
|
-
return this.traverseSchemaPath(nextSchema, remainingSegments, keyword)
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return undefined
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private getChildSchema(schema: JsonSchema, segment: string | undefined): JsonSchema | undefined {
|
|
392
|
-
if (!segment) return undefined
|
|
393
|
-
|
|
394
|
-
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
395
|
-
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema
|
|
396
|
-
|
|
397
|
-
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
398
|
-
return this.getArrayItemSchema(effectiveSchema, segment)
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return this.getObjectPropertySchema(effectiveSchema, segment)
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
private getArrayItemSchema(schema: JsonSchema, indexSegment: string): JsonSchema | undefined {
|
|
405
|
-
if (!schema.items) return undefined
|
|
406
|
-
|
|
407
|
-
if (Array.isArray(schema.items)) {
|
|
408
|
-
return schema.items[Number(indexSegment)]
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return schema.items
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
private getObjectPropertySchema(schema: JsonSchema, segment: string): JsonSchema | undefined {
|
|
415
|
-
return schema.properties?.[segment as keyof typeof schema.properties]
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function unwrapNullableAnyOf(schema: JsonSchema): JsonSchema | undefined {
|
|
420
|
-
const nullIndex = unwrapNullableAnyOfIndex(schema)
|
|
421
|
-
if (nullIndex === -1) return undefined
|
|
422
|
-
return schema.anyOf![1 - nullIndex]!
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function unwrapNullableAnyOfIndex(schema: JsonSchema): number {
|
|
426
|
-
if (schema.anyOf?.length !== 2) return -1
|
|
427
|
-
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null')
|
|
428
|
-
return nullIndex
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const separator = '\n'
|
|
432
|
-
|
|
433
|
-
export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA')
|
|
434
|
-
|
|
435
|
-
export type WithCachedAjvSchema<Base, OUT> = Base & {
|
|
436
|
-
[HIDDEN_AJV_SCHEMA]: AjvSchema<OUT>
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export interface AjvValidationOptions {
|
|
440
|
-
/**
|
|
441
|
-
* Defaults to true,
|
|
442
|
-
* because that's how AJV works by default,
|
|
443
|
-
* and what gives it performance advantage.
|
|
444
|
-
* (Because we have found that deep-clone is surprisingly slow,
|
|
445
|
-
* nearly as slow as Joi validation).
|
|
446
|
-
*
|
|
447
|
-
* If set to true - AJV will mutate the input in case it needs to apply transformations
|
|
448
|
-
* (strip unknown properties, convert types, etc).
|
|
449
|
-
*
|
|
450
|
-
* If false - it will deep-clone (using JSON.stringify+parse) the input to prevent its mutation.
|
|
451
|
-
* Will return the cloned/mutated object.
|
|
452
|
-
* Please note that JSON.stringify+parse has side-effects,
|
|
453
|
-
* e.g it will transform Buffer into a weird object.
|
|
454
|
-
*/
|
|
455
|
-
mutateInput?: boolean
|
|
456
|
-
inputName?: string
|
|
457
|
-
inputId?: string
|
|
458
|
-
/**
|
|
459
|
-
* Function that returns "original input".
|
|
460
|
-
* What is original input?
|
|
461
|
-
* It's an input in its original non-mutated form.
|
|
462
|
-
* Why is it needed?
|
|
463
|
-
* Because we mutates the Input here. And after its been mutated - we no longer
|
|
464
|
-
* can include it "how it was" in an error message. So, for that reason we'll use
|
|
465
|
-
* `getOriginalInput()`, if it's provided.
|
|
466
|
-
*/
|
|
467
|
-
getOriginalInput?: () => unknown
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export interface AjvSchemaCfg {
|
|
471
|
-
/**
|
|
472
|
-
* Pass Ajv instance, otherwise Ajv will be created with
|
|
473
|
-
* AjvSchema default (not the same as Ajv defaults) parameters
|
|
474
|
-
*/
|
|
475
|
-
ajv: Ajv
|
|
476
|
-
|
|
477
|
-
inputName?: string
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Option of Ajv.
|
|
481
|
-
* If set to true - will mutate your input objects!
|
|
482
|
-
* Defaults to false.
|
|
483
|
-
*
|
|
484
|
-
* This option is a "shortcut" to skip creating and passing Ajv instance.
|
|
485
|
-
*/
|
|
486
|
-
// coerceTypes?: boolean
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* If true - schema will be compiled on-demand (lazily).
|
|
490
|
-
* Default: false.
|
|
491
|
-
*/
|
|
492
|
-
lazy?: boolean
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
export type SchemaHandledByAjv<OUT> =
|
|
496
|
-
| JsonSchemaTerminal<OUT, any>
|
|
497
|
-
| JsonSchema<OUT>
|
|
498
|
-
| AjvSchema<OUT>
|
|
499
|
-
|
|
500
|
-
// ===== JsonSchemaBuilders ===== //
|
|
57
|
+
// ==== j (factory object) ====
|
|
501
58
|
|
|
502
59
|
export const j = {
|
|
503
60
|
/**
|
|
504
61
|
* Matches literally any value - equivalent to TypeScript's `any` type.
|
|
505
62
|
* Use sparingly, as it bypasses type validation entirely.
|
|
506
63
|
*/
|
|
507
|
-
any():
|
|
508
|
-
return new
|
|
64
|
+
any(): JBuilder<any, false> {
|
|
65
|
+
return new JBuilder({})
|
|
509
66
|
},
|
|
510
67
|
|
|
511
|
-
string():
|
|
512
|
-
return new
|
|
68
|
+
string(): JString<string, false> {
|
|
69
|
+
return new JString()
|
|
513
70
|
},
|
|
514
71
|
|
|
515
|
-
number():
|
|
516
|
-
return new
|
|
72
|
+
number(): JNumber<number, false> {
|
|
73
|
+
return new JNumber()
|
|
517
74
|
},
|
|
518
75
|
|
|
519
|
-
boolean():
|
|
520
|
-
return new
|
|
76
|
+
boolean(): JBoolean<boolean, false> {
|
|
77
|
+
return new JBoolean()
|
|
521
78
|
},
|
|
522
79
|
|
|
523
80
|
object: Object.assign(object, {
|
|
@@ -527,16 +84,14 @@ export const j = {
|
|
|
527
84
|
return j.object<AnyObject>({}).allowAdditionalProperties()
|
|
528
85
|
},
|
|
529
86
|
|
|
530
|
-
stringMap<S extends
|
|
531
|
-
schema: S,
|
|
532
|
-
): JsonSchemaObjectBuilder<StringMap<SchemaOut<S>>> {
|
|
87
|
+
stringMap<S extends JSchema<any, any>>(schema: S): JObject<StringMap<SchemaOut<S>>> {
|
|
533
88
|
const isValueOptional = schema.getSchema().optionalField
|
|
534
89
|
const builtSchema = schema.build()
|
|
535
90
|
const finalValueSchema: JsonSchema = isValueOptional
|
|
536
91
|
? { anyOf: [{ isUndefined: true }, builtSchema] }
|
|
537
92
|
: builtSchema
|
|
538
93
|
|
|
539
|
-
return new
|
|
94
|
+
return new JObject<StringMap<SchemaOut<S>>>(
|
|
540
95
|
{},
|
|
541
96
|
{
|
|
542
97
|
hasIsOfTypeCheck: false,
|
|
@@ -584,26 +139,28 @@ export const j = {
|
|
|
584
139
|
withRegexKeys,
|
|
585
140
|
}),
|
|
586
141
|
|
|
587
|
-
array<OUT, Opt>(itemSchema:
|
|
588
|
-
return new
|
|
142
|
+
array<OUT, Opt>(itemSchema: JBuilder<OUT, Opt>): JArray<OUT, Opt> {
|
|
143
|
+
return new JArray(itemSchema)
|
|
589
144
|
},
|
|
590
145
|
|
|
591
|
-
tuple<const S extends
|
|
592
|
-
return new
|
|
146
|
+
tuple<const S extends JBuilder<any, any>[]>(items: S): JTuple<S> {
|
|
147
|
+
return new JTuple<S>(items)
|
|
593
148
|
},
|
|
594
149
|
|
|
595
|
-
set<OUT, Opt>(itemSchema:
|
|
596
|
-
return new
|
|
150
|
+
set<OUT, Opt>(itemSchema: JBuilder<OUT, Opt>): JSet2Builder<OUT, Opt> {
|
|
151
|
+
return new JSet2Builder(itemSchema)
|
|
597
152
|
},
|
|
598
153
|
|
|
599
|
-
buffer():
|
|
600
|
-
return new
|
|
154
|
+
buffer(): JBuilder<Buffer, false> {
|
|
155
|
+
return new JBuilder<Buffer, false>({
|
|
156
|
+
Buffer: true,
|
|
157
|
+
})
|
|
601
158
|
},
|
|
602
159
|
|
|
603
160
|
enum<const T extends readonly (string | number | boolean | null)[] | StringEnum | NumberEnum>(
|
|
604
161
|
input: T,
|
|
605
162
|
opt?: JsonBuilderRuleOpt,
|
|
606
|
-
):
|
|
163
|
+
): JEnum<
|
|
607
164
|
T extends readonly (infer U)[]
|
|
608
165
|
? U
|
|
609
166
|
: T extends StringEnum
|
|
@@ -634,7 +191,7 @@ export const j = {
|
|
|
634
191
|
}
|
|
635
192
|
|
|
636
193
|
_assert(enumValues, 'Unsupported enum input')
|
|
637
|
-
return new
|
|
194
|
+
return new JEnum(enumValues as any, baseType, opt)
|
|
638
195
|
},
|
|
639
196
|
|
|
640
197
|
/**
|
|
@@ -648,16 +205,16 @@ export const j = {
|
|
|
648
205
|
* Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
|
|
649
206
|
* Use `oneOf` when schemas are mutually exclusive.
|
|
650
207
|
*/
|
|
651
|
-
oneOf<B extends readonly
|
|
208
|
+
oneOf<B extends readonly JBuilder<any, boolean>[], OUT = BuilderOutUnion<B>>(
|
|
652
209
|
items: [...B],
|
|
653
|
-
):
|
|
210
|
+
): JBuilder<OUT, false> {
|
|
654
211
|
const schemas = items.map(b => b.build())
|
|
655
212
|
_assert(
|
|
656
213
|
schemas.every(hasNoObjectSchemas),
|
|
657
214
|
'Do not use `oneOf` validation with non-primitive types!',
|
|
658
215
|
)
|
|
659
216
|
|
|
660
|
-
return new
|
|
217
|
+
return new JBuilder<OUT, false>({
|
|
661
218
|
oneOf: schemas,
|
|
662
219
|
})
|
|
663
220
|
},
|
|
@@ -673,16 +230,16 @@ export const j = {
|
|
|
673
230
|
* Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
|
|
674
231
|
* Use `oneOf` when schemas are mutually exclusive.
|
|
675
232
|
*/
|
|
676
|
-
anyOf<B extends readonly
|
|
233
|
+
anyOf<B extends readonly JBuilder<any, boolean>[], OUT = BuilderOutUnion<B>>(
|
|
677
234
|
items: [...B],
|
|
678
|
-
):
|
|
235
|
+
): JBuilder<OUT, false> {
|
|
679
236
|
const schemas = items.map(b => b.build())
|
|
680
237
|
_assert(
|
|
681
238
|
schemas.every(hasNoObjectSchemas),
|
|
682
239
|
'Do not use `anyOf` validation with non-primitive types!',
|
|
683
240
|
)
|
|
684
241
|
|
|
685
|
-
return new
|
|
242
|
+
return new JBuilder<OUT, false>({
|
|
686
243
|
anyOf: schemas,
|
|
687
244
|
})
|
|
688
245
|
},
|
|
@@ -699,11 +256,23 @@ export const j = {
|
|
|
699
256
|
* const schema = j.anyOfBy('success', schemaMap)
|
|
700
257
|
* ```
|
|
701
258
|
*/
|
|
702
|
-
anyOfBy<D extends Record<PropertyKey,
|
|
259
|
+
anyOfBy<D extends Record<PropertyKey, JSchema<any, any>>, OUT = AnyOfByOut<D>>(
|
|
703
260
|
propertyName: string,
|
|
704
261
|
schemaDictionary: D,
|
|
705
|
-
):
|
|
706
|
-
|
|
262
|
+
): JBuilder<OUT, false> {
|
|
263
|
+
const builtSchemaDictionary: Record<string, JsonSchema> = {}
|
|
264
|
+
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
265
|
+
builtSchemaDictionary[key] = schema.build()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return new JBuilder<OUT, false>({
|
|
269
|
+
type: 'object',
|
|
270
|
+
hasIsOfTypeCheck: true,
|
|
271
|
+
anyOfBy: {
|
|
272
|
+
propertyName,
|
|
273
|
+
schemaDictionary: builtSchemaDictionary,
|
|
274
|
+
},
|
|
275
|
+
})
|
|
707
276
|
},
|
|
708
277
|
|
|
709
278
|
/**
|
|
@@ -714,10 +283,10 @@ export const j = {
|
|
|
714
283
|
* const schema = j.anyOfThese([successSchema, errorSchema])
|
|
715
284
|
* ```
|
|
716
285
|
*/
|
|
717
|
-
anyOfThese<B extends readonly
|
|
286
|
+
anyOfThese<B extends readonly JBuilder<any, boolean>[], OUT = BuilderOutUnion<B>>(
|
|
718
287
|
items: [...B],
|
|
719
|
-
):
|
|
720
|
-
return new
|
|
288
|
+
): JBuilder<OUT, false> {
|
|
289
|
+
return new JBuilder<OUT, false>({
|
|
721
290
|
anyOfThese: items.map(b => b.build()),
|
|
722
291
|
})
|
|
723
292
|
},
|
|
@@ -734,14 +303,32 @@ export const j = {
|
|
|
734
303
|
let baseType: EnumBaseType = 'other'
|
|
735
304
|
if (typeof v === 'string') baseType = 'string'
|
|
736
305
|
if (typeof v === 'number') baseType = 'number'
|
|
737
|
-
return new
|
|
306
|
+
return new JEnum<V>([v], baseType)
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Create a JSchema from a plain JsonSchema object.
|
|
311
|
+
* Useful when the schema is loaded from a JSON file or generated externally.
|
|
312
|
+
*
|
|
313
|
+
* Optionally accepts a custom Ajv instance and/or inputName for error messages.
|
|
314
|
+
*/
|
|
315
|
+
fromSchema<OUT>(
|
|
316
|
+
schema: JsonSchema<OUT>,
|
|
317
|
+
cfg?: { ajv?: Ajv; inputName?: string },
|
|
318
|
+
): JSchema<OUT, false> {
|
|
319
|
+
return new JSchema<OUT, false>(schema, cfg)
|
|
738
320
|
},
|
|
739
321
|
}
|
|
740
322
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
|
|
323
|
+
// ==== Symbol for caching compiled AjvSchema ====
|
|
324
|
+
|
|
325
|
+
export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA')
|
|
326
|
+
|
|
327
|
+
export type WithCachedAjvSchema<Base, OUT> = Base & {
|
|
328
|
+
[HIDDEN_AJV_SCHEMA]: AjvSchema<OUT>
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ==== JSchema (locked base) ====
|
|
745
332
|
|
|
746
333
|
/*
|
|
747
334
|
Notes for future reference
|
|
@@ -752,20 +339,54 @@ const TS_2000_MILLIS = TS_2000 * 1000
|
|
|
752
339
|
With `Opt`, we can infer it as `{ foo?: string | undefined }`.
|
|
753
340
|
*/
|
|
754
341
|
|
|
755
|
-
export class
|
|
756
|
-
protected [HIDDEN_AJV_SCHEMA]: AjvSchema<any> | undefined
|
|
757
|
-
protected schema: JsonSchema
|
|
342
|
+
export class JSchema<OUT, Opt> {
|
|
343
|
+
protected [HIDDEN_AJV_SCHEMA]: AjvSchema<any> | undefined
|
|
344
|
+
protected schema: JsonSchema
|
|
345
|
+
private _cfg?: { ajv?: Ajv; inputName?: string }
|
|
346
|
+
|
|
347
|
+
constructor(schema: JsonSchema, cfg?: { ajv?: Ajv; inputName?: string }) {
|
|
348
|
+
this.schema = schema
|
|
349
|
+
this._cfg = cfg
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private _builtSchema?: JsonSchema
|
|
353
|
+
private _compiledFns?: WeakMap<Ajv, any>
|
|
354
|
+
|
|
355
|
+
private _getBuiltSchema(): JsonSchema {
|
|
356
|
+
if (!this._builtSchema) {
|
|
357
|
+
const builtSchema = this.build()
|
|
358
|
+
|
|
359
|
+
if (this instanceof JBuilder) {
|
|
360
|
+
_assert(
|
|
361
|
+
builtSchema.type !== 'object' || builtSchema.hasIsOfTypeCheck,
|
|
362
|
+
'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.',
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
delete builtSchema.optionalField
|
|
367
|
+
this._builtSchema = builtSchema
|
|
368
|
+
}
|
|
758
369
|
|
|
759
|
-
|
|
760
|
-
this.schema = schema
|
|
370
|
+
return this._builtSchema
|
|
761
371
|
}
|
|
762
372
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
373
|
+
private _getCompiled(overrideAjv?: Ajv): { fn: any; builtSchema: JsonSchema } {
|
|
374
|
+
const builtSchema = this._getBuiltSchema()
|
|
375
|
+
const ajv = overrideAjv ?? this._cfg?.ajv ?? getAjv()
|
|
376
|
+
|
|
377
|
+
this._compiledFns ??= new WeakMap()
|
|
378
|
+
let fn = this._compiledFns.get(ajv)
|
|
379
|
+
if (!fn) {
|
|
380
|
+
fn = ajv.compile(builtSchema as any)
|
|
381
|
+
this._compiledFns.set(ajv, fn)
|
|
382
|
+
|
|
383
|
+
// Cache AjvSchema wrapper for HIDDEN_AJV_SCHEMA backward compat (default ajv only)
|
|
384
|
+
if (!overrideAjv) {
|
|
385
|
+
this[HIDDEN_AJV_SCHEMA] = AjvSchema._wrap<any>(builtSchema, fn)
|
|
386
|
+
}
|
|
766
387
|
}
|
|
767
388
|
|
|
768
|
-
return
|
|
389
|
+
return { fn, builtSchema }
|
|
769
390
|
}
|
|
770
391
|
|
|
771
392
|
getSchema(): JsonSchema {
|
|
@@ -795,6 +416,7 @@ export class JsonSchemaTerminal<OUT, Opt> {
|
|
|
795
416
|
clone(): this {
|
|
796
417
|
const cloned = Object.create(Object.getPrototypeOf(this))
|
|
797
418
|
cloned.schema = deepCopyPreservingFunctions(this.schema)
|
|
419
|
+
cloned._cfg = this._cfg
|
|
798
420
|
return cloned
|
|
799
421
|
}
|
|
800
422
|
|
|
@@ -805,22 +427,37 @@ export class JsonSchemaTerminal<OUT, Opt> {
|
|
|
805
427
|
}
|
|
806
428
|
|
|
807
429
|
validate(input: unknown, opt?: AjvValidationOptions): OUT {
|
|
808
|
-
|
|
430
|
+
const [err, output] = this.getValidationResult(input, opt)
|
|
431
|
+
if (err) throw err
|
|
432
|
+
return output
|
|
809
433
|
}
|
|
810
434
|
|
|
811
435
|
isValid(input: unknown, opt?: AjvValidationOptions): boolean {
|
|
812
|
-
|
|
436
|
+
const [err] = this.getValidationResult(input, opt)
|
|
437
|
+
return !err
|
|
813
438
|
}
|
|
814
439
|
|
|
815
440
|
getValidationResult(
|
|
816
441
|
input: unknown,
|
|
817
442
|
opt: AjvValidationOptions = {},
|
|
818
443
|
): ValidationFunctionResult<OUT, AjvValidationError> {
|
|
819
|
-
|
|
444
|
+
const { fn, builtSchema } = this._getCompiled(opt.ajv)
|
|
445
|
+
const inputName =
|
|
446
|
+
this._cfg?.inputName || (builtSchema.$id ? _substringBefore(builtSchema.$id, '.') : undefined)
|
|
447
|
+
return executeValidation<OUT>(fn, builtSchema, input, opt, inputName)
|
|
820
448
|
}
|
|
821
449
|
|
|
822
|
-
getValidationFunction(
|
|
823
|
-
|
|
450
|
+
getValidationFunction(
|
|
451
|
+
opt: AjvValidationOptions = {},
|
|
452
|
+
): ValidationFunction<OUT, AjvValidationError> {
|
|
453
|
+
return (input, opt2) => {
|
|
454
|
+
return this.getValidationResult(input, {
|
|
455
|
+
ajv: opt.ajv,
|
|
456
|
+
mutateInput: opt2?.mutateInput ?? opt.mutateInput,
|
|
457
|
+
inputName: opt2?.inputName ?? opt.inputName,
|
|
458
|
+
inputId: opt2?.inputId ?? opt.inputId,
|
|
459
|
+
})
|
|
460
|
+
}
|
|
824
461
|
}
|
|
825
462
|
|
|
826
463
|
/**
|
|
@@ -832,11 +469,11 @@ export class JsonSchemaTerminal<OUT, Opt> {
|
|
|
832
469
|
*
|
|
833
470
|
* If you throw an error from this function, it will show up as an error in the validation.
|
|
834
471
|
*/
|
|
835
|
-
postValidation<OUT2 = OUT>(fn: PostValidatonFn<OUT, OUT2>):
|
|
472
|
+
postValidation<OUT2 = OUT>(fn: PostValidatonFn<OUT, OUT2>): JSchema<OUT2, Opt> {
|
|
836
473
|
const clone = this.cloneAndUpdateSchema({
|
|
837
474
|
postValidation: fn,
|
|
838
475
|
})
|
|
839
|
-
return clone as unknown as
|
|
476
|
+
return clone as unknown as JSchema<OUT2, Opt>
|
|
840
477
|
}
|
|
841
478
|
|
|
842
479
|
/**
|
|
@@ -846,7 +483,9 @@ export class JsonSchemaTerminal<OUT, Opt> {
|
|
|
846
483
|
opt!: Opt
|
|
847
484
|
}
|
|
848
485
|
|
|
849
|
-
|
|
486
|
+
// ==== JBuilder (chainable base) ====
|
|
487
|
+
|
|
488
|
+
export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
|
|
850
489
|
protected setErrorMessage(ruleName: string, errorMessage: string | undefined): void {
|
|
851
490
|
if (_isUndefined(errorMessage)) return
|
|
852
491
|
|
|
@@ -859,15 +498,6 @@ export class JsonSchemaAnyBuilder<OUT, Opt> extends JsonSchemaTerminal<OUT, Opt>
|
|
|
859
498
|
*
|
|
860
499
|
* When the type inferred from the schema differs from the passed-in type,
|
|
861
500
|
* the schema becomes unusable, by turning its type into `never`.
|
|
862
|
-
*
|
|
863
|
-
* ```ts
|
|
864
|
-
* const schemaGood = j.string().isOfType<string>() // ✅
|
|
865
|
-
*
|
|
866
|
-
* const schemaBad = j.string().isOfType<number>() // ❌
|
|
867
|
-
* schemaBad.build() // TypeError: property "build" does not exist on type "never"
|
|
868
|
-
*
|
|
869
|
-
* const result = ajvValidateRequest.body(req, schemaBad) // result will have `unknown` type
|
|
870
|
-
* ```
|
|
871
501
|
*/
|
|
872
502
|
isOfType<ExpectedType>(): ExactMatch<ExpectedType, OUT> extends true ? this : never {
|
|
873
503
|
return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true }) as any
|
|
@@ -901,78 +531,364 @@ export class JsonSchemaAnyBuilder<OUT, Opt> extends JsonSchemaTerminal<OUT, Opt>
|
|
|
901
531
|
return this.cloneAndUpdateSchema({ type })
|
|
902
532
|
}
|
|
903
533
|
|
|
904
|
-
default(v: any): this {
|
|
905
|
-
return this.cloneAndUpdateSchema({ default: v })
|
|
534
|
+
default(v: any): this {
|
|
535
|
+
return this.cloneAndUpdateSchema({ default: v })
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
instanceof(of: string): this {
|
|
539
|
+
return this.cloneAndUpdateSchema({ type: 'object', instanceof: of })
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
optional(): JBuilder<OUT | undefined, true> {
|
|
543
|
+
const clone = this.cloneAndUpdateSchema({ optionalField: true })
|
|
544
|
+
return clone as unknown as JBuilder<OUT | undefined, true>
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
nullable(): JBuilder<OUT | null, Opt> {
|
|
548
|
+
return new JBuilder({
|
|
549
|
+
anyOf: [this.build(), { type: 'null' }],
|
|
550
|
+
})
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* @deprecated
|
|
555
|
+
* The usage of this function is discouraged as it defeats the purpose of having type-safe validation.
|
|
556
|
+
*/
|
|
557
|
+
castAs<T>(): JBuilder<T, Opt> {
|
|
558
|
+
return this as unknown as JBuilder<T, Opt>
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Locks the given schema chain and no other modification can be done to it.
|
|
563
|
+
*/
|
|
564
|
+
final(): JSchema<OUT, Opt> {
|
|
565
|
+
return new JSchema<OUT, Opt>(this.schema)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
*
|
|
570
|
+
* @param validator A validator function that returns an error message or undefined.
|
|
571
|
+
*
|
|
572
|
+
* You may add multiple custom validators and they will be executed in the order you added them.
|
|
573
|
+
*/
|
|
574
|
+
custom<OUT2 = OUT>(validator: CustomValidatorFn): JBuilder<OUT2, Opt> {
|
|
575
|
+
const { customValidations = [] } = this.schema
|
|
576
|
+
return this.cloneAndUpdateSchema({
|
|
577
|
+
customValidations: [...customValidations, validator],
|
|
578
|
+
}) as unknown as JBuilder<OUT2, Opt>
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
*
|
|
583
|
+
* @param converter A converter function that returns a new value.
|
|
584
|
+
*
|
|
585
|
+
* You may add multiple converters and they will be executed in the order you added them,
|
|
586
|
+
* each converter receiving the result from the previous one.
|
|
587
|
+
*
|
|
588
|
+
* This feature only works when the current schema is nested in an object or array schema,
|
|
589
|
+
* due to how mutability works in Ajv.
|
|
590
|
+
*/
|
|
591
|
+
convert<OUT2>(converter: CustomConverterFn<OUT2>): JBuilder<OUT2, Opt> {
|
|
592
|
+
const { customConversions = [] } = this.schema
|
|
593
|
+
return this.cloneAndUpdateSchema({
|
|
594
|
+
customConversions: [...customConversions, converter],
|
|
595
|
+
}) as unknown as JBuilder<OUT2, Opt>
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ==== Consts
|
|
600
|
+
|
|
601
|
+
const TS_2500 = 16725225600 // 2500-01-01
|
|
602
|
+
const TS_2500_MILLIS = TS_2500 * 1000
|
|
603
|
+
const TS_2000 = 946684800 // 2000-01-01
|
|
604
|
+
const TS_2000_MILLIS = TS_2000 * 1000
|
|
605
|
+
|
|
606
|
+
// ==== Type-specific builders ====
|
|
607
|
+
|
|
608
|
+
export class JString<
|
|
609
|
+
OUT extends string | undefined = string,
|
|
610
|
+
Opt extends boolean = false,
|
|
611
|
+
> extends JBuilder<OUT, Opt> {
|
|
612
|
+
constructor() {
|
|
613
|
+
super({
|
|
614
|
+
type: 'string',
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* @param optionalValues List of values that should be considered/converted as `undefined`.
|
|
620
|
+
*
|
|
621
|
+
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
622
|
+
* due to how mutability works in Ajv.
|
|
623
|
+
*
|
|
624
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
625
|
+
*
|
|
626
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
627
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
628
|
+
*/
|
|
629
|
+
override optional<T extends readonly (string | null)[] | undefined = undefined>(
|
|
630
|
+
optionalValues?: T,
|
|
631
|
+
): T extends readonly (infer U)[]
|
|
632
|
+
? null extends U
|
|
633
|
+
? JSchema<OUT | undefined, true>
|
|
634
|
+
: JString<OUT | undefined, true>
|
|
635
|
+
: JString<OUT | undefined, true> {
|
|
636
|
+
if (!optionalValues) {
|
|
637
|
+
return super.optional() as any
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
_typeCast<(string | null)[]>(optionalValues)
|
|
641
|
+
|
|
642
|
+
let newBuilder: JSchema<OUT | undefined, true> = new JString<OUT, Opt>().optional()
|
|
643
|
+
const alternativesSchema = j.enum(optionalValues)
|
|
644
|
+
Object.assign(newBuilder.getSchema(), {
|
|
645
|
+
anyOf: [this.build(), alternativesSchema.build()],
|
|
646
|
+
optionalValues,
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
650
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
651
|
+
// but the typing should not reflect that.
|
|
652
|
+
// We also cannot accept more rules attached, since we're not building a StringSchema anymore.
|
|
653
|
+
if (optionalValues.includes(null)) {
|
|
654
|
+
newBuilder = new JSchema({
|
|
655
|
+
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
656
|
+
optionalField: true,
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return newBuilder as any
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
regex(pattern: RegExp, opt?: JsonBuilderRuleOpt): this {
|
|
664
|
+
_assert(
|
|
665
|
+
!pattern.flags,
|
|
666
|
+
`Regex flags are not supported by JSON Schema. Received: /${pattern.source}/${pattern.flags}`,
|
|
667
|
+
)
|
|
668
|
+
return this.pattern(pattern.source, opt)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
pattern(pattern: string, opt?: JsonBuilderRuleOpt): this {
|
|
672
|
+
const clone = this.cloneAndUpdateSchema({ pattern })
|
|
673
|
+
if (opt?.name) clone.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
674
|
+
if (opt?.msg) clone.setErrorMessage('pattern', opt.msg)
|
|
675
|
+
return clone
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
minLength(minLength: number): this {
|
|
679
|
+
return this.cloneAndUpdateSchema({ minLength })
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
maxLength(maxLength: number): this {
|
|
683
|
+
return this.cloneAndUpdateSchema({ maxLength })
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
length(exactLength: number): this
|
|
687
|
+
length(minLength: number, maxLength: number): this
|
|
688
|
+
length(minLengthOrExactLength: number, maxLength?: number): this {
|
|
689
|
+
const maxLengthActual = maxLength ?? minLengthOrExactLength
|
|
690
|
+
return this.minLength(minLengthOrExactLength).maxLength(maxLengthActual)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
|
|
694
|
+
const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
|
|
695
|
+
return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
|
|
696
|
+
.trim()
|
|
697
|
+
.toLowerCase()
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
trim(): this {
|
|
701
|
+
return this.cloneAndUpdateSchema({ transform: { ...this.schema.transform, trim: true } })
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
toLowerCase(): this {
|
|
705
|
+
return this.cloneAndUpdateSchema({
|
|
706
|
+
transform: { ...this.schema.transform, toLowerCase: true },
|
|
707
|
+
})
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
toUpperCase(): this {
|
|
711
|
+
return this.cloneAndUpdateSchema({
|
|
712
|
+
transform: { ...this.schema.transform, toUpperCase: true },
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
truncate(toLength: number): this {
|
|
717
|
+
return this.cloneAndUpdateSchema({
|
|
718
|
+
transform: { ...this.schema.transform, truncate: toLength },
|
|
719
|
+
})
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
branded<B extends string>(): JString<B, Opt> {
|
|
723
|
+
return this as unknown as JString<B, Opt>
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
|
|
728
|
+
*
|
|
729
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
730
|
+
* because this call effectively starts a new schema chain.
|
|
731
|
+
*/
|
|
732
|
+
isoDate(): JIsoDate {
|
|
733
|
+
return new JIsoDate()
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
isoDateTime(): JString<IsoDateTime, Opt> {
|
|
737
|
+
return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded<IsoDateTime>()
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
isoMonth(): JBuilder<IsoMonth, false> {
|
|
741
|
+
return new JBuilder<IsoMonth, false>({
|
|
742
|
+
type: 'string',
|
|
743
|
+
IsoMonth: {},
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Validates the string format to be JWT.
|
|
749
|
+
* Expects the JWT to be signed!
|
|
750
|
+
*/
|
|
751
|
+
jwt(): this {
|
|
752
|
+
return this.regex(JWT_REGEX, { msg: 'is not a valid JWT format' })
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
url(): this {
|
|
756
|
+
return this.regex(URL_REGEX, { msg: 'is not a valid URL format' })
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
ipv4(): this {
|
|
760
|
+
return this.regex(IPV4_REGEX, { msg: 'is not a valid IPv4 format' })
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
ipv6(): this {
|
|
764
|
+
return this.regex(IPV6_REGEX, { msg: 'is not a valid IPv6 format' })
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
slug(): this {
|
|
768
|
+
return this.regex(SLUG_REGEX, { msg: 'is not a valid slug format' })
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
semVer(): this {
|
|
772
|
+
return this.regex(SEMVER_REGEX, { msg: 'is not a valid semver format' })
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
languageTag(): this {
|
|
776
|
+
return this.regex(LANGUAGE_TAG_REGEX, { msg: 'is not a valid language format' })
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
countryCode(): this {
|
|
780
|
+
return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' })
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
currency(): this {
|
|
784
|
+
return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' })
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Validates that the input is a valid IANATimzone value.
|
|
789
|
+
*
|
|
790
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
791
|
+
* because this call effectively starts a new schema chain as an `enum` validation.
|
|
792
|
+
*/
|
|
793
|
+
ianaTimezone(): JEnum<IANATimezone, false> {
|
|
794
|
+
// UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
|
|
795
|
+
return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded<IANATimezone>()
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
base64Url(): this {
|
|
799
|
+
return this.regex(BASE64URL_REGEX, {
|
|
800
|
+
msg: 'contains characters not allowed in Base64 URL characterset',
|
|
801
|
+
})
|
|
906
802
|
}
|
|
907
803
|
|
|
908
|
-
|
|
909
|
-
return this.
|
|
804
|
+
uuid(): this {
|
|
805
|
+
return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' })
|
|
910
806
|
}
|
|
807
|
+
}
|
|
911
808
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
809
|
+
export interface JsonSchemaStringEmailOptions {
|
|
810
|
+
checkTLD: boolean
|
|
811
|
+
}
|
|
916
812
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
813
|
+
export class JIsoDate<Opt extends boolean = false> extends JBuilder<IsoDate, Opt> {
|
|
814
|
+
constructor() {
|
|
815
|
+
super({
|
|
816
|
+
type: 'string',
|
|
817
|
+
IsoDate: {},
|
|
920
818
|
})
|
|
921
819
|
}
|
|
922
820
|
|
|
923
821
|
/**
|
|
924
|
-
* @
|
|
925
|
-
*
|
|
822
|
+
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
823
|
+
*
|
|
824
|
+
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
825
|
+
* due to how mutability works in Ajv.
|
|
826
|
+
*
|
|
827
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
828
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
926
829
|
*/
|
|
927
|
-
|
|
928
|
-
|
|
830
|
+
override optional<N extends null | undefined = undefined>(
|
|
831
|
+
nullValue?: N,
|
|
832
|
+
): N extends null ? JSchema<IsoDate | undefined, true> : JBuilder<IsoDate | undefined, true> {
|
|
833
|
+
if (nullValue === undefined) {
|
|
834
|
+
return super.optional() as any
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
838
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
839
|
+
// but the typing should not reflect that.
|
|
840
|
+
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
841
|
+
return new JSchema({
|
|
842
|
+
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
843
|
+
optionalField: true,
|
|
844
|
+
}) as any
|
|
929
845
|
}
|
|
930
846
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
*/
|
|
934
|
-
final(): JsonSchemaTerminal<OUT, Opt> {
|
|
935
|
-
return new JsonSchemaTerminal<OUT, Opt>(this.schema)
|
|
847
|
+
before(date: string): this {
|
|
848
|
+
return this.cloneAndUpdateSchema({ IsoDate: { before: date } })
|
|
936
849
|
}
|
|
937
850
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
* @param validator A validator function that returns an error message or undefined.
|
|
941
|
-
*
|
|
942
|
-
* You may add multiple custom validators and they will be executed in the order you added them.
|
|
943
|
-
*/
|
|
944
|
-
custom<OUT2 = OUT>(validator: CustomValidatorFn): JsonSchemaAnyBuilder<OUT2, Opt> {
|
|
945
|
-
const { customValidations = [] } = this.schema
|
|
946
|
-
return this.cloneAndUpdateSchema({
|
|
947
|
-
customValidations: [...customValidations, validator],
|
|
948
|
-
}) as unknown as JsonSchemaAnyBuilder<OUT2, Opt>
|
|
851
|
+
sameOrBefore(date: string): this {
|
|
852
|
+
return this.cloneAndUpdateSchema({ IsoDate: { sameOrBefore: date } })
|
|
949
853
|
}
|
|
950
854
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
855
|
+
after(date: string): this {
|
|
856
|
+
return this.cloneAndUpdateSchema({ IsoDate: { after: date } })
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
sameOrAfter(date: string): this {
|
|
860
|
+
return this.cloneAndUpdateSchema({ IsoDate: { sameOrAfter: date } })
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
between(fromDate: string, toDate: string, incl: Inclusiveness): this {
|
|
864
|
+
let schemaPatch: Partial<JsonSchema> = {}
|
|
865
|
+
|
|
866
|
+
if (incl === '[)') {
|
|
867
|
+
schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } }
|
|
868
|
+
} else if (incl === '[]') {
|
|
869
|
+
schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } }
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return this.cloneAndUpdateSchema(schemaPatch)
|
|
966
873
|
}
|
|
967
874
|
}
|
|
968
875
|
|
|
969
|
-
export
|
|
970
|
-
|
|
876
|
+
export interface JsonSchemaIsoDateOptions {
|
|
877
|
+
before?: string
|
|
878
|
+
sameOrBefore?: string
|
|
879
|
+
after?: string
|
|
880
|
+
sameOrAfter?: string
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export interface JsonSchemaIsoMonthOptions {}
|
|
884
|
+
|
|
885
|
+
export class JNumber<
|
|
886
|
+
OUT extends number | undefined = number,
|
|
971
887
|
Opt extends boolean = false,
|
|
972
|
-
> extends
|
|
888
|
+
> extends JBuilder<OUT, Opt> {
|
|
973
889
|
constructor() {
|
|
974
890
|
super({
|
|
975
|
-
type: '
|
|
891
|
+
type: 'number',
|
|
976
892
|
})
|
|
977
893
|
}
|
|
978
894
|
|
|
@@ -984,26 +900,23 @@ export class JsonSchemaStringBuilder<
|
|
|
984
900
|
*
|
|
985
901
|
* Make sure this `optional()` call is at the end of your call chain.
|
|
986
902
|
*
|
|
987
|
-
* When `null` is included in optionalValues, the return type becomes `
|
|
903
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
988
904
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
989
905
|
*/
|
|
990
|
-
override optional<T extends readonly (
|
|
906
|
+
override optional<T extends readonly (number | null)[] | undefined = undefined>(
|
|
991
907
|
optionalValues?: T,
|
|
992
908
|
): T extends readonly (infer U)[]
|
|
993
909
|
? null extends U
|
|
994
|
-
?
|
|
995
|
-
:
|
|
996
|
-
:
|
|
910
|
+
? JSchema<OUT | undefined, true>
|
|
911
|
+
: JNumber<OUT | undefined, true>
|
|
912
|
+
: JNumber<OUT | undefined, true> {
|
|
997
913
|
if (!optionalValues) {
|
|
998
914
|
return super.optional() as any
|
|
999
915
|
}
|
|
1000
916
|
|
|
1001
|
-
_typeCast<(
|
|
917
|
+
_typeCast<(number | null)[]>(optionalValues)
|
|
1002
918
|
|
|
1003
|
-
let newBuilder:
|
|
1004
|
-
OUT,
|
|
1005
|
-
Opt
|
|
1006
|
-
>().optional()
|
|
919
|
+
let newBuilder: JSchema<OUT | undefined, true> = new JNumber<OUT, Opt>().optional()
|
|
1007
920
|
const alternativesSchema = j.enum(optionalValues)
|
|
1008
921
|
Object.assign(newBuilder.getSchema(), {
|
|
1009
922
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -1013,9 +926,9 @@ export class JsonSchemaStringBuilder<
|
|
|
1013
926
|
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
1014
927
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1015
928
|
// but the typing should not reflect that.
|
|
1016
|
-
// We also cannot accept more rules attached, since we're not building a
|
|
929
|
+
// We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
|
|
1017
930
|
if (optionalValues.includes(null)) {
|
|
1018
|
-
newBuilder = new
|
|
931
|
+
newBuilder = new JSchema({
|
|
1019
932
|
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
1020
933
|
optionalField: true,
|
|
1021
934
|
})
|
|
@@ -1024,162 +937,318 @@ export class JsonSchemaStringBuilder<
|
|
|
1024
937
|
return newBuilder as any
|
|
1025
938
|
}
|
|
1026
939
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
!pattern.flags,
|
|
1030
|
-
`Regex flags are not supported by JSON Schema. Received: /${pattern.source}/${pattern.flags}`,
|
|
1031
|
-
)
|
|
1032
|
-
return this.pattern(pattern.source, opt)
|
|
940
|
+
integer(): this {
|
|
941
|
+
return this.cloneAndUpdateSchema({ type: 'integer' })
|
|
1033
942
|
}
|
|
1034
943
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
if (opt?.name) clone.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
1038
|
-
if (opt?.msg) clone.setErrorMessage('pattern', opt.msg)
|
|
1039
|
-
return clone
|
|
944
|
+
branded<B extends number>(): JNumber<B, Opt> {
|
|
945
|
+
return this as unknown as JNumber<B, Opt>
|
|
1040
946
|
}
|
|
1041
947
|
|
|
1042
|
-
|
|
1043
|
-
return this.cloneAndUpdateSchema({
|
|
948
|
+
multipleOf(multipleOf: number): this {
|
|
949
|
+
return this.cloneAndUpdateSchema({ multipleOf })
|
|
1044
950
|
}
|
|
1045
951
|
|
|
1046
|
-
|
|
1047
|
-
return this.cloneAndUpdateSchema({
|
|
952
|
+
min(minimum: number): this {
|
|
953
|
+
return this.cloneAndUpdateSchema({ minimum })
|
|
1048
954
|
}
|
|
1049
955
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
length(minLengthOrExactLength: number, maxLength?: number): this {
|
|
1053
|
-
const maxLengthActual = maxLength ?? minLengthOrExactLength
|
|
1054
|
-
return this.minLength(minLengthOrExactLength).maxLength(maxLengthActual)
|
|
956
|
+
exclusiveMin(exclusiveMinimum: number): this {
|
|
957
|
+
return this.cloneAndUpdateSchema({ exclusiveMinimum })
|
|
1055
958
|
}
|
|
1056
959
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
|
|
1060
|
-
.trim()
|
|
1061
|
-
.toLowerCase()
|
|
960
|
+
max(maximum: number): this {
|
|
961
|
+
return this.cloneAndUpdateSchema({ maximum })
|
|
1062
962
|
}
|
|
1063
963
|
|
|
1064
|
-
|
|
1065
|
-
return this.cloneAndUpdateSchema({
|
|
964
|
+
exclusiveMax(exclusiveMaximum: number): this {
|
|
965
|
+
return this.cloneAndUpdateSchema({ exclusiveMaximum })
|
|
1066
966
|
}
|
|
1067
967
|
|
|
1068
|
-
|
|
1069
|
-
return this.
|
|
1070
|
-
|
|
968
|
+
lessThan(value: number): this {
|
|
969
|
+
return this.exclusiveMax(value)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
lessThanOrEqual(value: number): this {
|
|
973
|
+
return this.max(value)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
moreThan(value: number): this {
|
|
977
|
+
return this.exclusiveMin(value)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
moreThanOrEqual(value: number): this {
|
|
981
|
+
return this.min(value)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
equal(value: number): this {
|
|
985
|
+
return this.min(value).max(value)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
range(minimum: number, maximum: number, incl: Inclusiveness): this {
|
|
989
|
+
if (incl === '[)') {
|
|
990
|
+
return this.moreThanOrEqual(minimum).lessThan(maximum)
|
|
991
|
+
}
|
|
992
|
+
return this.moreThanOrEqual(minimum).lessThanOrEqual(maximum)
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
int32(): this {
|
|
996
|
+
const MIN_INT32 = -(2 ** 31)
|
|
997
|
+
const MAX_INT32 = 2 ** 31 - 1
|
|
998
|
+
const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
|
|
999
|
+
const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
1000
|
+
const newMin = Math.max(MIN_INT32, currentMin)
|
|
1001
|
+
const newMax = Math.min(MAX_INT32, currentMax)
|
|
1002
|
+
return this.integer().min(newMin).max(newMax)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
int64(): this {
|
|
1006
|
+
const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
|
|
1007
|
+
const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
1008
|
+
const newMin = Math.max(Number.MIN_SAFE_INTEGER, currentMin)
|
|
1009
|
+
const newMax = Math.min(Number.MAX_SAFE_INTEGER, currentMax)
|
|
1010
|
+
return this.integer().min(newMin).max(newMax)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
float(): this {
|
|
1014
|
+
return this
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
double(): this {
|
|
1018
|
+
return this
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
unixTimestamp(): JNumber<UnixTimestamp, Opt> {
|
|
1022
|
+
return this.integer().min(0).max(TS_2500).branded<UnixTimestamp>()
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
unixTimestamp2000(): JNumber<UnixTimestamp, Opt> {
|
|
1026
|
+
return this.integer().min(TS_2000).max(TS_2500).branded<UnixTimestamp>()
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
unixTimestampMillis(): JNumber<UnixTimestampMillis, Opt> {
|
|
1030
|
+
return this.integer().min(0).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
unixTimestamp2000Millis(): JNumber<UnixTimestampMillis, Opt> {
|
|
1034
|
+
return this.integer().min(TS_2000_MILLIS).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
utcOffset(): this {
|
|
1038
|
+
return this.integer()
|
|
1039
|
+
.multipleOf(15)
|
|
1040
|
+
.min(-12 * 60)
|
|
1041
|
+
.max(14 * 60)
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
utcOffsetHour(): this {
|
|
1045
|
+
return this.integer().min(-12).max(14)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Specify the precision of the floating point numbers by the number of digits after the ".".
|
|
1050
|
+
* Excess digits will be cut-off when the current schema is nested in an object or array schema,
|
|
1051
|
+
* due to how mutability works in Ajv.
|
|
1052
|
+
*/
|
|
1053
|
+
precision(numberOfDigits: number): this {
|
|
1054
|
+
return this.cloneAndUpdateSchema({ precision: numberOfDigits })
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export class JBoolean<
|
|
1059
|
+
OUT extends boolean | undefined = boolean,
|
|
1060
|
+
Opt extends boolean = false,
|
|
1061
|
+
> extends JBuilder<OUT, Opt> {
|
|
1062
|
+
constructor() {
|
|
1063
|
+
super({
|
|
1064
|
+
type: 'boolean',
|
|
1071
1065
|
})
|
|
1072
1066
|
}
|
|
1073
1067
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1068
|
+
/**
|
|
1069
|
+
* @param optionalValue One of the two possible boolean values that should be considered/converted as `undefined`.
|
|
1070
|
+
*
|
|
1071
|
+
* This `optionalValue` feature only works when the current schema is nested in an object or array schema,
|
|
1072
|
+
* due to how mutability works in Ajv.
|
|
1073
|
+
*/
|
|
1074
|
+
override optional(optionalValue?: boolean): JBoolean<OUT | undefined, true> {
|
|
1075
|
+
if (typeof optionalValue === 'undefined') {
|
|
1076
|
+
return super.optional() as unknown as JBoolean<OUT | undefined, true>
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const newBuilder = new JBoolean<OUT, Opt>().optional()
|
|
1080
|
+
const alternativesSchema = j.enum([optionalValue])
|
|
1081
|
+
Object.assign(newBuilder.getSchema(), {
|
|
1082
|
+
anyOf: [this.build(), alternativesSchema.build()],
|
|
1083
|
+
optionalValues: [optionalValue],
|
|
1077
1084
|
})
|
|
1085
|
+
|
|
1086
|
+
return newBuilder
|
|
1078
1087
|
}
|
|
1088
|
+
}
|
|
1079
1089
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1090
|
+
export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends JBuilder<
|
|
1091
|
+
OUT,
|
|
1092
|
+
Opt
|
|
1093
|
+
> {
|
|
1094
|
+
constructor(props?: AnyObject, opt?: JObjectOpts) {
|
|
1095
|
+
super({
|
|
1096
|
+
type: 'object',
|
|
1097
|
+
properties: {},
|
|
1098
|
+
required: [],
|
|
1099
|
+
additionalProperties: false,
|
|
1100
|
+
hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
|
|
1101
|
+
patternProperties: opt?.patternProperties ?? undefined,
|
|
1102
|
+
keySchema: opt?.keySchema ?? undefined,
|
|
1083
1103
|
})
|
|
1084
|
-
}
|
|
1085
1104
|
|
|
1086
|
-
|
|
1087
|
-
return this as unknown as JsonSchemaStringBuilder<B, Opt>
|
|
1105
|
+
if (props) addPropertiesToSchema(this.schema, props)
|
|
1088
1106
|
}
|
|
1089
1107
|
|
|
1090
1108
|
/**
|
|
1091
|
-
*
|
|
1109
|
+
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
1092
1110
|
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1111
|
+
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1112
|
+
* due to how mutability works in Ajv.
|
|
1113
|
+
*
|
|
1114
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1115
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1095
1116
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1117
|
+
override optional<N extends null | undefined = undefined>(
|
|
1118
|
+
nullValue?: N,
|
|
1119
|
+
): N extends null ? JSchema<OUT | undefined, true> : JBuilder<OUT | undefined, true> {
|
|
1120
|
+
if (nullValue === undefined) {
|
|
1121
|
+
return super.optional() as any
|
|
1122
|
+
}
|
|
1103
1123
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1124
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
1125
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
1126
|
+
// but the typing should not reflect that.
|
|
1127
|
+
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1128
|
+
return new JSchema({
|
|
1129
|
+
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1130
|
+
optionalField: true,
|
|
1131
|
+
}) as any
|
|
1106
1132
|
}
|
|
1107
1133
|
|
|
1108
1134
|
/**
|
|
1109
|
-
*
|
|
1110
|
-
* Expects the JWT to be signed!
|
|
1135
|
+
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
1111
1136
|
*/
|
|
1112
|
-
jwt(): this {
|
|
1113
|
-
return this.regex(JWT_REGEX, { msg: 'is not a valid JWT format' })
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
url(): this {
|
|
1117
|
-
return this.regex(URL_REGEX, { msg: 'is not a valid URL format' })
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
ipv4(): this {
|
|
1121
|
-
return this.regex(IPV4_REGEX, { msg: 'is not a valid IPv4 format' })
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
ipv6(): this {
|
|
1125
|
-
return this.regex(IPV6_REGEX, { msg: 'is not a valid IPv6 format' })
|
|
1126
|
-
}
|
|
1127
1137
|
|
|
1128
|
-
|
|
1129
|
-
return this.
|
|
1138
|
+
allowAdditionalProperties(): this {
|
|
1139
|
+
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1130
1140
|
}
|
|
1131
1141
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1142
|
+
extend<P extends Record<string, JBuilder<any, any>>>(
|
|
1143
|
+
props: P,
|
|
1144
|
+
): JObject<
|
|
1145
|
+
Override<
|
|
1146
|
+
OUT,
|
|
1147
|
+
{
|
|
1148
|
+
// required keys
|
|
1149
|
+
[K in keyof P as P[K] extends JBuilder<any, infer IsOpt>
|
|
1150
|
+
? IsOpt extends true
|
|
1151
|
+
? never
|
|
1152
|
+
: K
|
|
1153
|
+
: never]: P[K] extends JBuilder<infer OUT2, any> ? OUT2 : never
|
|
1154
|
+
} & {
|
|
1155
|
+
// optional keys
|
|
1156
|
+
[K in keyof P as P[K] extends JBuilder<any, infer IsOpt>
|
|
1157
|
+
? IsOpt extends true
|
|
1158
|
+
? K
|
|
1159
|
+
: never
|
|
1160
|
+
: never]?: P[K] extends JBuilder<infer OUT2, any> ? OUT2 : never
|
|
1161
|
+
}
|
|
1162
|
+
>,
|
|
1163
|
+
false
|
|
1164
|
+
> {
|
|
1165
|
+
const newBuilder = new JObject()
|
|
1166
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1135
1167
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1168
|
+
const incomingSchemaBuilder = new JObject(props)
|
|
1169
|
+
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
1139
1170
|
|
|
1140
|
-
|
|
1141
|
-
return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' })
|
|
1142
|
-
}
|
|
1171
|
+
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
|
|
1143
1172
|
|
|
1144
|
-
|
|
1145
|
-
return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' })
|
|
1173
|
+
return newBuilder as any
|
|
1146
1174
|
}
|
|
1147
1175
|
|
|
1148
1176
|
/**
|
|
1149
|
-
*
|
|
1177
|
+
* Concatenates another schema to the current schema.
|
|
1150
1178
|
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1179
|
+
* It expects you to use `isOfType<T>()` in the chain,
|
|
1180
|
+
* otherwise the validation will throw. This is to ensure
|
|
1181
|
+
* that the schemas you concatenated match the intended final type.
|
|
1153
1182
|
*/
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1183
|
+
concat<OUT2 extends AnyObject>(other: JObject<OUT2, any>): JObject<OUT & OUT2, false> {
|
|
1184
|
+
const clone = this.clone()
|
|
1185
|
+
mergeJsonSchemaObjects(clone.schema as any, other.schema as any)
|
|
1186
|
+
_objectAssign(clone.schema, { hasIsOfTypeCheck: false })
|
|
1187
|
+
return clone as unknown as JObject<OUT & OUT2, false>
|
|
1157
1188
|
}
|
|
1158
1189
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
|
|
1192
|
+
*/
|
|
1193
|
+
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
|
|
1194
|
+
dbEntity() {
|
|
1195
|
+
return this.extend({
|
|
1196
|
+
id: j.string(),
|
|
1197
|
+
created: j.number().unixTimestamp2000(),
|
|
1198
|
+
updated: j.number().unixTimestamp2000(),
|
|
1162
1199
|
})
|
|
1163
1200
|
}
|
|
1164
1201
|
|
|
1165
|
-
|
|
1166
|
-
return this.
|
|
1202
|
+
minProperties(minProperties: number): this {
|
|
1203
|
+
return this.cloneAndUpdateSchema({ minProperties, minProperties2: minProperties })
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
maxProperties(maxProperties: number): this {
|
|
1207
|
+
return this.cloneAndUpdateSchema({ maxProperties })
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
exclusiveProperties(propNames: readonly (keyof OUT & string)[]): this {
|
|
1211
|
+
const exclusiveProperties = this.schema.exclusiveProperties ?? []
|
|
1212
|
+
return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] })
|
|
1167
1213
|
}
|
|
1168
1214
|
}
|
|
1169
1215
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1216
|
+
interface JObjectOpts {
|
|
1217
|
+
hasIsOfTypeCheck?: false
|
|
1218
|
+
patternProperties?: StringMap<JsonSchema<any>>
|
|
1219
|
+
keySchema?: JsonSchema
|
|
1172
1220
|
}
|
|
1173
1221
|
|
|
1174
|
-
export class
|
|
1175
|
-
|
|
1222
|
+
export class JObjectInfer<
|
|
1223
|
+
PROPS extends Record<string, JBuilder<any, any>>,
|
|
1224
|
+
Opt extends boolean = false,
|
|
1225
|
+
> extends JBuilder<
|
|
1226
|
+
Expand<
|
|
1227
|
+
{
|
|
1228
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1229
|
+
? IsOpt extends true
|
|
1230
|
+
? never
|
|
1231
|
+
: K
|
|
1232
|
+
: never]: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1233
|
+
} & {
|
|
1234
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1235
|
+
? IsOpt extends true
|
|
1236
|
+
? K
|
|
1237
|
+
: never
|
|
1238
|
+
: never]?: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1239
|
+
}
|
|
1240
|
+
>,
|
|
1176
1241
|
Opt
|
|
1177
1242
|
> {
|
|
1178
|
-
constructor() {
|
|
1243
|
+
constructor(props?: PROPS) {
|
|
1179
1244
|
super({
|
|
1180
|
-
type: '
|
|
1181
|
-
|
|
1245
|
+
type: 'object',
|
|
1246
|
+
properties: {},
|
|
1247
|
+
required: [],
|
|
1248
|
+
additionalProperties: false,
|
|
1182
1249
|
})
|
|
1250
|
+
|
|
1251
|
+
if (props) addPropertiesToSchema(this.schema, props)
|
|
1183
1252
|
}
|
|
1184
1253
|
|
|
1185
1254
|
/**
|
|
@@ -1188,14 +1257,51 @@ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonS
|
|
|
1188
1257
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1189
1258
|
* due to how mutability works in Ajv.
|
|
1190
1259
|
*
|
|
1191
|
-
* When `null` is passed, the return type becomes `
|
|
1260
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1192
1261
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1193
1262
|
*/
|
|
1263
|
+
// @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
|
|
1194
1264
|
override optional<N extends null | undefined = undefined>(
|
|
1195
1265
|
nullValue?: N,
|
|
1196
1266
|
): N extends null
|
|
1197
|
-
?
|
|
1198
|
-
|
|
1267
|
+
? JSchema<
|
|
1268
|
+
| Expand<
|
|
1269
|
+
{
|
|
1270
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1271
|
+
? IsOpt extends true
|
|
1272
|
+
? never
|
|
1273
|
+
: K
|
|
1274
|
+
: never]: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1275
|
+
} & {
|
|
1276
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1277
|
+
? IsOpt extends true
|
|
1278
|
+
? K
|
|
1279
|
+
: never
|
|
1280
|
+
: never]?: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1281
|
+
}
|
|
1282
|
+
>
|
|
1283
|
+
| undefined,
|
|
1284
|
+
true
|
|
1285
|
+
>
|
|
1286
|
+
: JBuilder<
|
|
1287
|
+
| Expand<
|
|
1288
|
+
{
|
|
1289
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1290
|
+
? IsOpt extends true
|
|
1291
|
+
? never
|
|
1292
|
+
: K
|
|
1293
|
+
: never]: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1294
|
+
} & {
|
|
1295
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1296
|
+
? IsOpt extends true
|
|
1297
|
+
? K
|
|
1298
|
+
: never
|
|
1299
|
+
: never]?: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1300
|
+
}
|
|
1301
|
+
>
|
|
1302
|
+
| undefined,
|
|
1303
|
+
true
|
|
1304
|
+
> {
|
|
1199
1305
|
if (nullValue === undefined) {
|
|
1200
1306
|
return super.optional() as any
|
|
1201
1307
|
}
|
|
@@ -1204,745 +1310,811 @@ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonS
|
|
|
1204
1310
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1205
1311
|
// but the typing should not reflect that.
|
|
1206
1312
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1207
|
-
return new
|
|
1313
|
+
return new JSchema({
|
|
1208
1314
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1209
1315
|
optionalField: true,
|
|
1210
1316
|
}) as any
|
|
1211
1317
|
}
|
|
1212
1318
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1319
|
+
/**
|
|
1320
|
+
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
1321
|
+
*/
|
|
1322
|
+
|
|
1323
|
+
allowAdditionalProperties(): this {
|
|
1324
|
+
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
extend<NEW_PROPS extends Record<string, JBuilder<any, any>>>(
|
|
1328
|
+
props: NEW_PROPS,
|
|
1329
|
+
): JObjectInfer<
|
|
1330
|
+
{
|
|
1331
|
+
[K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
|
|
1332
|
+
? NEW_PROPS[K]
|
|
1333
|
+
: K extends keyof PROPS
|
|
1334
|
+
? PROPS[K]
|
|
1335
|
+
: never
|
|
1336
|
+
},
|
|
1337
|
+
Opt
|
|
1338
|
+
> {
|
|
1339
|
+
const newBuilder = new JObjectInfer<PROPS, Opt>()
|
|
1340
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1341
|
+
|
|
1342
|
+
const incomingSchemaBuilder = new JObjectInfer<NEW_PROPS, false>(props)
|
|
1343
|
+
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
1344
|
+
|
|
1345
|
+
// This extend function is not type-safe as it is inferring,
|
|
1346
|
+
// so even if the base schema was already type-checked,
|
|
1347
|
+
// the new schema loses that quality.
|
|
1348
|
+
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
|
|
1349
|
+
|
|
1350
|
+
return newBuilder as unknown as JObjectInfer<
|
|
1351
|
+
{
|
|
1352
|
+
[K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
|
|
1353
|
+
? NEW_PROPS[K]
|
|
1354
|
+
: K extends keyof PROPS
|
|
1355
|
+
? PROPS[K]
|
|
1356
|
+
: never
|
|
1357
|
+
},
|
|
1358
|
+
Opt
|
|
1359
|
+
>
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
|
|
1364
|
+
*/
|
|
1365
|
+
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
|
|
1366
|
+
dbEntity() {
|
|
1367
|
+
return this.extend({
|
|
1368
|
+
id: j.string(),
|
|
1369
|
+
created: j.number().unixTimestamp2000(),
|
|
1370
|
+
updated: j.number().unixTimestamp2000(),
|
|
1371
|
+
})
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
export class JArray<OUT, Opt> extends JBuilder<OUT[], Opt> {
|
|
1376
|
+
constructor(itemsSchema: JBuilder<OUT, Opt>) {
|
|
1377
|
+
super({
|
|
1378
|
+
type: 'array',
|
|
1379
|
+
items: itemsSchema.build(),
|
|
1380
|
+
})
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
minLength(minItems: number): this {
|
|
1384
|
+
return this.cloneAndUpdateSchema({ minItems })
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
maxLength(maxItems: number): this {
|
|
1388
|
+
return this.cloneAndUpdateSchema({ maxItems })
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
length(exactLength: number): this
|
|
1392
|
+
length(minItems: number, maxItems: number): this
|
|
1393
|
+
length(minItemsOrExact: number, maxItems?: number): this {
|
|
1394
|
+
const maxItemsActual = maxItems ?? minItemsOrExact
|
|
1395
|
+
return this.minLength(minItemsOrExact).maxLength(maxItemsActual)
|
|
1215
1396
|
}
|
|
1216
1397
|
|
|
1217
|
-
|
|
1218
|
-
return this.
|
|
1398
|
+
exactLength(length: number): this {
|
|
1399
|
+
return this.minLength(length).maxLength(length)
|
|
1219
1400
|
}
|
|
1220
1401
|
|
|
1221
|
-
|
|
1222
|
-
return this.cloneAndUpdateSchema({
|
|
1402
|
+
unique(): this {
|
|
1403
|
+
return this.cloneAndUpdateSchema({ uniqueItems: true })
|
|
1223
1404
|
}
|
|
1405
|
+
}
|
|
1224
1406
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1407
|
+
class JSet2Builder<OUT, Opt> extends JBuilder<Set2<OUT>, Opt> {
|
|
1408
|
+
constructor(itemsSchema: JBuilder<OUT, Opt>) {
|
|
1409
|
+
super({
|
|
1410
|
+
type: ['array', 'object'],
|
|
1411
|
+
Set2: itemsSchema.build(),
|
|
1412
|
+
})
|
|
1227
1413
|
}
|
|
1228
1414
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (incl === '[)') {
|
|
1233
|
-
schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } }
|
|
1234
|
-
} else if (incl === '[]') {
|
|
1235
|
-
schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } }
|
|
1236
|
-
}
|
|
1415
|
+
min(minItems: number): this {
|
|
1416
|
+
return this.cloneAndUpdateSchema({ minItems })
|
|
1417
|
+
}
|
|
1237
1418
|
|
|
1238
|
-
|
|
1419
|
+
max(maxItems: number): this {
|
|
1420
|
+
return this.cloneAndUpdateSchema({ maxItems })
|
|
1239
1421
|
}
|
|
1240
1422
|
}
|
|
1241
1423
|
|
|
1242
|
-
export
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1424
|
+
export class JEnum<
|
|
1425
|
+
OUT extends string | number | boolean | null,
|
|
1426
|
+
Opt extends boolean = false,
|
|
1427
|
+
> extends JBuilder<OUT, Opt> {
|
|
1428
|
+
constructor(enumValues: readonly OUT[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt) {
|
|
1429
|
+
const jsonSchema: JsonSchema = { enum: enumValues }
|
|
1430
|
+
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
1431
|
+
// Having only the `enum` in the schema does not trigger a coercion in Ajv.
|
|
1432
|
+
if (baseType === 'string') jsonSchema.type = 'string'
|
|
1433
|
+
if (baseType === 'number') jsonSchema.type = 'number'
|
|
1248
1434
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
constructor() {
|
|
1254
|
-
super({
|
|
1255
|
-
type: 'string',
|
|
1256
|
-
IsoMonth: {},
|
|
1257
|
-
})
|
|
1435
|
+
super(jsonSchema)
|
|
1436
|
+
|
|
1437
|
+
if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
1438
|
+
if (opt?.msg) this.setErrorMessage('enum', opt.msg)
|
|
1258
1439
|
}
|
|
1259
|
-
}
|
|
1260
1440
|
|
|
1261
|
-
|
|
1441
|
+
branded<B extends OUT>(): JEnum<B, Opt> {
|
|
1442
|
+
return this as unknown as JEnum<B, Opt>
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1262
1445
|
|
|
1263
|
-
export class
|
|
1264
|
-
|
|
1265
|
-
Opt extends boolean = false,
|
|
1266
|
-
> extends JsonSchemaAnyBuilder<OUT, Opt> {
|
|
1267
|
-
constructor() {
|
|
1446
|
+
export class JTuple<ITEMS extends JBuilder<any, any>[]> extends JBuilder<TupleOut<ITEMS>, false> {
|
|
1447
|
+
constructor(items: ITEMS) {
|
|
1268
1448
|
super({
|
|
1269
|
-
type: '
|
|
1449
|
+
type: 'array',
|
|
1450
|
+
prefixItems: items.map(i => i.build()),
|
|
1451
|
+
minItems: items.length,
|
|
1452
|
+
maxItems: items.length,
|
|
1270
1453
|
})
|
|
1271
1454
|
}
|
|
1455
|
+
}
|
|
1272
1456
|
|
|
1273
|
-
|
|
1274
|
-
* @param optionalValues List of values that should be considered/converted as `undefined`.
|
|
1275
|
-
*
|
|
1276
|
-
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
1277
|
-
* due to how mutability works in Ajv.
|
|
1278
|
-
*
|
|
1279
|
-
* Make sure this `optional()` call is at the end of your call chain.
|
|
1280
|
-
*
|
|
1281
|
-
* When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
|
|
1282
|
-
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1283
|
-
*/
|
|
1284
|
-
override optional<T extends readonly (number | null)[] | undefined = undefined>(
|
|
1285
|
-
optionalValues?: T,
|
|
1286
|
-
): T extends readonly (infer U)[]
|
|
1287
|
-
? null extends U
|
|
1288
|
-
? JsonSchemaTerminal<OUT | undefined, true>
|
|
1289
|
-
: JsonSchemaNumberBuilder<OUT | undefined, true>
|
|
1290
|
-
: JsonSchemaNumberBuilder<OUT | undefined, true> {
|
|
1291
|
-
if (!optionalValues) {
|
|
1292
|
-
return super.optional() as any
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
_typeCast<(number | null)[]>(optionalValues)
|
|
1457
|
+
// ==== Standalone functions for j.object ====
|
|
1296
1458
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const alternativesSchema = j.enum(optionalValues)
|
|
1302
|
-
Object.assign(newBuilder.getSchema(), {
|
|
1303
|
-
anyOf: [this.build(), alternativesSchema.build()],
|
|
1304
|
-
optionalValues,
|
|
1305
|
-
})
|
|
1459
|
+
function object(props: AnyObject): never
|
|
1460
|
+
function object<OUT extends AnyObject>(props: {
|
|
1461
|
+
[K in keyof Required<OUT>]-?: JSchema<OUT[K], any>
|
|
1462
|
+
}): JObject<OUT, false>
|
|
1306
1463
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
newBuilder = new JsonSchemaTerminal({
|
|
1313
|
-
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
1314
|
-
optionalField: true,
|
|
1315
|
-
})
|
|
1316
|
-
}
|
|
1464
|
+
function object<OUT extends AnyObject>(props: {
|
|
1465
|
+
[key in keyof OUT]: JSchema<OUT[key], any>
|
|
1466
|
+
}): JObject<OUT, false> {
|
|
1467
|
+
return new JObject<OUT, false>(props)
|
|
1468
|
+
}
|
|
1317
1469
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1470
|
+
function objectInfer<P extends Record<string, JBuilder<any, any>>>(
|
|
1471
|
+
props: P,
|
|
1472
|
+
): JObjectInfer<P, false> {
|
|
1473
|
+
return new JObjectInfer<P, false>(props)
|
|
1474
|
+
}
|
|
1320
1475
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1476
|
+
function objectDbEntity(props: AnyObject): never
|
|
1477
|
+
function objectDbEntity<
|
|
1478
|
+
OUT extends BaseDBEntity,
|
|
1479
|
+
EXTRA_KEYS extends Exclude<keyof OUT, keyof BaseDBEntity> = Exclude<
|
|
1480
|
+
keyof OUT,
|
|
1481
|
+
keyof BaseDBEntity
|
|
1482
|
+
>,
|
|
1483
|
+
>(
|
|
1484
|
+
props: {
|
|
1485
|
+
// ✅ all non-system fields must be explicitly provided
|
|
1486
|
+
[K in EXTRA_KEYS]-?: BuilderFor<OUT[K]>
|
|
1487
|
+
} &
|
|
1488
|
+
// ✅ if `id` differs, it's required
|
|
1489
|
+
(ExactMatch<OUT['id'], BaseDBEntity['id']> extends true
|
|
1490
|
+
? { id?: BuilderFor<BaseDBEntity['id']> }
|
|
1491
|
+
: { id: BuilderFor<OUT['id']> }) &
|
|
1492
|
+
(ExactMatch<OUT['created'], BaseDBEntity['created']> extends true
|
|
1493
|
+
? { created?: BuilderFor<BaseDBEntity['created']> }
|
|
1494
|
+
: { created: BuilderFor<OUT['created']> }) &
|
|
1495
|
+
(ExactMatch<OUT['updated'], BaseDBEntity['updated']> extends true
|
|
1496
|
+
? { updated?: BuilderFor<BaseDBEntity['updated']> }
|
|
1497
|
+
: { updated: BuilderFor<OUT['updated']> }),
|
|
1498
|
+
): JObject<OUT, false>
|
|
1324
1499
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1500
|
+
function objectDbEntity(props: AnyObject): any {
|
|
1501
|
+
return j.object({
|
|
1502
|
+
id: j.string(),
|
|
1503
|
+
created: j.number().unixTimestamp2000(),
|
|
1504
|
+
updated: j.number().unixTimestamp2000(),
|
|
1505
|
+
...props,
|
|
1506
|
+
})
|
|
1507
|
+
}
|
|
1328
1508
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1509
|
+
function record<
|
|
1510
|
+
KS extends JBuilder<any, any>,
|
|
1511
|
+
VS extends JBuilder<any, any>,
|
|
1512
|
+
Opt extends boolean = SchemaOpt<VS>,
|
|
1513
|
+
>(
|
|
1514
|
+
keySchema: KS,
|
|
1515
|
+
valueSchema: VS,
|
|
1516
|
+
): JObject<
|
|
1517
|
+
Opt extends true
|
|
1518
|
+
? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
|
|
1519
|
+
: Record<SchemaOut<KS>, SchemaOut<VS>>,
|
|
1520
|
+
false
|
|
1521
|
+
> {
|
|
1522
|
+
const keyJsonSchema = keySchema.build()
|
|
1523
|
+
// Check if value schema is optional before build() strips the optionalField flag
|
|
1524
|
+
const isValueOptional = (valueSchema as JSchema<any, any>).getSchema().optionalField
|
|
1525
|
+
const valueJsonSchema = valueSchema.build()
|
|
1332
1526
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1527
|
+
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
1528
|
+
const finalValueSchema: JsonSchema = isValueOptional
|
|
1529
|
+
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
1530
|
+
: valueJsonSchema
|
|
1336
1531
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1532
|
+
return new JObject<
|
|
1533
|
+
Opt extends true
|
|
1534
|
+
? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
|
|
1535
|
+
: Record<SchemaOut<KS>, SchemaOut<VS>>,
|
|
1536
|
+
false
|
|
1537
|
+
>([], {
|
|
1538
|
+
hasIsOfTypeCheck: false,
|
|
1539
|
+
keySchema: keyJsonSchema,
|
|
1540
|
+
patternProperties: {
|
|
1541
|
+
['^.*$']: finalValueSchema,
|
|
1542
|
+
},
|
|
1543
|
+
})
|
|
1544
|
+
}
|
|
1340
1545
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1546
|
+
function withRegexKeys<S extends JBuilder<any, any>>(
|
|
1547
|
+
keyRegex: RegExp | string,
|
|
1548
|
+
schema: S,
|
|
1549
|
+
): JObject<StringMap<SchemaOut<S>>, false> {
|
|
1550
|
+
if (keyRegex instanceof RegExp) {
|
|
1551
|
+
_assert(
|
|
1552
|
+
!keyRegex.flags,
|
|
1553
|
+
`Regex flags are not supported by JSON Schema. Received: /${keyRegex.source}/${keyRegex.flags}`,
|
|
1554
|
+
)
|
|
1343
1555
|
}
|
|
1556
|
+
const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex
|
|
1557
|
+
const jsonSchema = schema.build()
|
|
1344
1558
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1559
|
+
return new JObject<StringMap<SchemaOut<S>>, false>([], {
|
|
1560
|
+
hasIsOfTypeCheck: false,
|
|
1561
|
+
patternProperties: {
|
|
1562
|
+
[pattern]: jsonSchema,
|
|
1563
|
+
},
|
|
1564
|
+
})
|
|
1565
|
+
}
|
|
1348
1566
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1567
|
+
/**
|
|
1568
|
+
* Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
|
|
1569
|
+
*/
|
|
1570
|
+
function withEnumKeys<
|
|
1571
|
+
const T extends readonly (string | number)[] | StringEnum | NumberEnum,
|
|
1572
|
+
S extends JBuilder<any, any>,
|
|
1573
|
+
K extends string | number = EnumKeyUnion<T>,
|
|
1574
|
+
Opt extends boolean = SchemaOpt<S>,
|
|
1575
|
+
>(
|
|
1576
|
+
keys: T,
|
|
1577
|
+
schema: S,
|
|
1578
|
+
): JObject<Opt extends true ? { [P in K]?: SchemaOut<S> } : { [P in K]: SchemaOut<S> }, false> {
|
|
1579
|
+
let enumValues: readonly (string | number)[] | undefined
|
|
1580
|
+
if (Array.isArray(keys)) {
|
|
1581
|
+
_assert(
|
|
1582
|
+
isEveryItemPrimitive(keys),
|
|
1583
|
+
'Every item in the key list should be string, number or symbol',
|
|
1584
|
+
)
|
|
1585
|
+
enumValues = keys
|
|
1586
|
+
} else if (typeof keys === 'object') {
|
|
1587
|
+
const enumType = getEnumType(keys)
|
|
1588
|
+
_assert(
|
|
1589
|
+
enumType === 'NumberEnum' || enumType === 'StringEnum',
|
|
1590
|
+
'The key list should be StringEnum or NumberEnum',
|
|
1591
|
+
)
|
|
1592
|
+
if (enumType === 'NumberEnum') {
|
|
1593
|
+
enumValues = _numberEnumValues(keys as NumberEnum)
|
|
1594
|
+
} else if (enumType === 'StringEnum') {
|
|
1595
|
+
enumValues = _stringEnumValues(keys as StringEnum)
|
|
1596
|
+
}
|
|
1351
1597
|
}
|
|
1352
1598
|
|
|
1353
|
-
|
|
1354
|
-
return this.max(value)
|
|
1355
|
-
}
|
|
1599
|
+
_assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum')
|
|
1356
1600
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1601
|
+
const typedValues = enumValues as readonly K[]
|
|
1602
|
+
const props = Object.fromEntries(typedValues.map(key => [key, schema])) as any
|
|
1360
1603
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1604
|
+
return new JObject<
|
|
1605
|
+
Opt extends true ? { [P in K]?: SchemaOut<S> } : { [P in K]: SchemaOut<S> },
|
|
1606
|
+
false
|
|
1607
|
+
>(props, { hasIsOfTypeCheck: false })
|
|
1608
|
+
}
|
|
1364
1609
|
|
|
1365
|
-
|
|
1366
|
-
return this.min(value).max(value)
|
|
1367
|
-
}
|
|
1610
|
+
// ==== AjvSchema compat wrapper ====
|
|
1368
1611
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1612
|
+
/**
|
|
1613
|
+
* On creation - compiles ajv validation function.
|
|
1614
|
+
* Provides convenient methods, error reporting, etc.
|
|
1615
|
+
*/
|
|
1616
|
+
export class AjvSchema<OUT> {
|
|
1617
|
+
private constructor(
|
|
1618
|
+
public schema: JsonSchema<OUT>,
|
|
1619
|
+
cfg: Partial<AjvSchemaCfg> = {},
|
|
1620
|
+
preCompiledFn?: any,
|
|
1621
|
+
) {
|
|
1622
|
+
this.cfg = {
|
|
1623
|
+
lazy: false,
|
|
1624
|
+
...cfg,
|
|
1625
|
+
ajv: cfg.ajv || getAjv(),
|
|
1626
|
+
// Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
|
|
1627
|
+
inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
|
1372
1628
|
}
|
|
1373
|
-
return this.moreThanOrEqual(minimum).lessThanOrEqual(maximum)
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
int32(): this {
|
|
1377
|
-
const MIN_INT32 = -(2 ** 31)
|
|
1378
|
-
const MAX_INT32 = 2 ** 31 - 1
|
|
1379
|
-
const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
|
|
1380
|
-
const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
1381
|
-
const newMin = Math.max(MIN_INT32, currentMin)
|
|
1382
|
-
const newMax = Math.min(MAX_INT32, currentMax)
|
|
1383
|
-
return this.integer().min(newMin).max(newMax)
|
|
1384
|
-
}
|
|
1385
1629
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
return this.integer().min(newMin).max(newMax)
|
|
1630
|
+
if (preCompiledFn) {
|
|
1631
|
+
this._compiledFn = preCompiledFn
|
|
1632
|
+
} else if (!cfg.lazy) {
|
|
1633
|
+
this._getValidateFn() // compile eagerly
|
|
1634
|
+
}
|
|
1392
1635
|
}
|
|
1393
1636
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1637
|
+
/**
|
|
1638
|
+
* Shortcut for AjvSchema.create(schema, { lazy: true })
|
|
1639
|
+
*/
|
|
1640
|
+
static createLazy<OUT>(
|
|
1641
|
+
schema: SchemaHandledByAjv<OUT>,
|
|
1642
|
+
cfg?: Partial<AjvSchemaCfg>,
|
|
1643
|
+
): AjvSchema<OUT> {
|
|
1644
|
+
return AjvSchema.create(schema, {
|
|
1645
|
+
lazy: true,
|
|
1646
|
+
...cfg,
|
|
1647
|
+
})
|
|
1396
1648
|
}
|
|
1397
1649
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1650
|
+
/**
|
|
1651
|
+
* Conveniently allows to pass either JsonSchema or JSchema builder, or existing AjvSchema.
|
|
1652
|
+
* If it's already an AjvSchema - it'll just return it without any processing.
|
|
1653
|
+
* If it's a Builder - will call `build` before proceeding.
|
|
1654
|
+
* Otherwise - will construct AjvSchema instance ready to be used.
|
|
1655
|
+
*/
|
|
1656
|
+
static create<OUT>(schema: SchemaHandledByAjv<OUT>, cfg?: Partial<AjvSchemaCfg>): AjvSchema<OUT> {
|
|
1657
|
+
if (schema instanceof AjvSchema) return schema
|
|
1401
1658
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1659
|
+
if (AjvSchema.isSchemaWithCachedAjvSchema<typeof schema, OUT>(schema)) {
|
|
1660
|
+
return AjvSchema.requireCachedAjvSchema<typeof schema, OUT>(schema)
|
|
1661
|
+
}
|
|
1405
1662
|
|
|
1406
|
-
|
|
1407
|
-
return this.integer().min(TS_2000).max(TS_2500).branded<UnixTimestamp>()
|
|
1408
|
-
}
|
|
1663
|
+
let jsonSchema: JsonSchema<OUT>
|
|
1409
1664
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1665
|
+
if (schema instanceof JSchema) {
|
|
1666
|
+
// oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
|
|
1667
|
+
jsonSchema = (schema as JSchema<OUT, any>).build()
|
|
1668
|
+
AjvSchema.requireValidJsonSchema(jsonSchema)
|
|
1669
|
+
} else {
|
|
1670
|
+
jsonSchema = schema
|
|
1671
|
+
}
|
|
1413
1672
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1673
|
+
// This is our own helper which marks a schema as optional
|
|
1674
|
+
// in case it is going to be used in an object schema,
|
|
1675
|
+
// where we need to mark the given property as not-required.
|
|
1676
|
+
// But once all compilation is done, the presence of this field
|
|
1677
|
+
// really upsets Ajv.
|
|
1678
|
+
delete jsonSchema.optionalField
|
|
1417
1679
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
.multipleOf(15)
|
|
1421
|
-
.min(-12 * 60)
|
|
1422
|
-
.max(14 * 60)
|
|
1423
|
-
}
|
|
1680
|
+
const ajvSchema = new AjvSchema<OUT>(jsonSchema, cfg)
|
|
1681
|
+
AjvSchema.cacheAjvSchema(schema, ajvSchema)
|
|
1424
1682
|
|
|
1425
|
-
|
|
1426
|
-
return this.integer().min(-12).max(14)
|
|
1683
|
+
return ajvSchema
|
|
1427
1684
|
}
|
|
1428
1685
|
|
|
1429
1686
|
/**
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
* due to how mutability works in Ajv.
|
|
1687
|
+
* Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
|
|
1688
|
+
* Used internally by JSchema to cache a compatible AjvSchema instance.
|
|
1433
1689
|
*/
|
|
1434
|
-
|
|
1435
|
-
return
|
|
1690
|
+
static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: any): AjvSchema<OUT> {
|
|
1691
|
+
return new AjvSchema<OUT>(schema, {}, compiledFn)
|
|
1436
1692
|
}
|
|
1437
|
-
}
|
|
1438
1693
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
constructor() {
|
|
1444
|
-
super({
|
|
1445
|
-
type: 'boolean',
|
|
1446
|
-
})
|
|
1694
|
+
static isSchemaWithCachedAjvSchema<Base, OUT>(
|
|
1695
|
+
schema: Base,
|
|
1696
|
+
): schema is WithCachedAjvSchema<Base, OUT> {
|
|
1697
|
+
return !!(schema as any)?.[HIDDEN_AJV_SCHEMA]
|
|
1447
1698
|
}
|
|
1448
1699
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
*/
|
|
1455
|
-
override optional(optionalValue?: boolean): JsonSchemaBooleanBuilder<OUT | undefined, true> {
|
|
1456
|
-
if (typeof optionalValue === 'undefined') {
|
|
1457
|
-
return super.optional() as unknown as JsonSchemaBooleanBuilder<OUT | undefined, true>
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
const newBuilder = new JsonSchemaBooleanBuilder<OUT, Opt>().optional()
|
|
1461
|
-
const alternativesSchema = j.enum([optionalValue])
|
|
1462
|
-
Object.assign(newBuilder.getSchema(), {
|
|
1463
|
-
anyOf: [this.build(), alternativesSchema.build()],
|
|
1464
|
-
optionalValues: [optionalValue],
|
|
1465
|
-
})
|
|
1466
|
-
|
|
1467
|
-
return newBuilder
|
|
1700
|
+
static cacheAjvSchema<Base extends AnyObject, OUT>(
|
|
1701
|
+
schema: Base,
|
|
1702
|
+
ajvSchema: AjvSchema<OUT>,
|
|
1703
|
+
): WithCachedAjvSchema<Base, OUT> {
|
|
1704
|
+
return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema })
|
|
1468
1705
|
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
export class JsonSchemaObjectBuilder<
|
|
1472
|
-
OUT extends AnyObject,
|
|
1473
|
-
Opt extends boolean = false,
|
|
1474
|
-
> extends JsonSchemaAnyBuilder<OUT, Opt> {
|
|
1475
|
-
constructor(props?: AnyObject, opt?: JsonSchemaObjectBuilderOpts) {
|
|
1476
|
-
super({
|
|
1477
|
-
type: 'object',
|
|
1478
|
-
properties: {},
|
|
1479
|
-
required: [],
|
|
1480
|
-
additionalProperties: false,
|
|
1481
|
-
hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
|
|
1482
|
-
patternProperties: opt?.patternProperties ?? undefined,
|
|
1483
|
-
keySchema: opt?.keySchema ?? undefined,
|
|
1484
|
-
})
|
|
1485
1706
|
|
|
1486
|
-
|
|
1707
|
+
static requireCachedAjvSchema<Base, OUT>(schema: WithCachedAjvSchema<Base, OUT>): AjvSchema<OUT> {
|
|
1708
|
+
return schema[HIDDEN_AJV_SCHEMA]
|
|
1487
1709
|
}
|
|
1488
1710
|
|
|
1489
|
-
|
|
1490
|
-
const properties: Record<string, JsonSchema> = {}
|
|
1491
|
-
const required: string[] = []
|
|
1711
|
+
readonly cfg: AjvSchemaCfg
|
|
1492
1712
|
|
|
1493
|
-
|
|
1494
|
-
const isOptional = (builder as JsonSchemaTerminal<any, any>).getSchema().optionalField
|
|
1495
|
-
if (!isOptional) {
|
|
1496
|
-
required.push(key)
|
|
1497
|
-
}
|
|
1713
|
+
private _compiledFn: any
|
|
1498
1714
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1715
|
+
private _getValidateFn(): any {
|
|
1716
|
+
if (!this._compiledFn) {
|
|
1717
|
+
this._compiledFn = this.cfg.ajv.compile(this.schema as any)
|
|
1501
1718
|
}
|
|
1502
|
-
|
|
1503
|
-
this.schema.properties = properties
|
|
1504
|
-
this.schema.required = _uniq(required).sort()
|
|
1505
|
-
|
|
1506
|
-
return this
|
|
1719
|
+
return this._compiledFn
|
|
1507
1720
|
}
|
|
1508
1721
|
|
|
1509
1722
|
/**
|
|
1510
|
-
*
|
|
1511
|
-
*
|
|
1512
|
-
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1513
|
-
* due to how mutability works in Ajv.
|
|
1514
|
-
*
|
|
1515
|
-
* When `null` is passed, the return type becomes `JsonSchemaTerminal`
|
|
1516
|
-
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1723
|
+
* It returns the original object just for convenience.
|
|
1517
1724
|
*/
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
: JsonSchemaAnyBuilder<OUT | undefined, true> {
|
|
1523
|
-
if (nullValue === undefined) {
|
|
1524
|
-
return super.optional() as any
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
1528
|
-
// so we must allow `null` values to be parsed by Ajv,
|
|
1529
|
-
// but the typing should not reflect that.
|
|
1530
|
-
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1531
|
-
return new JsonSchemaTerminal({
|
|
1532
|
-
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1533
|
-
optionalField: true,
|
|
1534
|
-
}) as any
|
|
1725
|
+
validate(input: unknown, opt: AjvValidationOptions = {}): OUT {
|
|
1726
|
+
const [err, output] = this.getValidationResult(input, opt)
|
|
1727
|
+
if (err) throw err
|
|
1728
|
+
return output
|
|
1535
1729
|
}
|
|
1536
1730
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
allowAdditionalProperties(): this {
|
|
1542
|
-
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1731
|
+
isValid(input: unknown, opt?: AjvValidationOptions): boolean {
|
|
1732
|
+
const [err] = this.getValidationResult(input, opt)
|
|
1733
|
+
return !err
|
|
1543
1734
|
}
|
|
1544
1735
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
// required keys
|
|
1552
|
-
[K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, infer IsOpt>
|
|
1553
|
-
? IsOpt extends true
|
|
1554
|
-
? never
|
|
1555
|
-
: K
|
|
1556
|
-
: never]: P[K] extends JsonSchemaAnyBuilder<infer OUT2, any> ? OUT2 : never
|
|
1557
|
-
} & {
|
|
1558
|
-
// optional keys
|
|
1559
|
-
[K in keyof P as P[K] extends JsonSchemaAnyBuilder<any, infer IsOpt>
|
|
1560
|
-
? IsOpt extends true
|
|
1561
|
-
? K
|
|
1562
|
-
: never
|
|
1563
|
-
: never]?: P[K] extends JsonSchemaAnyBuilder<infer OUT2, any> ? OUT2 : never
|
|
1564
|
-
}
|
|
1565
|
-
>,
|
|
1566
|
-
false
|
|
1567
|
-
> {
|
|
1568
|
-
const newBuilder = new JsonSchemaObjectBuilder()
|
|
1569
|
-
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1570
|
-
|
|
1571
|
-
const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props)
|
|
1572
|
-
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
1573
|
-
|
|
1574
|
-
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
|
|
1575
|
-
|
|
1576
|
-
return newBuilder as any
|
|
1736
|
+
getValidationResult(
|
|
1737
|
+
input: unknown,
|
|
1738
|
+
opt: AjvValidationOptions = {},
|
|
1739
|
+
): ValidationFunctionResult<OUT, AjvValidationError> {
|
|
1740
|
+
const fn = this._getValidateFn()
|
|
1741
|
+
return executeValidation<OUT>(fn, this.schema, input, opt, this.cfg.inputName)
|
|
1577
1742
|
}
|
|
1578
1743
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
* interface Foo { foo: string }
|
|
1588
|
-
* const fooSchema = j.object<Foo>({ foo: j.string() })
|
|
1589
|
-
*
|
|
1590
|
-
* interface Bar { bar: number }
|
|
1591
|
-
* const barSchema = j.object<Bar>({ bar: j.number() })
|
|
1592
|
-
*
|
|
1593
|
-
* interface Shu { foo: string, bar: number }
|
|
1594
|
-
* const shuSchema = fooSchema.concat(barSchema).isOfType<Shu>() // important
|
|
1595
|
-
* ```
|
|
1596
|
-
*/
|
|
1597
|
-
concat<OUT2 extends AnyObject>(
|
|
1598
|
-
other: JsonSchemaObjectBuilder<OUT2, any>,
|
|
1599
|
-
): JsonSchemaObjectBuilder<OUT & OUT2, false> {
|
|
1600
|
-
const clone = this.clone()
|
|
1601
|
-
mergeJsonSchemaObjects(clone.schema as any, other.schema as any)
|
|
1602
|
-
_objectAssign(clone.schema, { hasIsOfTypeCheck: false })
|
|
1603
|
-
return clone as unknown as JsonSchemaObjectBuilder<OUT & OUT2, false>
|
|
1744
|
+
getValidationFunction(): ValidationFunction<OUT, AjvValidationError> {
|
|
1745
|
+
return (input, opt) => {
|
|
1746
|
+
return this.getValidationResult(input, {
|
|
1747
|
+
mutateInput: opt?.mutateInput,
|
|
1748
|
+
inputName: opt?.inputName,
|
|
1749
|
+
inputId: opt?.inputId,
|
|
1750
|
+
})
|
|
1751
|
+
}
|
|
1604
1752
|
}
|
|
1605
1753
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
updated: j.number().unixTimestamp2000(),
|
|
1615
|
-
})
|
|
1754
|
+
private static requireValidJsonSchema(schema: JsonSchema): void {
|
|
1755
|
+
// For object schemas we require that it is type checked against an external type, e.g.:
|
|
1756
|
+
// interface Foo { name: string }
|
|
1757
|
+
// const schema = j.object({ name: j.string() }).ofType<Foo>()
|
|
1758
|
+
_assert(
|
|
1759
|
+
schema.type !== 'object' || schema.hasIsOfTypeCheck,
|
|
1760
|
+
'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.',
|
|
1761
|
+
)
|
|
1616
1762
|
}
|
|
1763
|
+
}
|
|
1617
1764
|
|
|
1618
|
-
|
|
1619
|
-
return this.cloneAndUpdateSchema({ minProperties, minProperties2: minProperties })
|
|
1620
|
-
}
|
|
1765
|
+
// ==== Shared validation logic ====
|
|
1621
1766
|
|
|
1622
|
-
|
|
1623
|
-
return this.cloneAndUpdateSchema({ maxProperties })
|
|
1624
|
-
}
|
|
1767
|
+
const separator = '\n'
|
|
1625
1768
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1769
|
+
function executeValidation<OUT>(
|
|
1770
|
+
fn: any,
|
|
1771
|
+
builtSchema: JsonSchema,
|
|
1772
|
+
input: unknown,
|
|
1773
|
+
opt: AjvValidationOptions = {},
|
|
1774
|
+
defaultInputName?: string,
|
|
1775
|
+
): ValidationFunctionResult<OUT, AjvValidationError> {
|
|
1776
|
+
const item =
|
|
1777
|
+
opt.mutateInput !== false || typeof input !== 'object'
|
|
1778
|
+
? input // mutate
|
|
1779
|
+
: _deepCopy(input) // not mutate
|
|
1780
|
+
|
|
1781
|
+
let valid = fn(item) // mutates item, but not input
|
|
1782
|
+
_typeCast<OUT>(item)
|
|
1783
|
+
|
|
1784
|
+
let output: OUT = item
|
|
1785
|
+
if (valid && builtSchema.postValidation) {
|
|
1786
|
+
const [err, result] = _try(() => builtSchema.postValidation!(output))
|
|
1787
|
+
if (err) {
|
|
1788
|
+
valid = false
|
|
1789
|
+
;(fn as any).errors = [
|
|
1790
|
+
{
|
|
1791
|
+
instancePath: '',
|
|
1792
|
+
message: err.message,
|
|
1793
|
+
},
|
|
1794
|
+
]
|
|
1795
|
+
} else {
|
|
1796
|
+
output = result as OUT
|
|
1797
|
+
}
|
|
1629
1798
|
}
|
|
1630
|
-
}
|
|
1631
1799
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1800
|
+
if (valid) return [null, output]
|
|
1801
|
+
|
|
1802
|
+
const errors = fn.errors!
|
|
1803
|
+
|
|
1804
|
+
const {
|
|
1805
|
+
inputId = _isObject(input) ? (input as any)['id'] : undefined,
|
|
1806
|
+
inputName = defaultInputName || 'Object',
|
|
1807
|
+
} = opt
|
|
1808
|
+
const dataVar = [inputName, inputId].filter(Boolean).join('.')
|
|
1809
|
+
|
|
1810
|
+
applyImprovementsOnErrorMessages(errors, builtSchema)
|
|
1811
|
+
|
|
1812
|
+
let message = getAjv().errorsText(errors, {
|
|
1813
|
+
dataVar,
|
|
1814
|
+
separator,
|
|
1815
|
+
})
|
|
1816
|
+
|
|
1817
|
+
// Note: if we mutated the input already, e.g stripped unknown properties,
|
|
1818
|
+
// the error message Input would contain already mutated object print, such as Input: {}
|
|
1819
|
+
// Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
|
|
1820
|
+
const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 })
|
|
1821
|
+
message = [message, 'Input: ' + inputStringified].join(separator)
|
|
1822
|
+
|
|
1823
|
+
const err = new AjvValidationError(
|
|
1824
|
+
message,
|
|
1825
|
+
_filterNullishValues({
|
|
1826
|
+
errors,
|
|
1827
|
+
inputName,
|
|
1828
|
+
inputId,
|
|
1829
|
+
}),
|
|
1830
|
+
)
|
|
1831
|
+
return [err, output]
|
|
1636
1832
|
}
|
|
1637
1833
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
>
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1834
|
+
// ==== Error formatting helpers ====
|
|
1835
|
+
|
|
1836
|
+
function applyImprovementsOnErrorMessages(
|
|
1837
|
+
errors: ErrorObject<string, Record<string, any>, unknown>[] | null | undefined,
|
|
1838
|
+
schema: JsonSchema,
|
|
1839
|
+
): void {
|
|
1840
|
+
if (!errors) return
|
|
1841
|
+
|
|
1842
|
+
filterNullableAnyOfErrors(errors, schema)
|
|
1843
|
+
|
|
1844
|
+
const { errorMessages } = schema
|
|
1845
|
+
|
|
1846
|
+
for (const error of errors) {
|
|
1847
|
+
const errorMessage = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword)
|
|
1848
|
+
|
|
1849
|
+
if (errorMessage) {
|
|
1850
|
+
error.message = errorMessage
|
|
1851
|
+
} else if (errorMessages?.[error.keyword]) {
|
|
1852
|
+
error.message = errorMessages[error.keyword]
|
|
1853
|
+
} else {
|
|
1854
|
+
const unwrapped = unwrapNullableAnyOf(schema)
|
|
1855
|
+
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
1856
|
+
error.message = unwrapped.errorMessages[error.keyword]
|
|
1857
|
+
}
|
|
1655
1858
|
}
|
|
1656
|
-
>,
|
|
1657
|
-
Opt
|
|
1658
|
-
> {
|
|
1659
|
-
constructor(props?: PROPS) {
|
|
1660
|
-
super({
|
|
1661
|
-
type: 'object',
|
|
1662
|
-
properties: {},
|
|
1663
|
-
required: [],
|
|
1664
|
-
additionalProperties: false,
|
|
1665
|
-
})
|
|
1666
1859
|
|
|
1667
|
-
|
|
1860
|
+
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.')
|
|
1668
1861
|
}
|
|
1862
|
+
}
|
|
1669
1863
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1864
|
+
/**
|
|
1865
|
+
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
1866
|
+
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
1867
|
+
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
1868
|
+
* that are confusing. This method splices them out, keeping only the real errors.
|
|
1869
|
+
*/
|
|
1870
|
+
function filterNullableAnyOfErrors(
|
|
1871
|
+
errors: ErrorObject<string, Record<string, any>, unknown>[],
|
|
1872
|
+
schema: JsonSchema,
|
|
1873
|
+
): void {
|
|
1874
|
+
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
1875
|
+
const exactPaths: string[] = []
|
|
1876
|
+
const nullBranchPrefixes: string[] = []
|
|
1673
1877
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
if (!isOptional) {
|
|
1677
|
-
required.push(key)
|
|
1678
|
-
}
|
|
1878
|
+
for (const error of errors) {
|
|
1879
|
+
if (error.keyword !== 'anyOf') continue
|
|
1679
1880
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
}
|
|
1881
|
+
const parentSchema = resolveSchemaPath(schema, error.schemaPath)
|
|
1882
|
+
if (!parentSchema) continue
|
|
1683
1883
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1884
|
+
const nullIndex = unwrapNullableAnyOfIndex(parentSchema)
|
|
1885
|
+
if (nullIndex === -1) continue
|
|
1686
1886
|
|
|
1687
|
-
|
|
1887
|
+
exactPaths.push(error.schemaPath) // e.g. "#/anyOf"
|
|
1888
|
+
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length)
|
|
1889
|
+
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`) // e.g. "#/anyOf/1/"
|
|
1688
1890
|
}
|
|
1689
1891
|
|
|
1690
|
-
|
|
1691
|
-
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
1692
|
-
*
|
|
1693
|
-
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1694
|
-
* due to how mutability works in Ajv.
|
|
1695
|
-
*
|
|
1696
|
-
* When `null` is passed, the return type becomes `JsonSchemaTerminal`
|
|
1697
|
-
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1698
|
-
*/
|
|
1699
|
-
// @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
|
|
1700
|
-
override optional<N extends null | undefined = undefined>(
|
|
1701
|
-
nullValue?: N,
|
|
1702
|
-
): N extends null
|
|
1703
|
-
? JsonSchemaTerminal<
|
|
1704
|
-
| Expand<
|
|
1705
|
-
{
|
|
1706
|
-
[K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, infer IsOpt>
|
|
1707
|
-
? IsOpt extends true
|
|
1708
|
-
? never
|
|
1709
|
-
: K
|
|
1710
|
-
: never]: PROPS[K] extends JsonSchemaAnyBuilder<infer OUT, any> ? OUT : never
|
|
1711
|
-
} & {
|
|
1712
|
-
[K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, infer IsOpt>
|
|
1713
|
-
? IsOpt extends true
|
|
1714
|
-
? K
|
|
1715
|
-
: never
|
|
1716
|
-
: never]?: PROPS[K] extends JsonSchemaAnyBuilder<infer OUT, any> ? OUT : never
|
|
1717
|
-
}
|
|
1718
|
-
>
|
|
1719
|
-
| undefined,
|
|
1720
|
-
true
|
|
1721
|
-
>
|
|
1722
|
-
: JsonSchemaAnyBuilder<
|
|
1723
|
-
| Expand<
|
|
1724
|
-
{
|
|
1725
|
-
[K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, infer IsOpt>
|
|
1726
|
-
? IsOpt extends true
|
|
1727
|
-
? never
|
|
1728
|
-
: K
|
|
1729
|
-
: never]: PROPS[K] extends JsonSchemaAnyBuilder<infer OUT, any> ? OUT : never
|
|
1730
|
-
} & {
|
|
1731
|
-
[K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, infer IsOpt>
|
|
1732
|
-
? IsOpt extends true
|
|
1733
|
-
? K
|
|
1734
|
-
: never
|
|
1735
|
-
: never]?: PROPS[K] extends JsonSchemaAnyBuilder<infer OUT, any> ? OUT : never
|
|
1736
|
-
}
|
|
1737
|
-
>
|
|
1738
|
-
| undefined,
|
|
1739
|
-
true
|
|
1740
|
-
> {
|
|
1741
|
-
if (nullValue === undefined) {
|
|
1742
|
-
return super.optional() as any
|
|
1743
|
-
}
|
|
1892
|
+
if (!exactPaths.length) return
|
|
1744
1893
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1751
|
-
optionalField: true,
|
|
1752
|
-
}) as any
|
|
1894
|
+
for (let i = errors.length - 1; i >= 0; i--) {
|
|
1895
|
+
const sp = errors[i]!.schemaPath
|
|
1896
|
+
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
1897
|
+
errors.splice(i, 1)
|
|
1898
|
+
}
|
|
1753
1899
|
}
|
|
1900
|
+
}
|
|
1754
1901
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1902
|
+
/**
|
|
1903
|
+
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
1904
|
+
* and returns the parent schema containing the last keyword.
|
|
1905
|
+
*/
|
|
1906
|
+
function resolveSchemaPath(schema: JsonSchema, schemaPath: string): JsonSchema | undefined {
|
|
1907
|
+
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
1908
|
+
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
1909
|
+
const segments = schemaPath.replace(/^#\//, '').split('/')
|
|
1910
|
+
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
1911
|
+
segments.pop()
|
|
1912
|
+
|
|
1913
|
+
let current: any = schema
|
|
1914
|
+
for (const segment of segments) {
|
|
1915
|
+
if (!current || typeof current !== 'object') return undefined
|
|
1916
|
+
current = current[segment]
|
|
1917
|
+
}
|
|
1918
|
+
return current as JsonSchema | undefined
|
|
1919
|
+
}
|
|
1758
1920
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1921
|
+
function getErrorMessageForInstancePath(
|
|
1922
|
+
schema: JsonSchema | undefined,
|
|
1923
|
+
instancePath: string,
|
|
1924
|
+
keyword: string,
|
|
1925
|
+
): string | undefined {
|
|
1926
|
+
if (!schema || !instancePath) return undefined
|
|
1762
1927
|
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
{
|
|
1767
|
-
[K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
|
|
1768
|
-
? NEW_PROPS[K]
|
|
1769
|
-
: K extends keyof PROPS
|
|
1770
|
-
? PROPS[K]
|
|
1771
|
-
: never
|
|
1772
|
-
},
|
|
1773
|
-
Opt
|
|
1774
|
-
> {
|
|
1775
|
-
const newBuilder = new JsonSchemaObjectInferringBuilder<PROPS, Opt>()
|
|
1776
|
-
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1928
|
+
const segments = instancePath.split('/').filter(Boolean)
|
|
1929
|
+
return traverseSchemaPath(schema, segments, keyword)
|
|
1930
|
+
}
|
|
1777
1931
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1932
|
+
function traverseSchemaPath(
|
|
1933
|
+
schema: JsonSchema,
|
|
1934
|
+
segments: string[],
|
|
1935
|
+
keyword: string,
|
|
1936
|
+
): string | undefined {
|
|
1937
|
+
if (!segments.length) return undefined
|
|
1780
1938
|
|
|
1781
|
-
|
|
1782
|
-
// so even if the base schema was already type-checked,
|
|
1783
|
-
// the new schema loses that quality.
|
|
1784
|
-
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
|
|
1939
|
+
const [currentSegment, ...remainingSegments] = segments
|
|
1785
1940
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
[K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
|
|
1789
|
-
? NEW_PROPS[K]
|
|
1790
|
-
: K extends keyof PROPS
|
|
1791
|
-
? PROPS[K]
|
|
1792
|
-
: never
|
|
1793
|
-
},
|
|
1794
|
-
Opt
|
|
1795
|
-
>
|
|
1796
|
-
}
|
|
1941
|
+
const nextSchema = getChildSchema(schema, currentSegment)
|
|
1942
|
+
if (!nextSchema) return undefined
|
|
1797
1943
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
*/
|
|
1801
|
-
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
|
|
1802
|
-
dbEntity() {
|
|
1803
|
-
return this.extend({
|
|
1804
|
-
id: j.string(),
|
|
1805
|
-
created: j.number().unixTimestamp2000(),
|
|
1806
|
-
updated: j.number().unixTimestamp2000(),
|
|
1807
|
-
})
|
|
1944
|
+
if (nextSchema.errorMessages?.[keyword]) {
|
|
1945
|
+
return nextSchema.errorMessages[keyword]
|
|
1808
1946
|
}
|
|
1809
|
-
}
|
|
1810
1947
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
items: itemsSchema.build(),
|
|
1816
|
-
})
|
|
1948
|
+
// Check through nullable wrapper
|
|
1949
|
+
const unwrapped = unwrapNullableAnyOf(nextSchema)
|
|
1950
|
+
if (unwrapped?.errorMessages?.[keyword]) {
|
|
1951
|
+
return unwrapped.errorMessages[keyword]
|
|
1817
1952
|
}
|
|
1818
1953
|
|
|
1819
|
-
|
|
1820
|
-
return
|
|
1954
|
+
if (remainingSegments.length) {
|
|
1955
|
+
return traverseSchemaPath(nextSchema, remainingSegments, keyword)
|
|
1821
1956
|
}
|
|
1822
1957
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1958
|
+
return undefined
|
|
1959
|
+
}
|
|
1826
1960
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
length(minItemsOrExact: number, maxItems?: number): this {
|
|
1830
|
-
const maxItemsActual = maxItems ?? minItemsOrExact
|
|
1831
|
-
return this.minLength(minItemsOrExact).maxLength(maxItemsActual)
|
|
1832
|
-
}
|
|
1961
|
+
function getChildSchema(schema: JsonSchema, segment: string | undefined): JsonSchema | undefined {
|
|
1962
|
+
if (!segment) return undefined
|
|
1833
1963
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1964
|
+
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
1965
|
+
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema
|
|
1837
1966
|
|
|
1838
|
-
|
|
1839
|
-
return
|
|
1967
|
+
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
1968
|
+
return getArrayItemSchema(effectiveSchema, segment)
|
|
1840
1969
|
}
|
|
1970
|
+
|
|
1971
|
+
return getObjectPropertySchema(effectiveSchema, segment)
|
|
1841
1972
|
}
|
|
1842
1973
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
super({
|
|
1846
|
-
type: ['array', 'object'],
|
|
1847
|
-
Set2: itemsSchema.build(),
|
|
1848
|
-
})
|
|
1849
|
-
}
|
|
1974
|
+
function getArrayItemSchema(schema: JsonSchema, indexSegment: string): JsonSchema | undefined {
|
|
1975
|
+
if (!schema.items) return undefined
|
|
1850
1976
|
|
|
1851
|
-
|
|
1852
|
-
return
|
|
1977
|
+
if (Array.isArray(schema.items)) {
|
|
1978
|
+
return schema.items[Number(indexSegment)]
|
|
1853
1979
|
}
|
|
1854
1980
|
|
|
1855
|
-
|
|
1856
|
-
return this.cloneAndUpdateSchema({ maxItems })
|
|
1857
|
-
}
|
|
1981
|
+
return schema.items
|
|
1858
1982
|
}
|
|
1859
1983
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
super({
|
|
1863
|
-
Buffer: true,
|
|
1864
|
-
})
|
|
1865
|
-
}
|
|
1984
|
+
function getObjectPropertySchema(schema: JsonSchema, segment: string): JsonSchema | undefined {
|
|
1985
|
+
return schema.properties?.[segment as keyof typeof schema.properties]
|
|
1866
1986
|
}
|
|
1867
1987
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
const jsonSchema: JsonSchema = { enum: enumValues }
|
|
1874
|
-
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
1875
|
-
// Having only the `enum` in the schema does not trigger a coercion in Ajv.
|
|
1876
|
-
if (baseType === 'string') jsonSchema.type = 'string'
|
|
1877
|
-
if (baseType === 'number') jsonSchema.type = 'number'
|
|
1988
|
+
function unwrapNullableAnyOf(schema: JsonSchema): JsonSchema | undefined {
|
|
1989
|
+
const nullIndex = unwrapNullableAnyOfIndex(schema)
|
|
1990
|
+
if (nullIndex === -1) return undefined
|
|
1991
|
+
return schema.anyOf![1 - nullIndex]!
|
|
1992
|
+
}
|
|
1878
1993
|
|
|
1879
|
-
|
|
1994
|
+
function unwrapNullableAnyOfIndex(schema: JsonSchema): number {
|
|
1995
|
+
if (schema.anyOf?.length !== 2) return -1
|
|
1996
|
+
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null')
|
|
1997
|
+
return nullIndex
|
|
1998
|
+
}
|
|
1880
1999
|
|
|
1881
|
-
|
|
1882
|
-
|
|
2000
|
+
// ==== Utility helpers ====
|
|
2001
|
+
|
|
2002
|
+
function addPropertiesToSchema(schema: JsonSchema, props: AnyObject): void {
|
|
2003
|
+
const properties: Record<string, JsonSchema> = {}
|
|
2004
|
+
const required: string[] = []
|
|
2005
|
+
|
|
2006
|
+
for (const [key, builder] of Object.entries(props)) {
|
|
2007
|
+
const isOptional = (builder as JSchema<any, any>).getSchema().optionalField
|
|
2008
|
+
if (!isOptional) {
|
|
2009
|
+
required.push(key)
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const builtSchema = builder.build()
|
|
2013
|
+
properties[key] = builtSchema
|
|
1883
2014
|
}
|
|
1884
2015
|
|
|
1885
|
-
|
|
1886
|
-
|
|
2016
|
+
schema.properties = properties
|
|
2017
|
+
schema.required = _uniq(required).sort()
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function hasNoObjectSchemas(schema: JsonSchema): boolean {
|
|
2021
|
+
if (Array.isArray(schema.type)) {
|
|
2022
|
+
return schema.type.every(type =>
|
|
2023
|
+
['string', 'number', 'integer', 'boolean', 'null'].includes(type),
|
|
2024
|
+
)
|
|
2025
|
+
} else if (schema.anyOf) {
|
|
2026
|
+
return schema.anyOf.every(hasNoObjectSchemas)
|
|
2027
|
+
} else if (schema.oneOf) {
|
|
2028
|
+
return schema.oneOf.every(hasNoObjectSchemas)
|
|
2029
|
+
} else if (schema.enum) {
|
|
2030
|
+
return true
|
|
2031
|
+
} else if (schema.type === 'array') {
|
|
2032
|
+
return !schema.items || hasNoObjectSchemas(schema.items)
|
|
2033
|
+
} else {
|
|
2034
|
+
return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)
|
|
1887
2035
|
}
|
|
2036
|
+
|
|
2037
|
+
return false
|
|
1888
2038
|
}
|
|
1889
2039
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2040
|
+
type EnumBaseType = 'string' | 'number' | 'other'
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* Deep copy that preserves functions in customValidations/customConversions.
|
|
2044
|
+
* Unlike structuredClone, this handles function references (which only exist in those two properties).
|
|
2045
|
+
*/
|
|
2046
|
+
function deepCopyPreservingFunctions<T>(obj: T): T {
|
|
2047
|
+
if (obj === null || typeof obj !== 'object') return obj
|
|
2048
|
+
if (Array.isArray(obj)) return obj.map(deepCopyPreservingFunctions) as T
|
|
2049
|
+
const copy = {} as T
|
|
2050
|
+
for (const key of Object.keys(obj)) {
|
|
2051
|
+
const value = (obj as any)[key]
|
|
2052
|
+
// customValidations/customConversions are arrays of functions - shallow copy the array
|
|
2053
|
+
;(copy as any)[key] =
|
|
2054
|
+
(key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
|
|
2055
|
+
? [...value]
|
|
2056
|
+
: deepCopyPreservingFunctions(value)
|
|
1900
2057
|
}
|
|
2058
|
+
return copy
|
|
1901
2059
|
}
|
|
1902
2060
|
|
|
1903
|
-
|
|
1904
|
-
constructor(
|
|
1905
|
-
propertyName: string,
|
|
1906
|
-
schemaDictionary: Record<PropertyKey, JsonSchemaTerminal<any, any>>,
|
|
1907
|
-
) {
|
|
1908
|
-
const builtSchemaDictionary: Record<string, JsonSchema> = {}
|
|
1909
|
-
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
1910
|
-
builtSchemaDictionary[key] = schema.build()
|
|
1911
|
-
}
|
|
2061
|
+
// ==== Types & Interfaces ====
|
|
1912
2062
|
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2063
|
+
export interface AjvValidationOptions {
|
|
2064
|
+
/**
|
|
2065
|
+
* Custom Ajv instance to use for this validation.
|
|
2066
|
+
* Overrides the default Ajv or any Ajv set at construction time.
|
|
2067
|
+
* Compiled functions are cached per Ajv instance.
|
|
2068
|
+
*/
|
|
2069
|
+
ajv?: Ajv
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Defaults to true,
|
|
2073
|
+
* because that's how AJV works by default,
|
|
2074
|
+
* and what gives it performance advantage.
|
|
2075
|
+
* (Because we have found that deep-clone is surprisingly slow,
|
|
2076
|
+
* nearly as slow as Joi validation).
|
|
2077
|
+
*
|
|
2078
|
+
* If set to true - AJV will mutate the input in case it needs to apply transformations
|
|
2079
|
+
* (strip unknown properties, convert types, etc).
|
|
2080
|
+
*
|
|
2081
|
+
* If false - it will deep-clone (using JSON.stringify+parse) the input to prevent its mutation.
|
|
2082
|
+
* Will return the cloned/mutated object.
|
|
2083
|
+
* Please note that JSON.stringify+parse has side-effects,
|
|
2084
|
+
* e.g it will transform Buffer into a weird object.
|
|
2085
|
+
*/
|
|
2086
|
+
mutateInput?: boolean
|
|
2087
|
+
inputName?: string
|
|
2088
|
+
inputId?: string
|
|
2089
|
+
/**
|
|
2090
|
+
* Function that returns "original input".
|
|
2091
|
+
* What is original input?
|
|
2092
|
+
* It's an input in its original non-mutated form.
|
|
2093
|
+
* Why is it needed?
|
|
2094
|
+
* Because we mutates the Input here. And after its been mutated - we no longer
|
|
2095
|
+
* can include it "how it was" in an error message. So, for that reason we'll use
|
|
2096
|
+
* `getOriginalInput()`, if it's provided.
|
|
2097
|
+
*/
|
|
2098
|
+
getOriginalInput?: () => unknown
|
|
1922
2099
|
}
|
|
1923
2100
|
|
|
1924
|
-
export
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
1931
|
-
builtSchemaDictionary[key] = schema.build()
|
|
1932
|
-
}
|
|
2101
|
+
export interface AjvSchemaCfg {
|
|
2102
|
+
/**
|
|
2103
|
+
* Pass Ajv instance, otherwise Ajv will be created with
|
|
2104
|
+
* AjvSchema default (not the same as Ajv defaults) parameters
|
|
2105
|
+
*/
|
|
2106
|
+
ajv: Ajv
|
|
1933
2107
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
})
|
|
1942
|
-
}
|
|
2108
|
+
inputName?: string
|
|
2109
|
+
|
|
2110
|
+
/**
|
|
2111
|
+
* If true - schema will be compiled on-demand (lazily).
|
|
2112
|
+
* Default: false.
|
|
2113
|
+
*/
|
|
2114
|
+
lazy?: boolean
|
|
1943
2115
|
}
|
|
1944
2116
|
|
|
1945
|
-
type
|
|
2117
|
+
export type SchemaHandledByAjv<OUT> = JSchema<OUT, any> | JsonSchema<OUT> | AjvSchema<OUT>
|
|
1946
2118
|
|
|
1947
2119
|
export interface JsonSchema<OUT = unknown> {
|
|
1948
2120
|
readonly out?: OUT
|
|
@@ -1951,7 +2123,6 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
1951
2123
|
$id?: string
|
|
1952
2124
|
title?: string
|
|
1953
2125
|
description?: string
|
|
1954
|
-
// $comment?: string
|
|
1955
2126
|
deprecated?: boolean
|
|
1956
2127
|
readOnly?: boolean
|
|
1957
2128
|
writeOnly?: boolean
|
|
@@ -2032,177 +2203,9 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
2032
2203
|
postValidation?: PostValidatonFn<any, OUT>
|
|
2033
2204
|
}
|
|
2034
2205
|
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
}): JsonSchemaObjectBuilder<OUT, false>
|
|
2039
|
-
|
|
2040
|
-
function object<OUT extends AnyObject>(props: {
|
|
2041
|
-
[key in keyof OUT]: JsonSchemaTerminal<OUT[key], any>
|
|
2042
|
-
}): JsonSchemaObjectBuilder<OUT, false> {
|
|
2043
|
-
return new JsonSchemaObjectBuilder<OUT, false>(props)
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
function objectInfer<P extends Record<string, JsonSchemaAnyBuilder<any, any>>>(
|
|
2047
|
-
props: P,
|
|
2048
|
-
): JsonSchemaObjectInferringBuilder<P, false> {
|
|
2049
|
-
return new JsonSchemaObjectInferringBuilder<P, false>(props)
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
function objectDbEntity(props: AnyObject): never
|
|
2053
|
-
function objectDbEntity<
|
|
2054
|
-
OUT extends BaseDBEntity,
|
|
2055
|
-
EXTRA_KEYS extends Exclude<keyof OUT, keyof BaseDBEntity> = Exclude<
|
|
2056
|
-
keyof OUT,
|
|
2057
|
-
keyof BaseDBEntity
|
|
2058
|
-
>,
|
|
2059
|
-
>(
|
|
2060
|
-
props: {
|
|
2061
|
-
// ✅ all non-system fields must be explicitly provided
|
|
2062
|
-
[K in EXTRA_KEYS]-?: BuilderFor<OUT[K]>
|
|
2063
|
-
} &
|
|
2064
|
-
// ✅ if `id` differs, it's required
|
|
2065
|
-
(ExactMatch<OUT['id'], BaseDBEntity['id']> extends true
|
|
2066
|
-
? { id?: BuilderFor<BaseDBEntity['id']> }
|
|
2067
|
-
: { id: BuilderFor<OUT['id']> }) &
|
|
2068
|
-
(ExactMatch<OUT['created'], BaseDBEntity['created']> extends true
|
|
2069
|
-
? { created?: BuilderFor<BaseDBEntity['created']> }
|
|
2070
|
-
: { created: BuilderFor<OUT['created']> }) &
|
|
2071
|
-
(ExactMatch<OUT['updated'], BaseDBEntity['updated']> extends true
|
|
2072
|
-
? { updated?: BuilderFor<BaseDBEntity['updated']> }
|
|
2073
|
-
: { updated: BuilderFor<OUT['updated']> }),
|
|
2074
|
-
): JsonSchemaObjectBuilder<OUT, false>
|
|
2075
|
-
|
|
2076
|
-
function objectDbEntity(props: AnyObject): any {
|
|
2077
|
-
return j.object({
|
|
2078
|
-
id: j.string(),
|
|
2079
|
-
created: j.number().unixTimestamp2000(),
|
|
2080
|
-
updated: j.number().unixTimestamp2000(),
|
|
2081
|
-
...props,
|
|
2082
|
-
})
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
function record<
|
|
2086
|
-
KS extends JsonSchemaAnyBuilder<any, any>,
|
|
2087
|
-
VS extends JsonSchemaAnyBuilder<any, any>,
|
|
2088
|
-
Opt extends boolean = SchemaOpt<VS>,
|
|
2089
|
-
>(
|
|
2090
|
-
keySchema: KS,
|
|
2091
|
-
valueSchema: VS,
|
|
2092
|
-
): JsonSchemaObjectBuilder<
|
|
2093
|
-
Opt extends true
|
|
2094
|
-
? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
|
|
2095
|
-
: Record<SchemaOut<KS>, SchemaOut<VS>>,
|
|
2096
|
-
false
|
|
2097
|
-
> {
|
|
2098
|
-
const keyJsonSchema = keySchema.build()
|
|
2099
|
-
// Check if value schema is optional before build() strips the optionalField flag
|
|
2100
|
-
const isValueOptional = (valueSchema as JsonSchemaTerminal<any, any>).getSchema().optionalField
|
|
2101
|
-
const valueJsonSchema = valueSchema.build()
|
|
2102
|
-
|
|
2103
|
-
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
2104
|
-
const finalValueSchema: JsonSchema = isValueOptional
|
|
2105
|
-
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
2106
|
-
: valueJsonSchema
|
|
2107
|
-
|
|
2108
|
-
return new JsonSchemaObjectBuilder<
|
|
2109
|
-
Opt extends true
|
|
2110
|
-
? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
|
|
2111
|
-
: Record<SchemaOut<KS>, SchemaOut<VS>>,
|
|
2112
|
-
false
|
|
2113
|
-
>([], {
|
|
2114
|
-
hasIsOfTypeCheck: false,
|
|
2115
|
-
keySchema: keyJsonSchema,
|
|
2116
|
-
patternProperties: {
|
|
2117
|
-
['^.*$']: finalValueSchema,
|
|
2118
|
-
},
|
|
2119
|
-
})
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
function withRegexKeys<S extends JsonSchemaAnyBuilder<any, any>>(
|
|
2123
|
-
keyRegex: RegExp | string,
|
|
2124
|
-
schema: S,
|
|
2125
|
-
): JsonSchemaObjectBuilder<StringMap<SchemaOut<S>>, false> {
|
|
2126
|
-
if (keyRegex instanceof RegExp) {
|
|
2127
|
-
_assert(
|
|
2128
|
-
!keyRegex.flags,
|
|
2129
|
-
`Regex flags are not supported by JSON Schema. Received: /${keyRegex.source}/${keyRegex.flags}`,
|
|
2130
|
-
)
|
|
2131
|
-
}
|
|
2132
|
-
const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex
|
|
2133
|
-
const jsonSchema = schema.build()
|
|
2134
|
-
|
|
2135
|
-
return new JsonSchemaObjectBuilder<StringMap<SchemaOut<S>>, false>([], {
|
|
2136
|
-
hasIsOfTypeCheck: false,
|
|
2137
|
-
patternProperties: {
|
|
2138
|
-
[pattern]: jsonSchema,
|
|
2139
|
-
},
|
|
2140
|
-
})
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
/**
|
|
2144
|
-
* Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
|
|
2145
|
-
*/
|
|
2146
|
-
function withEnumKeys<
|
|
2147
|
-
const T extends readonly (string | number)[] | StringEnum | NumberEnum,
|
|
2148
|
-
S extends JsonSchemaAnyBuilder<any, any>,
|
|
2149
|
-
K extends string | number = EnumKeyUnion<T>,
|
|
2150
|
-
Opt extends boolean = SchemaOpt<S>,
|
|
2151
|
-
>(
|
|
2152
|
-
keys: T,
|
|
2153
|
-
schema: S,
|
|
2154
|
-
): JsonSchemaObjectBuilder<
|
|
2155
|
-
Opt extends true ? { [P in K]?: SchemaOut<S> } : { [P in K]: SchemaOut<S> },
|
|
2156
|
-
false
|
|
2157
|
-
> {
|
|
2158
|
-
let enumValues: readonly (string | number)[] | undefined
|
|
2159
|
-
if (Array.isArray(keys)) {
|
|
2160
|
-
_assert(
|
|
2161
|
-
isEveryItemPrimitive(keys),
|
|
2162
|
-
'Every item in the key list should be string, number or symbol',
|
|
2163
|
-
)
|
|
2164
|
-
enumValues = keys
|
|
2165
|
-
} else if (typeof keys === 'object') {
|
|
2166
|
-
const enumType = getEnumType(keys)
|
|
2167
|
-
_assert(
|
|
2168
|
-
enumType === 'NumberEnum' || enumType === 'StringEnum',
|
|
2169
|
-
'The key list should be StringEnum or NumberEnum',
|
|
2170
|
-
)
|
|
2171
|
-
if (enumType === 'NumberEnum') {
|
|
2172
|
-
enumValues = _numberEnumValues(keys as NumberEnum)
|
|
2173
|
-
} else if (enumType === 'StringEnum') {
|
|
2174
|
-
enumValues = _stringEnumValues(keys as StringEnum)
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
_assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum')
|
|
2179
|
-
|
|
2180
|
-
const typedValues = enumValues as readonly K[]
|
|
2181
|
-
const props = Object.fromEntries(typedValues.map(key => [key, schema])) as any
|
|
2182
|
-
|
|
2183
|
-
return new JsonSchemaObjectBuilder<
|
|
2184
|
-
Opt extends true ? { [P in K]?: SchemaOut<S> } : { [P in K]: SchemaOut<S> },
|
|
2185
|
-
false
|
|
2186
|
-
>(props, { hasIsOfTypeCheck: false })
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
function hasNoObjectSchemas(schema: JsonSchema): boolean {
|
|
2190
|
-
if (Array.isArray(schema.type)) {
|
|
2191
|
-
schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type))
|
|
2192
|
-
} else if (schema.anyOf) {
|
|
2193
|
-
return schema.anyOf.every(hasNoObjectSchemas)
|
|
2194
|
-
} else if (schema.oneOf) {
|
|
2195
|
-
return schema.oneOf.every(hasNoObjectSchemas)
|
|
2196
|
-
} else if (schema.enum) {
|
|
2197
|
-
return true
|
|
2198
|
-
} else if (schema.type === 'array') {
|
|
2199
|
-
return !schema.items || hasNoObjectSchemas(schema.items)
|
|
2200
|
-
} else {
|
|
2201
|
-
return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
return false
|
|
2205
|
-
}
|
|
2206
|
+
export type PostValidatonFn<OUT, OUT2> = (v: OUT) => OUT2
|
|
2207
|
+
export type CustomValidatorFn = (v: any) => string | undefined
|
|
2208
|
+
export type CustomConverterFn<OUT> = (v: any) => OUT
|
|
2206
2209
|
|
|
2207
2210
|
type Expand<T> = { [K in keyof T]: T[K] }
|
|
2208
2211
|
|
|
@@ -2255,17 +2258,17 @@ type ExactMatch<A, B> =
|
|
|
2255
2258
|
? true
|
|
2256
2259
|
: ExactMatchBase<Expand<StripIndexSignatureDeep<A>>, Expand<StripIndexSignatureDeep<B>>>
|
|
2257
2260
|
|
|
2258
|
-
type BuilderOutUnion<B extends readonly
|
|
2259
|
-
[K in keyof B]: B[K] extends
|
|
2261
|
+
type BuilderOutUnion<B extends readonly JBuilder<any, any>[]> = {
|
|
2262
|
+
[K in keyof B]: B[K] extends JBuilder<infer O, any> ? O : never
|
|
2260
2263
|
}[number]
|
|
2261
2264
|
|
|
2262
|
-
type AnyOfByOut<D extends Record<PropertyKey,
|
|
2263
|
-
[K in keyof D]: D[K] extends
|
|
2265
|
+
type AnyOfByOut<D extends Record<PropertyKey, JSchema<any, any>>> = {
|
|
2266
|
+
[K in keyof D]: D[K] extends JSchema<infer O, any> ? O : never
|
|
2264
2267
|
}[keyof D]
|
|
2265
2268
|
|
|
2266
|
-
type BuilderFor<T> =
|
|
2269
|
+
type BuilderFor<T> = JBuilder<T, any>
|
|
2267
2270
|
|
|
2268
|
-
interface JsonBuilderRuleOpt {
|
|
2271
|
+
export interface JsonBuilderRuleOpt {
|
|
2269
2272
|
/**
|
|
2270
2273
|
* Text of error message to return when the validation fails for the given rule:
|
|
2271
2274
|
*
|
|
@@ -2289,33 +2292,9 @@ type EnumKeyUnion<T> =
|
|
|
2289
2292
|
? T[keyof T]
|
|
2290
2293
|
: never
|
|
2291
2294
|
|
|
2292
|
-
type SchemaOut<S> = S extends
|
|
2293
|
-
type SchemaOpt<S> =
|
|
2294
|
-
S extends JsonSchemaAnyBuilder<any, infer Opt> ? (Opt extends true ? true : false) : false
|
|
2295
|
-
|
|
2296
|
-
type TupleOut<T extends readonly JsonSchemaAnyBuilder<any, any>[]> = {
|
|
2297
|
-
[K in keyof T]: T[K] extends JsonSchemaAnyBuilder<infer O, any> ? O : never
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
export type PostValidatonFn<OUT, OUT2> = (v: OUT) => OUT2
|
|
2301
|
-
export type CustomValidatorFn = (v: any) => string | undefined
|
|
2302
|
-
export type CustomConverterFn<OUT> = (v: any) => OUT
|
|
2295
|
+
type SchemaOut<S> = S extends JBuilder<infer OUT, any> ? OUT : never
|
|
2296
|
+
type SchemaOpt<S> = S extends JBuilder<any, infer Opt> ? (Opt extends true ? true : false) : false
|
|
2303
2297
|
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
* Unlike structuredClone, this handles function references (which only exist in those two properties).
|
|
2307
|
-
*/
|
|
2308
|
-
function deepCopyPreservingFunctions<T>(obj: T): T {
|
|
2309
|
-
if (obj === null || typeof obj !== 'object') return obj
|
|
2310
|
-
if (Array.isArray(obj)) return obj.map(deepCopyPreservingFunctions) as T
|
|
2311
|
-
const copy = {} as T
|
|
2312
|
-
for (const key of Object.keys(obj)) {
|
|
2313
|
-
const value = (obj as any)[key]
|
|
2314
|
-
// customValidations/customConversions are arrays of functions - shallow copy the array
|
|
2315
|
-
;(copy as any)[key] =
|
|
2316
|
-
(key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
|
|
2317
|
-
? [...value]
|
|
2318
|
-
: deepCopyPreservingFunctions(value)
|
|
2319
|
-
}
|
|
2320
|
-
return copy
|
|
2298
|
+
type TupleOut<T extends readonly JBuilder<any, any>[]> = {
|
|
2299
|
+
[K in keyof T]: T[K] extends JBuilder<infer O, any> ? O : never
|
|
2321
2300
|
}
|