@naturalcycles/nodejs-lib 15.90.2 → 15.92.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.
@@ -1,12 +1,18 @@
1
- import { _isObject, _lazyValue } from '@naturalcycles/js-lib';
2
- import { _assert } from '@naturalcycles/js-lib/error';
3
- import { _deepCopy, _filterNullishValues } from '@naturalcycles/js-lib/object';
1
+ /* eslint-disable id-denylist */
2
+ // oxlint-disable max-lines
3
+ import { _isObject, _isUndefined, _lazyValue, _numberEnumValues, _stringEnumValues, getEnumType, } from '@naturalcycles/js-lib';
4
+ import { _uniq } from '@naturalcycles/js-lib/array';
5
+ import { _assert, _try } from '@naturalcycles/js-lib/error';
6
+ import { _deepCopy, _filterNullishValues, _sortObject } from '@naturalcycles/js-lib/object';
4
7
  import { _substringBefore } from '@naturalcycles/js-lib/string';
5
- import { _typeCast } from '@naturalcycles/js-lib/types';
8
+ import { _objectAssign, _typeCast, JWT_REGEX } from '@naturalcycles/js-lib/types';
6
9
  import { _inspect } from '../../string/inspect.js';
10
+ import { BASE64URL_REGEX, COUNTRY_CODE_REGEX, CURRENCY_REGEX, IPV4_REGEX, IPV6_REGEX, LANGUAGE_TAG_REGEX, SEMVER_REGEX, SLUG_REGEX, URL_REGEX, UUID_REGEX, } from '../regexes.js';
11
+ import { TIMEZONES } from '../timezones.js';
7
12
  import { AjvValidationError } from './ajvValidationError.js';
8
13
  import { getAjv } from './getAjv.js';
