@naturalcycles/nodejs-lib 15.37.2 → 15.39.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.
@@ -0,0 +1,752 @@
1
+ /* eslint-disable id-denylist */
2
+ // oxlint-disable max-lines
3
+
4
+ import {
5
+ _isUndefined,
6
+ _numberEnumValues,
7
+ _stringEnumValues,
8
+ getEnumType,
9
+ } from '@naturalcycles/js-lib'
10
+ import { _uniq } from '@naturalcycles/js-lib/array'
11
+ import { _assert } from '@naturalcycles/js-lib/error'
12
+ import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from '@naturalcycles/js-lib/json-schema'
13
+ import type { Set2 } from '@naturalcycles/js-lib/object'
14
+ import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object'
15
+ import {
16
+ type AnyObject,
17
+ type IsoDate,
18
+ type IsoDateTime,
19
+ JWT_REGEX,
20
+ type NumberEnum,
21
+ type StringEnum,
22
+ type StringMap,
23
+ type UnixTimestamp,
24
+ type UnixTimestampMillis,
25
+ } from '@naturalcycles/js-lib/types'
26
+
27
+ export const j = {
28
+ string(): JsonSchemaStringBuilder<string, string, false> {
29
+ return new JsonSchemaStringBuilder()
30
+ },
31
+
32
+ number(): JsonSchemaNumberBuilder<number, number, false> {
33
+ return new JsonSchemaNumberBuilder()
34
+ },
35
+
36
+ boolean(): JsonSchemaBooleanBuilder<boolean, boolean, false> {
37
+ return new JsonSchemaBooleanBuilder()
38
+ },
39
+
40
+ object<P extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(
41
+ props: P,
42
+ ): JsonSchemaObjectBuilder<P, false> {
43
+ return new JsonSchemaObjectBuilder<P, false>(props)
44
+ },
45
+
46
+ array<IN, OUT, Opt>(
47
+ itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>,
48
+ ): JsonSchemaArrayBuilder<IN, OUT, Opt> {
49
+ return new JsonSchemaArrayBuilder(itemSchema)
50
+ },
51
+
52
+ set<IN, OUT, Opt>(
53
+ itemSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>,
54
+ ): JsonSchemaSet2Builder<IN, OUT, Opt> {
55
+ return new JsonSchemaSet2Builder(itemSchema)
56
+ },
57
+
58
+ buffer(): JsonSchemaBufferBuilder {
59
+ return new JsonSchemaBufferBuilder()
60
+ },
61
+
62
+ enum<const T extends readonly (string | number | boolean | null)[] | StringEnum | NumberEnum>(
63
+ input: T,
64
+ ): JsonSchemaEnumBuilder<
65
+ T extends readonly (infer U)[]
66
+ ? U
67
+ : T extends StringEnum
68
+ ? T[keyof T]
69
+ : T extends NumberEnum
70
+ ? T[keyof T]
71
+ : never
72
+ > {
73
+ let enumValues: readonly (string | number | boolean | null)[] | undefined
74
+
75
+ if (Array.isArray(input)) {
76
+ enumValues = input
77
+ } else if (typeof input === 'object') {
78
+ const enumType = getEnumType(input)
79
+ if (enumType === 'NumberEnum') {
80
+ enumValues = _numberEnumValues(input as NumberEnum)
81
+ } else if (enumType === 'StringEnum') {
82
+ enumValues = _stringEnumValues(input as StringEnum)
83
+ }
84
+ }
85
+
86
+ _assert(enumValues, 'Unsupported enum input')
87
+ return new JsonSchemaEnumBuilder(enumValues as any)
88
+ },
89
+
90
+ oneOf<
91
+ B extends readonly JsonSchemaAnyBuilder<any, any, boolean>[],
92
+ IN = BuilderInUnion<B>,
93
+ OUT = BuilderOutUnion<B>,
94
+ >(items: [...B]): JsonSchemaAnyBuilder<IN, OUT, false> {
95
+ const schemas = items.map(b => b.build())
96
+ return new JsonSchemaAnyBuilder<IN, OUT, false>({
97
+ oneOf: schemas,
98
+ })
99
+ },
100
+ }
101
+
102
+ const TS_2500 = 16725225600 // 2500-01-01
103
+ const TS_2500_MILLIS = TS_2500 * 1000
104
+ const TS_2000 = 946684800 // 2000-01-01
105
+ const TS_2000_MILLIS = TS_2000 * 1000
106
+
107
+ /*
108
+ Notes for future reference
109
+
110
+ Q: Why do we need `Opt` - when `IN` and `OUT` already carries the `| undefined`?
111
+ A: Because of objects. Without `Opt`, an optional field would be inferred as `{ foo: string | undefined }`,
112
+ which means that the `foo` property would be mandatory, it's just that its value can be `undefined` as well.
113
+ With `Opt`, we can infer it as `{ foo?: string | undefined }`.
114
+ */
115
+
116
+ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
117
+ constructor(protected schema: JsonSchema) {}
118
+
119
+ protected setErrorMessage(ruleName: string, errorMessage: string | undefined): void {
120
+ if (_isUndefined(errorMessage)) return
121
+
122
+ this.schema.errorMessages ||= {}
123
+ this.schema.errorMessages[ruleName] = errorMessage
124
+ }
125
+
126
+ /**
127
+ * A helper function that takes a type parameter and compares it with the type inferred from the schema.
128
+ *
129
+ * When the type inferred from the schema differs from the passed-in type,
130
+ * the schema becomes unusable, by turning its type into `never`.
131
+ *
132
+ * ```ts
133
+ * const schemaGood = j.string().isOfType<string>() // ✅
134
+ *
135
+ * const schemaBad = j.string().isOfType<number>() // ❌
136
+ * schemaBad.build() // TypeError: property "build" does not exist on type "never"
137
+ * ```
138
+ */
139
+ isOfType<ExpectedType>(): ExactMatch<ExpectedType, OUT> extends true ? this : never {
140
+ Object.assign(this.schema, { hasIsOfTypeCheck: true })
141
+ return this as any
142
+ }
143
+
144
+ getSchema(): JsonSchema {
145
+ return this.schema
146
+ }
147
+
148
+ $schema($schema: string): this {
149
+ Object.assign(this.schema, { $schema })
150
+ return this
151
+ }
152
+
153
+ $schemaDraft7(): this {
154
+ this.$schema('http://json-schema.org/draft-07/schema#')
155
+ return this
156
+ }
157
+
158
+ $id($id: string): this {
159
+ Object.assign(this.schema, { $id })
160
+ return this
161
+ }
162
+
163
+ title(title: string): this {
164
+ Object.assign(this.schema, { title })
165
+ return this
166
+ }
167
+
168
+ description(description: string): this {
169
+ Object.assign(this.schema, { description })
170
+ return this
171
+ }
172
+
173
+ deprecated(deprecated = true): this {
174
+ Object.assign(this.schema, { deprecated })
175
+ return this
176
+ }
177
+
178
+ type(type: string): this {
179
+ Object.assign(this.schema, { type })
180
+ return this
181
+ }
182
+
183
+ default(v: any): this {
184
+ Object.assign(this.schema, { default: v })
185
+ return this
186
+ }
187
+
188
+ instanceof(of: string): this {
189
+ Object.assign(this.schema, { type: 'object', instanceof: of })
190
+ return this
191
+ }
192
+
193
+ optional(): JsonSchemaAnyBuilder<IN | undefined, OUT | undefined, true> {
194
+ this.schema.optionalField = true
195
+ return this as unknown as JsonSchemaAnyBuilder<IN | undefined, OUT | undefined, true>
196
+ }
197
+
198
+ nullable(): JsonSchemaAnyBuilder<IN | null, OUT | null, Opt> {
199
+ return new JsonSchemaAnyBuilder({
200
+ anyOf: [this.build(), { type: 'null' }],
201
+ })
202
+ }
203
+
204
+ /**
205
+ * Produces a "clean schema object" without methods.
206
+ * Same as if it would be JSON.stringified.
207
+ */
208
+ build(): JsonSchema<IN, OUT> {
209
+ return _sortObject(JSON.parse(JSON.stringify(this.schema)), JSON_SCHEMA_ORDER)
210
+ }
211
+
212
+ clone(): JsonSchemaAnyBuilder<IN, OUT, Opt> {
213
+ return new JsonSchemaAnyBuilder<IN, OUT, Opt>(_deepCopy(this.schema))
214
+ }
215
+
216
+ /**
217
+ * @experimental
218
+ */
219
+ in!: IN
220
+ out!: OUT
221
+ }
222
+
223
+ export class JsonSchemaStringBuilder<
224
+ IN extends string = string,
225
+ OUT = IN,
226
+ Opt extends boolean = false,
227
+ > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
228
+ constructor() {
229
+ super({
230
+ type: 'string',
231
+ })
232
+ }
233
+
234
+ regex(pattern: RegExp, opt?: JsonBuilderRuleOpt): this {
235
+ return this.pattern(pattern.source, opt)
236
+ }
237
+
238
+ pattern(pattern: string, opt?: JsonBuilderRuleOpt): this {
239
+ if (opt?.msg) this.setErrorMessage('pattern', opt.msg)
240
+ Object.assign(this.schema, { pattern })
241
+ return this
242
+ }
243
+
244
+ min(minLength: number): this {
245
+ Object.assign(this.schema, { minLength })
246
+ return this
247
+ }
248
+
249
+ max(maxLength: number): this {
250
+ Object.assign(this.schema, { maxLength })
251
+ return this
252
+ }
253
+
254
+ length(minLength: number, maxLength: number): this {
255
+ Object.assign(this.schema, { minLength, maxLength })
256
+ return this
257
+ }
258
+
259
+ email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
260
+ const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
261
+ Object.assign(this.schema, { email: { ...defaultOptions, ...opt } })
262
+
263
+ // from `ajv-formats`
264
+ const regex =
265
+ /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i
266
+ return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase()
267
+ }
268
+
269
+ trim(): this {
270
+ Object.assign(this.schema, { transform: { ...this.schema.transform, trim: true } })
271
+ return this
272
+ }
273
+
274
+ toLowerCase(): this {
275
+ Object.assign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } })
276
+ return this
277
+ }
278
+
279
+ toUpperCase(): this {
280
+ Object.assign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } })
281
+ return this
282
+ }
283
+
284
+ truncate(toLength: number): this {
285
+ Object.assign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } })
286
+ return this
287
+ }
288
+
289
+ branded<B extends string>(): JsonSchemaStringBuilder<B, B, Opt> {
290
+ return this as unknown as JsonSchemaStringBuilder<B, B, Opt>
291
+ }
292
+
293
+ isoDate(): JsonSchemaStringBuilder<IsoDate | IN, IsoDate, Opt> {
294
+ Object.assign(this.schema, { IsoDate: true })
295
+ return this.branded<IsoDate>()
296
+ }
297
+
298
+ isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt> {
299
+ Object.assign(this.schema, { IsoDateTime: true })
300
+ return this.branded<IsoDateTime>()
301
+ }
302
+
303
+ /**
304
+ * Validates the string format to be JWT.
305
+ * Expects the JWT to be signed!
306
+ */
307
+ jwt(): this {
308
+ return this.regex(JWT_REGEX, { msg: 'is not a valid JWT format' })
309
+ }
310
+
311
+ url(): this {
312
+ // from `ajv-formats`
313
+ const regex =
314
+ /^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00A1}-\u{FFFF}]+-)*[a-z0-9\u{00A1}-\u{FFFF}]+)(?:\.(?:[a-z0-9\u{00A1}-\u{FFFF}]+-)*[a-z0-9\u{00A1}-\u{FFFF}]+)*(?:\.(?:[a-z\u{00A1}-\u{FFFF}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu
315
+ return this.regex(regex, { msg: 'is not a valid URL format' })
316
+ }
317
+
318
+ ipv4(): this {
319
+ // from `ajv-formats`
320
+ const regex =
321
+ /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/
322
+ return this.regex(regex, { msg: 'is not a valid IPv4 format' })
323
+ }
324
+
325
+ ipv6(): this {
326
+ // from `ajv-formats`
327
+ const regex =
328
+ /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i
329
+ return this.regex(regex, { msg: 'is not a valid IPv6 format' })
330
+ }
331
+
332
+ id(): this {
333
+ const regex = /^[a-z0-9_]{6,64}$/
334
+ return this.regex(regex, { msg: 'is not a valid ID format' })
335
+ }
336
+
337
+ slug(): this {
338
+ const regex = /^[a-z0-9-]+$/
339
+ return this.regex(regex, { msg: 'is not a valid slug format' })
340
+ }
341
+
342
+ semVer(): this {
343
+ const regex = /^[0-9]+\.[0-9]+\.[0-9]+$/
344
+ return this.regex(regex, { msg: 'is not a valid semver format' })
345
+ }
346
+
347
+ languageTag(): this {
348
+ // IETF language tag (https://en.wikipedia.org/wiki/IETF_language_tag)
349
+ const regex = /^[a-z]{2}(-[A-Z]{2})?$/
350
+ return this.regex(regex, { msg: 'is not a valid language format' })
351
+ }
352
+
353
+ countryCode(): this {
354
+ const regex = /^[A-Z]{2}$/
355
+ return this.regex(regex, { msg: 'is not a valid country code format' })
356
+ }
357
+
358
+ currency(): this {
359
+ const regex = /^[A-Z]{3}$/
360
+ return this.regex(regex, { msg: 'is not a valid currency format' })
361
+ }
362
+ }
363
+
364
+ export interface JsonSchemaStringEmailOptions {
365
+ checkTLD: boolean
366
+ }
367
+
368
+ export class JsonSchemaNumberBuilder<
369
+ IN extends number = number,
370
+ OUT = IN,
371
+ Opt extends boolean = false,
372
+ > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
373
+ constructor() {
374
+ super({
375
+ type: 'number',
376
+ })
377
+ }
378
+
379
+ integer(): this {
380
+ Object.assign(this.schema, { type: 'integer' })
381
+ return this
382
+ }
383
+
384
+ branded<B extends number>(): JsonSchemaNumberBuilder<B, B, Opt> {
385
+ return this as unknown as JsonSchemaNumberBuilder<B, B, Opt>
386
+ }
387
+
388
+ multipleOf(multipleOf: number): this {
389
+ Object.assign(this.schema, { multipleOf })
390
+ return this
391
+ }
392
+
393
+ min(minimum: number): this {
394
+ Object.assign(this.schema, { minimum })
395
+ return this
396
+ }
397
+
398
+ exclusiveMin(exclusiveMinimum: number): this {
399
+ Object.assign(this.schema, { exclusiveMinimum })
400
+ return this
401
+ }
402
+
403
+ max(maximum: number): this {
404
+ Object.assign(this.schema, { maximum })
405
+ return this
406
+ }
407
+
408
+ exclusiveMax(exclusiveMaximum: number): this {
409
+ Object.assign(this.schema, { exclusiveMaximum })
410
+ return this
411
+ }
412
+
413
+ /**
414
+ * Both ranges are inclusive.
415
+ */
416
+ range(minimum: number, maximum: number): this {
417
+ Object.assign(this.schema, { minimum, maximum })
418
+ return this
419
+ }
420
+
421
+ int32(): this {
422
+ const MIN_INT32 = -(2 ** 31)
423
+ const MAX_INT32 = 2 ** 31 - 1
424
+ const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
425
+ const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
426
+ const newMin = Math.max(MIN_INT32, currentMin)
427
+ const newMax = Math.min(MAX_INT32, currentMax)
428
+ return this.integer().min(newMin).max(newMax)
429
+ }
430
+
431
+ int64(): this {
432
+ const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER
433
+ const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER
434
+ const newMin = Math.max(Number.MIN_SAFE_INTEGER, currentMin)
435
+ const newMax = Math.min(Number.MAX_SAFE_INTEGER, currentMax)
436
+ return this.integer().min(newMin).max(newMax)
437
+ }
438
+
439
+ float(): this {
440
+ return this
441
+ }
442
+
443
+ double(): this {
444
+ return this
445
+ }
446
+
447
+ unixTimestamp(): JsonSchemaNumberBuilder<UnixTimestamp, UnixTimestamp, Opt> {
448
+ return this.integer().min(0).max(TS_2500).branded<UnixTimestamp>()
449
+ }
450
+
451
+ unixTimestamp2000(): JsonSchemaNumberBuilder<UnixTimestamp, UnixTimestamp, Opt> {
452
+ return this.integer().min(TS_2000).max(TS_2500).branded<UnixTimestamp>()
453
+ }
454
+
455
+ unixTimestampMillis(): JsonSchemaNumberBuilder<UnixTimestampMillis, UnixTimestampMillis, Opt> {
456
+ return this.integer().min(0).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
457
+ }
458
+
459
+ unixTimestamp2000Millis(): JsonSchemaNumberBuilder<
460
+ UnixTimestampMillis,
461
+ UnixTimestampMillis,
462
+ Opt
463
+ > {
464
+ return this.integer().min(TS_2000_MILLIS).max(TS_2500_MILLIS).branded<UnixTimestampMillis>()
465
+ }
466
+
467
+ utcOffset(): this {
468
+ return this.integer()
469
+ .multipleOf(15)
470
+ .min(-12 * 60)
471
+ .max(14 * 60)
472
+ }
473
+
474
+ utcOffsetHour(): this {
475
+ return this.integer().min(-12).max(14)
476
+ }
477
+ }
478
+
479
+ export class JsonSchemaBooleanBuilder<
480
+ IN extends boolean = boolean,
481
+ OUT = IN,
482
+ Opt extends boolean = false,
483
+ > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
484
+ constructor() {
485
+ super({
486
+ type: 'boolean',
487
+ })
488
+ }
489
+ }
490
+
491
+ export class JsonSchemaObjectBuilder<
492
+ PROPS extends Record<string, JsonSchemaAnyBuilder<any, any, any>>,
493
+ Opt extends boolean = false,
494
+ > extends JsonSchemaAnyBuilder<
495
+ Expand<
496
+ {
497
+ [K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, any, infer IsOpt>
498
+ ? IsOpt extends true
499
+ ? never
500
+ : K
501
+ : never]: PROPS[K] extends JsonSchemaAnyBuilder<infer IN, any, any> ? IN : never
502
+ } & {
503
+ [K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, any, infer IsOpt>
504
+ ? IsOpt extends true
505
+ ? K
506
+ : never
507
+ : never]?: PROPS[K] extends JsonSchemaAnyBuilder<infer IN, any, any> ? IN : never
508
+ }
509
+ >,
510
+ Expand<
511
+ {
512
+ [K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, any, infer IsOpt>
513
+ ? IsOpt extends true
514
+ ? never
515
+ : K
516
+ : never]: PROPS[K] extends JsonSchemaAnyBuilder<any, infer OUT, any> ? OUT : never
517
+ } & {
518
+ [K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, any, infer IsOpt>
519
+ ? IsOpt extends true
520
+ ? K
521
+ : never
522
+ : never]?: PROPS[K] extends JsonSchemaAnyBuilder<any, infer OUT, any> ? OUT : never
523
+ }
524
+ >,
525
+ Opt
526
+ > {
527
+ constructor(props?: PROPS) {
528
+ super({
529
+ type: 'object',
530
+ properties: {},
531
+ required: [],
532
+ additionalProperties: false,
533
+ })
534
+
535
+ if (props) this.addProperties(props)
536
+ }
537
+
538
+ addProperties(props: PROPS): this {
539
+ const properties: Record<string, JsonSchema> = {}
540
+ const required: string[] = []
541
+
542
+ for (const [key, builder] of Object.entries(props)) {
543
+ const schema = builder.build()
544
+ if (!schema.optionalField) {
545
+ required.push(key)
546
+ } else {
547
+ schema.optionalField = undefined
548
+ }
549
+ properties[key] = schema
550
+ }
551
+
552
+ this.schema.properties = properties
553
+ this.schema.required = _uniq(required).sort()
554
+
555
+ return this
556
+ }
557
+
558
+ extend<NEW_PROPS extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(
559
+ props: NEW_PROPS,
560
+ ): JsonSchemaObjectBuilder<
561
+ {
562
+ [K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
563
+ ? NEW_PROPS[K]
564
+ : K extends keyof PROPS
565
+ ? PROPS[K]
566
+ : never
567
+ },
568
+ Opt
569
+ > {
570
+ const newBuilder = new JsonSchemaObjectBuilder<PROPS, Opt>()
571
+ Object.assign(newBuilder.schema, _deepCopy(this.schema))
572
+
573
+ const incomingSchemaBuilder = new JsonSchemaObjectBuilder<NEW_PROPS, false>(props)
574
+ mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
575
+
576
+ return newBuilder as JsonSchemaObjectBuilder<
577
+ {
578
+ [K in keyof PROPS | keyof NEW_PROPS]: K extends keyof NEW_PROPS
579
+ ? NEW_PROPS[K]
580
+ : K extends keyof PROPS
581
+ ? PROPS[K]
582
+ : never
583
+ },
584
+ Opt
585
+ >
586
+ }
587
+
588
+ /**
589
+ * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
590
+ */
591
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
592
+ dbEntity() {
593
+ return this.extend({
594
+ id: j.string(),
595
+ created: j.number().unixTimestamp2000(),
596
+ updated: j.number().unixTimestamp2000(),
597
+ })
598
+ }
599
+ }
600
+
601
+ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<IN[], OUT[], Opt> {
602
+ constructor(itemsSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>) {
603
+ super({
604
+ type: 'array',
605
+ items: itemsSchema.build(),
606
+ })
607
+ }
608
+
609
+ min(minItems: number): this {
610
+ Object.assign(this.schema, { minItems })
611
+ return this
612
+ }
613
+
614
+ max(maxItems: number): this {
615
+ Object.assign(this.schema, { maxItems })
616
+ return this
617
+ }
618
+
619
+ unique(uniqueItems: number): this {
620
+ Object.assign(this.schema, { uniqueItems })
621
+ return this
622
+ }
623
+ }
624
+
625
+ export class JsonSchemaSet2Builder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<
626
+ Iterable<IN>,
627
+ Set2<OUT>,
628
+ Opt
629
+ > {
630
+ constructor(itemsSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>) {
631
+ super({
632
+ type: ['array', 'object'],
633
+ Set2: itemsSchema.build(),
634
+ })
635
+ }
636
+
637
+ min(minItems: number): this {
638
+ Object.assign(this.schema, { minItems })
639
+ return this
640
+ }
641
+
642
+ max(maxItems: number): this {
643
+ Object.assign(this.schema, { maxItems })
644
+ return this
645
+ }
646
+ }
647
+
648
+ export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder<
649
+ string | any[] | ArrayBuffer | Buffer,
650
+ Buffer,
651
+ false
652
+ > {
653
+ constructor() {
654
+ super({
655
+ Buffer: true,
656
+ })
657
+ }
658
+ }
659
+
660
+ export class JsonSchemaEnumBuilder<
661
+ IN extends string | number | boolean | null,
662
+ OUT extends IN = IN,
663
+ Opt extends boolean = false,
664
+ > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
665
+ constructor(enumValues: readonly IN[]) {
666
+ super({ enum: enumValues })
667
+ }
668
+ }
669
+
670
+ export interface JsonSchema<IN = unknown, OUT = IN> {
671
+ readonly in?: IN
672
+ readonly out?: OUT
673
+
674
+ $schema?: AnyObject
675
+ $id?: string
676
+ title?: string
677
+ description?: string
678
+ // $comment?: string
679
+ deprecated?: boolean
680
+ readOnly?: boolean
681
+ writeOnly?: boolean
682
+
683
+ type?: string | string[]
684
+ items?: JsonSchema
685
+ properties?: {
686
+ [K in keyof IN & keyof OUT]: JsonSchema<IN[K], OUT[K]>
687
+ }
688
+ required?: string[]
689
+ additionalProperties?: boolean
690
+ minProperties?: number
691
+ maxProperties?: number
692
+
693
+ default?: IN
694
+
695
+ // https://json-schema.org/understanding-json-schema/reference/conditionals.html#id6
696
+ if?: JsonSchema
697
+ then?: JsonSchema
698
+ else?: JsonSchema
699
+
700
+ anyOf?: JsonSchema[]
701
+ oneOf?: JsonSchema[]
702
+
703
+ /**
704
+ * This is a temporary "intermediate AST" field that is used inside the parser.
705
+ * In the final schema this field will NOT be present.
706
+ */
707
+ optionalField?: true
708
+
709
+ pattern?: string
710
+ minLength?: number
711
+ maxLength?: number
712
+ format?: string
713
+
714
+ contentMediaType?: string
715
+ contentEncoding?: string // e.g 'base64'
716
+
717
+ multipleOf?: number
718
+ minimum?: number
719
+ exclusiveMinimum?: number
720
+ maximum?: number
721
+ exclusiveMaximum?: number
722
+
723
+ enum?: any
724
+
725
+ // Below we add custom Ajv keywords
726
+
727
+ Set2?: JsonSchema
728
+ Buffer?: true
729
+ IsoDate?: true
730
+ IsoDateTime?: true
731
+ instanceof?: string | string[]
732
+ transform?: { trim?: true; toLowerCase?: true; toUpperCase?: true; truncate?: number }
733
+ errorMessages?: StringMap<string>
734
+ hasIsOfTypeCheck?: boolean
735
+ }
736
+
737
+ type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
738
+
739
+ type ExactMatch<A, B> =
740
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
741
+
742
+ type BuilderOutUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
743
+ [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<any, infer O, any> ? O : never
744
+ }[number]
745
+
746
+ type BuilderInUnion<B extends readonly JsonSchemaAnyBuilder<any, any, any>[]> = {
747
+ [K in keyof B]: B[K] extends JsonSchemaAnyBuilder<infer I, any, any> ? I : never
748
+ }[number]
749
+
750
+ interface JsonBuilderRuleOpt {
751
+ msg?: string
752
+ }