@naturalcycles/nodejs-lib 15.92.1 → 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.
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable id-denylist */
2
2
  // oxlint-disable max-lines
3
- import { _isObject, _isUndefined, _lazyValue, _numberEnumValues, _stringEnumValues, getEnumType, } from '@naturalcycles/js-lib';
3
+ import { _isObject, _isUndefined, _numberEnumValues, _stringEnumValues, getEnumType, } from '@naturalcycles/js-lib';
4
4
  import { _uniq } from '@naturalcycles/js-lib/array';
5
5
  import { _assert, _try } from '@naturalcycles/js-lib/error';
6
6
  import { _deepCopy, _filterNullishValues, _sortObject } from '@naturalcycles/js-lib/object';
@@ -12,314 +12,23 @@ import { TIMEZONES } from '../timezones.js';
12
12
  import { AjvValidationError } from './ajvValidationError.js';
13
13
  import { getAjv } from './getAjv.js';
14
14
  import { isEveryItemNumber, isEveryItemPrimitive, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
15
- // ==== AJV =====
16
- /**
17
- * On creation - compiles ajv validation function.
18
- * Provides convenient methods, error reporting, etc.
19
- */
20
- export class AjvSchema {
21
- schema;
22
- constructor(schema, cfg = {}) {
23
- this.schema = schema;
24
- this.cfg = {
25
- lazy: false,
26
- ...cfg,
27
- ajv: cfg.ajv || getAjv(),
28
- // Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
29
- inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
30
- };
31
- if (!cfg.lazy) {
32
- this.getAJVValidateFunction(); // compile eagerly
33
- }
34
- }
35
- /**
36
- * Shortcut for AjvSchema.create(schema, { lazy: true })
37
- */
38
- static createLazy(schema, cfg) {
39
- return AjvSchema.create(schema, {
40
- lazy: true,
41
- ...cfg,
42
- });
43
- }
44
- /**
45
- * Conveniently allows to pass either JsonSchema or JsonSchemaBuilder, or existing AjvSchema.
46
- * If it's already an AjvSchema - it'll just return it without any processing.
47
- * If it's a Builder - will call `build` before proceeding.
48
- * Otherwise - will construct AjvSchema instance ready to be used.
49
- *
50
- * Implementation note: JsonSchemaBuilder goes first in the union type, otherwise TypeScript fails to infer <T> type
51
- * correctly for some reason.
52
- */
53
- static create(schema, cfg) {
54
- if (schema instanceof AjvSchema)
55
- return schema;
56
- if (AjvSchema.isSchemaWithCachedAjvSchema(schema)) {
57
- return AjvSchema.requireCachedAjvSchema(schema);
58
- }
59
- let jsonSchema;
60
- if (AjvSchema.isJsonSchemaBuilder(schema)) {
61
- // oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
62
- jsonSchema = schema.build();
63
- AjvSchema.requireValidJsonSchema(jsonSchema);
64
- }
65
- else {
66
- jsonSchema = schema;
67
- }
68
- // This is our own helper which marks a schema as optional
69
- // in case it is going to be used in an object schema,
70
- // where we need to mark the given property as not-required.
71
- // But once all compilation is done, the presence of this field
72
- // really upsets Ajv.
73
- delete jsonSchema.optionalField;
74
- const ajvSchema = new AjvSchema(jsonSchema, cfg);
75
- AjvSchema.cacheAjvSchema(schema, ajvSchema);
76
- return ajvSchema;
77
- }
78
- static isJsonSchemaBuilder(schema) {
79
- return schema instanceof JsonSchemaTerminal;
80
- }
81
- cfg;
82
- /**
83
- * It returns the original object just for convenience.
84
- * Reminder: Ajv will MUTATE your object under 2 circumstances:
85
- * 1. `useDefaults` option (enabled by default!), which will set missing/empty values that have `default` set in the schema.
86
- * 2. `coerceTypes` (false by default).
87
- *
88
- * Returned object is always the same object (`===`) that was passed, so it is returned just for convenience.
89
- */
90
- validate(input, opt = {}) {
91
- const [err, output] = this.getValidationResult(input, opt);
92
- if (err)
93
- throw err;
94
- return output;
95
- }
96
- isValid(input, opt) {
97
- // todo: we can make it both fast and non-mutating by using Ajv
98
- // with "removeAdditional" and "useDefaults" disabled.
99
- const [err] = this.getValidationResult(input, opt);
100
- return !err;
101
- }
102
- getValidationResult(input, opt = {}) {
103
- const fn = this.getAJVValidateFunction();
104
- const item = opt.mutateInput !== false || typeof input !== 'object'
105
- ? input // mutate
106
- : _deepCopy(input); // not mutate
107
- let valid = fn(item); // mutates item, but not input
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
- }
125
- if (valid)
126
- return [null, output];
127
- const errors = fn.errors;
128
- const { inputId = _isObject(input) ? input['id'] : undefined, inputName = this.cfg.inputName || 'Object', } = opt;
129
- const dataVar = [inputName, inputId].filter(Boolean).join('.');
130
- this.applyImprovementsOnErrorMessages(errors);
131
- let message = this.cfg.ajv.errorsText(errors, {
132
- dataVar,
133
- separator,
134
- });
135
- // Note: if we mutated the input already, e.g stripped unknown properties,
136
- // the error message Input would contain already mutated object print, such as Input: {}
137
- // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
138
- const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
139
- message = [message, 'Input: ' + inputStringified].join(separator);
140
- const err = new AjvValidationError(message, _filterNullishValues({
141
- errors,
142
- inputName,
143
- inputId,
144
- }));
145
- return [err, output];
146
- }
147
- getValidationFunction() {
148
- return (input, opt) => {
149
- return this.getValidationResult(input, {
150
- mutateInput: opt?.mutateInput,
151
- inputName: opt?.inputName,
152
- inputId: opt?.inputId,
153
- });
154
- };
155
- }
156
- static isSchemaWithCachedAjvSchema(schema) {
157
- return !!schema?.[HIDDEN_AJV_SCHEMA];
158
- }
159
- static cacheAjvSchema(schema, ajvSchema) {
160
- return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema });
161
- }
162
- static requireCachedAjvSchema(schema) {
163
- return schema[HIDDEN_AJV_SCHEMA];
164
- }
165
- getAJVValidateFunction = _lazyValue(() => this.cfg.ajv.compile(this.schema));
166
- static requireValidJsonSchema(schema) {
167
- // For object schemas we require that it is type checked against an external type, e.g.:
168
- // interface Foo { name: string }
169
- // const schema = j.object({ name: j.string() }).ofType<Foo>()
170
- _assert(schema.type !== 'object' || schema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
171
- }
172
- applyImprovementsOnErrorMessages(errors) {
173
- if (!errors)
174
- return;
175
- this.filterNullableAnyOfErrors(errors);
176
- const { errorMessages } = this.schema;
177
- for (const error of errors) {
178
- const errorMessage = this.getErrorMessageForInstancePath(this.schema, error.instancePath, error.keyword);
179
- if (errorMessage) {
180
- error.message = errorMessage;
181
- }
182
- else if (errorMessages?.[error.keyword]) {
183
- error.message = errorMessages[error.keyword];
184
- }
185
- else {
186
- const unwrapped = unwrapNullableAnyOf(this.schema);
187
- if (unwrapped?.errorMessages?.[error.keyword]) {
188
- error.message = unwrapped.errorMessages[error.keyword];
189
- }
190
- }
191
- error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
192
- }
193
- }
194
- /**
195
- * Filters out noisy errors produced by nullable anyOf patterns.
196
- * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
197
- * AJV produces "must be null" and "must match a schema in anyOf" errors
198
- * that are confusing. This method splices them out, keeping only the real errors.
199
- */
200
- filterNullableAnyOfErrors(errors) {
201
- // Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
202
- const exactPaths = [];
203
- const nullBranchPrefixes = [];
204
- for (const error of errors) {
205
- if (error.keyword !== 'anyOf')
206
- continue;
207
- const parentSchema = this.resolveSchemaPath(error.schemaPath);
208
- if (!parentSchema)
209
- continue;
210
- const nullIndex = unwrapNullableAnyOfIndex(parentSchema);
211
- if (nullIndex === -1)
212
- continue;
213
- exactPaths.push(error.schemaPath); // e.g. "#/anyOf"
214
- const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length);
215
- nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`); // e.g. "#/anyOf/1/"
216
- }
217
- if (!exactPaths.length)
218
- return;
219
- for (let i = errors.length - 1; i >= 0; i--) {
220
- const sp = errors[i].schemaPath;
221
- if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
222
- errors.splice(i, 1);
223
- }
224
- }
225
- }
226
- /**
227
- * Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
228
- * and returns the parent schema containing the last keyword.
229
- */
230
- resolveSchemaPath(schemaPath) {
231
- // schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
232
- // We want the schema that contains the final keyword (e.g. "anyOf")
233
- const segments = schemaPath.replace(/^#\//, '').split('/');
234
- // Remove the last segment (the keyword itself, e.g. "anyOf")
235
- segments.pop();
236
- let current = this.schema;
237
- for (const segment of segments) {
238
- if (!current || typeof current !== 'object')
239
- return undefined;
240
- current = current[segment];
241
- }
242
- return current;
243
- }
244
- getErrorMessageForInstancePath(schema, instancePath, keyword) {
245
- if (!schema || !instancePath)
246
- return undefined;
247
- const segments = instancePath.split('/').filter(Boolean);
248
- return this.traverseSchemaPath(schema, segments, keyword);
249
- }
250
- traverseSchemaPath(schema, segments, keyword) {
251
- if (!segments.length)
252
- return undefined;
253
- const [currentSegment, ...remainingSegments] = segments;
254
- const nextSchema = this.getChildSchema(schema, currentSegment);
255
- if (!nextSchema)
256
- return undefined;
257
- if (nextSchema.errorMessages?.[keyword]) {
258
- return nextSchema.errorMessages[keyword];
259
- }
260
- // Check through nullable wrapper
261
- const unwrapped = unwrapNullableAnyOf(nextSchema);
262
- if (unwrapped?.errorMessages?.[keyword]) {
263
- return unwrapped.errorMessages[keyword];
264
- }
265
- if (remainingSegments.length) {
266
- return this.traverseSchemaPath(nextSchema, remainingSegments, keyword);
267
- }
268
- return undefined;
269
- }
270
- getChildSchema(schema, segment) {
271
- if (!segment)
272
- return undefined;
273
- // Unwrap nullable anyOf to find properties/items through nullable wrappers
274
- const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema;
275
- if (/^\d+$/.test(segment) && effectiveSchema.items) {
276
- return this.getArrayItemSchema(effectiveSchema, segment);
277
- }
278
- return this.getObjectPropertySchema(effectiveSchema, segment);
279
- }
280
- getArrayItemSchema(schema, indexSegment) {
281
- if (!schema.items)
282
- return undefined;
283
- if (Array.isArray(schema.items)) {
284
- return schema.items[Number(indexSegment)];
285
- }
286
- return schema.items;
287
- }
288
- getObjectPropertySchema(schema, segment) {
289
- return schema.properties?.[segment];
290
- }
291
- }
292
- function unwrapNullableAnyOf(schema) {
293
- const nullIndex = unwrapNullableAnyOfIndex(schema);
294
- if (nullIndex === -1)
295
- return undefined;
296
- return schema.anyOf[1 - nullIndex];
297
- }
298
- function unwrapNullableAnyOfIndex(schema) {
299
- if (schema.anyOf?.length !== 2)
300
- return -1;
301
- const nullIndex = schema.anyOf.findIndex(s => s.type === 'null');
302
- return nullIndex;
303
- }
304
- const separator = '\n';
305
- export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA');
306
- // ===== JsonSchemaBuilders ===== //
15
+ // ==== j (factory object) ====
307
16
  export const j = {
308
17
  /**
309
18
  * Matches literally any value - equivalent to TypeScript's `any` type.
310
19
  * Use sparingly, as it bypasses type validation entirely.
311
20
  */
312
21
  any() {
313
- return new JsonSchemaAnyBuilder({});
22
+ return new JBuilder({});
314
23
  },
315
24
  string() {
316
- return new JsonSchemaStringBuilder();
25
+ return new JString();
317
26
  },
318
27
  number() {
319
- return new JsonSchemaNumberBuilder();
28
+ return new JNumber();
320
29
  },
321
30
  boolean() {
322
- return new JsonSchemaBooleanBuilder();
31
+ return new JBoolean();
323
32
  },
324
33
  object: Object.assign(object, {
325
34
  dbEntity: objectDbEntity,
@@ -333,7 +42,7 @@ export const j = {
333
42
  const finalValueSchema = isValueOptional
334
43
  ? { anyOf: [{ isUndefined: true }, builtSchema] }
335
44
  : builtSchema;
336
- return new JsonSchemaObjectBuilder({}, {
45
+ return new JObject({}, {
337
46
  hasIsOfTypeCheck: false,
338
47
  patternProperties: {
339
48
  '^.+$': finalValueSchema,
@@ -376,16 +85,18 @@ export const j = {
376
85
  withRegexKeys,
377
86
  }),
378
87
  array(itemSchema) {
379
- return new JsonSchemaArrayBuilder(itemSchema);
88
+ return new JArray(itemSchema);
380
89
  },
381
90
  tuple(items) {
382
- return new JsonSchemaTupleBuilder(items);
91
+ return new JTuple(items);
383
92
  },
384
93
  set(itemSchema) {
385
- return new JsonSchemaSet2Builder(itemSchema);
94
+ return new JSet2Builder(itemSchema);
386
95
  },
387
96
  buffer() {
388
- return new JsonSchemaBufferBuilder();
97
+ return new JBuilder({
98
+ Buffer: true,
99
+ });
389
100
  },
390
101
  enum(input, opt) {
391
102
  let enumValues;
@@ -411,7 +122,7 @@ export const j = {
411
122
  }
412
123
  }
413
124
  _assert(enumValues, 'Unsupported enum input');
414
- return new JsonSchemaEnumBuilder(enumValues, baseType, opt);
125
+ return new JEnum(enumValues, baseType, opt);
415
126
  },
416
127
  /**
417
128
  * Use only with primitive values, otherwise this function will throw to avoid bugs.
@@ -427,7 +138,7 @@ export const j = {
427
138
  oneOf(items) {
428
139
  const schemas = items.map(b => b.build());
429
140
  _assert(schemas.every(hasNoObjectSchemas), 'Do not use `oneOf` validation with non-primitive types!');
430
- return new JsonSchemaAnyBuilder({
141
+ return new JBuilder({
431
142
  oneOf: schemas,
432
143
  });
433
144
  },
@@ -445,7 +156,7 @@ export const j = {
445
156
  anyOf(items) {
446
157
  const schemas = items.map(b => b.build());
447
158
  _assert(schemas.every(hasNoObjectSchemas), 'Do not use `anyOf` validation with non-primitive types!');
448
- return new JsonSchemaAnyBuilder({
159
+ return new JBuilder({
449
160
  anyOf: schemas,
450
161
  });
451
162
  },
@@ -462,7 +173,18 @@ export const j = {
462
173
  * ```
463
174
  */
464
175
  anyOfBy(propertyName, schemaDictionary) {
465
- return new JsonSchemaAnyOfByBuilder(propertyName, schemaDictionary);
176
+ const builtSchemaDictionary = {};
177
+ for (const [key, schema] of Object.entries(schemaDictionary)) {
178
+ builtSchemaDictionary[key] = schema.build();
179
+ }
180
+ return new JBuilder({
181
+ type: 'object',
182
+ hasIsOfTypeCheck: true,
183
+ anyOfBy: {
184
+ propertyName,
185
+ schemaDictionary: builtSchemaDictionary,
186
+ },
187
+ });
466
188
  },
467
189
  /**
468
190
  * Custom version of `anyOf` which - in contrast to the original function - does not mutate the input.
@@ -473,7 +195,7 @@ export const j = {
473
195
  * ```
474
196
  */
475
197
  anyOfThese(items) {
476
- return new JsonSchemaAnyBuilder({
198
+ return new JBuilder({
477
199
  anyOfThese: items.map(b => b.build()),
478
200
  });
479
201
  },
@@ -490,13 +212,21 @@ export const j = {
490
212
  baseType = 'string';
491
213
  if (typeof v === 'number')
492
214
  baseType = 'number';
493
- return new JsonSchemaEnumBuilder([v], baseType);
215
+ return new JEnum([v], baseType);
216
+ },
217
+ /**
218
+ * Create a JSchema from a plain JsonSchema object.
219
+ * Useful when the schema is loaded from a JSON file or generated externally.
220
+ *
221
+ * Optionally accepts a custom Ajv instance and/or inputName for error messages.
222
+ */
223
+ fromSchema(schema, cfg) {
224
+ return new JSchema(schema, cfg);
494
225
  },
495
226
  };
496
- const TS_2500 = 16725225600; // 2500-01-01
497
- const TS_2500_MILLIS = TS_2500 * 1000;
498
- const TS_2000 = 946684800; // 2000-01-01
499
- const TS_2000_MILLIS = TS_2000 * 1000;
227
+ // ==== Symbol for caching compiled AjvSchema ====
228
+ export const HIDDEN_AJV_SCHEMA = Symbol('HIDDEN_AJV_SCHEMA');
229
+ // ==== JSchema (locked base) ====
500
230
  /*
501
231
  Notes for future reference
502
232
 
@@ -505,17 +235,41 @@ const TS_2000_MILLIS = TS_2000 * 1000;
505
235
  which means that the `foo` property would be mandatory, it's just that its value can be `undefined` as well.
506
236
  With `Opt`, we can infer it as `{ foo?: string | undefined }`.
507
237
  */
508
- export class JsonSchemaTerminal {
238
+ export class JSchema {
509
239
  [HIDDEN_AJV_SCHEMA];
510
240
  schema;
511
- constructor(schema) {
241
+ _cfg;
242
+ constructor(schema, cfg) {
512
243
  this.schema = schema;
513
- }
514
- get ajvSchema() {
515
- if (!this[HIDDEN_AJV_SCHEMA]) {
516
- this[HIDDEN_AJV_SCHEMA] = AjvSchema.create(this);
244
+ this._cfg = cfg;
245
+ }
246
+ _builtSchema;
247
+ _compiledFns;
248
+ _getBuiltSchema() {
249
+ if (!this._builtSchema) {
250
+ const builtSchema = this.build();
251
+ if (this instanceof JBuilder) {
252
+ _assert(builtSchema.type !== 'object' || builtSchema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
253
+ }
254
+ delete builtSchema.optionalField;
255
+ this._builtSchema = builtSchema;
256
+ }
257
+ return this._builtSchema;
258
+ }
259
+ _getCompiled(overrideAjv) {
260
+ const builtSchema = this._getBuiltSchema();
261
+ const ajv = overrideAjv ?? this._cfg?.ajv ?? getAjv();
262
+ this._compiledFns ??= new WeakMap();
263
+ let fn = this._compiledFns.get(ajv);
264
+ if (!fn) {
265
+ fn = ajv.compile(builtSchema);
266
+ this._compiledFns.set(ajv, fn);
267
+ // Cache AjvSchema wrapper for HIDDEN_AJV_SCHEMA backward compat (default ajv only)
268
+ if (!overrideAjv) {
269
+ this[HIDDEN_AJV_SCHEMA] = AjvSchema._wrap(builtSchema, fn);
270
+ }
517
271
  }
518
- return this[HIDDEN_AJV_SCHEMA];
272
+ return { fn, builtSchema };
519
273
  }
520
274
  getSchema() {
521
275
  return this.schema;
@@ -533,6 +287,7 @@ export class JsonSchemaTerminal {
533
287
  clone() {
534
288
  const cloned = Object.create(Object.getPrototypeOf(this));
535
289
  cloned.schema = deepCopyPreservingFunctions(this.schema);
290
+ cloned._cfg = this._cfg;
536
291
  return cloned;
537
292
  }
538
293
  cloneAndUpdateSchema(schema) {
@@ -541,16 +296,28 @@ export class JsonSchemaTerminal {
541
296
  return clone;
542
297
  }
543
298
  validate(input, opt) {
544
- return this.ajvSchema.validate(input, opt);
299
+ const [err, output] = this.getValidationResult(input, opt);
300
+ if (err)
301
+ throw err;
302
+ return output;
545
303
  }
546
304
  isValid(input, opt) {
547
- return this.ajvSchema.isValid(input, opt);
305
+ const [err] = this.getValidationResult(input, opt);
306
+ return !err;
548
307
  }
549
308
  getValidationResult(input, opt = {}) {
550
- return this.ajvSchema.getValidationResult(input, opt);
309
+ const { fn, builtSchema } = this._getCompiled(opt.ajv);
310
+ const inputName = this._cfg?.inputName || (builtSchema.$id ? _substringBefore(builtSchema.$id, '.') : undefined);
311
+ return executeValidation(fn, builtSchema, input, opt, inputName);
551
312
  }
552
313
  getValidationFunction() {
553
- return this.ajvSchema.getValidationFunction();
314
+ return (input, opt) => {
315
+ return this.getValidationResult(input, {
316
+ mutateInput: opt?.mutateInput,
317
+ inputName: opt?.inputName,
318
+ inputId: opt?.inputId,
319
+ });
320
+ };
554
321
  }
555
322
  /**
556
323
  * Specify a function to be called after the normal validation is finished.
@@ -573,7 +340,8 @@ export class JsonSchemaTerminal {
573
340
  out;
574
341
  opt;
575
342
  }
576
- export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
343
+ // ==== JBuilder (chainable base) ====
344
+ export class JBuilder extends JSchema {
577
345
  setErrorMessage(ruleName, errorMessage) {
578
346
  if (_isUndefined(errorMessage))
579
347
  return;
@@ -585,15 +353,6 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
585
353
  *
586
354
  * When the type inferred from the schema differs from the passed-in type,
587
355
  * the schema becomes unusable, by turning its type into `never`.
588
- *
589
- * ```ts
590
- * const schemaGood = j.string().isOfType<string>() // ✅
591
- *
592
- * const schemaBad = j.string().isOfType<number>() // ❌
593
- * schemaBad.build() // TypeError: property "build" does not exist on type "never"
594
- *
595
- * const result = ajvValidateRequest.body(req, schemaBad) // result will have `unknown` type
596
- * ```
597
356
  */
598
357
  isOfType() {
599
358
  return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true });
@@ -630,7 +389,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
630
389
  return clone;
631
390
  }
632
391
  nullable() {
633
- return new JsonSchemaAnyBuilder({
392
+ return new JBuilder({
634
393
  anyOf: [this.build(), { type: 'null' }],
635
394
  });
636
395
  }
@@ -645,7 +404,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
645
404
  * Locks the given schema chain and no other modification can be done to it.
646
405
  */
647
406
  final() {
648
- return new JsonSchemaTerminal(this.schema);
407
+ return new JSchema(this.schema);
649
408
  }
650
409
  /**
651
410
  *
@@ -676,7 +435,13 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
676
435
  });
677
436
  }
678
437
  }
679
- export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
438
+ // ==== Consts
439
+ const TS_2500 = 16725225600; // 2500-01-01
440
+ const TS_2500_MILLIS = TS_2500 * 1000;
441
+ const TS_2000 = 946684800; // 2000-01-01
442
+ const TS_2000_MILLIS = TS_2000 * 1000;
443
+ // ==== Type-specific builders ====
444
+ export class JString extends JBuilder {
680
445
  constructor() {
681
446
  super({
682
447
  type: 'string',
@@ -690,7 +455,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
690
455
  *
691
456
  * Make sure this `optional()` call is at the end of your call chain.
692
457
  *
693
- * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
458
+ * When `null` is included in optionalValues, the return type becomes `JSchema`
694
459
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
695
460
  */
696
461
  optional(optionalValues) {
@@ -698,7 +463,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
698
463
  return super.optional();
699
464
  }
700
465
  _typeCast(optionalValues);
701
- let newBuilder = new JsonSchemaStringBuilder().optional();
466
+ let newBuilder = new JString().optional();
702
467
  const alternativesSchema = j.enum(optionalValues);
703
468
  Object.assign(newBuilder.getSchema(), {
704
469
  anyOf: [this.build(), alternativesSchema.build()],
@@ -709,7 +474,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
709
474
  // but the typing should not reflect that.
710
475
  // We also cannot accept more rules attached, since we're not building a StringSchema anymore.
711
476
  if (optionalValues.includes(null)) {
712
- newBuilder = new JsonSchemaTerminal({
477
+ newBuilder = new JSchema({
713
478
  anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
714
479
  optionalField: true,
715
480
  });
@@ -772,13 +537,16 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
772
537
  * because this call effectively starts a new schema chain.
773
538
  */
774
539
  isoDate() {
775
- return new JsonSchemaIsoDateBuilder();
540
+ return new JIsoDate();
776
541
  }
777
542
  isoDateTime() {
778
543
  return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
779
544
  }
780
545
  isoMonth() {
781
- return new JsonSchemaIsoMonthBuilder();
546
+ return new JBuilder({
547
+ type: 'string',
548
+ IsoMonth: {},
549
+ });
782
550
  }
783
551
  /**
784
552
  * Validates the string format to be JWT.
@@ -830,7 +598,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
830
598
  return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' });
831
599
  }
832
600
  }
833
- export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
601
+ export class JIsoDate extends JBuilder {
834
602
  constructor() {
835
603
  super({
836
604
  type: 'string',
@@ -843,7 +611,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
843
611
  * This `null` feature only works when the current schema is nested in an object or array schema,
844
612
  * due to how mutability works in Ajv.
845
613
  *
846
- * When `null` is passed, the return type becomes `JsonSchemaTerminal`
614
+ * When `null` is passed, the return type becomes `JSchema`
847
615
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
848
616
  */
849
617
  optional(nullValue) {
@@ -854,7 +622,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
854
622
  // so we must allow `null` values to be parsed by Ajv,
855
623
  // but the typing should not reflect that.
856
624
  // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
857
- return new JsonSchemaTerminal({
625
+ return new JSchema({
858
626
  anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
859
627
  optionalField: true,
860
628
  });
@@ -882,15 +650,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
882
650
  return this.cloneAndUpdateSchema(schemaPatch);
883
651
  }
884
652
  }
885
- export class JsonSchemaIsoMonthBuilder extends JsonSchemaAnyBuilder {
886
- constructor() {
887
- super({
888
- type: 'string',
889
- IsoMonth: {},
890
- });
891
- }
892
- }
893
- export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
653
+ export class JNumber extends JBuilder {
894
654
  constructor() {
895
655
  super({
896
656
  type: 'number',
@@ -904,7 +664,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
904
664
  *
905
665
  * Make sure this `optional()` call is at the end of your call chain.
906
666
  *
907
- * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
667
+ * When `null` is included in optionalValues, the return type becomes `JSchema`
908
668
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
909
669
  */
910
670
  optional(optionalValues) {
@@ -912,7 +672,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
912
672
  return super.optional();
913
673
  }
914
674
  _typeCast(optionalValues);
915
- let newBuilder = new JsonSchemaNumberBuilder().optional();
675
+ let newBuilder = new JNumber().optional();
916
676
  const alternativesSchema = j.enum(optionalValues);
917
677
  Object.assign(newBuilder.getSchema(), {
918
678
  anyOf: [this.build(), alternativesSchema.build()],
@@ -923,7 +683,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
923
683
  // but the typing should not reflect that.
924
684
  // We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
925
685
  if (optionalValues.includes(null)) {
926
- newBuilder = new JsonSchemaTerminal({
686
+ newBuilder = new JSchema({
927
687
  anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
928
688
  optionalField: true,
929
689
  });
@@ -1024,7 +784,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
1024
784
  return this.cloneAndUpdateSchema({ precision: numberOfDigits });
1025
785
  }
1026
786
  }
1027
- export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
787
+ export class JBoolean extends JBuilder {
1028
788
  constructor() {
1029
789
  super({
1030
790
  type: 'boolean',
@@ -1040,7 +800,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
1040
800
  if (typeof optionalValue === 'undefined') {
1041
801
  return super.optional();
1042
802
  }
1043
- const newBuilder = new JsonSchemaBooleanBuilder().optional();
803
+ const newBuilder = new JBoolean().optional();
1044
804
  const alternativesSchema = j.enum([optionalValue]);
1045
805
  Object.assign(newBuilder.getSchema(), {
1046
806
  anyOf: [this.build(), alternativesSchema.build()],
@@ -1049,7 +809,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
1049
809
  return newBuilder;
1050
810
  }
1051
811
  }
1052
- export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
812
+ export class JObject extends JBuilder {
1053
813
  constructor(props, opt) {
1054
814
  super({
1055
815
  type: 'object',
@@ -1061,22 +821,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1061
821
  keySchema: opt?.keySchema ?? undefined,
1062
822
  });
1063
823
  if (props)
1064
- this.addProperties(props);
1065
- }
1066
- addProperties(props) {
1067
- const properties = {};
1068
- const required = [];
1069
- for (const [key, builder] of Object.entries(props)) {
1070
- const isOptional = builder.getSchema().optionalField;
1071
- if (!isOptional) {
1072
- required.push(key);
1073
- }
1074
- const schema = builder.build();
1075
- properties[key] = schema;
1076
- }
1077
- this.schema.properties = properties;
1078
- this.schema.required = _uniq(required).sort();
1079
- return this;
824
+ addPropertiesToSchema(this.schema, props);
1080
825
  }
1081
826
  /**
1082
827
  * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
@@ -1084,7 +829,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1084
829
  * This `null` feature only works when the current schema is nested in an object or array schema,
1085
830
  * due to how mutability works in Ajv.
1086
831
  *
1087
- * When `null` is passed, the return type becomes `JsonSchemaTerminal`
832
+ * When `null` is passed, the return type becomes `JSchema`
1088
833
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
1089
834
  */
1090
835
  optional(nullValue) {
@@ -1095,7 +840,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1095
840
  // so we must allow `null` values to be parsed by Ajv,
1096
841
  // but the typing should not reflect that.
1097
842
  // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
1098
- return new JsonSchemaTerminal({
843
+ return new JSchema({
1099
844
  anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
1100
845
  optionalField: true,
1101
846
  });
@@ -1107,9 +852,9 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1107
852
  return this.cloneAndUpdateSchema({ additionalProperties: true });
1108
853
  }
1109
854
  extend(props) {
1110
- const newBuilder = new JsonSchemaObjectBuilder();
855
+ const newBuilder = new JObject();
1111
856
  _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
1112
- const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
857
+ const incomingSchemaBuilder = new JObject(props);
1113
858
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
1114
859
  _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
1115
860
  return newBuilder;
@@ -1120,17 +865,6 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1120
865
  * It expects you to use `isOfType<T>()` in the chain,
1121
866
  * otherwise the validation will throw. This is to ensure
1122
867
  * that the schemas you concatenated match the intended final type.
1123
- *
1124
- * ```ts
1125
- * interface Foo { foo: string }
1126
- * const fooSchema = j.object<Foo>({ foo: j.string() })
1127
- *
1128
- * interface Bar { bar: number }
1129
- * const barSchema = j.object<Bar>({ bar: j.number() })
1130
- *
1131
- * interface Shu { foo: string, bar: number }
1132
- * const shuSchema = fooSchema.concat(barSchema).isOfType<Shu>() // important
1133
- * ```
1134
868
  */
1135
869
  concat(other) {
1136
870
  const clone = this.clone();
@@ -1160,7 +894,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1160
894
  return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] });
1161
895
  }
1162
896
  }
1163
- export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
897
+ export class JObjectInfer extends JBuilder {
1164
898
  constructor(props) {
1165
899
  super({
1166
900
  type: 'object',
@@ -1169,22 +903,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1169
903
  additionalProperties: false,
1170
904
  });
1171
905
  if (props)
1172
- this.addProperties(props);
1173
- }
1174
- addProperties(props) {
1175
- const properties = {};
1176
- const required = [];
1177
- for (const [key, builder] of Object.entries(props)) {
1178
- const isOptional = builder.getSchema().optionalField;
1179
- if (!isOptional) {
1180
- required.push(key);
1181
- }
1182
- const schema = builder.build();
1183
- properties[key] = schema;
1184
- }
1185
- this.schema.properties = properties;
1186
- this.schema.required = _uniq(required).sort();
1187
- return this;
906
+ addPropertiesToSchema(this.schema, props);
1188
907
  }
1189
908
  /**
1190
909
  * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
@@ -1192,7 +911,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1192
911
  * This `null` feature only works when the current schema is nested in an object or array schema,
1193
912
  * due to how mutability works in Ajv.
1194
913
  *
1195
- * When `null` is passed, the return type becomes `JsonSchemaTerminal`
914
+ * When `null` is passed, the return type becomes `JSchema`
1196
915
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
1197
916
  */
1198
917
  // @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
@@ -1204,7 +923,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1204
923
  // so we must allow `null` values to be parsed by Ajv,
1205
924
  // but the typing should not reflect that.
1206
925
  // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
1207
- return new JsonSchemaTerminal({
926
+ return new JSchema({
1208
927
  anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
1209
928
  optionalField: true,
1210
929
  });
@@ -1216,9 +935,9 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1216
935
  return this.cloneAndUpdateSchema({ additionalProperties: true });
1217
936
  }
1218
937
  extend(props) {
1219
- const newBuilder = new JsonSchemaObjectInferringBuilder();
938
+ const newBuilder = new JObjectInfer();
1220
939
  _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
1221
- const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
940
+ const incomingSchemaBuilder = new JObjectInfer(props);
1222
941
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
1223
942
  // This extend function is not type-safe as it is inferring,
1224
943
  // so even if the base schema was already type-checked,
@@ -1238,7 +957,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1238
957
  });
1239
958
  }
1240
959
  }
1241
- export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
960
+ export class JArray extends JBuilder {
1242
961
  constructor(itemsSchema) {
1243
962
  super({
1244
963
  type: 'array',
@@ -1262,7 +981,7 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
1262
981
  return this.cloneAndUpdateSchema({ uniqueItems: true });
1263
982
  }
1264
983
  }
1265
- export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
984
+ class JSet2Builder extends JBuilder {
1266
985
  constructor(itemsSchema) {
1267
986
  super({
1268
987
  type: ['array', 'object'],
@@ -1276,14 +995,7 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
1276
995
  return this.cloneAndUpdateSchema({ maxItems });
1277
996
  }
1278
997
  }
1279
- export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
1280
- constructor() {
1281
- super({
1282
- Buffer: true,
1283
- });
1284
- }
1285
- }
1286
- export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
998
+ export class JEnum extends JBuilder {
1287
999
  constructor(enumValues, baseType, opt) {
1288
1000
  const jsonSchema = { enum: enumValues };
1289
1001
  // Specifying the base type helps in cases when we ask Ajv to coerce the types.
@@ -1302,7 +1014,7 @@ export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
1302
1014
  return this;
1303
1015
  }
1304
1016
  }
1305
- export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
1017
+ export class JTuple extends JBuilder {
1306
1018
  constructor(items) {
1307
1019
  super({
1308
1020
  type: 'array',
@@ -1312,43 +1024,11 @@ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
1312
1024
  });
1313
1025
  }
1314
1026
  }
1315
- export class JsonSchemaAnyOfByBuilder extends JsonSchemaAnyBuilder {
1316
- constructor(propertyName, schemaDictionary) {
1317
- const builtSchemaDictionary = {};
1318
- for (const [key, schema] of Object.entries(schemaDictionary)) {
1319
- builtSchemaDictionary[key] = schema.build();
1320
- }
1321
- super({
1322
- type: 'object',
1323
- hasIsOfTypeCheck: true,
1324
- anyOfBy: {
1325
- propertyName,
1326
- schemaDictionary: builtSchemaDictionary,
1327
- },
1328
- });
1329
- }
1330
- }
1331
- export class JsonSchemaAnyOfTheseBuilder extends JsonSchemaAnyBuilder {
1332
- constructor(propertyName, schemaDictionary) {
1333
- const builtSchemaDictionary = {};
1334
- for (const [key, schema] of Object.entries(schemaDictionary)) {
1335
- builtSchemaDictionary[key] = schema.build();
1336
- }
1337
- super({
1338
- type: 'object',
1339
- hasIsOfTypeCheck: true,
1340
- anyOfBy: {
1341
- propertyName,
1342
- schemaDictionary: builtSchemaDictionary,
1343
- },
1344
- });
1345
- }
1346
- }
1347
1027
  function object(props) {
1348
- return new JsonSchemaObjectBuilder(props);
1028
+ return new JObject(props);
1349
1029
  }
1350
1030
  function objectInfer(props) {
1351
- return new JsonSchemaObjectInferringBuilder(props);
1031
+ return new JObjectInfer(props);
1352
1032
  }
1353
1033
  function objectDbEntity(props) {
1354
1034
  return j.object({
@@ -1367,7 +1047,7 @@ function record(keySchema, valueSchema) {
1367
1047
  const finalValueSchema = isValueOptional
1368
1048
  ? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
1369
1049
  : valueJsonSchema;
1370
- return new JsonSchemaObjectBuilder([], {
1050
+ return new JObject([], {
1371
1051
  hasIsOfTypeCheck: false,
1372
1052
  keySchema: keyJsonSchema,
1373
1053
  patternProperties: {
@@ -1381,7 +1061,7 @@ function withRegexKeys(keyRegex, schema) {
1381
1061
  }
1382
1062
  const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex;
1383
1063
  const jsonSchema = schema.build();
1384
- return new JsonSchemaObjectBuilder([], {
1064
+ return new JObject([], {
1385
1065
  hasIsOfTypeCheck: false,
1386
1066
  patternProperties: {
1387
1067
  [pattern]: jsonSchema,
@@ -1410,11 +1090,324 @@ function withEnumKeys(keys, schema) {
1410
1090
  _assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum');
1411
1091
  const typedValues = enumValues;
1412
1092
  const props = Object.fromEntries(typedValues.map(key => [key, schema]));
1413
- return new JsonSchemaObjectBuilder(props, { hasIsOfTypeCheck: false });
1093
+ return new JObject(props, { hasIsOfTypeCheck: false });
1094
+ }
1095
+ // ==== AjvSchema compat wrapper ====
1096
+ /**
1097
+ * On creation - compiles ajv validation function.
1098
+ * Provides convenient methods, error reporting, etc.
1099
+ */
1100
+ export class AjvSchema {
1101
+ schema;
1102
+ constructor(schema, cfg = {}, preCompiledFn) {
1103
+ this.schema = schema;
1104
+ this.cfg = {
1105
+ lazy: false,
1106
+ ...cfg,
1107
+ ajv: cfg.ajv || getAjv(),
1108
+ // Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
1109
+ inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
1110
+ };
1111
+ if (preCompiledFn) {
1112
+ this._compiledFn = preCompiledFn;
1113
+ }
1114
+ else if (!cfg.lazy) {
1115
+ this._getValidateFn(); // compile eagerly
1116
+ }
1117
+ }
1118
+ /**
1119
+ * Shortcut for AjvSchema.create(schema, { lazy: true })
1120
+ */
1121
+ static createLazy(schema, cfg) {
1122
+ return AjvSchema.create(schema, {
1123
+ lazy: true,
1124
+ ...cfg,
1125
+ });
1126
+ }
1127
+ /**
1128
+ * Conveniently allows to pass either JsonSchema or JSchema builder, or existing AjvSchema.
1129
+ * If it's already an AjvSchema - it'll just return it without any processing.
1130
+ * If it's a Builder - will call `build` before proceeding.
1131
+ * Otherwise - will construct AjvSchema instance ready to be used.
1132
+ */
1133
+ static create(schema, cfg) {
1134
+ if (schema instanceof AjvSchema)
1135
+ return schema;
1136
+ if (AjvSchema.isSchemaWithCachedAjvSchema(schema)) {
1137
+ return AjvSchema.requireCachedAjvSchema(schema);
1138
+ }
1139
+ let jsonSchema;
1140
+ if (schema instanceof JSchema) {
1141
+ // oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
1142
+ jsonSchema = schema.build();
1143
+ AjvSchema.requireValidJsonSchema(jsonSchema);
1144
+ }
1145
+ else {
1146
+ jsonSchema = schema;
1147
+ }
1148
+ // This is our own helper which marks a schema as optional
1149
+ // in case it is going to be used in an object schema,
1150
+ // where we need to mark the given property as not-required.
1151
+ // But once all compilation is done, the presence of this field
1152
+ // really upsets Ajv.
1153
+ delete jsonSchema.optionalField;
1154
+ const ajvSchema = new AjvSchema(jsonSchema, cfg);
1155
+ AjvSchema.cacheAjvSchema(schema, ajvSchema);
1156
+ return ajvSchema;
1157
+ }
1158
+ /**
1159
+ * Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
1160
+ * Used internally by JSchema to cache a compatible AjvSchema instance.
1161
+ */
1162
+ static _wrap(schema, compiledFn) {
1163
+ return new AjvSchema(schema, {}, compiledFn);
1164
+ }
1165
+ static isSchemaWithCachedAjvSchema(schema) {
1166
+ return !!schema?.[HIDDEN_AJV_SCHEMA];
1167
+ }
1168
+ static cacheAjvSchema(schema, ajvSchema) {
1169
+ return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema });
1170
+ }
1171
+ static requireCachedAjvSchema(schema) {
1172
+ return schema[HIDDEN_AJV_SCHEMA];
1173
+ }
1174
+ cfg;
1175
+ _compiledFn;
1176
+ _getValidateFn() {
1177
+ if (!this._compiledFn) {
1178
+ this._compiledFn = this.cfg.ajv.compile(this.schema);
1179
+ }
1180
+ return this._compiledFn;
1181
+ }
1182
+ /**
1183
+ * It returns the original object just for convenience.
1184
+ */
1185
+ validate(input, opt = {}) {
1186
+ const [err, output] = this.getValidationResult(input, opt);
1187
+ if (err)
1188
+ throw err;
1189
+ return output;
1190
+ }
1191
+ isValid(input, opt) {
1192
+ const [err] = this.getValidationResult(input, opt);
1193
+ return !err;
1194
+ }
1195
+ getValidationResult(input, opt = {}) {
1196
+ const fn = this._getValidateFn();
1197
+ return executeValidation(fn, this.schema, input, opt, this.cfg.inputName);
1198
+ }
1199
+ getValidationFunction() {
1200
+ return (input, opt) => {
1201
+ return this.getValidationResult(input, {
1202
+ mutateInput: opt?.mutateInput,
1203
+ inputName: opt?.inputName,
1204
+ inputId: opt?.inputId,
1205
+ });
1206
+ };
1207
+ }
1208
+ static requireValidJsonSchema(schema) {
1209
+ // For object schemas we require that it is type checked against an external type, e.g.:
1210
+ // interface Foo { name: string }
1211
+ // const schema = j.object({ name: j.string() }).ofType<Foo>()
1212
+ _assert(schema.type !== 'object' || schema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
1213
+ }
1214
+ }
1215
+ // ==== Shared validation logic ====
1216
+ const separator = '\n';
1217
+ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1218
+ const item = opt.mutateInput !== false || typeof input !== 'object'
1219
+ ? input // mutate
1220
+ : _deepCopy(input); // not mutate
1221
+ let valid = fn(item); // mutates item, but not input
1222
+ _typeCast(item);
1223
+ let output = item;
1224
+ if (valid && builtSchema.postValidation) {
1225
+ const [err, result] = _try(() => builtSchema.postValidation(output));
1226
+ if (err) {
1227
+ valid = false;
1228
+ fn.errors = [
1229
+ {
1230
+ instancePath: '',
1231
+ message: err.message,
1232
+ },
1233
+ ];
1234
+ }
1235
+ else {
1236
+ output = result;
1237
+ }
1238
+ }
1239
+ if (valid)
1240
+ return [null, output];
1241
+ const errors = fn.errors;
1242
+ const { inputId = _isObject(input) ? input['id'] : undefined, inputName = defaultInputName || 'Object', } = opt;
1243
+ const dataVar = [inputName, inputId].filter(Boolean).join('.');
1244
+ applyImprovementsOnErrorMessages(errors, builtSchema);
1245
+ let message = getAjv().errorsText(errors, {
1246
+ dataVar,
1247
+ separator,
1248
+ });
1249
+ // Note: if we mutated the input already, e.g stripped unknown properties,
1250
+ // the error message Input would contain already mutated object print, such as Input: {}
1251
+ // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1252
+ const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
1253
+ message = [message, 'Input: ' + inputStringified].join(separator);
1254
+ const err = new AjvValidationError(message, _filterNullishValues({
1255
+ errors,
1256
+ inputName,
1257
+ inputId,
1258
+ }));
1259
+ return [err, output];
1260
+ }
1261
+ // ==== Error formatting helpers ====
1262
+ function applyImprovementsOnErrorMessages(errors, schema) {
1263
+ if (!errors)
1264
+ return;
1265
+ filterNullableAnyOfErrors(errors, schema);
1266
+ const { errorMessages } = schema;
1267
+ for (const error of errors) {
1268
+ const errorMessage = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword);
1269
+ if (errorMessage) {
1270
+ error.message = errorMessage;
1271
+ }
1272
+ else if (errorMessages?.[error.keyword]) {
1273
+ error.message = errorMessages[error.keyword];
1274
+ }
1275
+ else {
1276
+ const unwrapped = unwrapNullableAnyOf(schema);
1277
+ if (unwrapped?.errorMessages?.[error.keyword]) {
1278
+ error.message = unwrapped.errorMessages[error.keyword];
1279
+ }
1280
+ }
1281
+ error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
1282
+ }
1283
+ }
1284
+ /**
1285
+ * Filters out noisy errors produced by nullable anyOf patterns.
1286
+ * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
1287
+ * AJV produces "must be null" and "must match a schema in anyOf" errors
1288
+ * that are confusing. This method splices them out, keeping only the real errors.
1289
+ */
1290
+ function filterNullableAnyOfErrors(errors, schema) {
1291
+ // Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
1292
+ const exactPaths = [];
1293
+ const nullBranchPrefixes = [];
1294
+ for (const error of errors) {
1295
+ if (error.keyword !== 'anyOf')
1296
+ continue;
1297
+ const parentSchema = resolveSchemaPath(schema, error.schemaPath);
1298
+ if (!parentSchema)
1299
+ continue;
1300
+ const nullIndex = unwrapNullableAnyOfIndex(parentSchema);
1301
+ if (nullIndex === -1)
1302
+ continue;
1303
+ exactPaths.push(error.schemaPath); // e.g. "#/anyOf"
1304
+ const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length);
1305
+ nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`); // e.g. "#/anyOf/1/"
1306
+ }
1307
+ if (!exactPaths.length)
1308
+ return;
1309
+ for (let i = errors.length - 1; i >= 0; i--) {
1310
+ const sp = errors[i].schemaPath;
1311
+ if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
1312
+ errors.splice(i, 1);
1313
+ }
1314
+ }
1315
+ }
1316
+ /**
1317
+ * Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
1318
+ * and returns the parent schema containing the last keyword.
1319
+ */
1320
+ function resolveSchemaPath(schema, schemaPath) {
1321
+ // schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
1322
+ // We want the schema that contains the final keyword (e.g. "anyOf")
1323
+ const segments = schemaPath.replace(/^#\//, '').split('/');
1324
+ // Remove the last segment (the keyword itself, e.g. "anyOf")
1325
+ segments.pop();
1326
+ let current = schema;
1327
+ for (const segment of segments) {
1328
+ if (!current || typeof current !== 'object')
1329
+ return undefined;
1330
+ current = current[segment];
1331
+ }
1332
+ return current;
1333
+ }
1334
+ function getErrorMessageForInstancePath(schema, instancePath, keyword) {
1335
+ if (!schema || !instancePath)
1336
+ return undefined;
1337
+ const segments = instancePath.split('/').filter(Boolean);
1338
+ return traverseSchemaPath(schema, segments, keyword);
1339
+ }
1340
+ function traverseSchemaPath(schema, segments, keyword) {
1341
+ if (!segments.length)
1342
+ return undefined;
1343
+ const [currentSegment, ...remainingSegments] = segments;
1344
+ const nextSchema = getChildSchema(schema, currentSegment);
1345
+ if (!nextSchema)
1346
+ return undefined;
1347
+ if (nextSchema.errorMessages?.[keyword]) {
1348
+ return nextSchema.errorMessages[keyword];
1349
+ }
1350
+ // Check through nullable wrapper
1351
+ const unwrapped = unwrapNullableAnyOf(nextSchema);
1352
+ if (unwrapped?.errorMessages?.[keyword]) {
1353
+ return unwrapped.errorMessages[keyword];
1354
+ }
1355
+ if (remainingSegments.length) {
1356
+ return traverseSchemaPath(nextSchema, remainingSegments, keyword);
1357
+ }
1358
+ return undefined;
1359
+ }
1360
+ function getChildSchema(schema, segment) {
1361
+ if (!segment)
1362
+ return undefined;
1363
+ // Unwrap nullable anyOf to find properties/items through nullable wrappers
1364
+ const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema;
1365
+ if (/^\d+$/.test(segment) && effectiveSchema.items) {
1366
+ return getArrayItemSchema(effectiveSchema, segment);
1367
+ }
1368
+ return getObjectPropertySchema(effectiveSchema, segment);
1369
+ }
1370
+ function getArrayItemSchema(schema, indexSegment) {
1371
+ if (!schema.items)
1372
+ return undefined;
1373
+ if (Array.isArray(schema.items)) {
1374
+ return schema.items[Number(indexSegment)];
1375
+ }
1376
+ return schema.items;
1377
+ }
1378
+ function getObjectPropertySchema(schema, segment) {
1379
+ return schema.properties?.[segment];
1380
+ }
1381
+ function unwrapNullableAnyOf(schema) {
1382
+ const nullIndex = unwrapNullableAnyOfIndex(schema);
1383
+ if (nullIndex === -1)
1384
+ return undefined;
1385
+ return schema.anyOf[1 - nullIndex];
1386
+ }
1387
+ function unwrapNullableAnyOfIndex(schema) {
1388
+ if (schema.anyOf?.length !== 2)
1389
+ return -1;
1390
+ const nullIndex = schema.anyOf.findIndex(s => s.type === 'null');
1391
+ return nullIndex;
1392
+ }
1393
+ // ==== Utility helpers ====
1394
+ function addPropertiesToSchema(schema, props) {
1395
+ const properties = {};
1396
+ const required = [];
1397
+ for (const [key, builder] of Object.entries(props)) {
1398
+ const isOptional = builder.getSchema().optionalField;
1399
+ if (!isOptional) {
1400
+ required.push(key);
1401
+ }
1402
+ const builtSchema = builder.build();
1403
+ properties[key] = builtSchema;
1404
+ }
1405
+ schema.properties = properties;
1406
+ schema.required = _uniq(required).sort();
1414
1407
  }
1415
1408
  function hasNoObjectSchemas(schema) {
1416
1409
  if (Array.isArray(schema.type)) {
1417
- schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
1410
+ return schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
1418
1411
  }
1419
1412
  else if (schema.anyOf) {
1420
1413
  return schema.anyOf.every(hasNoObjectSchemas);