@naturalcycles/nodejs-lib 15.93.0 → 15.94.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} +232 -271
- package/dist/validation/ajv/{ajvSchema.js → jSchema.js} +453 -460
- package/dist/validation/ajv/jsonSchemaBuilder.util.d.ts +1 -1
- package/package.json +2 -2
- 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} +1447 -1471
- 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
|
|
342
|
+
export class JSchema<OUT, Opt> {
|
|
756
343
|
protected [HIDDEN_AJV_SCHEMA]: AjvSchema<any> | undefined
|
|
757
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,34 @@ 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
450
|
getValidationFunction(): ValidationFunction<OUT, AjvValidationError> {
|
|
823
|
-
return
|
|
451
|
+
return (input, opt) => {
|
|
452
|
+
return this.getValidationResult(input, {
|
|
453
|
+
mutateInput: opt?.mutateInput,
|
|
454
|
+
inputName: opt?.inputName,
|
|
455
|
+
inputId: opt?.inputId,
|
|
456
|
+
})
|
|
457
|
+
}
|
|
824
458
|
}
|
|
825
459
|
|
|
826
460
|
/**
|
|
@@ -832,11 +466,11 @@ export class JsonSchemaTerminal<OUT, Opt> {
|
|
|
832
466
|
*
|
|
833
467
|
* If you throw an error from this function, it will show up as an error in the validation.
|
|
834
468
|
*/
|
|
835
|
-
postValidation<OUT2 = OUT>(fn: PostValidatonFn<OUT, OUT2>):
|
|
469
|
+
postValidation<OUT2 = OUT>(fn: PostValidatonFn<OUT, OUT2>): JSchema<OUT2, Opt> {
|
|
836
470
|
const clone = this.cloneAndUpdateSchema({
|
|
837
471
|
postValidation: fn,
|
|
838
472
|
})
|
|
839
|
-
return clone as unknown as
|
|
473
|
+
return clone as unknown as JSchema<OUT2, Opt>
|
|
840
474
|
}
|
|
841
475
|
|
|
842
476
|
/**
|
|
@@ -846,7 +480,9 @@ export class JsonSchemaTerminal<OUT, Opt> {
|
|
|
846
480
|
opt!: Opt
|
|
847
481
|
}
|
|
848
482
|
|
|
849
|
-
|
|
483
|
+
// ==== JBuilder (chainable base) ====
|
|
484
|
+
|
|
485
|
+
export class JBuilder<OUT, Opt> extends JSchema<OUT, Opt> {
|
|
850
486
|
protected setErrorMessage(ruleName: string, errorMessage: string | undefined): void {
|
|
851
487
|
if (_isUndefined(errorMessage)) return
|
|
852
488
|
|
|
@@ -859,15 +495,6 @@ export class JsonSchemaAnyBuilder<OUT, Opt> extends JsonSchemaTerminal<OUT, Opt>
|
|
|
859
495
|
*
|
|
860
496
|
* When the type inferred from the schema differs from the passed-in type,
|
|
861
497
|
* 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
498
|
*/
|
|
872
499
|
isOfType<ExpectedType>(): ExactMatch<ExpectedType, OUT> extends true ? this : never {
|
|
873
500
|
return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true }) as any
|
|
@@ -901,78 +528,364 @@ export class JsonSchemaAnyBuilder<OUT, Opt> extends JsonSchemaTerminal<OUT, Opt>
|
|
|
901
528
|
return this.cloneAndUpdateSchema({ type })
|
|
902
529
|
}
|
|
903
530
|
|
|
904
|
-
default(v: any): this {
|
|
905
|
-
return this.cloneAndUpdateSchema({ default: v })
|
|
531
|
+
default(v: any): this {
|
|
532
|
+
return this.cloneAndUpdateSchema({ default: v })
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
instanceof(of: string): this {
|
|
536
|
+
return this.cloneAndUpdateSchema({ type: 'object', instanceof: of })
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
optional(): JBuilder<OUT | undefined, true> {
|
|
540
|
+
const clone = this.cloneAndUpdateSchema({ optionalField: true })
|
|
541
|
+
return clone as unknown as JBuilder<OUT | undefined, true>
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
nullable(): JBuilder<OUT | null, Opt> {
|
|
545
|
+
return new JBuilder({
|
|
546
|
+
anyOf: [this.build(), { type: 'null' }],
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @deprecated
|
|
552
|
+
* The usage of this function is discouraged as it defeats the purpose of having type-safe validation.
|
|
553
|
+
*/
|
|
554
|
+
castAs<T>(): JBuilder<T, Opt> {
|
|
555
|
+
return this as unknown as JBuilder<T, Opt>
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Locks the given schema chain and no other modification can be done to it.
|
|
560
|
+
*/
|
|
561
|
+
final(): JSchema<OUT, Opt> {
|
|
562
|
+
return new JSchema<OUT, Opt>(this.schema)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
*
|
|
567
|
+
* @param validator A validator function that returns an error message or undefined.
|
|
568
|
+
*
|
|
569
|
+
* You may add multiple custom validators and they will be executed in the order you added them.
|
|
570
|
+
*/
|
|
571
|
+
custom<OUT2 = OUT>(validator: CustomValidatorFn): JBuilder<OUT2, Opt> {
|
|
572
|
+
const { customValidations = [] } = this.schema
|
|
573
|
+
return this.cloneAndUpdateSchema({
|
|
574
|
+
customValidations: [...customValidations, validator],
|
|
575
|
+
}) as unknown as JBuilder<OUT2, Opt>
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
*
|
|
580
|
+
* @param converter A converter function that returns a new value.
|
|
581
|
+
*
|
|
582
|
+
* You may add multiple converters and they will be executed in the order you added them,
|
|
583
|
+
* each converter receiving the result from the previous one.
|
|
584
|
+
*
|
|
585
|
+
* This feature only works when the current schema is nested in an object or array schema,
|
|
586
|
+
* due to how mutability works in Ajv.
|
|
587
|
+
*/
|
|
588
|
+
convert<OUT2>(converter: CustomConverterFn<OUT2>): JBuilder<OUT2, Opt> {
|
|
589
|
+
const { customConversions = [] } = this.schema
|
|
590
|
+
return this.cloneAndUpdateSchema({
|
|
591
|
+
customConversions: [...customConversions, converter],
|
|
592
|
+
}) as unknown as JBuilder<OUT2, Opt>
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ==== Consts
|
|
597
|
+
|
|
598
|
+
const TS_2500 = 16725225600 // 2500-01-01
|
|
599
|
+
const TS_2500_MILLIS = TS_2500 * 1000
|
|
600
|
+
const TS_2000 = 946684800 // 2000-01-01
|
|
601
|
+
const TS_2000_MILLIS = TS_2000 * 1000
|
|
602
|
+
|
|
603
|
+
// ==== Type-specific builders ====
|
|
604
|
+
|
|
605
|
+
export class JString<
|
|
606
|
+
OUT extends string | undefined = string,
|
|
607
|
+
Opt extends boolean = false,
|
|
608
|
+
> extends JBuilder<OUT, Opt> {
|
|
609
|
+
constructor() {
|
|
610
|
+
super({
|
|
611
|
+
type: 'string',
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @param optionalValues List of values that should be considered/converted as `undefined`.
|
|
617
|
+
*
|
|
618
|
+
* This `optionalValues` feature only works when the current schema is nested in an object or array schema,
|
|
619
|
+
* due to how mutability works in Ajv.
|
|
620
|
+
*
|
|
621
|
+
* Make sure this `optional()` call is at the end of your call chain.
|
|
622
|
+
*
|
|
623
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
624
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
625
|
+
*/
|
|
626
|
+
override optional<T extends readonly (string | null)[] | undefined = undefined>(
|
|
627
|
+
optionalValues?: T,
|
|
628
|
+
): T extends readonly (infer U)[]
|
|
629
|
+
? null extends U
|
|
630
|
+
? JSchema<OUT | undefined, true>
|
|
631
|
+
: JString<OUT | undefined, true>
|
|
632
|
+
: JString<OUT | undefined, true> {
|
|
633
|
+
if (!optionalValues) {
|
|
634
|
+
return super.optional() as any
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
_typeCast<(string | null)[]>(optionalValues)
|
|
638
|
+
|
|
639
|
+
let newBuilder: JSchema<OUT | undefined, true> = new JString<OUT, Opt>().optional()
|
|
640
|
+
const alternativesSchema = j.enum(optionalValues)
|
|
641
|
+
Object.assign(newBuilder.getSchema(), {
|
|
642
|
+
anyOf: [this.build(), alternativesSchema.build()],
|
|
643
|
+
optionalValues,
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
647
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
648
|
+
// but the typing should not reflect that.
|
|
649
|
+
// We also cannot accept more rules attached, since we're not building a StringSchema anymore.
|
|
650
|
+
if (optionalValues.includes(null)) {
|
|
651
|
+
newBuilder = new JSchema({
|
|
652
|
+
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
653
|
+
optionalField: true,
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return newBuilder as any
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
regex(pattern: RegExp, opt?: JsonBuilderRuleOpt): this {
|
|
661
|
+
_assert(
|
|
662
|
+
!pattern.flags,
|
|
663
|
+
`Regex flags are not supported by JSON Schema. Received: /${pattern.source}/${pattern.flags}`,
|
|
664
|
+
)
|
|
665
|
+
return this.pattern(pattern.source, opt)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
pattern(pattern: string, opt?: JsonBuilderRuleOpt): this {
|
|
669
|
+
const clone = this.cloneAndUpdateSchema({ pattern })
|
|
670
|
+
if (opt?.name) clone.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
671
|
+
if (opt?.msg) clone.setErrorMessage('pattern', opt.msg)
|
|
672
|
+
return clone
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
minLength(minLength: number): this {
|
|
676
|
+
return this.cloneAndUpdateSchema({ minLength })
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
maxLength(maxLength: number): this {
|
|
680
|
+
return this.cloneAndUpdateSchema({ maxLength })
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
length(exactLength: number): this
|
|
684
|
+
length(minLength: number, maxLength: number): this
|
|
685
|
+
length(minLengthOrExactLength: number, maxLength?: number): this {
|
|
686
|
+
const maxLengthActual = maxLength ?? minLengthOrExactLength
|
|
687
|
+
return this.minLength(minLengthOrExactLength).maxLength(maxLengthActual)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
|
|
691
|
+
const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
|
|
692
|
+
return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
|
|
693
|
+
.trim()
|
|
694
|
+
.toLowerCase()
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
trim(): this {
|
|
698
|
+
return this.cloneAndUpdateSchema({ transform: { ...this.schema.transform, trim: true } })
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
toLowerCase(): this {
|
|
702
|
+
return this.cloneAndUpdateSchema({
|
|
703
|
+
transform: { ...this.schema.transform, toLowerCase: true },
|
|
704
|
+
})
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
toUpperCase(): this {
|
|
708
|
+
return this.cloneAndUpdateSchema({
|
|
709
|
+
transform: { ...this.schema.transform, toUpperCase: true },
|
|
710
|
+
})
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
truncate(toLength: number): this {
|
|
714
|
+
return this.cloneAndUpdateSchema({
|
|
715
|
+
transform: { ...this.schema.transform, truncate: toLength },
|
|
716
|
+
})
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
branded<B extends string>(): JString<B, Opt> {
|
|
720
|
+
return this as unknown as JString<B, Opt>
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
|
|
725
|
+
*
|
|
726
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
727
|
+
* because this call effectively starts a new schema chain.
|
|
728
|
+
*/
|
|
729
|
+
isoDate(): JIsoDate {
|
|
730
|
+
return new JIsoDate()
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
isoDateTime(): JString<IsoDateTime, Opt> {
|
|
734
|
+
return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded<IsoDateTime>()
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
isoMonth(): JBuilder<IsoMonth, false> {
|
|
738
|
+
return new JBuilder<IsoMonth, false>({
|
|
739
|
+
type: 'string',
|
|
740
|
+
IsoMonth: {},
|
|
741
|
+
})
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Validates the string format to be JWT.
|
|
746
|
+
* Expects the JWT to be signed!
|
|
747
|
+
*/
|
|
748
|
+
jwt(): this {
|
|
749
|
+
return this.regex(JWT_REGEX, { msg: 'is not a valid JWT format' })
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
url(): this {
|
|
753
|
+
return this.regex(URL_REGEX, { msg: 'is not a valid URL format' })
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
ipv4(): this {
|
|
757
|
+
return this.regex(IPV4_REGEX, { msg: 'is not a valid IPv4 format' })
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
ipv6(): this {
|
|
761
|
+
return this.regex(IPV6_REGEX, { msg: 'is not a valid IPv6 format' })
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
slug(): this {
|
|
765
|
+
return this.regex(SLUG_REGEX, { msg: 'is not a valid slug format' })
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
semVer(): this {
|
|
769
|
+
return this.regex(SEMVER_REGEX, { msg: 'is not a valid semver format' })
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
languageTag(): this {
|
|
773
|
+
return this.regex(LANGUAGE_TAG_REGEX, { msg: 'is not a valid language format' })
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
countryCode(): this {
|
|
777
|
+
return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' })
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
currency(): this {
|
|
781
|
+
return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' })
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Validates that the input is a valid IANATimzone value.
|
|
786
|
+
*
|
|
787
|
+
* All previous expectations in the schema chain are dropped - including `.optional()` -
|
|
788
|
+
* because this call effectively starts a new schema chain as an `enum` validation.
|
|
789
|
+
*/
|
|
790
|
+
ianaTimezone(): JEnum<IANATimezone, false> {
|
|
791
|
+
// UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
|
|
792
|
+
return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded<IANATimezone>()
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
base64Url(): this {
|
|
796
|
+
return this.regex(BASE64URL_REGEX, {
|
|
797
|
+
msg: 'contains characters not allowed in Base64 URL characterset',
|
|
798
|
+
})
|
|
906
799
|
}
|
|
907
800
|
|
|
908
|
-
|
|
909
|
-
return this.
|
|
801
|
+
uuid(): this {
|
|
802
|
+
return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' })
|
|
910
803
|
}
|
|
804
|
+
}
|
|
911
805
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
806
|
+
export interface JsonSchemaStringEmailOptions {
|
|
807
|
+
checkTLD: boolean
|
|
808
|
+
}
|
|
916
809
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
810
|
+
export class JIsoDate<Opt extends boolean = false> extends JBuilder<IsoDate, Opt> {
|
|
811
|
+
constructor() {
|
|
812
|
+
super({
|
|
813
|
+
type: 'string',
|
|
814
|
+
IsoDate: {},
|
|
920
815
|
})
|
|
921
816
|
}
|
|
922
817
|
|
|
923
818
|
/**
|
|
924
|
-
* @
|
|
925
|
-
*
|
|
819
|
+
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
820
|
+
*
|
|
821
|
+
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
822
|
+
* due to how mutability works in Ajv.
|
|
823
|
+
*
|
|
824
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
825
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
926
826
|
*/
|
|
927
|
-
|
|
928
|
-
|
|
827
|
+
override optional<N extends null | undefined = undefined>(
|
|
828
|
+
nullValue?: N,
|
|
829
|
+
): N extends null ? JSchema<IsoDate | undefined, true> : JBuilder<IsoDate | undefined, true> {
|
|
830
|
+
if (nullValue === undefined) {
|
|
831
|
+
return super.optional() as any
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
835
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
836
|
+
// but the typing should not reflect that.
|
|
837
|
+
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
838
|
+
return new JSchema({
|
|
839
|
+
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
840
|
+
optionalField: true,
|
|
841
|
+
}) as any
|
|
929
842
|
}
|
|
930
843
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
*/
|
|
934
|
-
final(): JsonSchemaTerminal<OUT, Opt> {
|
|
935
|
-
return new JsonSchemaTerminal<OUT, Opt>(this.schema)
|
|
844
|
+
before(date: string): this {
|
|
845
|
+
return this.cloneAndUpdateSchema({ IsoDate: { before: date } })
|
|
936
846
|
}
|
|
937
847
|
|
|
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>
|
|
848
|
+
sameOrBefore(date: string): this {
|
|
849
|
+
return this.cloneAndUpdateSchema({ IsoDate: { sameOrBefore: date } })
|
|
949
850
|
}
|
|
950
851
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
852
|
+
after(date: string): this {
|
|
853
|
+
return this.cloneAndUpdateSchema({ IsoDate: { after: date } })
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
sameOrAfter(date: string): this {
|
|
857
|
+
return this.cloneAndUpdateSchema({ IsoDate: { sameOrAfter: date } })
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
between(fromDate: string, toDate: string, incl: Inclusiveness): this {
|
|
861
|
+
let schemaPatch: Partial<JsonSchema> = {}
|
|
862
|
+
|
|
863
|
+
if (incl === '[)') {
|
|
864
|
+
schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } }
|
|
865
|
+
} else if (incl === '[]') {
|
|
866
|
+
schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } }
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return this.cloneAndUpdateSchema(schemaPatch)
|
|
966
870
|
}
|
|
967
871
|
}
|
|
968
872
|
|
|
969
|
-
export
|
|
970
|
-
|
|
873
|
+
export interface JsonSchemaIsoDateOptions {
|
|
874
|
+
before?: string
|
|
875
|
+
sameOrBefore?: string
|
|
876
|
+
after?: string
|
|
877
|
+
sameOrAfter?: string
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export interface JsonSchemaIsoMonthOptions {}
|
|
881
|
+
|
|
882
|
+
export class JNumber<
|
|
883
|
+
OUT extends number | undefined = number,
|
|
971
884
|
Opt extends boolean = false,
|
|
972
|
-
> extends
|
|
885
|
+
> extends JBuilder<OUT, Opt> {
|
|
973
886
|
constructor() {
|
|
974
887
|
super({
|
|
975
|
-
type: '
|
|
888
|
+
type: 'number',
|
|
976
889
|
})
|
|
977
890
|
}
|
|
978
891
|
|
|
@@ -984,26 +897,23 @@ export class JsonSchemaStringBuilder<
|
|
|
984
897
|
*
|
|
985
898
|
* Make sure this `optional()` call is at the end of your call chain.
|
|
986
899
|
*
|
|
987
|
-
* When `null` is included in optionalValues, the return type becomes `
|
|
900
|
+
* When `null` is included in optionalValues, the return type becomes `JSchema`
|
|
988
901
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
989
902
|
*/
|
|
990
|
-
override optional<T extends readonly (
|
|
903
|
+
override optional<T extends readonly (number | null)[] | undefined = undefined>(
|
|
991
904
|
optionalValues?: T,
|
|
992
905
|
): T extends readonly (infer U)[]
|
|
993
906
|
? null extends U
|
|
994
|
-
?
|
|
995
|
-
:
|
|
996
|
-
:
|
|
907
|
+
? JSchema<OUT | undefined, true>
|
|
908
|
+
: JNumber<OUT | undefined, true>
|
|
909
|
+
: JNumber<OUT | undefined, true> {
|
|
997
910
|
if (!optionalValues) {
|
|
998
911
|
return super.optional() as any
|
|
999
912
|
}
|
|
1000
913
|
|
|
1001
|
-
_typeCast<(
|
|
914
|
+
_typeCast<(number | null)[]>(optionalValues)
|
|
1002
915
|
|
|
1003
|
-
let newBuilder:
|
|
1004
|
-
OUT,
|
|
1005
|
-
Opt
|
|
1006
|
-
>().optional()
|
|
916
|
+
let newBuilder: JSchema<OUT | undefined, true> = new JNumber<OUT, Opt>().optional()
|
|
1007
917
|
const alternativesSchema = j.enum(optionalValues)
|
|
1008
918
|
Object.assign(newBuilder.getSchema(), {
|
|
1009
919
|
anyOf: [this.build(), alternativesSchema.build()],
|
|
@@ -1013,9 +923,9 @@ export class JsonSchemaStringBuilder<
|
|
|
1013
923
|
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
1014
924
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1015
925
|
// but the typing should not reflect that.
|
|
1016
|
-
// We also cannot accept more rules attached, since we're not building a
|
|
926
|
+
// We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
|
|
1017
927
|
if (optionalValues.includes(null)) {
|
|
1018
|
-
newBuilder = new
|
|
928
|
+
newBuilder = new JSchema({
|
|
1019
929
|
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
1020
930
|
optionalField: true,
|
|
1021
931
|
})
|
|
@@ -1024,162 +934,318 @@ export class JsonSchemaStringBuilder<
|
|
|
1024
934
|
return newBuilder as any
|
|
1025
935
|
}
|
|
1026
936
|
|
|
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)
|
|
937
|
+
integer(): this {
|
|
938
|
+
return this.cloneAndUpdateSchema({ type: 'integer' })
|
|
1033
939
|
}
|
|
1034
940
|
|
|
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
|
|
941
|
+
branded<B extends number>(): JNumber<B, Opt> {
|
|
942
|
+
return this as unknown as JNumber<B, Opt>
|
|
1040
943
|
}
|
|
1041
944
|
|
|
1042
|
-
|
|
1043
|
-
return this.cloneAndUpdateSchema({
|
|
945
|
+
multipleOf(multipleOf: number): this {
|
|
946
|
+
return this.cloneAndUpdateSchema({ multipleOf })
|
|
1044
947
|
}
|
|
1045
948
|
|
|
1046
|
-
|
|
1047
|
-
return this.cloneAndUpdateSchema({
|
|
949
|
+
min(minimum: number): this {
|
|
950
|
+
return this.cloneAndUpdateSchema({ minimum })
|
|
1048
951
|
}
|
|
1049
952
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
length(minLengthOrExactLength: number, maxLength?: number): this {
|
|
1053
|
-
const maxLengthActual = maxLength ?? minLengthOrExactLength
|
|
1054
|
-
return this.minLength(minLengthOrExactLength).maxLength(maxLengthActual)
|
|
953
|
+
exclusiveMin(exclusiveMinimum: number): this {
|
|
954
|
+
return this.cloneAndUpdateSchema({ exclusiveMinimum })
|
|
1055
955
|
}
|
|
1056
956
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
|
|
1060
|
-
.trim()
|
|
1061
|
-
.toLowerCase()
|
|
957
|
+
max(maximum: number): this {
|
|
958
|
+
return this.cloneAndUpdateSchema({ maximum })
|
|
1062
959
|
}
|
|
1063
960
|
|
|
1064
|
-
|
|
1065
|
-
return this.cloneAndUpdateSchema({
|
|
961
|
+
exclusiveMax(exclusiveMaximum: number): this {
|
|
962
|
+
return this.cloneAndUpdateSchema({ exclusiveMaximum })
|
|
1066
963
|
}
|
|
1067
964
|
|
|
1068
|
-
|
|
1069
|
-
return this.
|
|
1070
|
-
|
|
965
|
+
lessThan(value: number): this {
|
|
966
|
+
return this.exclusiveMax(value)
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
lessThanOrEqual(value: number): this {
|
|
970
|
+
return this.max(value)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
moreThan(value: number): this {
|
|
974
|
+
return this.exclusiveMin(value)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
moreThanOrEqual(value: number): this {
|
|
978
|
+
return this.min(value)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
equal(value: number): this {
|
|
982
|
+
return this.min(value).max(value)
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
range(minimum: number, maximum: number, incl: Inclusiveness): this {
|
|
986
|
+
if (incl === '[)') {
|
|
987
|
+
return this.moreThanOrEqual(minimum).lessThan(maximum)
|
|
988
|
+
}
|
|
989
|
+
return this.moreThanOrEqual(minimum).lessThanOrEqual(maximum)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
int32(): this {
|
|
993
|
+
const MIN_INT32 = -(2 ** 31)
|
|
994
|
+
const MAX_INT32 = 2 ** 31 - 1
|
|
995
|
+
const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
|
|
996
|
+
const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
997
|
+
const newMin = Math.max(MIN_INT32, currentMin)
|
|
998
|
+
const newMax = Math.min(MAX_INT32, currentMax)
|
|
999
|
+
return this.integer().min(newMin).max(newMax)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
int64(): this {
|
|
1003
|
+
const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
|
|
1004
|
+
const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
|
|
1005
|
+
const newMin = Math.max(Number.MIN_SAFE_INTEGER, currentMin)
|
|
1006
|
+
const newMax = Math.min(Number.MAX_SAFE_INTEGER, currentMax)
|
|
1007
|
+
return this.integer().min(newMin).max(newMax)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
float(): this {
|
|
1011
|
+
return this
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
double(): this {
|
|
1015
|
+
return this
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
unixTimestamp(): JNumber<UnixTimestamp, Opt> {
|
|
1019
|
+
return this.integer().min(0).max(TS_2500).branded<UnixTimestamp>()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
unixTimestamp2000(): JNumber<UnixTimestamp, Opt> {
|
|
1023
|
+
return this.integer().min(TS_2000).max(TS_2500).branded<UnixTimestamp>()
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
unixTimestampMillis(): JNumber<UnixTimestampMillis, Opt> {
|
|
1027
|
+
return this.integer().min(0).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
unixTimestamp2000Millis(): JNumber<UnixTimestampMillis, Opt> {
|
|
1031
|
+
return this.integer().min(TS_2000_MILLIS).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
utcOffset(): this {
|
|
1035
|
+
return this.integer()
|
|
1036
|
+
.multipleOf(15)
|
|
1037
|
+
.min(-12 * 60)
|
|
1038
|
+
.max(14 * 60)
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
utcOffsetHour(): this {
|
|
1042
|
+
return this.integer().min(-12).max(14)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Specify the precision of the floating point numbers by the number of digits after the ".".
|
|
1047
|
+
* Excess digits will be cut-off when the current schema is nested in an object or array schema,
|
|
1048
|
+
* due to how mutability works in Ajv.
|
|
1049
|
+
*/
|
|
1050
|
+
precision(numberOfDigits: number): this {
|
|
1051
|
+
return this.cloneAndUpdateSchema({ precision: numberOfDigits })
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export class JBoolean<
|
|
1056
|
+
OUT extends boolean | undefined = boolean,
|
|
1057
|
+
Opt extends boolean = false,
|
|
1058
|
+
> extends JBuilder<OUT, Opt> {
|
|
1059
|
+
constructor() {
|
|
1060
|
+
super({
|
|
1061
|
+
type: 'boolean',
|
|
1071
1062
|
})
|
|
1072
1063
|
}
|
|
1073
1064
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1065
|
+
/**
|
|
1066
|
+
* @param optionalValue One of the two possible boolean values that should be considered/converted as `undefined`.
|
|
1067
|
+
*
|
|
1068
|
+
* This `optionalValue` feature only works when the current schema is nested in an object or array schema,
|
|
1069
|
+
* due to how mutability works in Ajv.
|
|
1070
|
+
*/
|
|
1071
|
+
override optional(optionalValue?: boolean): JBoolean<OUT | undefined, true> {
|
|
1072
|
+
if (typeof optionalValue === 'undefined') {
|
|
1073
|
+
return super.optional() as unknown as JBoolean<OUT | undefined, true>
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const newBuilder = new JBoolean<OUT, Opt>().optional()
|
|
1077
|
+
const alternativesSchema = j.enum([optionalValue])
|
|
1078
|
+
Object.assign(newBuilder.getSchema(), {
|
|
1079
|
+
anyOf: [this.build(), alternativesSchema.build()],
|
|
1080
|
+
optionalValues: [optionalValue],
|
|
1077
1081
|
})
|
|
1082
|
+
|
|
1083
|
+
return newBuilder
|
|
1078
1084
|
}
|
|
1085
|
+
}
|
|
1079
1086
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1087
|
+
export class JObject<OUT extends AnyObject, Opt extends boolean = false> extends JBuilder<
|
|
1088
|
+
OUT,
|
|
1089
|
+
Opt
|
|
1090
|
+
> {
|
|
1091
|
+
constructor(props?: AnyObject, opt?: JObjectOpts) {
|
|
1092
|
+
super({
|
|
1093
|
+
type: 'object',
|
|
1094
|
+
properties: {},
|
|
1095
|
+
required: [],
|
|
1096
|
+
additionalProperties: false,
|
|
1097
|
+
hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
|
|
1098
|
+
patternProperties: opt?.patternProperties ?? undefined,
|
|
1099
|
+
keySchema: opt?.keySchema ?? undefined,
|
|
1083
1100
|
})
|
|
1084
|
-
}
|
|
1085
1101
|
|
|
1086
|
-
|
|
1087
|
-
return this as unknown as JsonSchemaStringBuilder<B, Opt>
|
|
1102
|
+
if (props) addPropertiesToSchema(this.schema, props)
|
|
1088
1103
|
}
|
|
1089
1104
|
|
|
1090
1105
|
/**
|
|
1091
|
-
*
|
|
1106
|
+
* @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
|
|
1092
1107
|
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1108
|
+
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1109
|
+
* due to how mutability works in Ajv.
|
|
1110
|
+
*
|
|
1111
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1112
|
+
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1095
1113
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1114
|
+
override optional<N extends null | undefined = undefined>(
|
|
1115
|
+
nullValue?: N,
|
|
1116
|
+
): N extends null ? JSchema<OUT | undefined, true> : JBuilder<OUT | undefined, true> {
|
|
1117
|
+
if (nullValue === undefined) {
|
|
1118
|
+
return super.optional() as any
|
|
1119
|
+
}
|
|
1103
1120
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1121
|
+
// When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
|
|
1122
|
+
// so we must allow `null` values to be parsed by Ajv,
|
|
1123
|
+
// but the typing should not reflect that.
|
|
1124
|
+
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1125
|
+
return new JSchema({
|
|
1126
|
+
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1127
|
+
optionalField: true,
|
|
1128
|
+
}) as any
|
|
1106
1129
|
}
|
|
1107
1130
|
|
|
1108
1131
|
/**
|
|
1109
|
-
*
|
|
1110
|
-
* Expects the JWT to be signed!
|
|
1132
|
+
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
1111
1133
|
*/
|
|
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
1134
|
|
|
1128
|
-
|
|
1129
|
-
return this.
|
|
1135
|
+
allowAdditionalProperties(): this {
|
|
1136
|
+
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1130
1137
|
}
|
|
1131
1138
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1139
|
+
extend<P extends Record<string, JBuilder<any, any>>>(
|
|
1140
|
+
props: P,
|
|
1141
|
+
): JObject<
|
|
1142
|
+
Override<
|
|
1143
|
+
OUT,
|
|
1144
|
+
{
|
|
1145
|
+
// required keys
|
|
1146
|
+
[K in keyof P as P[K] extends JBuilder<any, infer IsOpt>
|
|
1147
|
+
? IsOpt extends true
|
|
1148
|
+
? never
|
|
1149
|
+
: K
|
|
1150
|
+
: never]: P[K] extends JBuilder<infer OUT2, any> ? OUT2 : never
|
|
1151
|
+
} & {
|
|
1152
|
+
// optional keys
|
|
1153
|
+
[K in keyof P as P[K] extends JBuilder<any, infer IsOpt>
|
|
1154
|
+
? IsOpt extends true
|
|
1155
|
+
? K
|
|
1156
|
+
: never
|
|
1157
|
+
: never]?: P[K] extends JBuilder<infer OUT2, any> ? OUT2 : never
|
|
1158
|
+
}
|
|
1159
|
+
>,
|
|
1160
|
+
false
|
|
1161
|
+
> {
|
|
1162
|
+
const newBuilder = new JObject()
|
|
1163
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1135
1164
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1165
|
+
const incomingSchemaBuilder = new JObject(props)
|
|
1166
|
+
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
1139
1167
|
|
|
1140
|
-
|
|
1141
|
-
return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' })
|
|
1142
|
-
}
|
|
1168
|
+
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
|
|
1143
1169
|
|
|
1144
|
-
|
|
1145
|
-
return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' })
|
|
1170
|
+
return newBuilder as any
|
|
1146
1171
|
}
|
|
1147
1172
|
|
|
1148
1173
|
/**
|
|
1149
|
-
*
|
|
1174
|
+
* Concatenates another schema to the current schema.
|
|
1150
1175
|
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1176
|
+
* It expects you to use `isOfType<T>()` in the chain,
|
|
1177
|
+
* otherwise the validation will throw. This is to ensure
|
|
1178
|
+
* that the schemas you concatenated match the intended final type.
|
|
1153
1179
|
*/
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1180
|
+
concat<OUT2 extends AnyObject>(other: JObject<OUT2, any>): JObject<OUT & OUT2, false> {
|
|
1181
|
+
const clone = this.clone()
|
|
1182
|
+
mergeJsonSchemaObjects(clone.schema as any, other.schema as any)
|
|
1183
|
+
_objectAssign(clone.schema, { hasIsOfTypeCheck: false })
|
|
1184
|
+
return clone as unknown as JObject<OUT & OUT2, false>
|
|
1157
1185
|
}
|
|
1158
1186
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
|
|
1189
|
+
*/
|
|
1190
|
+
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
|
|
1191
|
+
dbEntity() {
|
|
1192
|
+
return this.extend({
|
|
1193
|
+
id: j.string(),
|
|
1194
|
+
created: j.number().unixTimestamp2000(),
|
|
1195
|
+
updated: j.number().unixTimestamp2000(),
|
|
1162
1196
|
})
|
|
1163
1197
|
}
|
|
1164
1198
|
|
|
1165
|
-
|
|
1166
|
-
return this.
|
|
1199
|
+
minProperties(minProperties: number): this {
|
|
1200
|
+
return this.cloneAndUpdateSchema({ minProperties, minProperties2: minProperties })
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
maxProperties(maxProperties: number): this {
|
|
1204
|
+
return this.cloneAndUpdateSchema({ maxProperties })
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
exclusiveProperties(propNames: readonly (keyof OUT & string)[]): this {
|
|
1208
|
+
const exclusiveProperties = this.schema.exclusiveProperties ?? []
|
|
1209
|
+
return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] })
|
|
1167
1210
|
}
|
|
1168
1211
|
}
|
|
1169
1212
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1213
|
+
interface JObjectOpts {
|
|
1214
|
+
hasIsOfTypeCheck?: false
|
|
1215
|
+
patternProperties?: StringMap<JsonSchema<any>>
|
|
1216
|
+
keySchema?: JsonSchema
|
|
1172
1217
|
}
|
|
1173
1218
|
|
|
1174
|
-
export class
|
|
1175
|
-
|
|
1219
|
+
export class JObjectInfer<
|
|
1220
|
+
PROPS extends Record<string, JBuilder<any, any>>,
|
|
1221
|
+
Opt extends boolean = false,
|
|
1222
|
+
> extends JBuilder<
|
|
1223
|
+
Expand<
|
|
1224
|
+
{
|
|
1225
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1226
|
+
? IsOpt extends true
|
|
1227
|
+
? never
|
|
1228
|
+
: K
|
|
1229
|
+
: never]: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1230
|
+
} & {
|
|
1231
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1232
|
+
? IsOpt extends true
|
|
1233
|
+
? K
|
|
1234
|
+
: never
|
|
1235
|
+
: never]?: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1236
|
+
}
|
|
1237
|
+
>,
|
|
1176
1238
|
Opt
|
|
1177
1239
|
> {
|
|
1178
|
-
constructor() {
|
|
1240
|
+
constructor(props?: PROPS) {
|
|
1179
1241
|
super({
|
|
1180
|
-
type: '
|
|
1181
|
-
|
|
1242
|
+
type: 'object',
|
|
1243
|
+
properties: {},
|
|
1244
|
+
required: [],
|
|
1245
|
+
additionalProperties: false,
|
|
1182
1246
|
})
|
|
1247
|
+
|
|
1248
|
+
if (props) addPropertiesToSchema(this.schema, props)
|
|
1183
1249
|
}
|
|
1184
1250
|
|
|
1185
1251
|
/**
|
|
@@ -1188,14 +1254,51 @@ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonS
|
|
|
1188
1254
|
* This `null` feature only works when the current schema is nested in an object or array schema,
|
|
1189
1255
|
* due to how mutability works in Ajv.
|
|
1190
1256
|
*
|
|
1191
|
-
* When `null` is passed, the return type becomes `
|
|
1257
|
+
* When `null` is passed, the return type becomes `JSchema`
|
|
1192
1258
|
* (no further chaining allowed) because the schema is wrapped in an anyOf structure.
|
|
1193
1259
|
*/
|
|
1260
|
+
// @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
|
|
1194
1261
|
override optional<N extends null | undefined = undefined>(
|
|
1195
1262
|
nullValue?: N,
|
|
1196
1263
|
): N extends null
|
|
1197
|
-
?
|
|
1198
|
-
|
|
1264
|
+
? JSchema<
|
|
1265
|
+
| Expand<
|
|
1266
|
+
{
|
|
1267
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1268
|
+
? IsOpt extends true
|
|
1269
|
+
? never
|
|
1270
|
+
: K
|
|
1271
|
+
: never]: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1272
|
+
} & {
|
|
1273
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1274
|
+
? IsOpt extends true
|
|
1275
|
+
? K
|
|
1276
|
+
: never
|
|
1277
|
+
: never]?: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1278
|
+
}
|
|
1279
|
+
>
|
|
1280
|
+
| undefined,
|
|
1281
|
+
true
|
|
1282
|
+
>
|
|
1283
|
+
: JBuilder<
|
|
1284
|
+
| Expand<
|
|
1285
|
+
{
|
|
1286
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1287
|
+
? IsOpt extends true
|
|
1288
|
+
? never
|
|
1289
|
+
: K
|
|
1290
|
+
: never]: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1291
|
+
} & {
|
|
1292
|
+
[K in keyof PROPS as PROPS[K] extends JBuilder<any, infer IsOpt>
|
|
1293
|
+
? IsOpt extends true
|
|
1294
|
+
? K
|
|
1295
|
+
: never
|
|
1296
|
+
: never]?: PROPS[K] extends JBuilder<infer OUT, any> ? OUT : never
|
|
1297
|
+
}
|
|
1298
|
+
>
|
|
1299
|
+
| undefined,
|
|
1300
|
+
true
|
|
1301
|
+
> {
|
|
1199
1302
|
if (nullValue === undefined) {
|
|
1200
1303
|
return super.optional() as any
|
|
1201
1304
|
}
|
|
@@ -1204,745 +1307,811 @@ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonS
|
|
|
1204
1307
|
// so we must allow `null` values to be parsed by Ajv,
|
|
1205
1308
|
// but the typing should not reflect that.
|
|
1206
1309
|
// We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
|
|
1207
|
-
return new
|
|
1310
|
+
return new JSchema({
|
|
1208
1311
|
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1209
1312
|
optionalField: true,
|
|
1210
1313
|
}) as any
|
|
1211
1314
|
}
|
|
1212
1315
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1316
|
+
/**
|
|
1317
|
+
* When set, the validation will not strip away properties that are not specified explicitly in the schema.
|
|
1318
|
+
*/
|
|
1319
|
+
|
|
1320
|
+
allowAdditionalProperties(): this {
|
|
1321
|
+
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
extend<NEW_PROPS extends Record<string, JBuilder<any, any>>>(
|
|
1325
|
+
props: NEW_PROPS,
|
|
1326
|
+
): JObjectInfer<
|
|
1327
|
+
{
|
|
1328
|
+
[K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
|
|
1329
|
+
? NEW_PROPS[K]
|
|
1330
|
+
: K extends keyof PROPS
|
|
1331
|
+
? PROPS[K]
|
|
1332
|
+
: never
|
|
1333
|
+
},
|
|
1334
|
+
Opt
|
|
1335
|
+
> {
|
|
1336
|
+
const newBuilder = new JObjectInfer<PROPS, Opt>()
|
|
1337
|
+
_objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema))
|
|
1338
|
+
|
|
1339
|
+
const incomingSchemaBuilder = new JObjectInfer<NEW_PROPS, false>(props)
|
|
1340
|
+
mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
|
|
1341
|
+
|
|
1342
|
+
// This extend function is not type-safe as it is inferring,
|
|
1343
|
+
// so even if the base schema was already type-checked,
|
|
1344
|
+
// the new schema loses that quality.
|
|
1345
|
+
_objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false })
|
|
1346
|
+
|
|
1347
|
+
return newBuilder as unknown as JObjectInfer<
|
|
1348
|
+
{
|
|
1349
|
+
[K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
|
|
1350
|
+
? NEW_PROPS[K]
|
|
1351
|
+
: K extends keyof PROPS
|
|
1352
|
+
? PROPS[K]
|
|
1353
|
+
: never
|
|
1354
|
+
},
|
|
1355
|
+
Opt
|
|
1356
|
+
>
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
|
|
1361
|
+
*/
|
|
1362
|
+
// oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
|
|
1363
|
+
dbEntity() {
|
|
1364
|
+
return this.extend({
|
|
1365
|
+
id: j.string(),
|
|
1366
|
+
created: j.number().unixTimestamp2000(),
|
|
1367
|
+
updated: j.number().unixTimestamp2000(),
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
export class JArray<OUT, Opt> extends JBuilder<OUT[], Opt> {
|
|
1373
|
+
constructor(itemsSchema: JBuilder<OUT, Opt>) {
|
|
1374
|
+
super({
|
|
1375
|
+
type: 'array',
|
|
1376
|
+
items: itemsSchema.build(),
|
|
1377
|
+
})
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
minLength(minItems: number): this {
|
|
1381
|
+
return this.cloneAndUpdateSchema({ minItems })
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
maxLength(maxItems: number): this {
|
|
1385
|
+
return this.cloneAndUpdateSchema({ maxItems })
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
length(exactLength: number): this
|
|
1389
|
+
length(minItems: number, maxItems: number): this
|
|
1390
|
+
length(minItemsOrExact: number, maxItems?: number): this {
|
|
1391
|
+
const maxItemsActual = maxItems ?? minItemsOrExact
|
|
1392
|
+
return this.minLength(minItemsOrExact).maxLength(maxItemsActual)
|
|
1215
1393
|
}
|
|
1216
1394
|
|
|
1217
|
-
|
|
1218
|
-
return this.
|
|
1395
|
+
exactLength(length: number): this {
|
|
1396
|
+
return this.minLength(length).maxLength(length)
|
|
1219
1397
|
}
|
|
1220
1398
|
|
|
1221
|
-
|
|
1222
|
-
return this.cloneAndUpdateSchema({
|
|
1399
|
+
unique(): this {
|
|
1400
|
+
return this.cloneAndUpdateSchema({ uniqueItems: true })
|
|
1223
1401
|
}
|
|
1402
|
+
}
|
|
1224
1403
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1404
|
+
class JSet2Builder<OUT, Opt> extends JBuilder<Set2<OUT>, Opt> {
|
|
1405
|
+
constructor(itemsSchema: JBuilder<OUT, Opt>) {
|
|
1406
|
+
super({
|
|
1407
|
+
type: ['array', 'object'],
|
|
1408
|
+
Set2: itemsSchema.build(),
|
|
1409
|
+
})
|
|
1227
1410
|
}
|
|
1228
1411
|
|
|
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
|
-
}
|
|
1412
|
+
min(minItems: number): this {
|
|
1413
|
+
return this.cloneAndUpdateSchema({ minItems })
|
|
1414
|
+
}
|
|
1237
1415
|
|
|
1238
|
-
|
|
1416
|
+
max(maxItems: number): this {
|
|
1417
|
+
return this.cloneAndUpdateSchema({ maxItems })
|
|
1239
1418
|
}
|
|
1240
1419
|
}
|
|
1241
1420
|
|
|
1242
|
-
export
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1421
|
+
export class JEnum<
|
|
1422
|
+
OUT extends string | number | boolean | null,
|
|
1423
|
+
Opt extends boolean = false,
|
|
1424
|
+
> extends JBuilder<OUT, Opt> {
|
|
1425
|
+
constructor(enumValues: readonly OUT[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt) {
|
|
1426
|
+
const jsonSchema: JsonSchema = { enum: enumValues }
|
|
1427
|
+
// Specifying the base type helps in cases when we ask Ajv to coerce the types.
|
|
1428
|
+
// Having only the `enum` in the schema does not trigger a coercion in Ajv.
|
|
1429
|
+
if (baseType === 'string') jsonSchema.type = 'string'
|
|
1430
|
+
if (baseType === 'number') jsonSchema.type = 'number'
|
|
1248
1431
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
constructor() {
|
|
1254
|
-
super({
|
|
1255
|
-
type: 'string',
|
|
1256
|
-
IsoMonth: {},
|
|
1257
|
-
})
|
|
1432
|
+
super(jsonSchema)
|
|
1433
|
+
|
|
1434
|
+
if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
|
|
1435
|
+
if (opt?.msg) this.setErrorMessage('enum', opt.msg)
|
|
1258
1436
|
}
|
|
1259
|
-
}
|
|
1260
1437
|
|
|
1261
|
-
|
|
1438
|
+
branded<B extends OUT>(): JEnum<B, Opt> {
|
|
1439
|
+
return this as unknown as JEnum<B, Opt>
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1262
1442
|
|
|
1263
|
-
export class
|
|
1264
|
-
|
|
1265
|
-
Opt extends boolean = false,
|
|
1266
|
-
> extends JsonSchemaAnyBuilder<OUT, Opt> {
|
|
1267
|
-
constructor() {
|
|
1443
|
+
export class JTuple<ITEMS extends JBuilder<any, any>[]> extends JBuilder<TupleOut<ITEMS>, false> {
|
|
1444
|
+
constructor(items: ITEMS) {
|
|
1268
1445
|
super({
|
|
1269
|
-
type: '
|
|
1446
|
+
type: 'array',
|
|
1447
|
+
prefixItems: items.map(i => i.build()),
|
|
1448
|
+
minItems: items.length,
|
|
1449
|
+
maxItems: items.length,
|
|
1270
1450
|
})
|
|
1271
1451
|
}
|
|
1452
|
+
}
|
|
1272
1453
|
|
|
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)
|
|
1454
|
+
// ==== Standalone functions for j.object ====
|
|
1296
1455
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
const alternativesSchema = j.enum(optionalValues)
|
|
1302
|
-
Object.assign(newBuilder.getSchema(), {
|
|
1303
|
-
anyOf: [this.build(), alternativesSchema.build()],
|
|
1304
|
-
optionalValues,
|
|
1305
|
-
})
|
|
1456
|
+
function object(props: AnyObject): never
|
|
1457
|
+
function object<OUT extends AnyObject>(props: {
|
|
1458
|
+
[K in keyof Required<OUT>]-?: JSchema<OUT[K], any>
|
|
1459
|
+
}): JObject<OUT, false>
|
|
1306
1460
|
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
newBuilder = new JsonSchemaTerminal({
|
|
1313
|
-
anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
|
|
1314
|
-
optionalField: true,
|
|
1315
|
-
})
|
|
1316
|
-
}
|
|
1461
|
+
function object<OUT extends AnyObject>(props: {
|
|
1462
|
+
[key in keyof OUT]: JSchema<OUT[key], any>
|
|
1463
|
+
}): JObject<OUT, false> {
|
|
1464
|
+
return new JObject<OUT, false>(props)
|
|
1465
|
+
}
|
|
1317
1466
|
|
|
1318
|
-
|
|
1319
|
-
|
|
1467
|
+
function objectInfer<P extends Record<string, JBuilder<any, any>>>(
|
|
1468
|
+
props: P,
|
|
1469
|
+
): JObjectInfer<P, false> {
|
|
1470
|
+
return new JObjectInfer<P, false>(props)
|
|
1471
|
+
}
|
|
1320
1472
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1473
|
+
function objectDbEntity(props: AnyObject): never
|
|
1474
|
+
function objectDbEntity<
|
|
1475
|
+
OUT extends BaseDBEntity,
|
|
1476
|
+
EXTRA_KEYS extends Exclude<keyof OUT, keyof BaseDBEntity> = Exclude<
|
|
1477
|
+
keyof OUT,
|
|
1478
|
+
keyof BaseDBEntity
|
|
1479
|
+
>,
|
|
1480
|
+
>(
|
|
1481
|
+
props: {
|
|
1482
|
+
// ✅ all non-system fields must be explicitly provided
|
|
1483
|
+
[K in EXTRA_KEYS]-?: BuilderFor<OUT[K]>
|
|
1484
|
+
} &
|
|
1485
|
+
// ✅ if `id` differs, it's required
|
|
1486
|
+
(ExactMatch<OUT['id'], BaseDBEntity['id']> extends true
|
|
1487
|
+
? { id?: BuilderFor<BaseDBEntity['id']> }
|
|
1488
|
+
: { id: BuilderFor<OUT['id']> }) &
|
|
1489
|
+
(ExactMatch<OUT['created'], BaseDBEntity['created']> extends true
|
|
1490
|
+
? { created?: BuilderFor<BaseDBEntity['created']> }
|
|
1491
|
+
: { created: BuilderFor<OUT['created']> }) &
|
|
1492
|
+
(ExactMatch<OUT['updated'], BaseDBEntity['updated']> extends true
|
|
1493
|
+
? { updated?: BuilderFor<BaseDBEntity['updated']> }
|
|
1494
|
+
: { updated: BuilderFor<OUT['updated']> }),
|
|
1495
|
+
): JObject<OUT, false>
|
|
1324
1496
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1497
|
+
function objectDbEntity(props: AnyObject): any {
|
|
1498
|
+
return j.object({
|
|
1499
|
+
id: j.string(),
|
|
1500
|
+
created: j.number().unixTimestamp2000(),
|
|
1501
|
+
updated: j.number().unixTimestamp2000(),
|
|
1502
|
+
...props,
|
|
1503
|
+
})
|
|
1504
|
+
}
|
|
1328
1505
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1506
|
+
function record<
|
|
1507
|
+
KS extends JBuilder<any, any>,
|
|
1508
|
+
VS extends JBuilder<any, any>,
|
|
1509
|
+
Opt extends boolean = SchemaOpt<VS>,
|
|
1510
|
+
>(
|
|
1511
|
+
keySchema: KS,
|
|
1512
|
+
valueSchema: VS,
|
|
1513
|
+
): JObject<
|
|
1514
|
+
Opt extends true
|
|
1515
|
+
? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
|
|
1516
|
+
: Record<SchemaOut<KS>, SchemaOut<VS>>,
|
|
1517
|
+
false
|
|
1518
|
+
> {
|
|
1519
|
+
const keyJsonSchema = keySchema.build()
|
|
1520
|
+
// Check if value schema is optional before build() strips the optionalField flag
|
|
1521
|
+
const isValueOptional = (valueSchema as JSchema<any, any>).getSchema().optionalField
|
|
1522
|
+
const valueJsonSchema = valueSchema.build()
|
|
1332
1523
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1524
|
+
// When value schema is optional, wrap in anyOf to allow undefined values
|
|
1525
|
+
const finalValueSchema: JsonSchema = isValueOptional
|
|
1526
|
+
? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
|
|
1527
|
+
: valueJsonSchema
|
|
1336
1528
|
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1529
|
+
return new JObject<
|
|
1530
|
+
Opt extends true
|
|
1531
|
+
? Partial<Record<SchemaOut<KS>, SchemaOut<VS>>>
|
|
1532
|
+
: Record<SchemaOut<KS>, SchemaOut<VS>>,
|
|
1533
|
+
false
|
|
1534
|
+
>([], {
|
|
1535
|
+
hasIsOfTypeCheck: false,
|
|
1536
|
+
keySchema: keyJsonSchema,
|
|
1537
|
+
patternProperties: {
|
|
1538
|
+
['^.*$']: finalValueSchema,
|
|
1539
|
+
},
|
|
1540
|
+
})
|
|
1541
|
+
}
|
|
1340
1542
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1543
|
+
function withRegexKeys<S extends JBuilder<any, any>>(
|
|
1544
|
+
keyRegex: RegExp | string,
|
|
1545
|
+
schema: S,
|
|
1546
|
+
): JObject<StringMap<SchemaOut<S>>, false> {
|
|
1547
|
+
if (keyRegex instanceof RegExp) {
|
|
1548
|
+
_assert(
|
|
1549
|
+
!keyRegex.flags,
|
|
1550
|
+
`Regex flags are not supported by JSON Schema. Received: /${keyRegex.source}/${keyRegex.flags}`,
|
|
1551
|
+
)
|
|
1343
1552
|
}
|
|
1553
|
+
const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex
|
|
1554
|
+
const jsonSchema = schema.build()
|
|
1344
1555
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1556
|
+
return new JObject<StringMap<SchemaOut<S>>, false>([], {
|
|
1557
|
+
hasIsOfTypeCheck: false,
|
|
1558
|
+
patternProperties: {
|
|
1559
|
+
[pattern]: jsonSchema,
|
|
1560
|
+
},
|
|
1561
|
+
})
|
|
1562
|
+
}
|
|
1348
1563
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1564
|
+
/**
|
|
1565
|
+
* Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
|
|
1566
|
+
*/
|
|
1567
|
+
function withEnumKeys<
|
|
1568
|
+
const T extends readonly (string | number)[] | StringEnum | NumberEnum,
|
|
1569
|
+
S extends JBuilder<any, any>,
|
|
1570
|
+
K extends string | number = EnumKeyUnion<T>,
|
|
1571
|
+
Opt extends boolean = SchemaOpt<S>,
|
|
1572
|
+
>(
|
|
1573
|
+
keys: T,
|
|
1574
|
+
schema: S,
|
|
1575
|
+
): JObject<Opt extends true ? { [P in K]?: SchemaOut<S> } : { [P in K]: SchemaOut<S> }, false> {
|
|
1576
|
+
let enumValues: readonly (string | number)[] | undefined
|
|
1577
|
+
if (Array.isArray(keys)) {
|
|
1578
|
+
_assert(
|
|
1579
|
+
isEveryItemPrimitive(keys),
|
|
1580
|
+
'Every item in the key list should be string, number or symbol',
|
|
1581
|
+
)
|
|
1582
|
+
enumValues = keys
|
|
1583
|
+
} else if (typeof keys === 'object') {
|
|
1584
|
+
const enumType = getEnumType(keys)
|
|
1585
|
+
_assert(
|
|
1586
|
+
enumType === 'NumberEnum' || enumType === 'StringEnum',
|
|
1587
|
+
'The key list should be StringEnum or NumberEnum',
|
|
1588
|
+
)
|
|
1589
|
+
if (enumType === 'NumberEnum') {
|
|
1590
|
+
enumValues = _numberEnumValues(keys as NumberEnum)
|
|
1591
|
+
} else if (enumType === 'StringEnum') {
|
|
1592
|
+
enumValues = _stringEnumValues(keys as StringEnum)
|
|
1593
|
+
}
|
|
1351
1594
|
}
|
|
1352
1595
|
|
|
1353
|
-
|
|
1354
|
-
return this.max(value)
|
|
1355
|
-
}
|
|
1596
|
+
_assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum')
|
|
1356
1597
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
}
|
|
1598
|
+
const typedValues = enumValues as readonly K[]
|
|
1599
|
+
const props = Object.fromEntries(typedValues.map(key => [key, schema])) as any
|
|
1360
1600
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1601
|
+
return new JObject<
|
|
1602
|
+
Opt extends true ? { [P in K]?: SchemaOut<S> } : { [P in K]: SchemaOut<S> },
|
|
1603
|
+
false
|
|
1604
|
+
>(props, { hasIsOfTypeCheck: false })
|
|
1605
|
+
}
|
|
1364
1606
|
|
|
1365
|
-
|
|
1366
|
-
return this.min(value).max(value)
|
|
1367
|
-
}
|
|
1607
|
+
// ==== AjvSchema compat wrapper ====
|
|
1368
1608
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1609
|
+
/**
|
|
1610
|
+
* On creation - compiles ajv validation function.
|
|
1611
|
+
* Provides convenient methods, error reporting, etc.
|
|
1612
|
+
*/
|
|
1613
|
+
export class AjvSchema<OUT> {
|
|
1614
|
+
private constructor(
|
|
1615
|
+
public schema: JsonSchema<OUT>,
|
|
1616
|
+
cfg: Partial<AjvSchemaCfg> = {},
|
|
1617
|
+
preCompiledFn?: any,
|
|
1618
|
+
) {
|
|
1619
|
+
this.cfg = {
|
|
1620
|
+
lazy: false,
|
|
1621
|
+
...cfg,
|
|
1622
|
+
ajv: cfg.ajv || getAjv(),
|
|
1623
|
+
// Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
|
|
1624
|
+
inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
|
|
1372
1625
|
}
|
|
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
1626
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
return this.integer().min(newMin).max(newMax)
|
|
1627
|
+
if (preCompiledFn) {
|
|
1628
|
+
this._compiledFn = preCompiledFn
|
|
1629
|
+
} else if (!cfg.lazy) {
|
|
1630
|
+
this._getValidateFn() // compile eagerly
|
|
1631
|
+
}
|
|
1392
1632
|
}
|
|
1393
1633
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1634
|
+
/**
|
|
1635
|
+
* Shortcut for AjvSchema.create(schema, { lazy: true })
|
|
1636
|
+
*/
|
|
1637
|
+
static createLazy<OUT>(
|
|
1638
|
+
schema: SchemaHandledByAjv<OUT>,
|
|
1639
|
+
cfg?: Partial<AjvSchemaCfg>,
|
|
1640
|
+
): AjvSchema<OUT> {
|
|
1641
|
+
return AjvSchema.create(schema, {
|
|
1642
|
+
lazy: true,
|
|
1643
|
+
...cfg,
|
|
1644
|
+
})
|
|
1396
1645
|
}
|
|
1397
1646
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1647
|
+
/**
|
|
1648
|
+
* Conveniently allows to pass either JsonSchema or JSchema builder, or existing AjvSchema.
|
|
1649
|
+
* If it's already an AjvSchema - it'll just return it without any processing.
|
|
1650
|
+
* If it's a Builder - will call `build` before proceeding.
|
|
1651
|
+
* Otherwise - will construct AjvSchema instance ready to be used.
|
|
1652
|
+
*/
|
|
1653
|
+
static create<OUT>(schema: SchemaHandledByAjv<OUT>, cfg?: Partial<AjvSchemaCfg>): AjvSchema<OUT> {
|
|
1654
|
+
if (schema instanceof AjvSchema) return schema
|
|
1401
1655
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1656
|
+
if (AjvSchema.isSchemaWithCachedAjvSchema<typeof schema, OUT>(schema)) {
|
|
1657
|
+
return AjvSchema.requireCachedAjvSchema<typeof schema, OUT>(schema)
|
|
1658
|
+
}
|
|
1405
1659
|
|
|
1406
|
-
|
|
1407
|
-
return this.integer().min(TS_2000).max(TS_2500).branded<UnixTimestamp>()
|
|
1408
|
-
}
|
|
1660
|
+
let jsonSchema: JsonSchema<OUT>
|
|
1409
1661
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1662
|
+
if (schema instanceof JSchema) {
|
|
1663
|
+
// oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
|
|
1664
|
+
jsonSchema = (schema as JSchema<OUT, any>).build()
|
|
1665
|
+
AjvSchema.requireValidJsonSchema(jsonSchema)
|
|
1666
|
+
} else {
|
|
1667
|
+
jsonSchema = schema
|
|
1668
|
+
}
|
|
1413
1669
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1670
|
+
// This is our own helper which marks a schema as optional
|
|
1671
|
+
// in case it is going to be used in an object schema,
|
|
1672
|
+
// where we need to mark the given property as not-required.
|
|
1673
|
+
// But once all compilation is done, the presence of this field
|
|
1674
|
+
// really upsets Ajv.
|
|
1675
|
+
delete jsonSchema.optionalField
|
|
1417
1676
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
.multipleOf(15)
|
|
1421
|
-
.min(-12 * 60)
|
|
1422
|
-
.max(14 * 60)
|
|
1423
|
-
}
|
|
1677
|
+
const ajvSchema = new AjvSchema<OUT>(jsonSchema, cfg)
|
|
1678
|
+
AjvSchema.cacheAjvSchema(schema, ajvSchema)
|
|
1424
1679
|
|
|
1425
|
-
|
|
1426
|
-
return this.integer().min(-12).max(14)
|
|
1680
|
+
return ajvSchema
|
|
1427
1681
|
}
|
|
1428
1682
|
|
|
1429
1683
|
/**
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1432
|
-
* due to how mutability works in Ajv.
|
|
1684
|
+
* Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
|
|
1685
|
+
* Used internally by JSchema to cache a compatible AjvSchema instance.
|
|
1433
1686
|
*/
|
|
1434
|
-
|
|
1435
|
-
return
|
|
1687
|
+
static _wrap<OUT>(schema: JsonSchema<OUT>, compiledFn: any): AjvSchema<OUT> {
|
|
1688
|
+
return new AjvSchema<OUT>(schema, {}, compiledFn)
|
|
1436
1689
|
}
|
|
1437
|
-
}
|
|
1438
1690
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
constructor() {
|
|
1444
|
-
super({
|
|
1445
|
-
type: 'boolean',
|
|
1446
|
-
})
|
|
1691
|
+
static isSchemaWithCachedAjvSchema<Base, OUT>(
|
|
1692
|
+
schema: Base,
|
|
1693
|
+
): schema is WithCachedAjvSchema<Base, OUT> {
|
|
1694
|
+
return !!(schema as any)?.[HIDDEN_AJV_SCHEMA]
|
|
1447
1695
|
}
|
|
1448
1696
|
|
|
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
|
|
1697
|
+
static cacheAjvSchema<Base extends AnyObject, OUT>(
|
|
1698
|
+
schema: Base,
|
|
1699
|
+
ajvSchema: AjvSchema<OUT>,
|
|
1700
|
+
): WithCachedAjvSchema<Base, OUT> {
|
|
1701
|
+
return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema })
|
|
1468
1702
|
}
|
|
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
1703
|
|
|
1486
|
-
|
|
1704
|
+
static requireCachedAjvSchema<Base, OUT>(schema: WithCachedAjvSchema<Base, OUT>): AjvSchema<OUT> {
|
|
1705
|
+
return schema[HIDDEN_AJV_SCHEMA]
|
|
1487
1706
|
}
|
|
1488
1707
|
|
|
1489
|
-
|
|
1490
|
-
const properties: Record<string, JsonSchema> = {}
|
|
1491
|
-
const required: string[] = []
|
|
1708
|
+
readonly cfg: AjvSchemaCfg
|
|
1492
1709
|
|
|
1493
|
-
|
|
1494
|
-
const isOptional = (builder as JsonSchemaTerminal<any, any>).getSchema().optionalField
|
|
1495
|
-
if (!isOptional) {
|
|
1496
|
-
required.push(key)
|
|
1497
|
-
}
|
|
1710
|
+
private _compiledFn: any
|
|
1498
1711
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1712
|
+
private _getValidateFn(): any {
|
|
1713
|
+
if (!this._compiledFn) {
|
|
1714
|
+
this._compiledFn = this.cfg.ajv.compile(this.schema as any)
|
|
1501
1715
|
}
|
|
1502
|
-
|
|
1503
|
-
this.schema.properties = properties
|
|
1504
|
-
this.schema.required = _uniq(required).sort()
|
|
1505
|
-
|
|
1506
|
-
return this
|
|
1716
|
+
return this._compiledFn
|
|
1507
1717
|
}
|
|
1508
1718
|
|
|
1509
1719
|
/**
|
|
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.
|
|
1720
|
+
* It returns the original object just for convenience.
|
|
1517
1721
|
*/
|
|
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
|
|
1722
|
+
validate(input: unknown, opt: AjvValidationOptions = {}): OUT {
|
|
1723
|
+
const [err, output] = this.getValidationResult(input, opt)
|
|
1724
|
+
if (err) throw err
|
|
1725
|
+
return output
|
|
1535
1726
|
}
|
|
1536
1727
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
allowAdditionalProperties(): this {
|
|
1542
|
-
return this.cloneAndUpdateSchema({ additionalProperties: true })
|
|
1728
|
+
isValid(input: unknown, opt?: AjvValidationOptions): boolean {
|
|
1729
|
+
const [err] = this.getValidationResult(input, opt)
|
|
1730
|
+
return !err
|
|
1543
1731
|
}
|
|
1544
1732
|
|
|
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
|
|
1733
|
+
getValidationResult(
|
|
1734
|
+
input: unknown,
|
|
1735
|
+
opt: AjvValidationOptions = {},
|
|
1736
|
+
): ValidationFunctionResult<OUT, AjvValidationError> {
|
|
1737
|
+
const fn = this._getValidateFn()
|
|
1738
|
+
return executeValidation<OUT>(fn, this.schema, input, opt, this.cfg.inputName)
|
|
1577
1739
|
}
|
|
1578
1740
|
|
|
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>
|
|
1741
|
+
getValidationFunction(): ValidationFunction<OUT, AjvValidationError> {
|
|
1742
|
+
return (input, opt) => {
|
|
1743
|
+
return this.getValidationResult(input, {
|
|
1744
|
+
mutateInput: opt?.mutateInput,
|
|
1745
|
+
inputName: opt?.inputName,
|
|
1746
|
+
inputId: opt?.inputId,
|
|
1747
|
+
})
|
|
1748
|
+
}
|
|
1604
1749
|
}
|
|
1605
1750
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
updated: j.number().unixTimestamp2000(),
|
|
1615
|
-
})
|
|
1751
|
+
private static requireValidJsonSchema(schema: JsonSchema): void {
|
|
1752
|
+
// For object schemas we require that it is type checked against an external type, e.g.:
|
|
1753
|
+
// interface Foo { name: string }
|
|
1754
|
+
// const schema = j.object({ name: j.string() }).ofType<Foo>()
|
|
1755
|
+
_assert(
|
|
1756
|
+
schema.type !== 'object' || schema.hasIsOfTypeCheck,
|
|
1757
|
+
'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.',
|
|
1758
|
+
)
|
|
1616
1759
|
}
|
|
1760
|
+
}
|
|
1617
1761
|
|
|
1618
|
-
|
|
1619
|
-
return this.cloneAndUpdateSchema({ minProperties, minProperties2: minProperties })
|
|
1620
|
-
}
|
|
1762
|
+
// ==== Shared validation logic ====
|
|
1621
1763
|
|
|
1622
|
-
|
|
1623
|
-
return this.cloneAndUpdateSchema({ maxProperties })
|
|
1624
|
-
}
|
|
1764
|
+
const separator = '\n'
|
|
1625
1765
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1766
|
+
function executeValidation<OUT>(
|
|
1767
|
+
fn: any,
|
|
1768
|
+
builtSchema: JsonSchema,
|
|
1769
|
+
input: unknown,
|
|
1770
|
+
opt: AjvValidationOptions = {},
|
|
1771
|
+
defaultInputName?: string,
|
|
1772
|
+
): ValidationFunctionResult<OUT, AjvValidationError> {
|
|
1773
|
+
const item =
|
|
1774
|
+
opt.mutateInput !== false || typeof input !== 'object'
|
|
1775
|
+
? input // mutate
|
|
1776
|
+
: _deepCopy(input) // not mutate
|
|
1777
|
+
|
|
1778
|
+
let valid = fn(item) // mutates item, but not input
|
|
1779
|
+
_typeCast<OUT>(item)
|
|
1780
|
+
|
|
1781
|
+
let output: OUT = item
|
|
1782
|
+
if (valid && builtSchema.postValidation) {
|
|
1783
|
+
const [err, result] = _try(() => builtSchema.postValidation!(output))
|
|
1784
|
+
if (err) {
|
|
1785
|
+
valid = false
|
|
1786
|
+
;(fn as any).errors = [
|
|
1787
|
+
{
|
|
1788
|
+
instancePath: '',
|
|
1789
|
+
message: err.message,
|
|
1790
|
+
},
|
|
1791
|
+
]
|
|
1792
|
+
} else {
|
|
1793
|
+
output = result as OUT
|
|
1794
|
+
}
|
|
1629
1795
|
}
|
|
1630
|
-
}
|
|
1631
1796
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1797
|
+
if (valid) return [null, output]
|
|
1798
|
+
|
|
1799
|
+
const errors = fn.errors!
|
|
1800
|
+
|
|
1801
|
+
const {
|
|
1802
|
+
inputId = _isObject(input) ? (input as any)['id'] : undefined,
|
|
1803
|
+
inputName = defaultInputName || 'Object',
|
|
1804
|
+
} = opt
|
|
1805
|
+
const dataVar = [inputName, inputId].filter(Boolean).join('.')
|
|
1806
|
+
|
|
1807
|
+
applyImprovementsOnErrorMessages(errors, builtSchema)
|
|
1808
|
+
|
|
1809
|
+
let message = getAjv().errorsText(errors, {
|
|
1810
|
+
dataVar,
|
|
1811
|
+
separator,
|
|
1812
|
+
})
|
|
1813
|
+
|
|
1814
|
+
// Note: if we mutated the input already, e.g stripped unknown properties,
|
|
1815
|
+
// the error message Input would contain already mutated object print, such as Input: {}
|
|
1816
|
+
// Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
|
|
1817
|
+
const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 })
|
|
1818
|
+
message = [message, 'Input: ' + inputStringified].join(separator)
|
|
1819
|
+
|
|
1820
|
+
const err = new AjvValidationError(
|
|
1821
|
+
message,
|
|
1822
|
+
_filterNullishValues({
|
|
1823
|
+
errors,
|
|
1824
|
+
inputName,
|
|
1825
|
+
inputId,
|
|
1826
|
+
}),
|
|
1827
|
+
)
|
|
1828
|
+
return [err, output]
|
|
1636
1829
|
}
|
|
1637
1830
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
>
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1831
|
+
// ==== Error formatting helpers ====
|
|
1832
|
+
|
|
1833
|
+
function applyImprovementsOnErrorMessages(
|
|
1834
|
+
errors: ErrorObject<string, Record<string, any>, unknown>[] | null | undefined,
|
|
1835
|
+
schema: JsonSchema,
|
|
1836
|
+
): void {
|
|
1837
|
+
if (!errors) return
|
|
1838
|
+
|
|
1839
|
+
filterNullableAnyOfErrors(errors, schema)
|
|
1840
|
+
|
|
1841
|
+
const { errorMessages } = schema
|
|
1842
|
+
|
|
1843
|
+
for (const error of errors) {
|
|
1844
|
+
const errorMessage = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword)
|
|
1845
|
+
|
|
1846
|
+
if (errorMessage) {
|
|
1847
|
+
error.message = errorMessage
|
|
1848
|
+
} else if (errorMessages?.[error.keyword]) {
|
|
1849
|
+
error.message = errorMessages[error.keyword]
|
|
1850
|
+
} else {
|
|
1851
|
+
const unwrapped = unwrapNullableAnyOf(schema)
|
|
1852
|
+
if (unwrapped?.errorMessages?.[error.keyword]) {
|
|
1853
|
+
error.message = unwrapped.errorMessages[error.keyword]
|
|
1854
|
+
}
|
|
1655
1855
|
}
|
|
1656
|
-
>,
|
|
1657
|
-
Opt
|
|
1658
|
-
> {
|
|
1659
|
-
constructor(props?: PROPS) {
|
|
1660
|
-
super({
|
|
1661
|
-
type: 'object',
|
|
1662
|
-
properties: {},
|
|
1663
|
-
required: [],
|
|
1664
|
-
additionalProperties: false,
|
|
1665
|
-
})
|
|
1666
1856
|
|
|
1667
|
-
|
|
1857
|
+
error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.')
|
|
1668
1858
|
}
|
|
1859
|
+
}
|
|
1669
1860
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1861
|
+
/**
|
|
1862
|
+
* Filters out noisy errors produced by nullable anyOf patterns.
|
|
1863
|
+
* When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
|
|
1864
|
+
* AJV produces "must be null" and "must match a schema in anyOf" errors
|
|
1865
|
+
* that are confusing. This method splices them out, keeping only the real errors.
|
|
1866
|
+
*/
|
|
1867
|
+
function filterNullableAnyOfErrors(
|
|
1868
|
+
errors: ErrorObject<string, Record<string, any>, unknown>[],
|
|
1869
|
+
schema: JsonSchema,
|
|
1870
|
+
): void {
|
|
1871
|
+
// Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
|
|
1872
|
+
const exactPaths: string[] = []
|
|
1873
|
+
const nullBranchPrefixes: string[] = []
|
|
1673
1874
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
if (!isOptional) {
|
|
1677
|
-
required.push(key)
|
|
1678
|
-
}
|
|
1875
|
+
for (const error of errors) {
|
|
1876
|
+
if (error.keyword !== 'anyOf') continue
|
|
1679
1877
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
}
|
|
1878
|
+
const parentSchema = resolveSchemaPath(schema, error.schemaPath)
|
|
1879
|
+
if (!parentSchema) continue
|
|
1683
1880
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1881
|
+
const nullIndex = unwrapNullableAnyOfIndex(parentSchema)
|
|
1882
|
+
if (nullIndex === -1) continue
|
|
1686
1883
|
|
|
1687
|
-
|
|
1884
|
+
exactPaths.push(error.schemaPath) // e.g. "#/anyOf"
|
|
1885
|
+
const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length)
|
|
1886
|
+
nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`) // e.g. "#/anyOf/1/"
|
|
1688
1887
|
}
|
|
1689
1888
|
|
|
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
|
-
}
|
|
1889
|
+
if (!exactPaths.length) return
|
|
1744
1890
|
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
|
|
1751
|
-
optionalField: true,
|
|
1752
|
-
}) as any
|
|
1891
|
+
for (let i = errors.length - 1; i >= 0; i--) {
|
|
1892
|
+
const sp = errors[i]!.schemaPath
|
|
1893
|
+
if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
|
|
1894
|
+
errors.splice(i, 1)
|
|
1895
|
+
}
|
|
1753
1896
|
}
|
|
1897
|
+
}
|
|
1754
1898
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1899
|
+
/**
|
|
1900
|
+
* Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
|
|
1901
|
+
* and returns the parent schema containing the last keyword.
|
|
1902
|
+
*/
|
|
1903
|
+
function resolveSchemaPath(schema: JsonSchema, schemaPath: string): JsonSchema | undefined {
|
|
1904
|
+
// schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
|
|
1905
|
+
// We want the schema that contains the final keyword (e.g. "anyOf")
|
|
1906
|
+
const segments = schemaPath.replace(/^#\//, '').split('/')
|
|
1907
|
+
// Remove the last segment (the keyword itself, e.g. "anyOf")
|
|
1908
|
+
segments.pop()
|
|
1909
|
+
|
|
1910
|
+
let current: any = schema
|
|
1911
|
+
for (const segment of segments) {
|
|
1912
|
+
if (!current || typeof current !== 'object') return undefined
|
|
1913
|
+
current = current[segment]
|
|
1914
|
+
}
|
|
1915
|
+
return current as JsonSchema | undefined
|
|
1916
|
+
}
|
|
1758
1917
|
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1918
|
+
function getErrorMessageForInstancePath(
|
|
1919
|
+
schema: JsonSchema | undefined,
|
|
1920
|
+
instancePath: string,
|
|
1921
|
+
keyword: string,
|
|
1922
|
+
): string | undefined {
|
|
1923
|
+
if (!schema || !instancePath) return undefined
|
|
1762
1924
|
|
|
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))
|
|
1925
|
+
const segments = instancePath.split('/').filter(Boolean)
|
|
1926
|
+
return traverseSchemaPath(schema, segments, keyword)
|
|
1927
|
+
}
|
|
1777
1928
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1929
|
+
function traverseSchemaPath(
|
|
1930
|
+
schema: JsonSchema,
|
|
1931
|
+
segments: string[],
|
|
1932
|
+
keyword: string,
|
|
1933
|
+
): string | undefined {
|
|
1934
|
+
if (!segments.length) return undefined
|
|
1780
1935
|
|
|
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 })
|
|
1936
|
+
const [currentSegment, ...remainingSegments] = segments
|
|
1785
1937
|
|
|
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
|
-
}
|
|
1938
|
+
const nextSchema = getChildSchema(schema, currentSegment)
|
|
1939
|
+
if (!nextSchema) return undefined
|
|
1797
1940
|
|
|
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
|
-
})
|
|
1941
|
+
if (nextSchema.errorMessages?.[keyword]) {
|
|
1942
|
+
return nextSchema.errorMessages[keyword]
|
|
1808
1943
|
}
|
|
1809
|
-
}
|
|
1810
1944
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
items: itemsSchema.build(),
|
|
1816
|
-
})
|
|
1945
|
+
// Check through nullable wrapper
|
|
1946
|
+
const unwrapped = unwrapNullableAnyOf(nextSchema)
|
|
1947
|
+
if (unwrapped?.errorMessages?.[keyword]) {
|
|
1948
|
+
return unwrapped.errorMessages[keyword]
|
|
1817
1949
|
}
|
|
1818
1950
|
|
|
1819
|
-
|
|
1820
|
-
return
|
|
1951
|
+
if (remainingSegments.length) {
|
|
1952
|
+
return traverseSchemaPath(nextSchema, remainingSegments, keyword)
|
|
1821
1953
|
}
|
|
1822
1954
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
}
|
|
1955
|
+
return undefined
|
|
1956
|
+
}
|
|
1826
1957
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
length(minItemsOrExact: number, maxItems?: number): this {
|
|
1830
|
-
const maxItemsActual = maxItems ?? minItemsOrExact
|
|
1831
|
-
return this.minLength(minItemsOrExact).maxLength(maxItemsActual)
|
|
1832
|
-
}
|
|
1958
|
+
function getChildSchema(schema: JsonSchema, segment: string | undefined): JsonSchema | undefined {
|
|
1959
|
+
if (!segment) return undefined
|
|
1833
1960
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1961
|
+
// Unwrap nullable anyOf to find properties/items through nullable wrappers
|
|
1962
|
+
const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema
|
|
1837
1963
|
|
|
1838
|
-
|
|
1839
|
-
return
|
|
1964
|
+
if (/^\d+$/.test(segment) && effectiveSchema.items) {
|
|
1965
|
+
return getArrayItemSchema(effectiveSchema, segment)
|
|
1840
1966
|
}
|
|
1967
|
+
|
|
1968
|
+
return getObjectPropertySchema(effectiveSchema, segment)
|
|
1841
1969
|
}
|
|
1842
1970
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
super({
|
|
1846
|
-
type: ['array', 'object'],
|
|
1847
|
-
Set2: itemsSchema.build(),
|
|
1848
|
-
})
|
|
1849
|
-
}
|
|
1971
|
+
function getArrayItemSchema(schema: JsonSchema, indexSegment: string): JsonSchema | undefined {
|
|
1972
|
+
if (!schema.items) return undefined
|
|
1850
1973
|
|
|
1851
|
-
|
|
1852
|
-
return
|
|
1974
|
+
if (Array.isArray(schema.items)) {
|
|
1975
|
+
return schema.items[Number(indexSegment)]
|
|
1853
1976
|
}
|
|
1854
1977
|
|
|
1855
|
-
|
|
1856
|
-
return this.cloneAndUpdateSchema({ maxItems })
|
|
1857
|
-
}
|
|
1978
|
+
return schema.items
|
|
1858
1979
|
}
|
|
1859
1980
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
super({
|
|
1863
|
-
Buffer: true,
|
|
1864
|
-
})
|
|
1865
|
-
}
|
|
1981
|
+
function getObjectPropertySchema(schema: JsonSchema, segment: string): JsonSchema | undefined {
|
|
1982
|
+
return schema.properties?.[segment as keyof typeof schema.properties]
|
|
1866
1983
|
}
|
|
1867
1984
|
|
|
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'
|
|
1985
|
+
function unwrapNullableAnyOf(schema: JsonSchema): JsonSchema | undefined {
|
|
1986
|
+
const nullIndex = unwrapNullableAnyOfIndex(schema)
|
|
1987
|
+
if (nullIndex === -1) return undefined
|
|
1988
|
+
return schema.anyOf![1 - nullIndex]!
|
|
1989
|
+
}
|
|
1878
1990
|
|
|
1879
|
-
|
|
1991
|
+
function unwrapNullableAnyOfIndex(schema: JsonSchema): number {
|
|
1992
|
+
if (schema.anyOf?.length !== 2) return -1
|
|
1993
|
+
const nullIndex = schema.anyOf.findIndex(s => s.type === 'null')
|
|
1994
|
+
return nullIndex
|
|
1995
|
+
}
|
|
1880
1996
|
|
|
1881
|
-
|
|
1882
|
-
|
|
1997
|
+
// ==== Utility helpers ====
|
|
1998
|
+
|
|
1999
|
+
function addPropertiesToSchema(schema: JsonSchema, props: AnyObject): void {
|
|
2000
|
+
const properties: Record<string, JsonSchema> = {}
|
|
2001
|
+
const required: string[] = []
|
|
2002
|
+
|
|
2003
|
+
for (const [key, builder] of Object.entries(props)) {
|
|
2004
|
+
const isOptional = (builder as JSchema<any, any>).getSchema().optionalField
|
|
2005
|
+
if (!isOptional) {
|
|
2006
|
+
required.push(key)
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const builtSchema = builder.build()
|
|
2010
|
+
properties[key] = builtSchema
|
|
1883
2011
|
}
|
|
1884
2012
|
|
|
1885
|
-
|
|
1886
|
-
|
|
2013
|
+
schema.properties = properties
|
|
2014
|
+
schema.required = _uniq(required).sort()
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
function hasNoObjectSchemas(schema: JsonSchema): boolean {
|
|
2018
|
+
if (Array.isArray(schema.type)) {
|
|
2019
|
+
return schema.type.every(type =>
|
|
2020
|
+
['string', 'number', 'integer', 'boolean', 'null'].includes(type),
|
|
2021
|
+
)
|
|
2022
|
+
} else if (schema.anyOf) {
|
|
2023
|
+
return schema.anyOf.every(hasNoObjectSchemas)
|
|
2024
|
+
} else if (schema.oneOf) {
|
|
2025
|
+
return schema.oneOf.every(hasNoObjectSchemas)
|
|
2026
|
+
} else if (schema.enum) {
|
|
2027
|
+
return true
|
|
2028
|
+
} else if (schema.type === 'array') {
|
|
2029
|
+
return !schema.items || hasNoObjectSchemas(schema.items)
|
|
2030
|
+
} else {
|
|
2031
|
+
return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)
|
|
1887
2032
|
}
|
|
2033
|
+
|
|
2034
|
+
return false
|
|
1888
2035
|
}
|
|
1889
2036
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
2037
|
+
type EnumBaseType = 'string' | 'number' | 'other'
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Deep copy that preserves functions in customValidations/customConversions.
|
|
2041
|
+
* Unlike structuredClone, this handles function references (which only exist in those two properties).
|
|
2042
|
+
*/
|
|
2043
|
+
function deepCopyPreservingFunctions<T>(obj: T): T {
|
|
2044
|
+
if (obj === null || typeof obj !== 'object') return obj
|
|
2045
|
+
if (Array.isArray(obj)) return obj.map(deepCopyPreservingFunctions) as T
|
|
2046
|
+
const copy = {} as T
|
|
2047
|
+
for (const key of Object.keys(obj)) {
|
|
2048
|
+
const value = (obj as any)[key]
|
|
2049
|
+
// customValidations/customConversions are arrays of functions - shallow copy the array
|
|
2050
|
+
;(copy as any)[key] =
|
|
2051
|
+
(key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
|
|
2052
|
+
? [...value]
|
|
2053
|
+
: deepCopyPreservingFunctions(value)
|
|
1900
2054
|
}
|
|
2055
|
+
return copy
|
|
1901
2056
|
}
|
|
1902
2057
|
|
|
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
|
-
}
|
|
2058
|
+
// ==== Types & Interfaces ====
|
|
1912
2059
|
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2060
|
+
export interface AjvValidationOptions {
|
|
2061
|
+
/**
|
|
2062
|
+
* Custom Ajv instance to use for this validation.
|
|
2063
|
+
* Overrides the default Ajv or any Ajv set at construction time.
|
|
2064
|
+
* Compiled functions are cached per Ajv instance.
|
|
2065
|
+
*/
|
|
2066
|
+
ajv?: Ajv
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Defaults to true,
|
|
2070
|
+
* because that's how AJV works by default,
|
|
2071
|
+
* and what gives it performance advantage.
|
|
2072
|
+
* (Because we have found that deep-clone is surprisingly slow,
|
|
2073
|
+
* nearly as slow as Joi validation).
|
|
2074
|
+
*
|
|
2075
|
+
* If set to true - AJV will mutate the input in case it needs to apply transformations
|
|
2076
|
+
* (strip unknown properties, convert types, etc).
|
|
2077
|
+
*
|
|
2078
|
+
* If false - it will deep-clone (using JSON.stringify+parse) the input to prevent its mutation.
|
|
2079
|
+
* Will return the cloned/mutated object.
|
|
2080
|
+
* Please note that JSON.stringify+parse has side-effects,
|
|
2081
|
+
* e.g it will transform Buffer into a weird object.
|
|
2082
|
+
*/
|
|
2083
|
+
mutateInput?: boolean
|
|
2084
|
+
inputName?: string
|
|
2085
|
+
inputId?: string
|
|
2086
|
+
/**
|
|
2087
|
+
* Function that returns "original input".
|
|
2088
|
+
* What is original input?
|
|
2089
|
+
* It's an input in its original non-mutated form.
|
|
2090
|
+
* Why is it needed?
|
|
2091
|
+
* Because we mutates the Input here. And after its been mutated - we no longer
|
|
2092
|
+
* can include it "how it was" in an error message. So, for that reason we'll use
|
|
2093
|
+
* `getOriginalInput()`, if it's provided.
|
|
2094
|
+
*/
|
|
2095
|
+
getOriginalInput?: () => unknown
|
|
1922
2096
|
}
|
|
1923
2097
|
|
|
1924
|
-
export
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
for (const [key, schema] of Object.entries(schemaDictionary)) {
|
|
1931
|
-
builtSchemaDictionary[key] = schema.build()
|
|
1932
|
-
}
|
|
2098
|
+
export interface AjvSchemaCfg {
|
|
2099
|
+
/**
|
|
2100
|
+
* Pass Ajv instance, otherwise Ajv will be created with
|
|
2101
|
+
* AjvSchema default (not the same as Ajv defaults) parameters
|
|
2102
|
+
*/
|
|
2103
|
+
ajv: Ajv
|
|
1933
2104
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
})
|
|
1942
|
-
}
|
|
2105
|
+
inputName?: string
|
|
2106
|
+
|
|
2107
|
+
/**
|
|
2108
|
+
* If true - schema will be compiled on-demand (lazily).
|
|
2109
|
+
* Default: false.
|
|
2110
|
+
*/
|
|
2111
|
+
lazy?: boolean
|
|
1943
2112
|
}
|
|
1944
2113
|
|
|
1945
|
-
type
|
|
2114
|
+
export type SchemaHandledByAjv<OUT> = JSchema<OUT, any> | JsonSchema<OUT> | AjvSchema<OUT>
|
|
1946
2115
|
|
|
1947
2116
|
export interface JsonSchema<OUT = unknown> {
|
|
1948
2117
|
readonly out?: OUT
|
|
@@ -1951,7 +2120,6 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
1951
2120
|
$id?: string
|
|
1952
2121
|
title?: string
|
|
1953
2122
|
description?: string
|
|
1954
|
-
// $comment?: string
|
|
1955
2123
|
deprecated?: boolean
|
|
1956
2124
|
readOnly?: boolean
|
|
1957
2125
|
writeOnly?: boolean
|
|
@@ -2032,177 +2200,9 @@ export interface JsonSchema<OUT = unknown> {
|
|
|
2032
2200
|
postValidation?: PostValidatonFn<any, OUT>
|
|
2033
2201
|
}
|
|
2034
2202
|
|
|
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
|
-
}
|
|
2203
|
+
export type PostValidatonFn<OUT, OUT2> = (v: OUT) => OUT2
|
|
2204
|
+
export type CustomValidatorFn = (v: any) => string | undefined
|
|
2205
|
+
export type CustomConverterFn<OUT> = (v: any) => OUT
|
|
2206
2206
|
|
|
2207
2207
|
type Expand<T> = { [K in keyof T]: T[K] }
|
|
2208
2208
|
|
|
@@ -2255,17 +2255,17 @@ type ExactMatch<A, B> =
|
|
|
2255
2255
|
? true
|
|
2256
2256
|
: ExactMatchBase<Expand<StripIndexSignatureDeep<A>>, Expand<StripIndexSignatureDeep<B>>>
|
|
2257
2257
|
|
|
2258
|
-
type BuilderOutUnion<B extends readonly
|
|
2259
|
-
[K in keyof B]: B[K] extends
|
|
2258
|
+
type BuilderOutUnion<B extends readonly JBuilder<any, any>[]> = {
|
|
2259
|
+
[K in keyof B]: B[K] extends JBuilder<infer O, any> ? O : never
|
|
2260
2260
|
}[number]
|
|
2261
2261
|
|
|
2262
|
-
type AnyOfByOut<D extends Record<PropertyKey,
|
|
2263
|
-
[K in keyof D]: D[K] extends
|
|
2262
|
+
type AnyOfByOut<D extends Record<PropertyKey, JSchema<any, any>>> = {
|
|
2263
|
+
[K in keyof D]: D[K] extends JSchema<infer O, any> ? O : never
|
|
2264
2264
|
}[keyof D]
|
|
2265
2265
|
|
|
2266
|
-
type BuilderFor<T> =
|
|
2266
|
+
type BuilderFor<T> = JBuilder<T, any>
|
|
2267
2267
|
|
|
2268
|
-
interface JsonBuilderRuleOpt {
|
|
2268
|
+
export interface JsonBuilderRuleOpt {
|
|
2269
2269
|
/**
|
|
2270
2270
|
* Text of error message to return when the validation fails for the given rule:
|
|
2271
2271
|
*
|
|
@@ -2289,33 +2289,9 @@ type EnumKeyUnion<T> =
|
|
|
2289
2289
|
? T[keyof T]
|
|
2290
2290
|
: never
|
|
2291
2291
|
|
|
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
|
|
2292
|
+
type SchemaOut<S> = S extends JBuilder<infer OUT, any> ? OUT : never
|
|
2293
|
+
type SchemaOpt<S> = S extends JBuilder<any, infer Opt> ? (Opt extends true ? true : false) : false
|
|
2303
2294
|
|
|
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
|
|
2295
|
+
type TupleOut<T extends readonly JBuilder<any, any>[]> = {
|
|
2296
|
+
[K in keyof T]: T[K] extends JBuilder<infer O, any> ? O : never
|
|
2321
2297
|
}
|