@naturalcycles/nodejs-lib 15.93.0 → 15.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,29 @@ 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
- getValidationFunction() {
553
- return this.ajvSchema.getValidationFunction();
313
+ getValidationFunction(opt = {}) {
314
+ return (input, opt2) => {
315
+ return this.getValidationResult(input, {
316
+ ajv: opt.ajv,
317
+ mutateInput: opt2?.mutateInput ?? opt.mutateInput,
318
+ inputName: opt2?.inputName ?? opt.inputName,
319
+ inputId: opt2?.inputId ?? opt.inputId,
320
+ });
321
+ };
554
322
  }
555
323
  /**
556
324
  * Specify a function to be called after the normal validation is finished.
@@ -573,7 +341,8 @@ export class JsonSchemaTerminal {
573
341
  out;
574
342
  opt;
575
343
  }
576
- export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
344
+ // ==== JBuilder (chainable base) ====
345
+ export class JBuilder extends JSchema {
577
346
  setErrorMessage(ruleName, errorMessage) {
578
347
  if (_isUndefined(errorMessage))
579
348
  return;
@@ -585,15 +354,6 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
585
354
  *
586
355
  * When the type inferred from the schema differs from the passed-in type,
587
356
  * 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
357
  */
598
358
  isOfType() {
599
359
  return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true });
@@ -630,7 +390,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
630
390
  return clone;
631
391
  }