9
- import { JsonSchemaTerminal } from './jsonSchemaBuilder.js';
14
+ import { isEveryItemNumber, isEveryItemPrimitive, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
15
+ // ==== AJV =====
10
16
  /**
11
17
  * On creation - compiles ajv validation function.
12
18
  * Provides convenient methods, error reporting, etc.
@@ -98,10 +104,26 @@ export class AjvSchema {
98
104
  const item = opt.mutateInput !== false || typeof input !== 'object'
99
105
  ? input // mutate
100
106
  : _deepCopy(input); // not mutate
101
- const valid = fn(item); // mutates item, but not input
107
+ let valid = fn(item); // mutates item, but not input
102
108
  _typeCast(item);
109
+ let output = item;
110
+ if (valid && this.schema.postValidation) {
111
+ const [err, result] = _try(() => this.schema.postValidation(output));
112
+ if (err) {
113
+ valid = false;
114
+ fn.errors = [
115
+ {
116
+ instancePath: '',
117
+ message: err.message,
118
+ },
119
+ ];
120
+ }
121
+ else {
122
+ output = result;
123
+ }
124
+ }
103
125
  if (valid)
104
- return [null, item];
126
+ return [null, output];
105
127
  const errors = fn.errors;
106
128
  const { inputId = _isObject(input) ? input['id'] : undefined, inputName = this.cfg.inputName || 'Object', } = opt;
107
129
  const dataVar = [inputName, inputId].filter(Boolean).join('.');
@@ -120,7 +142,7 @@ export class AjvSchema {
120
142
  inputName,
121
143
  inputId,
122
144
  }));
123
- return [err, item];
145
+ return [err, output];
124
146
  }
125
147
  getValidationFunction() {
126
148
  return (input, opt) => {
@@ -205,3 +227,1152 @@ export class AjvSchema {
205
227
  }
206
228
  const separator = '\n';
207
229
  export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA');
230
+ // ===== JsonSchemaBuilders ===== //
231
+ export const j = {
232
+ /**
233
+ * Matches literally any value - equivalent to TypeScript's `any` type.
234
+ * Use sparingly, as it bypasses type validation entirely.
235
+ */
236
+ any() {
237
+ return new JsonSchemaAnyBuilder({});
238
+ },
239
+ string() {
240
+ return new JsonSchemaStringBuilder();
241
+ },
242
+ number() {
243
+ return new JsonSchemaNumberBuilder();
244
+ },
245
+ boolean() {
246
+ return new JsonSchemaBooleanBuilder();
247
+ },
248
+ object: Object.assign(object, {
249
+ dbEntity: objectDbEntity,
250
+ infer: objectInfer,
251
+ any() {
252
+ return j.object({}).allowAdditionalProperties();
253
+ },
254
+ stringMap(schema) {
255
+ const isValueOptional = schema.getSchema().optionalField;
256
+ const builtSchema = schema.build();
257
+ const finalValueSchema = isValueOptional
258
+ ? { anyOf: [{ isUndefined: true }, builtSchema] }
259
+ : builtSchema;
260
+ return new JsonSchemaObjectBuilder({}, {
261
+ hasIsOfTypeCheck: false,
262
+ patternProperties: {
263
+ '^.+$': finalValueSchema,
264
+ },
265
+ });
266
+ },
267
+ /**
268
+ * @experimental Look around, maybe you find a rule that is better for your use-case.
269
+ *
270
+ * For Record<K, V> type of validations.
271
+ * ```ts
272
+ * const schema = j.object
273
+ * .record(
274
+ * j
275
+ * .string()
276
+ * .regex(/^\d{3,4}$/)
277
+ * .branded<B>(),
278
+ * j.number().nullable(),
279
+ * )
280
+ * .isOfType<Record<B, number | null>>()
281
+ * ```
282
+ *
283
+ * When the keys of the Record are values from an Enum, prefer `j.object.withEnumKeys`!
284
+ *
285
+ * Non-matching keys will be stripped from the object, i.e. they will not cause an error.
286
+ *
287
+ * Caveat: This rule first validates values of every properties of the object, and only then validates the keys.
288
+ * A consequence of that is that the validation will throw when there is an unexpected property with a value not matching the value schema.
289
+ */
290
+ record,
291
+ /**
292
+ * For Record<ENUM, V> type of validations.
293
+ *
294
+ * When the keys of the Record are values from an Enum,
295
+ * this helper is more performant and behaves in a more conventional manner than `j.object.record` would.
296
+ *
297
+ *
298
+ */
299
+ withEnumKeys,
300
+ withRegexKeys,
301
+ }),
302
+ array(itemSchema) {
303
+ return new JsonSchemaArrayBuilder(itemSchema);
304
+ },
305
+ tuple(items) {
306
+ return new JsonSchemaTupleBuilder(items);
307
+ },
308
+ set(itemSchema) {
309
+ return new JsonSchemaSet2Builder(itemSchema);
310
+ },
311
+ buffer() {
312
+ return new JsonSchemaBufferBuilder();
313
+ },
314
+ enum(input, opt) {
315
+ let enumValues;
316
+ let baseType = 'other';
317
+ if (Array.isArray(input)) {
318
+ enumValues = input;
319
+ if (isEveryItemNumber(input)) {
320
+ baseType = 'number';
321
+ }
322
+ else if (isEveryItemString(input)) {
323
+ baseType = 'string';
324
+ }
325
+ }
326
+ else if (typeof input === 'object') {
327
+ const enumType = getEnumType(input);
328
+ if (enumType === 'NumberEnum') {
329
+ enumValues = _numberEnumValues(input);
330
+ baseType = 'number';
331
+ }
332
+ else if (enumType === 'StringEnum') {
333
+ enumValues = _stringEnumValues(input);
334
+ baseType = 'string';
335
+ }
336
+ }
337
+ _assert(enumValues, 'Unsupported enum input');
338
+ return new JsonSchemaEnumBuilder(enumValues, baseType, opt);
339
+ },
340
+ /**
341
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
342
+ * To validate objects, use `anyOfBy`.
343
+ *
344
+ * Our Ajv is configured to strip unexpected properties from objects,
345
+ * and since Ajv is mutating the input, this means that it cannot
346
+ * properly validate the same data over multiple schemas.
347
+ *
348
+ * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
349
+ * Use `oneOf` when schemas are mutually exclusive.
350
+ */
351
+ oneOf(items) {
352
+ const schemas = items.map(b => b.build());
353
+ _assert(schemas.every(hasNoObjectSchemas), 'Do not use `oneOf` validation with non-primitive types!');
354
+ return new JsonSchemaAnyBuilder({
355
+ oneOf: schemas,
356
+ });
357
+ },
358
+ /**
359
+ * Use only with primitive values, otherwise this function will throw to avoid bugs.
360
+ * To validate objects, use `anyOfBy` or `anyOfThese`.
361
+ *
362
+ * Our Ajv is configured to strip unexpected properties from objects,
363
+ * and since Ajv is mutating the input, this means that it cannot
364
+ * properly validate the same data over multiple schemas.
365
+ *
366
+ * Use `anyOf` when schemas may overlap (e.g., AccountId | PartnerId with same format).
367
+ * Use `oneOf` when schemas are mutually exclusive.
368
+ */
369
+ anyOf(items) {
370
+ const schemas = items.map(b => b.build());
371
+ _assert(schemas.every(hasNoObjectSchemas), 'Do not use `anyOf` validation with non-primitive types!');
372
+ return new JsonSchemaAnyBuilder({
373
+ anyOf: schemas,
374
+ });
375
+ },
376
+ /**
377
+ * Pick validation schema for an object based on the value of a specific property.
378
+ *
379
+ * ```
380
+ * const schemaMap = {
381
+ * true: successSchema,
382
+ * false: errorSchema
383
+ * }
384
+ *
385
+ * const schema = j.anyOfBy('success', schemaMap)
386
+ * ```
387
+ */
388
+ anyOfBy(propertyName, schemaDictionary) {
389
+ return new JsonSchemaAnyOfByBuilder(propertyName, schemaDictionary);
390
+ },
391
+ /**
392
+ * Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
393
+ * This comes with a performance penalty, so do not use it where performance matters.
394
+ *
395
+ * ```
396
+ * const schema = j.anyOfThese([successSchema, errorSchema])
397
+ * ```
398
+ */
399
+ anyOfThese(items) {
400
+ return new JsonSchemaAnyBuilder({
401
+ anyOfThese: items.map(b => b.build()),
402
+ });
403
+ },
404
+ and() {
405
+ return {
406
+ silentBob: () => {
407
+ throw new Error('...strike back!');
408
+ },
409
+ };
410
+ },
411
+ literal(v) {
412
+ let baseType = 'other';
413
+ if (typeof v === 'string')
414
+ baseType = 'string';
415
+ if (typeof v === 'number')
416
+ baseType = 'number';
417
+ return new JsonSchemaEnumBuilder([v], baseType);
418
+ },
419
+ };
420
+ const TS_2500 = 16725225600; // 2500-01-01
421
+ const TS_2500_MILLIS = TS_2500 * 1000;
422
+ const TS_2000 = 946684800; // 2000-01-01
423
+ const TS_2000_MILLIS = TS_2000 * 1000;
424
+ /*
425
+ Notes for future reference
426
+
427
+ Q: Why do we need `Opt` - when `IN` and `OUT` already carries the `| undefined`?
428
+ A: Because of objects. Without `Opt`, an optional field would be inferred as `{ foo: string | undefined }`,
429
+ which means that the `foo` property would be mandatory, it's just that its value can be `undefined` as well.
430
+ With `Opt`, we can infer it as `{ foo?: string | undefined }`.
431
+ */
432
+ export class JsonSchemaTerminal {
433
+ [HIDDEN_AJV_SCHEMA];
434
+ schema;
435
+ constructor(schema) {
436
+ this.schema = schema;
437
+ }
438
+ get ajvSchema() {
439
+ if (!this[HIDDEN_AJV_SCHEMA]) {
440
+ this[HIDDEN_AJV_SCHEMA] = AjvSchema.create(this);
441
+ }
442
+ return this[HIDDEN_AJV_SCHEMA];
443
+ }
444
+ getSchema() {
445
+ return this.schema;
446
+ }
447
+ /**
448
+ * Produces a "clean schema object" without methods.
449
+ * Same as if it would be JSON.stringified.
450
+ */
451
+ build() {
452
+ _assert(!(this.schema.optionalField && this.schema.default !== undefined), '.optional() and .default() should not be used together - the default value makes .optional() redundant and causes incorrect type inference');
453
+ const jsonSchema = _sortObject(deepCopyPreservingFunctions(this.schema), JSON_SCHEMA_ORDER);
454
+ delete jsonSchema.optionalField;
455
+ return jsonSchema;
456
+ }
457
+ clone() {
458
+ const cloned = Object.create(Object.getPrototypeOf(this));
459
+ cloned.schema = deepCopyPreservingFunctions(this.schema);
460
+ return cloned;
461
+ }
462
+ cloneAndUpdateSchema(schema) {
463
+ const clone = this.clone();
464
+ _objectAssign(clone.schema, schema);
465
+ return clone;
466
+ }
467
+ validate(input, opt) {
468
+ return this.ajvSchema.validate(input, opt);
469
+ }
470
+ isValid(input, opt) {
471
+ return this.ajvSchema.isValid(input, opt);
472
+ }
473
+ getValidationResult(input, opt = {}) {
474
+ return this.ajvSchema.getValidationResult(input, opt);
475
+ }
476
+ getValidationFunction() {
477
+ return this.ajvSchema.getValidationFunction();
478
+ }
479
+ /**
480
+ * Specify a function to be called after the normal validation is finished.
481
+ *
482
+ * This function will receive the validated, type-safe data, and you can use it
483
+ * to do further validations, e.g. conditional validations based on certain property values,
484
+ * or to do data modifications either by mutating the input or returning a new value.
485
+ *
486
+ * If you throw an error from this function, it will show up as an error in the validation.
487
+ */
488
+ postValidation(fn) {
489
+ const clone = this.cloneAndUpdateSchema({
490
+ postValidation: fn,
491
+ });
492
+ return clone;
493
+ }
494
+ /**
495
+ * @experimental
496
+ */
497
+ out;
498
+ opt;
499
+ }
500
+ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
501
+ setErrorMessage(ruleName, errorMessage) {
502
+ if (_isUndefined(errorMessage))
503
+ return;
504
+ this.schema.errorMessages ||= {};
505
+ this.schema.errorMessages[ruleName] = errorMessage;
506
+ }
507
+ /**
508
+ * A helper function that takes a type parameter and compares it with the type inferred from the schema.
509
+ *
510
+ * When the type inferred from the schema differs from the passed-in type,
511
+ * the schema becomes unusable, by turning its type into `never`.
512
+ *
513
+ * ```ts
514
+ * const schemaGood = j.string().isOfType<string>() // ✅
515
+ *
516
+ * const schemaBad = j.string().isOfType<number>() // ❌
517
+ * schemaBad.build() // TypeError: property "build" does not exist on type "never"
518
+ *
519
+ * const result = ajvValidateRequest.body(req, schemaBad) // result will have `unknown` type
520
+ * ```
521
+ */
522
+ isOfType() {
523
+ return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true });
524
+ }
525
+ $schema($schema) {
526
+ return this.cloneAndUpdateSchema({ $schema });
527
+ }
528
+ $schemaDraft7() {
529
+ return this.$schema('http://json-schema.org/draft-07/schema#');
530
+ }
531
+ $id($id) {
532
+ return this.cloneAndUpdateSchema({ $id });
533
+ }
534
+ title(title) {
535
+ return this.cloneAndUpdateSchema({ title });
536
+ }
537
+ description(description) {
538
+ return this.cloneAndUpdateSchema({ description });
539
+ }
540
+ deprecated(deprecated = true) {
541
+ return this.cloneAndUpdateSchema({ deprecated });
542
+ }
543
+ type(type) {
544
+ return this.cloneAndUpdateSchema({ type });
545
+ }
546
+ default(v) {
547
+ return this.cloneAndUpdateSchema({ default: v });
548
+ }
549
+ instanceof(of) {
550
+ return this.cloneAndUpdateSchema({ type: 'object', instanceof: of });
551
+ }
552
+ optional() {
553
+ const clone = this.cloneAndUpdateSchema({ optionalField: true });
554
+ return clone;
555
+ }
556
+ nullable() {
557
+ return new JsonSchemaAnyBuilder({
558
+ anyOf: [this.build(), { type: 'null' }],
559
+ });
560
+ }
561
+ /**
562
+ * @deprecated
563
+ * The usage of this function is discouraged as it defeats the purpose of having type-safe validation.
564
+ */
565
+ castAs() {
566
+ return this;
567
+ }
568
+ /**
569
+ * Locks the given schema chain and no other modification can be done to it.
570
+ */
571
+ final() {
572
+ return new JsonSchemaTerminal(this.schema);
573
+ }
574
+ /**
575
+ *
576
+ * @param validator A validator function that returns an error message or undefined.
577
+ *
578
+ * You may add multiple custom validators and they will be executed in the order you added them.
579
+ */
580
+ custom(validator) {
581
+ const { customValidations = [] } = this.schema;
582
+ return this.cloneAndUpdateSchema({
583
+ customValidations: [...customValidations, validator],
584
+ });
585
+ }
586
+ /**
587
+ *
588
+ * @param converter A converter function that returns a new value.
589
+ *
590
+ * You may add multiple converters and they will be executed in the order you added them,
591
+ * each converter receiving the result from the previous one.
592
+ *
593
+ * This feature only works when the current schema is nested in an object or array schema,
594
+ * due to how mutability works in Ajv.
595
+ */
596
+ convert(converter) {
597
+ const { customConversions = [] } = this.schema;
598
+ return this.cloneAndUpdateSchema({
599
+ customConversions: [...customConversions, converter],
600
+ });
601
+ }
602
+ }
603
+ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
604
+ constructor() {
605
+ super({
606
+ type: 'string',
607
+ });
608
+ }
609
+ /**
610
+ * @param optionalValues List of values that should be considered/converted as `undefined`.
611
+ *
612
+ * This `optionalValues` feature only works when the current schema is nested in an object or array schema,
613
+ * due to how mutability works in Ajv.
614
+ *
615
+ * Make sure this `optional()` call is at the end of your call chain.
616
+ *
617
+ * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
618
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
619
+ */
620
+ optional(optionalValues) {
621
+ if (!optionalValues) {
622
+ return super.optional();
623
+ }
624
+ _typeCast(optionalValues);
625
+ let newBuilder = new JsonSchemaStringBuilder().optional();
626
+ const alternativesSchema = j.enum(optionalValues);
627
+ Object.assign(newBuilder.getSchema(), {
628
+ anyOf: [this.build(), alternativesSchema.build()],
629
+ optionalValues,
630
+ });
631
+ // When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
632
+ // so we must allow `null` values to be parsed by Ajv,
633
+ // but the typing should not reflect that.
634
+ // We also cannot accept more rules attached, since we're not building a StringSchema anymore.
635
+ if (optionalValues.includes(null)) {
636
+ newBuilder = new JsonSchemaTerminal({
637
+ anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
638
+ optionalField: true,
639
+ });
640
+ }
641
+ return newBuilder;
642
+ }
643
+ regex(pattern, opt) {
644
+ _assert(!pattern.flags, `Regex flags are not supported by JSON Schema. Received: /${pattern.source}/${pattern.flags}`);
645
+ return this.pattern(pattern.source, opt);
646
+ }
647
+ pattern(pattern, opt) {
648
+ const clone = this.cloneAndUpdateSchema({ pattern });
649
+ if (opt?.name)
650
+ clone.setErrorMessage('pattern', `is not a valid ${opt.name}`);
651
+ if (opt?.msg)
652
+ clone.setErrorMessage('pattern', opt.msg);
653
+ return clone;
654
+ }
655
+ minLength(minLength) {
656
+ return this.cloneAndUpdateSchema({ minLength });
657
+ }
658
+ maxLength(maxLength) {
659
+ return this.cloneAndUpdateSchema({ maxLength });
660
+ }
661
+ length(minLengthOrExactLength, maxLength) {
662
+ const maxLengthActual = maxLength ?? minLengthOrExactLength;
663
+ return this.minLength(minLengthOrExactLength).maxLength(maxLengthActual);
664
+ }
665
+ email(opt) {
666
+ const defaultOptions = { checkTLD: true };
667
+ return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
668
+ .trim()
669
+ .toLowerCase();
670
+ }
671
+ trim() {
672
+ return this.cloneAndUpdateSchema({ transform: { ...this.schema.transform, trim: true } });
673
+ }
674
+ toLowerCase() {
675
+ return this.cloneAndUpdateSchema({
676
+ transform: { ...this.schema.transform, toLowerCase: true },
677
+ });
678
+ }
679
+ toUpperCase() {
680
+ return this.cloneAndUpdateSchema({
681
+ transform: { ...this.schema.transform, toUpperCase: true },
682
+ });
683
+ }
684
+ truncate(toLength) {
685
+ return this.cloneAndUpdateSchema({
686
+ transform: { ...this.schema.transform, truncate: toLength },
687
+ });
688
+ }
689
+ branded() {
690
+ return this;
691
+ }
692
+ /**
693
+ * Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
694
+ *
695
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
696
+ * because this call effectively starts a new schema chain.
697
+ */
698
+ isoDate() {
699
+ return new JsonSchemaIsoDateBuilder();
700
+ }
701
+ isoDateTime() {
702
+ return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
703
+ }
704
+ isoMonth() {
705
+ return new JsonSchemaIsoMonthBuilder();
706
+ }
707
+ /**
708
+ * Validates the string format to be JWT.
709
+ * Expects the JWT to be signed!
710
+ */
711
+ jwt() {
712
+ return this.regex(JWT_REGEX, { msg: 'is not a valid JWT format' });
713
+ }
714
+ url() {
715
+ return this.regex(URL_REGEX, { msg: 'is not a valid URL format' });
716
+ }
717
+ ipv4() {
718
+ return this.regex(IPV4_REGEX, { msg: 'is not a valid IPv4 format' });
719
+ }
720
+ ipv6() {
721
+ return this.regex(IPV6_REGEX, { msg: 'is not a valid IPv6 format' });
722
+ }
723
+ slug() {
724
+ return this.regex(SLUG_REGEX, { msg: 'is not a valid slug format' });
725
+ }
726
+ semVer() {
727
+ return this.regex(SEMVER_REGEX, { msg: 'is not a valid semver format' });
728
+ }
729
+ languageTag() {
730
+ return this.regex(LANGUAGE_TAG_REGEX, { msg: 'is not a valid language format' });
731
+ }
732
+ countryCode() {
733
+ return this.regex(COUNTRY_CODE_REGEX, { msg: 'is not a valid country code format' });
734
+ }
735
+ currency() {
736
+ return this.regex(CURRENCY_REGEX, { msg: 'is not a valid currency format' });
737
+ }
738
+ /**
739
+ * Validates that the input is a valid IANATimzone value.
740
+ *
741
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
742
+ * because this call effectively starts a new schema chain as an `enum` validation.
743
+ */
744
+ ianaTimezone() {
745
+ // UTC is added to assist unit-testing, which uses UTC by default (not technically a valid Iana timezone identifier)
746
+ return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded();
747
+ }
748
+ base64Url() {
749
+ return this.regex(BASE64URL_REGEX, {
750
+ msg: 'contains characters not allowed in Base64 URL characterset',
751
+ });
752
+ }
753
+ uuid() {
754
+ return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' });
755
+ }
756
+ }
757
+ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
758
+ constructor() {
759
+ super({
760
+ type: 'string',
761
+ IsoDate: {},
762
+ });
763
+ }
764
+ /**
765
+ * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
766
+ *
767
+ * This `null` feature only works when the current schema is nested in an object or array schema,
768
+ * due to how mutability works in Ajv.
769
+ *
770
+ * When `null` is passed, the return type becomes `JsonSchemaTerminal`
771
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
772
+ */
773
+ optional(nullValue) {
774
+ if (nullValue === undefined) {
775
+ return super.optional();
776
+ }
777
+ // When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
778
+ // so we must allow `null` values to be parsed by Ajv,
779
+ // but the typing should not reflect that.
780
+ // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
781
+ return new JsonSchemaTerminal({
782
+ anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
783
+ optionalField: true,
784
+ });
785
+ }
786
+ before(date) {
787
+ return this.cloneAndUpdateSchema({ IsoDate: { before: date } });
788
+ }
789
+ sameOrBefore(date) {
790
+ return this.cloneAndUpdateSchema({ IsoDate: { sameOrBefore: date } });
791
+ }
792
+ after(date) {
793
+ return this.cloneAndUpdateSchema({ IsoDate: { after: date } });
794
+ }
795
+ sameOrAfter(date) {
796
+ return this.cloneAndUpdateSchema({ IsoDate: { sameOrAfter: date } });
797
+ }
798
+ between(fromDate, toDate, incl) {
799
+ let schemaPatch = {};
800
+ if (incl === '[)') {
801
+ schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } };
802
+ }
803
+ else if (incl === '[]') {
804
+ schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } };
805
+ }
806
+ return this.cloneAndUpdateSchema(schemaPatch);
807
+ }
808
+ }
809
+ export class JsonSchemaIsoMonthBuilder extends JsonSchemaAnyBuilder {
810
+ constructor() {
811
+ super({
812
+ type: 'string',
813
+ IsoMonth: {},
814
+ });
815
+ }
816
+ }
817
+ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
818
+ constructor() {
819
+ super({
820
+ type: 'number',
821
+ });
822
+ }
823
+ /**
824
+ * @param optionalValues List of values that should be considered/converted as `undefined`.
825
+ *
826
+ * This `optionalValues` feature only works when the current schema is nested in an object or array schema,
827
+ * due to how mutability works in Ajv.
828
+ *
829
+ * Make sure this `optional()` call is at the end of your call chain.
830
+ *
831
+ * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
832
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
833
+ */
834
+ optional(optionalValues) {
835
+ if (!optionalValues) {
836
+ return super.optional();
837
+ }
838
+ _typeCast(optionalValues);
839
+ let newBuilder = new JsonSchemaNumberBuilder().optional();
840
+ const alternativesSchema = j.enum(optionalValues);
841
+ Object.assign(newBuilder.getSchema(), {
842
+ anyOf: [this.build(), alternativesSchema.build()],
843
+ optionalValues,
844
+ });
845
+ // When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
846
+ // so we must allow `null` values to be parsed by Ajv,
847
+ // but the typing should not reflect that.
848
+ // We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
849
+ if (optionalValues.includes(null)) {
850
+ newBuilder = new JsonSchemaTerminal({
851
+ anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
852
+ optionalField: true,
853
+ });
854
+ }
855
+ return newBuilder;
856
+ }
857
+ integer() {
858
+ return this.cloneAndUpdateSchema({ type: 'integer' });
859
+ }
860
+ branded() {
861
+ return this;
862
+ }
863
+ multipleOf(multipleOf) {
864
+ return this.cloneAndUpdateSchema({ multipleOf });
865
+ }
866
+ min(minimum) {
867
+ return this.cloneAndUpdateSchema({ minimum });
868
+ }
869
+ exclusiveMin(exclusiveMinimum) {
870
+ return this.cloneAndUpdateSchema({ exclusiveMinimum });
871
+ }
872
+ max(maximum) {
873
+ return this.cloneAndUpdateSchema({ maximum });
874
+ }
875
+ exclusiveMax(exclusiveMaximum) {
876
+ return this.cloneAndUpdateSchema({ exclusiveMaximum });
877
+ }
878
+ lessThan(value) {
879
+ return this.exclusiveMax(value);
880
+ }
881
+ lessThanOrEqual(value) {
882
+ return this.max(value);
883
+ }
884
+ moreThan(value) {
885
+ return this.exclusiveMin(value);
886
+ }
887
+ moreThanOrEqual(value) {
888
+ return this.min(value);
889
+ }
890
+ equal(value) {
891
+ return this.min(value).max(value);
892
+ }
893
+ range(minimum, maximum, incl) {
894
+ if (incl === '[)') {
895
+ return this.moreThanOrEqual(minimum).lessThan(maximum);
896
+ }
897
+ return this.moreThanOrEqual(minimum).lessThanOrEqual(maximum);
898
+ }
899
+ int32() {
900
+ const MIN_INT32 = -(2 ** 31);
901
+ const MAX_INT32 = 2 ** 31 - 1;
902
+ const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER;
903
+ const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER;
904
+ const newMin = Math.max(MIN_INT32, currentMin);
905
+ const newMax = Math.min(MAX_INT32, currentMax);
906
+ return this.integer().min(newMin).max(newMax);
907
+ }
908
+ int64() {
909
+ const currentMin = this.schema.minimum ?? Number.MIN_SAFE_INTEGER;
910
+ const currentMax = this.schema.maximum ?? Number.MAX_SAFE_INTEGER;
911
+ const newMin = Math.max(Number.MIN_SAFE_INTEGER, currentMin);
912
+ const newMax = Math.min(Number.MAX_SAFE_INTEGER, currentMax);
913
+ return this.integer().min(newMin).max(newMax);
914
+ }
915
+ float() {
916
+ return this;
917
+ }
918
+ double() {
919
+ return this;
920
+ }
921
+ unixTimestamp() {
922
+ return this.integer().min(0).max(TS_2500).branded();
923
+ }
924
+ unixTimestamp2000() {
925
+ return this.integer().min(TS_2000).max(TS_2500).branded();
926
+ }
927
+ unixTimestampMillis() {
928
+ return this.integer().min(0).max(TS_2500_MILLIS).branded();
929
+ }
930
+ unixTimestamp2000Millis() {
931
+ return this.integer().min(TS_2000_MILLIS).max(TS_2500_MILLIS).branded();
932
+ }
933
+ utcOffset() {
934
+ return this.integer()
935
+ .multipleOf(15)
936
+ .min(-12 * 60)
937
+ .max(14 * 60);
938
+ }
939
+ utcOffsetHour() {
940
+ return this.integer().min(-12).max(14);
941
+ }
942
+ /**
943
+ * Specify the precision of the floating point numbers by the number of digits after the ".".
944
+ * Excess digits will be cut-off when the current schema is nested in an object or array schema,
945
+ * due to how mutability works in Ajv.
946
+ */
947
+ precision(numberOfDigits) {
948
+ return this.cloneAndUpdateSchema({ precision: numberOfDigits });
949
+ }
950
+ }
951
+ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
952
+ constructor() {
953
+ super({
954
+ type: 'boolean',
955
+ });
956
+ }
957
+ /**
958
+ * @param optionalValue One of the two possible boolean values that should be considered/converted as `undefined`.
959
+ *
960
+ * This `optionalValue` feature only works when the current schema is nested in an object or array schema,
961
+ * due to how mutability works in Ajv.
962
+ */
963
+ optional(optionalValue) {
964
+ if (typeof optionalValue === 'undefined') {
965
+ return super.optional();
966
+ }
967
+ const newBuilder = new JsonSchemaBooleanBuilder().optional();
968
+ const alternativesSchema = j.enum([optionalValue]);
969
+ Object.assign(newBuilder.getSchema(), {
970
+ anyOf: [this.build(), alternativesSchema.build()],
971
+ optionalValues: [optionalValue],
972
+ });
973
+ return newBuilder;
974
+ }
975
+ }
976
+ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
977
+ constructor(props, opt) {
978
+ super({
979
+ type: 'object',
980
+ properties: {},
981
+ required: [],
982
+ additionalProperties: false,
983
+ hasIsOfTypeCheck: opt?.hasIsOfTypeCheck ?? true,
984
+ patternProperties: opt?.patternProperties ?? undefined,
985
+ keySchema: opt?.keySchema ?? undefined,
986
+ });
987
+ if (props)
988
+ this.addProperties(props);
989
+ }
990
+ addProperties(props) {
991
+ const properties = {};
992
+ const required = [];
993
+ for (const [key, builder] of Object.entries(props)) {
994
+ const isOptional = builder.getSchema().optionalField;
995
+ if (!isOptional) {
996
+ required.push(key);
997
+ }
998
+ const schema = builder.build();
999
+ properties[key] = schema;
1000
+ }
1001
+ this.schema.properties = properties;
1002
+ this.schema.required = _uniq(required).sort();
1003
+ return this;
1004
+ }
1005
+ /**
1006
+ * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
1007
+ *
1008
+ * This `null` feature only works when the current schema is nested in an object or array schema,
1009
+ * due to how mutability works in Ajv.
1010
+ *
1011
+ * When `null` is passed, the return type becomes `JsonSchemaTerminal`
1012
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
1013
+ */
1014
+ optional(nullValue) {
1015
+ if (nullValue === undefined) {
1016
+ return super.optional();
1017
+ }
1018
+ // When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
1019
+ // so we must allow `null` values to be parsed by Ajv,
1020
+ // but the typing should not reflect that.
1021
+ // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
1022
+ return new JsonSchemaTerminal({
1023
+ anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
1024
+ optionalField: true,
1025
+ });
1026
+ }
1027
+ /**
1028
+ * When set, the validation will not strip away properties that are not specified explicitly in the schema.
1029
+ */
1030
+ allowAdditionalProperties() {
1031
+ return this.cloneAndUpdateSchema({ additionalProperties: true });
1032
+ }
1033
+ extend(props) {
1034
+ const newBuilder = new JsonSchemaObjectBuilder();
1035
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
1036
+ const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
1037
+ mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
1038
+ _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
1039
+ return newBuilder;
1040
+ }
1041
+ /**
1042
+ * Concatenates another schema to the current schema.
1043
+ *
1044
+ * It expects you to use `isOfType<T>()` in the chain,
1045
+ * otherwise the validation will throw. This is to ensure
1046
+ * that the schemas you concatenated match the intended final type.
1047
+ *
1048
+ * ```ts
1049
+ * interface Foo { foo: string }
1050
+ * const fooSchema = j.object<Foo>({ foo: j.string() })
1051
+ *
1052
+ * interface Bar { bar: number }
1053
+ * const barSchema = j.object<Bar>({ bar: j.number() })
1054
+ *
1055
+ * interface Shu { foo: string, bar: number }
1056
+ * const shuSchema = fooSchema.concat(barSchema).isOfType<Shu>() // important
1057
+ * ```
1058
+ */
1059
+ concat(other) {
1060
+ const clone = this.clone();
1061
+ mergeJsonSchemaObjects(clone.schema, other.schema);
1062
+ _objectAssign(clone.schema, { hasIsOfTypeCheck: false });
1063
+ return clone;
1064
+ }
1065
+ /**
1066
+ * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
1067
+ */
1068
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
1069
+ dbEntity() {
1070
+ return this.extend({
1071
+ id: j.string(),
1072
+ created: j.number().unixTimestamp2000(),
1073
+ updated: j.number().unixTimestamp2000(),
1074
+ });
1075
+ }
1076
+ minProperties(minProperties) {
1077
+ return this.cloneAndUpdateSchema({ minProperties, minProperties2: minProperties });
1078
+ }
1079
+ maxProperties(maxProperties) {
1080
+ return this.cloneAndUpdateSchema({ maxProperties });
1081
+ }
1082
+ exclusiveProperties(propNames) {
1083
+ const exclusiveProperties = this.schema.exclusiveProperties ?? [];
1084
+ return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] });
1085
+ }
1086
+ }
1087
+ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1088
+ constructor(props) {
1089
+ super({
1090
+ type: 'object',
1091
+ properties: {},
1092
+ required: [],
1093
+ additionalProperties: false,
1094
+ });
1095
+ if (props)
1096
+ this.addProperties(props);
1097
+ }
1098
+ addProperties(props) {
1099
+ const properties = {};
1100
+ const required = [];
1101
+ for (const [key, builder] of Object.entries(props)) {
1102
+ const isOptional = builder.getSchema().optionalField;
1103
+ if (!isOptional) {
1104
+ required.push(key);
1105
+ }
1106
+ const schema = builder.build();
1107
+ properties[key] = schema;
1108
+ }
1109
+ this.schema.properties = properties;
1110
+ this.schema.required = _uniq(required).sort();
1111
+ return this;
1112
+ }
1113
+ /**
1114
+ * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
1115
+ *
1116
+ * This `null` feature only works when the current schema is nested in an object or array schema,
1117
+ * due to how mutability works in Ajv.
1118
+ *
1119
+ * When `null` is passed, the return type becomes `JsonSchemaTerminal`
1120
+ * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
1121
+ */
1122
+ // @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
1123
+ optional(nullValue) {
1124
+ if (nullValue === undefined) {
1125
+ return super.optional();
1126
+ }
1127
+ // When `null` is specified, we want `null` to be stripped and the value to become `undefined`,
1128
+ // so we must allow `null` values to be parsed by Ajv,
1129
+ // but the typing should not reflect that.
1130
+ // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
1131
+ return new JsonSchemaTerminal({
1132
+ anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
1133
+ optionalField: true,
1134
+ });
1135
+ }
1136
+ /**
1137
+ * When set, the validation will not strip away properties that are not specified explicitly in the schema.
1138
+ */
1139
+ allowAdditionalProperties() {
1140
+ return this.cloneAndUpdateSchema({ additionalProperties: true });
1141
+ }
1142
+ extend(props) {
1143
+ const newBuilder = new JsonSchemaObjectInferringBuilder();
1144
+ _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
1145
+ const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
1146
+ mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
1147
+ // This extend function is not type-safe as it is inferring,
1148
+ // so even if the base schema was already type-checked,
1149
+ // the new schema loses that quality.
1150
+ _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
1151
+ return newBuilder;
1152
+ }
1153
+ /**
1154
+ * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
1155
+ */
1156
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
1157
+ dbEntity() {
1158
+ return this.extend({
1159
+ id: j.string(),
1160
+ created: j.number().unixTimestamp2000(),
1161
+ updated: j.number().unixTimestamp2000(),
1162
+ });
1163
+ }
1164
+ }
1165
+ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
1166
+ constructor(itemsSchema) {
1167
+ super({
1168
+ type: 'array',
1169
+ items: itemsSchema.build(),
1170
+ });
1171
+ }
1172
+ minLength(minItems) {
1173
+ return this.cloneAndUpdateSchema({ minItems });
1174
+ }
1175
+ maxLength(maxItems) {
1176
+ return this.cloneAndUpdateSchema({ maxItems });
1177
+ }
1178
+ length(minItemsOrExact, maxItems) {
1179
+ const maxItemsActual = maxItems ?? minItemsOrExact;
1180
+ return this.minLength(minItemsOrExact).maxLength(maxItemsActual);
1181
+ }
1182
+ exactLength(length) {
1183
+ return this.minLength(length).maxLength(length);
1184
+ }
1185
+ unique() {
1186
+ return this.cloneAndUpdateSchema({ uniqueItems: true });
1187
+ }
1188
+ }
1189
+ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
1190
+ constructor(itemsSchema) {
1191
+ super({
1192
+ type: ['array', 'object'],
1193
+ Set2: itemsSchema.build(),
1194
+ });
1195
+ }
1196
+ min(minItems) {
1197
+ return this.cloneAndUpdateSchema({ minItems });
1198
+ }
1199
+ max(maxItems) {
1200
+ return this.cloneAndUpdateSchema({ maxItems });
1201
+ }
1202
+ }
1203
+ export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
1204
+ constructor() {
1205
+ super({
1206
+ Buffer: true,
1207
+ });
1208
+ }
1209
+ }
1210
+ export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
1211
+ constructor(enumValues, baseType, opt) {
1212
+ const jsonSchema = { enum: enumValues };
1213
+ // Specifying the base type helps in cases when we ask Ajv to coerce the types.
1214
+ // Having only the `enum` in the schema does not trigger a coercion in Ajv.
1215
+ if (baseType === 'string')
1216
+ jsonSchema.type = 'string';
1217
+ if (baseType === 'number')
1218
+ jsonSchema.type = 'number';
1219
+ super(jsonSchema);
1220
+ if (opt?.name)
1221
+ this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
1222
+ if (opt?.msg)
1223
+ this.setErrorMessage('enum', opt.msg);
1224
+ }
1225
+ branded() {
1226
+ return this;
1227
+ }
1228
+ }
1229
+ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
1230
+ constructor(items) {
1231
+ super({
1232
+ type: 'array',
1233
+ prefixItems: items.map(i => i.build()),
1234
+ minItems: items.length,
1235
+ maxItems: items.length,
1236
+ });
1237
+ }
1238
+ }
1239
+ export class JsonSchemaAnyOfByBuilder extends JsonSchemaAnyBuilder {
1240
+ constructor(propertyName, schemaDictionary) {
1241
+ const builtSchemaDictionary = {};
1242
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
1243
+ builtSchemaDictionary[key] = schema.build();
1244
+ }
1245
+ super({
1246
+ type: 'object',
1247
+ hasIsOfTypeCheck: true,
1248
+ anyOfBy: {
1249
+ propertyName,
1250
+ schemaDictionary: builtSchemaDictionary,
1251
+ },
1252
+ });
1253
+ }
1254
+ }
1255
+ export class JsonSchemaAnyOfTheseBuilder extends JsonSchemaAnyBuilder {
1256
+ constructor(propertyName, schemaDictionary) {
1257
+ const builtSchemaDictionary = {};
1258
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
1259
+ builtSchemaDictionary[key] = schema.build();
1260
+ }
1261
+ super({
1262
+ type: 'object',
1263
+ hasIsOfTypeCheck: true,
1264
+ anyOfBy: {
1265
+ propertyName,
1266
+ schemaDictionary: builtSchemaDictionary,
1267
+ },
1268
+ });
1269
+ }
1270
+ }
1271
+ function object(props) {
1272
+ return new JsonSchemaObjectBuilder(props);
1273
+ }
1274
+ function objectInfer(props) {
1275
+ return new JsonSchemaObjectInferringBuilder(props);
1276
+ }
1277
+ function objectDbEntity(props) {
1278
+ return j.object({
1279
+ id: j.string(),
1280
+ created: j.number().unixTimestamp2000(),
1281
+ updated: j.number().unixTimestamp2000(),
1282
+ ...props,
1283
+ });
1284
+ }
1285
+ function record(keySchema, valueSchema) {
1286
+ const keyJsonSchema = keySchema.build();
1287
+ // Check if value schema is optional before build() strips the optionalField flag
1288
+ const isValueOptional = valueSchema.getSchema().optionalField;
1289
+ const valueJsonSchema = valueSchema.build();
1290
+ // When value schema is optional, wrap in anyOf to allow undefined values
1291
+ const finalValueSchema = isValueOptional
1292
+ ? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
1293
+ : valueJsonSchema;
1294
+ return new JsonSchemaObjectBuilder([], {
1295
+ hasIsOfTypeCheck: false,
1296
+ keySchema: keyJsonSchema,
1297
+ patternProperties: {
1298
+ ['^.*$']: finalValueSchema,
1299
+ },
1300
+ });
1301
+ }
1302
+ function withRegexKeys(keyRegex, schema) {
1303
+ if (keyRegex instanceof RegExp) {
1304
+ _assert(!keyRegex.flags, `Regex flags are not supported by JSON Schema. Received: /${keyRegex.source}/${keyRegex.flags}`);
1305
+ }
1306
+ const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex;
1307
+ const jsonSchema = schema.build();
1308
+ return new JsonSchemaObjectBuilder([], {
1309
+ hasIsOfTypeCheck: false,
1310
+ patternProperties: {
1311
+ [pattern]: jsonSchema,
1312
+ },
1313
+ });
1314
+ }
1315
+ /**
1316
+ * Builds the object schema with the indicated `keys` and uses the `schema` for their validation.
1317
+ */
1318
+ function withEnumKeys(keys, schema) {
1319
+ let enumValues;
1320
+ if (Array.isArray(keys)) {
1321
+ _assert(isEveryItemPrimitive(keys), 'Every item in the key list should be string, number or symbol');
1322
+ enumValues = keys;
1323
+ }
1324
+ else if (typeof keys === 'object') {
1325
+ const enumType = getEnumType(keys);
1326
+ _assert(enumType === 'NumberEnum' || enumType === 'StringEnum', 'The key list should be StringEnum or NumberEnum');
1327
+ if (enumType === 'NumberEnum') {
1328
+ enumValues = _numberEnumValues(keys);
1329
+ }
1330
+ else if (enumType === 'StringEnum') {
1331
+ enumValues = _stringEnumValues(keys);
1332
+ }
1333
+ }
1334
+ _assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum');
1335
+ const typedValues = enumValues;
1336
+ const props = Object.fromEntries(typedValues.map(key => [key, schema]));
1337
+ return new JsonSchemaObjectBuilder(props, { hasIsOfTypeCheck: false });
1338
+ }
1339
+ function hasNoObjectSchemas(schema) {
1340
+ if (Array.isArray(schema.type)) {
1341
+ schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
1342
+ }
1343
+ else if (schema.anyOf) {
1344
+ return schema.anyOf.every(hasNoObjectSchemas);
1345
+ }
1346
+ else if (schema.oneOf) {
1347
+ return schema.oneOf.every(hasNoObjectSchemas);
1348
+ }
1349
+ else if (schema.enum) {
1350
+ return true;
1351
+ }
1352
+ else if (schema.type === 'array') {
1353
+ return !schema.items || hasNoObjectSchemas(schema.items);
1354
+ }
1355
+ else {
1356
+ return !!schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type);
1357
+ }
1358
+ return false;
1359
+ }
1360
+ /**
1361
+ * Deep copy that preserves functions in customValidations/customConversions.
1362
+ * Unlike structuredClone, this handles function references (which only exist in those two properties).
1363
+ */
1364
+ function deepCopyPreservingFunctions(obj) {
1365
+ if (obj === null || typeof obj !== 'object')
1366
+ return obj;
1367
+ if (Array.isArray(obj))
1368
+ return obj.map(deepCopyPreservingFunctions);
1369
+ const copy = {};
1370
+ for (const key of Object.keys(obj)) {
1371
+ const value = obj[key];
1372
+ copy[key] =
1373
+ (key === 'customValidations' || key === 'customConversions') && Array.isArray(value)
1374
+ ? [...value]
1375
+ : deepCopyPreservingFunctions(value);
1376
+ }
1377
+ return copy;
1378
+ }