632
392
  nullable() {
633
- return new JsonSchemaAnyBuilder({
393
+ return new JBuilder({
634
394
  anyOf: [this.build(), { type: 'null' }],
635
395
  });
636
396
  }
@@ -645,7 +405,7 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
645
405
  * Locks the given schema chain and no other modification can be done to it.
646
406
  */
647
407
  final() {
648
- return new JsonSchemaTerminal(this.schema);
408
+ return new JSchema(this.schema);
649
409
  }
650
410
  /**
651
411
  *
@@ -676,7 +436,13 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
676
436
  });
677
437
  }
678
438
  }
679
- export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
439
+ // ==== Consts
440
+ const TS_2500 = 16725225600; // 2500-01-01
441
+ const TS_2500_MILLIS = TS_2500 * 1000;
442
+ const TS_2000 = 946684800; // 2000-01-01
443
+ const TS_2000_MILLIS = TS_2000 * 1000;
444
+ // ==== Type-specific builders ====
445
+ export class JString extends JBuilder {
680
446
  constructor() {
681
447
  super({
682
448
  type: 'string',
@@ -690,7 +456,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
690
456
  *
691
457
  * Make sure this `optional()` call is at the end of your call chain.
692
458
  *
693
- * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
459
+ * When `null` is included in optionalValues, the return type becomes `JSchema`
694
460
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
695
461
  */
696
462
  optional(optionalValues) {
@@ -698,7 +464,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
698
464
  return super.optional();
699
465
  }
700
466
  _typeCast(optionalValues);
701
- let newBuilder = new JsonSchemaStringBuilder().optional();
467
+ let newBuilder = new JString().optional();
702
468
  const alternativesSchema = j.enum(optionalValues);
703
469
  Object.assign(newBuilder.getSchema(), {
704
470
  anyOf: [this.build(), alternativesSchema.build()],
@@ -709,7 +475,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
709
475
  // but the typing should not reflect that.
710
476
  // We also cannot accept more rules attached, since we're not building a StringSchema anymore.
711
477
  if (optionalValues.includes(null)) {
712
- newBuilder = new JsonSchemaTerminal({
478
+ newBuilder = new JSchema({
713
479
  anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
714
480
  optionalField: true,
715
481
  });
@@ -772,13 +538,16 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
772
538
  * because this call effectively starts a new schema chain.
773
539
  */
774
540
  isoDate() {
775
- return new JsonSchemaIsoDateBuilder();
541
+ return new JIsoDate();
776
542
  }
777
543
  isoDateTime() {
778
544
  return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
779
545
  }
780
546
  isoMonth() {
781
- return new JsonSchemaIsoMonthBuilder();
547
+ return new JBuilder({
548
+ type: 'string',
549
+ IsoMonth: {},
550
+ });
782
551
  }
783
552
  /**
784
553
  * Validates the string format to be JWT.
@@ -830,7 +599,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
830
599
  return this.regex(UUID_REGEX, { msg: 'is an invalid UUID' });
831
600
  }
832
601
  }
833
- export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
602
+ export class JIsoDate extends JBuilder {
834
603
  constructor() {
835
604
  super({
836
605
  type: 'string',
@@ -843,7 +612,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
843
612
  * This `null` feature only works when the current schema is nested in an object or array schema,
844
613
  * due to how mutability works in Ajv.
845
614
  *
846
- * When `null` is passed, the return type becomes `JsonSchemaTerminal`
615
+ * When `null` is passed, the return type becomes `JSchema`
847
616
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
848
617
  */
849
618
  optional(nullValue) {
@@ -854,7 +623,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
854
623
  // so we must allow `null` values to be parsed by Ajv,
855
624
  // but the typing should not reflect that.
856
625
  // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
857
- return new JsonSchemaTerminal({
626
+ return new JSchema({
858
627
  anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
859
628
  optionalField: true,
860
629
  });
@@ -882,15 +651,7 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
882
651
  return this.cloneAndUpdateSchema(schemaPatch);
883
652
  }
884
653
  }
885
- export class JsonSchemaIsoMonthBuilder extends JsonSchemaAnyBuilder {
886
- constructor() {
887
- super({
888
- type: 'string',
889
- IsoMonth: {},
890
- });
891
- }
892
- }
893
- export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
654
+ export class JNumber extends JBuilder {
894
655
  constructor() {
895
656
  super({
896
657
  type: 'number',
@@ -904,7 +665,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
904
665
  *
905
666
  * Make sure this `optional()` call is at the end of your call chain.
906
667
  *
907
- * When `null` is included in optionalValues, the return type becomes `JsonSchemaTerminal`
668
+ * When `null` is included in optionalValues, the return type becomes `JSchema`
908
669
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
909
670
  */
910
671
  optional(optionalValues) {
@@ -912,7 +673,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
912
673
  return super.optional();
913
674
  }
914
675
  _typeCast(optionalValues);
915
- let newBuilder = new JsonSchemaNumberBuilder().optional();
676
+ let newBuilder = new JNumber().optional();
916
677
  const alternativesSchema = j.enum(optionalValues);
917
678
  Object.assign(newBuilder.getSchema(), {
918
679
  anyOf: [this.build(), alternativesSchema.build()],
@@ -923,7 +684,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
923
684
  // but the typing should not reflect that.
924
685
  // We also cannot accept more rules attached, since we're not building a NumberSchema anymore.
925
686
  if (optionalValues.includes(null)) {
926
- newBuilder = new JsonSchemaTerminal({
687
+ newBuilder = new JSchema({
927
688
  anyOf: [{ type: 'null', optionalValues }, newBuilder.build()],
928
689
  optionalField: true,
929
690
  });
@@ -1024,7 +785,7 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
1024
785
  return this.cloneAndUpdateSchema({ precision: numberOfDigits });
1025
786
  }
1026
787
  }
1027
- export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
788
+ export class JBoolean extends JBuilder {
1028
789
  constructor() {
1029
790
  super({
1030
791
  type: 'boolean',
@@ -1040,7 +801,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
1040
801
  if (typeof optionalValue === 'undefined') {
1041
802
  return super.optional();
1042
803
  }
1043
- const newBuilder = new JsonSchemaBooleanBuilder().optional();
804
+ const newBuilder = new JBoolean().optional();
1044
805
  const alternativesSchema = j.enum([optionalValue]);
1045
806
  Object.assign(newBuilder.getSchema(), {
1046
807
  anyOf: [this.build(), alternativesSchema.build()],
@@ -1049,7 +810,7 @@ export class JsonSchemaBooleanBuilder extends JsonSchemaAnyBuilder {
1049
810
  return newBuilder;
1050
811
  }
1051
812
  }
1052
- export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
813
+ export class JObject extends JBuilder {
1053
814
  constructor(props, opt) {
1054
815
  super({
1055
816
  type: 'object',
@@ -1061,22 +822,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1061
822
  keySchema: opt?.keySchema ?? undefined,
1062
823
  });
1063
824
  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;
825
+ addPropertiesToSchema(this.schema, props);
1080
826
  }
1081
827
  /**
1082
828
  * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
@@ -1084,7 +830,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1084
830
  * This `null` feature only works when the current schema is nested in an object or array schema,
1085
831
  * due to how mutability works in Ajv.
1086
832
  *
1087
- * When `null` is passed, the return type becomes `JsonSchemaTerminal`
833
+ * When `null` is passed, the return type becomes `JSchema`
1088
834
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
1089
835
  */
1090
836
  optional(nullValue) {
@@ -1095,7 +841,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1095
841
  // so we must allow `null` values to be parsed by Ajv,
1096
842
  // but the typing should not reflect that.
1097
843
  // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
1098
- return new JsonSchemaTerminal({
844
+ return new JSchema({
1099
845
  anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
1100
846
  optionalField: true,
1101
847
  });
@@ -1107,9 +853,9 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1107
853
  return this.cloneAndUpdateSchema({ additionalProperties: true });
1108
854
  }
1109
855
  extend(props) {
1110
- const newBuilder = new JsonSchemaObjectBuilder();
856
+ const newBuilder = new JObject();
1111
857
  _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
1112
- const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
858
+ const incomingSchemaBuilder = new JObject(props);
1113
859
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
1114
860
  _objectAssign(newBuilder.schema, { hasIsOfTypeCheck: false });
1115
861
  return newBuilder;
@@ -1120,17 +866,6 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1120
866
  * It expects you to use `isOfType<T>()` in the chain,
1121
867
  * otherwise the validation will throw. This is to ensure
1122
868
  * 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
869
  */
1135
870
  concat(other) {
1136
871
  const clone = this.clone();
@@ -1160,7 +895,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
1160
895
  return this.cloneAndUpdateSchema({ exclusiveProperties: [...exclusiveProperties, propNames] });
1161
896
  }
1162
897
  }
1163
- export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
898
+ export class JObjectInfer extends JBuilder {
1164
899
  constructor(props) {
1165
900
  super({
1166
901
  type: 'object',
@@ -1169,22 +904,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1169
904
  additionalProperties: false,
1170
905
  });
1171
906
  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;
907
+ addPropertiesToSchema(this.schema, props);
1188
908
  }
1189
909
  /**
1190
910
  * @param nullValue Pass `null` to have `null` values be considered/converted as `undefined`.
@@ -1192,7 +912,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1192
912
  * This `null` feature only works when the current schema is nested in an object or array schema,
1193
913
  * due to how mutability works in Ajv.
1194
914
  *
1195
- * When `null` is passed, the return type becomes `JsonSchemaTerminal`
915
+ * When `null` is passed, the return type becomes `JSchema`
1196
916
  * (no further chaining allowed) because the schema is wrapped in an anyOf structure.
1197
917
  */
1198
918
  // @ts-expect-error override adds optional parameter which is compatible but TS can't verify complex mapped types
@@ -1204,7 +924,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1204
924
  // so we must allow `null` values to be parsed by Ajv,
1205
925
  // but the typing should not reflect that.
1206
926
  // We also cannot accept more rules attached, since we're not building an ObjectSchema anymore.
1207
- return new JsonSchemaTerminal({
927
+ return new JSchema({
1208
928
  anyOf: [{ type: 'null', optionalValues: [null] }, this.build()],
1209
929
  optionalField: true,
1210
930
  });
@@ -1216,9 +936,9 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1216
936
  return this.cloneAndUpdateSchema({ additionalProperties: true });
1217
937
  }
1218
938
  extend(props) {
1219
- const newBuilder = new JsonSchemaObjectInferringBuilder();
939
+ const newBuilder = new JObjectInfer();
1220
940
  _objectAssign(newBuilder.schema, deepCopyPreservingFunctions(this.schema));
1221
- const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
941
+ const incomingSchemaBuilder = new JObjectInfer(props);
1222
942
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
1223
943
  // This extend function is not type-safe as it is inferring,
1224
944
  // so even if the base schema was already type-checked,
@@ -1238,7 +958,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
1238
958
  });
1239
959
  }
1240
960
  }
1241
- export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
961
+ export class JArray extends JBuilder {
1242
962
  constructor(itemsSchema) {
1243
963
  super({
1244
964
  type: 'array',
@@ -1262,7 +982,7 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
1262
982
  return this.cloneAndUpdateSchema({ uniqueItems: true });
1263
983
  }
1264
984
  }
1265
- export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
985
+ class JSet2Builder extends JBuilder {
1266
986
  constructor(itemsSchema) {
1267
987
  super({
1268
988
  type: ['array', 'object'],
@@ -1276,14 +996,7 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
1276
996
  return this.cloneAndUpdateSchema({ maxItems });
1277
997
  }
1278
998
  }
1279
- export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
1280
- constructor() {
1281
- super({
1282
- Buffer: true,
1283
- });
1284
- }
1285
- }
1286
- export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
999
+ export class JEnum extends JBuilder {
1287
1000
  constructor(enumValues, baseType, opt) {
1288
1001
  const jsonSchema = { enum: enumValues };
1289
1002
  // Specifying the base type helps in cases when we ask Ajv to coerce the types.
@@ -1302,7 +1015,7 @@ export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
1302
1015
  return this;
1303
1016
  }
1304
1017
  }
1305
- export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
1018
+ export class JTuple extends JBuilder {
1306
1019
  constructor(items) {
1307
1020
  super({
1308
1021
  type: 'array',
@@ -1312,43 +1025,11 @@ export class JsonSchemaTupleBuilder extends JsonSchemaAnyBuilder {
1312
1025
  });
1313
1026
  }
1314
1027
  }
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
1028
  function object(props) {
1348
- return new JsonSchemaObjectBuilder(props);
1029
+ return new JObject(props);
1349
1030
  }
1350
1031
  function objectInfer(props) {
1351
- return new JsonSchemaObjectInferringBuilder(props);
1032
+ return new JObjectInfer(props);
1352
1033
  }
1353
1034
  function objectDbEntity(props) {
1354
1035
  return j.object({
@@ -1367,7 +1048,7 @@ function record(keySchema, valueSchema) {
1367
1048
  const finalValueSchema = isValueOptional
1368
1049
  ? { anyOf: [{ isUndefined: true }, valueJsonSchema] }
1369
1050
  : valueJsonSchema;
1370
- return new JsonSchemaObjectBuilder([], {
1051
+ return new JObject([], {
1371
1052
  hasIsOfTypeCheck: false,
1372
1053
  keySchema: keyJsonSchema,
1373
1054
  patternProperties: {
@@ -1381,7 +1062,7 @@ function withRegexKeys(keyRegex, schema) {
1381
1062
  }
1382
1063
  const pattern = keyRegex instanceof RegExp ? keyRegex.source : keyRegex;
1383
1064
  const jsonSchema = schema.build();
1384
- return new JsonSchemaObjectBuilder([], {
1065
+ return new JObject([], {
1385
1066
  hasIsOfTypeCheck: false,
1386
1067
  patternProperties: {
1387
1068
  [pattern]: jsonSchema,
@@ -1410,11 +1091,324 @@ function withEnumKeys(keys, schema) {
1410
1091
  _assert(enumValues, 'The key list should be an array of values, NumberEnum or a StringEnum');
1411
1092
  const typedValues = enumValues;
1412
1093
  const props = Object.fromEntries(typedValues.map(key => [key, schema]));
1413
- return new JsonSchemaObjectBuilder(props, { hasIsOfTypeCheck: false });
1094
+ return new JObject(props, { hasIsOfTypeCheck: false });
1095
+ }
1096
+ // ==== AjvSchema compat wrapper ====
1097
+ /**
1098
+ * On creation - compiles ajv validation function.
1099
+ * Provides convenient methods, error reporting, etc.
1100
+ */
1101
+ export class AjvSchema {
1102
+ schema;
1103
+ constructor(schema, cfg = {}, preCompiledFn) {
1104
+ this.schema = schema;
1105
+ this.cfg = {
1106
+ lazy: false,
1107
+ ...cfg,
1108
+ ajv: cfg.ajv || getAjv(),
1109
+ // Auto-detecting "InputName" from $id of the schema (e.g "Address.schema.json")
1110
+ inputName: cfg.inputName || (schema.$id ? _substringBefore(schema.$id, '.') : undefined),
1111
+ };
1112
+ if (preCompiledFn) {
1113
+ this._compiledFn = preCompiledFn;
1114
+ }
1115
+ else if (!cfg.lazy) {
1116
+ this._getValidateFn(); // compile eagerly
1117
+ }
1118
+ }
1119
+ /**
1120
+ * Shortcut for AjvSchema.create(schema, { lazy: true })
1121
+ */
1122
+ static createLazy(schema, cfg) {
1123
+ return AjvSchema.create(schema, {
1124
+ lazy: true,
1125
+ ...cfg,
1126
+ });
1127
+ }
1128
+ /**
1129
+ * Conveniently allows to pass either JsonSchema or JSchema builder, or existing AjvSchema.
1130
+ * If it's already an AjvSchema - it'll just return it without any processing.
1131
+ * If it's a Builder - will call `build` before proceeding.
1132
+ * Otherwise - will construct AjvSchema instance ready to be used.
1133
+ */
1134
+ static create(schema, cfg) {
1135
+ if (schema instanceof AjvSchema)
1136
+ return schema;
1137
+ if (AjvSchema.isSchemaWithCachedAjvSchema(schema)) {
1138
+ return AjvSchema.requireCachedAjvSchema(schema);
1139
+ }
1140
+ let jsonSchema;
1141
+ if (schema instanceof JSchema) {
1142
+ // oxlint-disable typescript-eslint(no-unnecessary-type-assertion)
1143
+ jsonSchema = schema.build();
1144
+ AjvSchema.requireValidJsonSchema(jsonSchema);
1145
+ }
1146
+ else {
1147
+ jsonSchema = schema;
1148
+ }
1149
+ // This is our own helper which marks a schema as optional
1150
+ // in case it is going to be used in an object schema,
1151
+ // where we need to mark the given property as not-required.
1152
+ // But once all compilation is done, the presence of this field
1153
+ // really upsets Ajv.
1154
+ delete jsonSchema.optionalField;
1155
+ const ajvSchema = new AjvSchema(jsonSchema, cfg);
1156
+ AjvSchema.cacheAjvSchema(schema, ajvSchema);
1157
+ return ajvSchema;
1158
+ }
1159
+ /**
1160
+ * Creates a minimal AjvSchema wrapper from a pre-compiled validate function.
1161
+ * Used internally by JSchema to cache a compatible AjvSchema instance.
1162
+ */
1163
+ static _wrap(schema, compiledFn) {
1164
+ return new AjvSchema(schema, {}, compiledFn);
1165
+ }
1166
+ static isSchemaWithCachedAjvSchema(schema) {
1167
+ return !!schema?.[HIDDEN_AJV_SCHEMA];
1168
+ }
1169
+ static cacheAjvSchema(schema, ajvSchema) {
1170
+ return Object.assign(schema, { [HIDDEN_AJV_SCHEMA]: ajvSchema });
1171
+ }
1172
+ static requireCachedAjvSchema(schema) {
1173
+ return schema[HIDDEN_AJV_SCHEMA];
1174
+ }
1175
+ cfg;
1176
+ _compiledFn;
1177
+ _getValidateFn() {
1178
+ if (!this._compiledFn) {
1179
+ this._compiledFn = this.cfg.ajv.compile(this.schema);
1180
+ }
1181
+ return this._compiledFn;
1182
+ }
1183
+ /**
1184
+ * It returns the original object just for convenience.
1185
+ */
1186
+ validate(input, opt = {}) {
1187
+ const [err, output] = this.getValidationResult(input, opt);
1188
+ if (err)
1189
+ throw err;
1190
+ return output;
1191
+ }
1192
+ isValid(input, opt) {
1193
+ const [err] = this.getValidationResult(input, opt);
1194
+ return !err;
1195
+ }
1196
+ getValidationResult(input, opt = {}) {
1197
+ const fn = this._getValidateFn();
1198
+ return executeValidation(fn, this.schema, input, opt, this.cfg.inputName);
1199
+ }
1200
+ getValidationFunction() {
1201
+ return (input, opt) => {
1202
+ return this.getValidationResult(input, {
1203
+ mutateInput: opt?.mutateInput,
1204
+ inputName: opt?.inputName,
1205
+ inputId: opt?.inputId,
1206
+ });
1207
+ };
1208
+ }
1209
+ static requireValidJsonSchema(schema) {
1210
+ // For object schemas we require that it is type checked against an external type, e.g.:
1211
+ // interface Foo { name: string }
1212
+ // const schema = j.object({ name: j.string() }).ofType<Foo>()
1213
+ _assert(schema.type !== 'object' || schema.hasIsOfTypeCheck, 'The schema must be type checked against a type or interface, using the `.isOfType()` helper in `j`.');
1214
+ }
1215
+ }
1216
+ // ==== Shared validation logic ====
1217
+ const separator = '\n';
1218
+ function executeValidation(fn, builtSchema, input, opt = {}, defaultInputName) {
1219
+ const item = opt.mutateInput !== false || typeof input !== 'object'
1220
+ ? input // mutate
1221
+ : _deepCopy(input); // not mutate
1222
+ let valid = fn(item); // mutates item, but not input
1223
+ _typeCast(item);
1224
+ let output = item;
1225
+ if (valid && builtSchema.postValidation) {
1226
+ const [err, result] = _try(() => builtSchema.postValidation(output));
1227
+ if (err) {
1228
+ valid = false;
1229
+ fn.errors = [
1230
+ {
1231
+ instancePath: '',
1232
+ message: err.message,
1233
+ },
1234
+ ];
1235
+ }
1236
+ else {
1237
+ output = result;
1238
+ }
1239
+ }
1240
+ if (valid)
1241
+ return [null, output];
1242
+ const errors = fn.errors;
1243
+ const { inputId = _isObject(input) ? input['id'] : undefined, inputName = defaultInputName || 'Object', } = opt;
1244
+ const dataVar = [inputName, inputId].filter(Boolean).join('.');
1245
+ applyImprovementsOnErrorMessages(errors, builtSchema);
1246
+ let message = getAjv().errorsText(errors, {
1247
+ dataVar,
1248
+ separator,
1249
+ });
1250
+ // Note: if we mutated the input already, e.g stripped unknown properties,
1251
+ // the error message Input would contain already mutated object print, such as Input: {}
1252
+ // Unless `getOriginalInput` function is provided - then it will be used to preserve the Input pureness.
1253
+ const inputStringified = _inspect(opt.getOriginalInput?.() || input, { maxLen: 4000 });
1254
+ message = [message, 'Input: ' + inputStringified].join(separator);
1255
+ const err = new AjvValidationError(message, _filterNullishValues({
1256
+ errors,
1257
+ inputName,
1258
+ inputId,
1259
+ }));
1260
+ return [err, output];
1261
+ }
1262
+ // ==== Error formatting helpers ====
1263
+ function applyImprovementsOnErrorMessages(errors, schema) {
1264
+ if (!errors)
1265
+ return;
1266
+ filterNullableAnyOfErrors(errors, schema);
1267
+ const { errorMessages } = schema;
1268
+ for (const error of errors) {
1269
+ const errorMessage = getErrorMessageForInstancePath(schema, error.instancePath, error.keyword);
1270
+ if (errorMessage) {
1271
+ error.message = errorMessage;
1272
+ }
1273
+ else if (errorMessages?.[error.keyword]) {
1274
+ error.message = errorMessages[error.keyword];
1275
+ }
1276
+ else {
1277
+ const unwrapped = unwrapNullableAnyOf(schema);
1278
+ if (unwrapped?.errorMessages?.[error.keyword]) {
1279
+ error.message = unwrapped.errorMessages[error.keyword];
1280
+ }
1281
+ }
1282
+ error.instancePath = error.instancePath.replaceAll(/\/(\d+)/g, `[$1]`).replaceAll('/', '.');
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Filters out noisy errors produced by nullable anyOf patterns.
1287
+ * When `nullable()` wraps a schema in `anyOf: [realSchema, { type: 'null' }]`,
1288
+ * AJV produces "must be null" and "must match a schema in anyOf" errors
1289
+ * that are confusing. This method splices them out, keeping only the real errors.
1290
+ */
1291
+ function filterNullableAnyOfErrors(errors, schema) {
1292
+ // Collect exact schemaPaths to remove (anyOf aggregates) and prefixes (null branches)
1293
+ const exactPaths = [];
1294
+ const nullBranchPrefixes = [];
1295
+ for (const error of errors) {
1296
+ if (error.keyword !== 'anyOf')
1297
+ continue;
1298
+ const parentSchema = resolveSchemaPath(schema, error.schemaPath);
1299
+ if (!parentSchema)
1300
+ continue;
1301
+ const nullIndex = unwrapNullableAnyOfIndex(parentSchema);
1302
+ if (nullIndex === -1)
1303
+ continue;
1304
+ exactPaths.push(error.schemaPath); // e.g. "#/anyOf"
1305
+ const anyOfBase = error.schemaPath.slice(0, -'anyOf'.length);
1306
+ nullBranchPrefixes.push(`${anyOfBase}anyOf/${nullIndex}/`); // e.g. "#/anyOf/1/"
1307
+ }
1308
+ if (!exactPaths.length)
1309
+ return;
1310
+ for (let i = errors.length - 1; i >= 0; i--) {
1311
+ const sp = errors[i].schemaPath;
1312
+ if (exactPaths.includes(sp) || nullBranchPrefixes.some(p => sp.startsWith(p))) {
1313
+ errors.splice(i, 1);
1314
+ }
1315
+ }
1316
+ }
1317
+ /**
1318
+ * Navigates the schema tree using an AJV schemaPath (e.g. "#/properties/foo/anyOf")
1319
+ * and returns the parent schema containing the last keyword.
1320
+ */
1321
+ function resolveSchemaPath(schema, schemaPath) {
1322
+ // schemaPath looks like "#/properties/foo/anyOf" or "#/anyOf"
1323
+ // We want the schema that contains the final keyword (e.g. "anyOf")
1324
+ const segments = schemaPath.replace(/^#\//, '').split('/');
1325
+ // Remove the last segment (the keyword itself, e.g. "anyOf")
1326
+ segments.pop();
1327
+ let current = schema;
1328
+ for (const segment of segments) {
1329
+ if (!current || typeof current !== 'object')
1330
+ return undefined;
1331
+ current = current[segment];
1332
+ }
1333
+ return current;
1334
+ }
1335
+ function getErrorMessageForInstancePath(schema, instancePath, keyword) {
1336
+ if (!schema || !instancePath)
1337
+ return undefined;
1338
+ const segments = instancePath.split('/').filter(Boolean);
1339
+ return traverseSchemaPath(schema, segments, keyword);
1340
+ }
1341
+ function traverseSchemaPath(schema, segments, keyword) {
1342
+ if (!segments.length)
1343
+ return undefined;
1344
+ const [currentSegment, ...remainingSegments] = segments;
1345
+ const nextSchema = getChildSchema(schema, currentSegment);
1346
+ if (!nextSchema)
1347
+ return undefined;
1348
+ if (nextSchema.errorMessages?.[keyword]) {
1349
+ return nextSchema.errorMessages[keyword];
1350
+ }
1351
+ // Check through nullable wrapper
1352
+ const unwrapped = unwrapNullableAnyOf(nextSchema);
1353
+ if (unwrapped?.errorMessages?.[keyword]) {
1354
+ return unwrapped.errorMessages[keyword];
1355
+ }
1356
+ if (remainingSegments.length) {
1357
+ return traverseSchemaPath(nextSchema, remainingSegments, keyword);
1358
+ }
1359
+ return undefined;
1360
+ }
1361
+ function getChildSchema(schema, segment) {
1362
+ if (!segment)
1363
+ return undefined;
1364
+ // Unwrap nullable anyOf to find properties/items through nullable wrappers
1365
+ const effectiveSchema = unwrapNullableAnyOf(schema) ?? schema;
1366
+ if (/^\d+$/.test(segment) && effectiveSchema.items) {
1367
+ return getArrayItemSchema(effectiveSchema, segment);
1368
+ }
1369
+ return getObjectPropertySchema(effectiveSchema, segment);
1370
+ }
1371
+ function getArrayItemSchema(schema, indexSegment) {
1372
+ if (!schema.items)
1373
+ return undefined;
1374
+ if (Array.isArray(schema.items)) {
1375
+ return schema.items[Number(indexSegment)];
1376
+ }
1377
+ return schema.items;
1378
+ }
1379
+ function getObjectPropertySchema(schema, segment) {
1380
+ return schema.properties?.[segment];
1381
+ }
1382
+ function unwrapNullableAnyOf(schema) {
1383
+ const nullIndex = unwrapNullableAnyOfIndex(schema);
1384
+ if (nullIndex === -1)
1385
+ return undefined;
1386
+ return schema.anyOf[1 - nullIndex];
1387
+ }
1388
+ function unwrapNullableAnyOfIndex(schema) {
1389
+ if (schema.anyOf?.length !== 2)
1390
+ return -1;
1391
+ const nullIndex = schema.anyOf.findIndex(s => s.type === 'null');
1392
+ return nullIndex;
1393
+ }
1394
+ // ==== Utility helpers ====
1395
+ function addPropertiesToSchema(schema, props) {
1396
+ const properties = {};
1397
+ const required = [];
1398
+ for (const [key, builder] of Object.entries(props)) {
1399
+ const isOptional = builder.getSchema().optionalField;
1400
+ if (!isOptional) {
1401
+ required.push(key);
1402
+ }
1403
+ const builtSchema = builder.build();
1404
+ properties[key] = builtSchema;
1405
+ }
1406
+ schema.properties = properties;
1407
+ schema.required = _uniq(required).sort();
1414
1408
  }
1415
1409
  function hasNoObjectSchemas(schema) {
1416
1410
  if (Array.isArray(schema.type)) {
1417
- schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
1411
+ return schema.type.every(type => ['string', 'number', 'integer', 'boolean', 'null'].includes(type));
1418
1412
  }
1419
1413
  else if (schema.anyOf) {
1420
1414
  return schema.anyOf.every(hasNoObjectSchemas);