@naturalcycles/nodejs-lib 15.59.0 → 15.61.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,4 +1,4 @@
1
- import { _lazyValue } from '@naturalcycles/js-lib';
1
+ import { _isBetween, _lazyValue } from '@naturalcycles/js-lib';
2
2
  import { Set2 } from '@naturalcycles/js-lib/object';
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string';
4
4
  import { Ajv } from 'ajv';
@@ -349,6 +349,25 @@ export function createAjv(opt) {
349
349
  return false;
350
350
  },
351
351
  });
352
+ ajv.addKeyword({
353
+ keyword: 'IsoMonth',
354
+ type: 'string',
355
+ modifying: false,
356
+ errors: true,
357
+ schemaType: 'object',
358
+ validate: function validate(_opt, data, _schema, ctx) {
359
+ const isValid = isIsoMonthValid(data);
360
+ if (isValid)
361
+ return true;
362
+ validate.errors = [
363
+ {
364
+ instancePath: ctx?.instancePath ?? '',
365
+ message: `is an invalid IsoMonth`,
366
+ },
367
+ ];
368
+ return false;
369
+ },
370
+ });
352
371
  ajv.addKeyword({
353
372
  keyword: 'errorMessages',
354
373
  schemaType: 'object',
@@ -516,3 +535,21 @@ function isIsoTimezoneValid(s) {
516
535
  return false; // min is -12:00
517
536
  return true;
518
537
  }
538
+ /**
539
+ * This is a performance optimized correct validation
540
+ * for ISO month formatted as "YYYY-MM".
541
+ */
542
+ function isIsoMonthValid(s) {
543
+ // must be exactly "YYYY-MM"
544
+ if (s.length !== 7)
545
+ return false;
546
+ if (s.charCodeAt(4) !== DASH_CODE)
547
+ return false;
548
+ // fast parse numbers without substrings/Number()
549
+ const year = (s.charCodeAt(0) - ZERO_CODE) * 1000 +
550
+ (s.charCodeAt(1) - ZERO_CODE) * 100 +
551
+ (s.charCodeAt(2) - ZERO_CODE) * 10 +
552
+ (s.charCodeAt(3) - ZERO_CODE);
553
+ const month = (s.charCodeAt(5) - ZERO_CODE) * 10 + (s.charCodeAt(6) - ZERO_CODE);
554
+ return _isBetween(year, 1900, 2500, '[]') && _isBetween(month, 1, 12, '[]');
555
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Set2 } from '@naturalcycles/js-lib/object';
2
- import { type AnyObject, type BaseDBEntity, type IANATimezone, type Inclusiveness, type IsoDate, type IsoDateTime, type NumberEnum, type StringEnum, type StringMap, type UnixTimestamp, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
2
+ import { type AnyObject, type BaseDBEntity, type IANATimezone, type Inclusiveness, type IsoDate, type IsoDateTime, type IsoMonth, type NumberEnum, type StringEnum, type StringMap, type UnixTimestamp, type UnixTimestampMillis } from '@naturalcycles/js-lib/types';
3
3
  export declare const j: {
4
4
  string(): JsonSchemaStringBuilder<string, string, false>;
5
5
  number(): JsonSchemaNumberBuilder<number, number, false>;
@@ -62,12 +62,14 @@ export declare class JsonSchemaTerminal<IN, OUT, Opt> {
62
62
  * Same as if it would be JSON.stringified.
63
63
  */
64
64
  build(): JsonSchema<IN, OUT>;
65
- clone(): JsonSchemaAnyBuilder<IN, OUT, Opt>;
65
+ clone(): this;
66
+ cloneAndUpdateSchema(schema: Partial<JsonSchema>): this;
66
67
  /**
67
68
  * @experimental
68
69
  */
69
70
  in: IN;
70
71
  out: OUT;
72
+ opt: Opt;
71
73
  }
72
74
  export declare class JsonSchemaAnyBuilder<IN, OUT, Opt> extends JsonSchemaTerminal<IN, OUT, Opt> {
73
75
  protected setErrorMessage(ruleName: string, errorMessage: string | undefined): void;
@@ -137,6 +139,7 @@ export declare class JsonSchemaStringBuilder<IN extends string | undefined = str
137
139
  */
138
140
  isoDate(): JsonSchemaIsoDateBuilder;
139
141
  isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt>;
142
+ isoMonth(): JsonSchemaIsoMonthBuilder;
140
143
  /**
141
144
  * Validates the string format to be JWT.
142
145
  * Expects the JWT to be signed!
@@ -177,6 +180,11 @@ export interface JsonSchemaIsoDateOptions {
177
180
  after?: string;
178
181
  sameOrAfter?: string;
179
182
  }
183
+ export declare class JsonSchemaIsoMonthBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<string | IsoDate, IsoMonth, Opt> {
184
+ constructor();
185
+ }
186
+ export interface JsonSchemaIsoMonthOptions {
187
+ }
180
188
  export declare class JsonSchemaNumberBuilder<IN extends number | undefined = number, OUT = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
181
189
  constructor();
182
190
  /**
@@ -341,6 +349,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
341
349
  Buffer?: true;
342
350
  IsoDate?: JsonSchemaIsoDateOptions;
343
351
  IsoDateTime?: true;
352
+ IsoMonth?: JsonSchemaIsoMonthOptions;
344
353
  instanceof?: string | string[];
345
354
  transform?: {
346
355
  trim?: true;
@@ -147,13 +147,21 @@ export class JsonSchemaTerminal {
147
147
  return jsonSchema;
148
148
  }
149
149
  clone() {
150
- return new JsonSchemaAnyBuilder(_deepCopy(this.schema));
150
+ const cloned = Object.create(Object.getPrototypeOf(this));
151
+ cloned.schema = _deepCopy(this.schema);
152
+ return cloned;
153
+ }
154
+ cloneAndUpdateSchema(schema) {
155
+ const clone = this.clone();
156
+ _objectAssign(clone.schema, schema);
157
+ return clone;
151
158
  }
152
159
  /**
153
160
  * @experimental
154
161
  */
155
162
  in;
156
163
  out;
164
+ opt;
157
165
  }
158
166
  export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
159
167
  setErrorMessage(ruleName, errorMessage) {
@@ -178,48 +186,38 @@ export class JsonSchemaAnyBuilder extends JsonSchemaTerminal {
178
186
  * ```
179
187
  */
180
188
  isOfType() {
181
- _objectAssign(this.schema, { hasIsOfTypeCheck: true });
182
- return this;
189
+ return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true });
183
190
  }
184
191
  $schema($schema) {
185
- _objectAssign(this.schema, { $schema });
186
- return this;
192
+ return this.cloneAndUpdateSchema({ $schema });
187
193
  }
188
194
  $schemaDraft7() {
189
- this.$schema('http://json-schema.org/draft-07/schema#');
190
- return this;
195
+ return this.$schema('http://json-schema.org/draft-07/schema#');
191
196
  }
192
197
  $id($id) {
193
- _objectAssign(this.schema, { $id });
194
- return this;
198
+ return this.cloneAndUpdateSchema({ $id });
195
199
  }
196
200
  title(title) {
197
- _objectAssign(this.schema, { title });
198
- return this;
201
+ return this.cloneAndUpdateSchema({ title });
199
202
  }
200
203
  description(description) {
201
- _objectAssign(this.schema, { description });
202
- return this;
204
+ return this.cloneAndUpdateSchema({ description });
203
205
  }
204
206
  deprecated(deprecated = true) {
205
- _objectAssign(this.schema, { deprecated });
206
- return this;
207
+ return this.cloneAndUpdateSchema({ deprecated });
207
208
  }
208
209
  type(type) {
209
- _objectAssign(this.schema, { type });
210
- return this;
210
+ return this.cloneAndUpdateSchema({ type });
211
211
  }
212
212
  default(v) {
213
- _objectAssign(this.schema, { default: v });
214
- return this;
213
+ return this.cloneAndUpdateSchema({ default: v });
215
214
  }
216
215
  instanceof(of) {
217
- _objectAssign(this.schema, { type: 'object', instanceof: of });
218
- return this;
216
+ return this.cloneAndUpdateSchema({ type: 'object', instanceof: of });
219
217
  }
220
218
  optional() {
221
- this.schema.optionalField = true;
222
- return this;
219
+ const clone = this.cloneAndUpdateSchema({ optionalField: true });
220
+ return clone;
223
221
  }
224
222
  nullable() {
225
223
  return new JsonSchemaAnyBuilder({
@@ -268,20 +266,18 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
268
266
  return this.pattern(pattern.source, opt);
269
267
  }
270
268
  pattern(pattern, opt) {
269
+ const clone = this.cloneAndUpdateSchema({ pattern });
271
270
  if (opt?.name)
272
- this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
271
+ clone.setErrorMessage('pattern', `is not a valid ${opt.name}`);
273
272
  if (opt?.msg)
274
- this.setErrorMessage('pattern', opt.msg);
275
- _objectAssign(this.schema, { pattern });
276
- return this;
273
+ clone.setErrorMessage('pattern', opt.msg);
274
+ return clone;
277
275
  }
278
276
  minLength(minLength) {
279
- _objectAssign(this.schema, { minLength });
280
- return this;
277
+ return this.cloneAndUpdateSchema({ minLength });
281
278
  }
282
279
  maxLength(maxLength) {
283
- _objectAssign(this.schema, { maxLength });
284
- return this;
280
+ return this.cloneAndUpdateSchema({ maxLength });
285
281
  }
286
282
  length(minLengthOrExactLength, maxLength) {
287
283
  const maxLengthActual = maxLength ?? minLengthOrExactLength;
@@ -289,24 +285,27 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
289
285
  }
290
286
  email(opt) {
291
287
  const defaultOptions = { checkTLD: true };
292
- _objectAssign(this.schema, { email: { ...defaultOptions, ...opt } });
293
- return this.trim().toLowerCase();
288
+ return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
289
+ .trim()
290
+ .toLowerCase();
294
291
  }
295
292
  trim() {
296
- _objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } });
297
- return this;
293
+ return this.cloneAndUpdateSchema({ transform: { ...this.schema.transform, trim: true } });
298
294
  }
299
295
  toLowerCase() {
300
- _objectAssign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } });
301
- return this;
296
+ return this.cloneAndUpdateSchema({
297
+ transform: { ...this.schema.transform, toLowerCase: true },
298
+ });
302
299
  }
303
300
  toUpperCase() {
304
- _objectAssign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } });
305
- return this;
301
+ return this.cloneAndUpdateSchema({
302
+ transform: { ...this.schema.transform, toUpperCase: true },
303
+ });
306
304
  }
307
305
  truncate(toLength) {
308
- _objectAssign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } });
309
- return this;
306
+ return this.cloneAndUpdateSchema({
307
+ transform: { ...this.schema.transform, truncate: toLength },
308
+ });
310
309
  }
311
310
  branded() {
312
311
  return this;
@@ -318,12 +317,13 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
318
317
  * because this call effectively starts a new schema chain.
319
318
  */
320
319
  isoDate() {
321
- _objectAssign(this.schema, { IsoDate: {} });
322
320
  return new JsonSchemaIsoDateBuilder();
323
321
  }
324
322
  isoDateTime() {
325
- _objectAssign(this.schema, { IsoDateTime: true });
326
- return this.branded();
323
+ return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded();
324
+ }
325
+ isoMonth() {
326
+ return new JsonSchemaIsoMonthBuilder();
327
327
  }
328
328
  /**
329
329
  * Validates the string format to be JWT.
@@ -385,29 +385,34 @@ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
385
385
  });
386
386
  }
387
387
  before(date) {
388
- _objectAssign(this.schema.IsoDate, { before: date });
389
- return this;
388
+ return this.cloneAndUpdateSchema({ IsoDate: { before: date } });
390
389
  }
391
390
  sameOrBefore(date) {
392
- _objectAssign(this.schema.IsoDate, { sameOrBefore: date });
393
- return this;
391
+ return this.cloneAndUpdateSchema({ IsoDate: { sameOrBefore: date } });
394
392
  }
395
393
  after(date) {
396
- _objectAssign(this.schema.IsoDate, { after: date });
397
- return this;
394
+ return this.cloneAndUpdateSchema({ IsoDate: { after: date } });
398
395
  }
399
396
  sameOrAfter(date) {
400
- _objectAssign(this.schema.IsoDate, { sameOrAfter: date });
401
- return this;
397
+ return this.cloneAndUpdateSchema({ IsoDate: { sameOrAfter: date } });
402
398
  }
403
399
  between(fromDate, toDate, incl) {
400
+ let schemaPatch = {};
404
401
  if (incl === '[)') {
405
- _objectAssign(this.schema.IsoDate, { sameOrAfter: fromDate, before: toDate });
402
+ schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } };
406
403
  }
407
404
  else if (incl === '[]') {
408
- _objectAssign(this.schema.IsoDate, { sameOrAfter: fromDate, sameOrBefore: toDate });
405
+ schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } };
409
406
  }
410
- return this;
407
+ return this.cloneAndUpdateSchema(schemaPatch);
408
+ }
409
+ }
410
+ export class JsonSchemaIsoMonthBuilder extends JsonSchemaAnyBuilder {
411
+ constructor() {
412
+ super({
413
+ type: 'string',
414
+ IsoMonth: {},
415
+ });
411
416
  }
412
417
  }
413
418
  export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
@@ -435,31 +440,25 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
435
440
  return newBuilder;
436
441
  }
437
442
  integer() {
438
- _objectAssign(this.schema, { type: 'integer' });
439
- return this;
443
+ return this.cloneAndUpdateSchema({ type: 'integer' });
440
444
  }
441
445
  branded() {
442
446
  return this;
443
447
  }
444
448
  multipleOf(multipleOf) {
445
- _objectAssign(this.schema, { multipleOf });
446
- return this;
449
+ return this.cloneAndUpdateSchema({ multipleOf });
447
450
  }
448
451
  min(minimum) {
449
- _objectAssign(this.schema, { minimum });
450
- return this;
452
+ return this.cloneAndUpdateSchema({ minimum });
451
453
  }
452
454
  exclusiveMin(exclusiveMinimum) {
453
- _objectAssign(this.schema, { exclusiveMinimum });
454
- return this;
455
+ return this.cloneAndUpdateSchema({ exclusiveMinimum });
455
456
  }
456
457
  max(maximum) {
457
- _objectAssign(this.schema, { maximum });
458
- return this;
458
+ return this.cloneAndUpdateSchema({ maximum });
459
459
  }
460
460
  exclusiveMax(exclusiveMaximum) {
461
- _objectAssign(this.schema, { exclusiveMaximum });
462
- return this;
461
+ return this.cloneAndUpdateSchema({ exclusiveMaximum });
463
462
  }
464
463
  lessThan(value) {
465
464
  return this.exclusiveMax(value);
@@ -584,15 +583,13 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
584
583
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
585
584
  */
586
585
  allowAdditionalProperties() {
587
- _objectAssign(this.schema, { additionalProperties: true });
588
- return this;
586
+ return this.cloneAndUpdateSchema({ additionalProperties: true });
589
587
  }
590
588
  extend(props) {
591
- const newBuilder = new JsonSchemaObjectBuilder();
592
- _objectAssign(newBuilder.schema, _deepCopy(this.schema));
589
+ const clone = this.cloneAndUpdateSchema(_deepCopy(this.schema));
593
590
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
594
- mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
595
- return newBuilder;
591
+ mergeJsonSchemaObjects(clone.schema, incomingSchemaBuilder.schema);
592
+ return clone;
596
593
  }
597
594
  /**
598
595
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
@@ -606,12 +603,10 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
606
603
  });
607
604
  }
608
605
  minProperties(minProperties) {
609
- Object.assign(this.schema, { minProperties });
610
- return this;
606
+ return this.cloneAndUpdateSchema({ minProperties });
611
607
  }
612
608
  maxProperties(maxProperties) {
613
- Object.assign(this.schema, { maxProperties });
614
- return this;
609
+ return this.cloneAndUpdateSchema({ maxProperties });
615
610
  }
616
611
  }
617
612
  export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
@@ -644,8 +639,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
644
639
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
645
640
  */
646
641
  allowAdditionalProperties() {
647
- _objectAssign(this.schema, { additionalProperties: true });
648
- return this;
642
+ return this.cloneAndUpdateSchema({ additionalProperties: true });
649
643
  }
650
644
  extend(props) {
651
645
  const newBuilder = new JsonSchemaObjectInferringBuilder();
@@ -674,12 +668,10 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
674
668
  });
675
669
  }
676
670
  minLength(minItems) {
677
- _objectAssign(this.schema, { minItems });
678
- return this;
671
+ return this.cloneAndUpdateSchema({ minItems });
679
672
  }
680
673
  maxLength(maxItems) {
681
- _objectAssign(this.schema, { maxItems });
682
- return this;
674
+ return this.cloneAndUpdateSchema({ maxItems });
683
675
  }
684
676
  length(minItemsOrExact, maxItems) {
685
677
  const maxItemsActual = maxItems ?? minItemsOrExact;
@@ -689,8 +681,7 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
689
681
  return this.minLength(length).maxLength(length);
690
682
  }
691
683
  unique() {
692
- _objectAssign(this.schema, { uniqueItems: true });
693
- return this;
684
+ return this.cloneAndUpdateSchema({ uniqueItems: true });
694
685
  }
695
686
  }
696
687
  export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
@@ -701,12 +692,10 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
701
692
  });
702
693
  }
703
694
  min(minItems) {
704
- _objectAssign(this.schema, { minItems });
705
- return this;
695
+ return this.cloneAndUpdateSchema({ minItems });
706
696
  }
707
697
  max(maxItems) {
708
- _objectAssign(this.schema, { maxItems });
709
- return this;
698
+ return this.cloneAndUpdateSchema({ maxItems });
710
699
  }
711
700
  }
712
701
  export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.59.0",
4
+ "version": "15.61.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -1,10 +1,14 @@
1
- import { _lazyValue } from '@naturalcycles/js-lib'
1
+ import { _isBetween, _lazyValue } from '@naturalcycles/js-lib'
2
2
  import { Set2 } from '@naturalcycles/js-lib/object'
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string'
4
4
  import type { AnyObject } from '@naturalcycles/js-lib/types'
5
5
  import { Ajv, type Options, type ValidateFunction } from 'ajv'
6
6
  import { validTLDs } from '../tlds.js'
7
- import type { JsonSchemaIsoDateOptions, JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
7
+ import type {
8
+ JsonSchemaIsoDateOptions,
9
+ JsonSchemaIsoMonthOptions,
10
+ JsonSchemaStringEmailOptions,
11
+ } from './jsonSchemaBuilder.js'
8
12
 
9
13
  /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
10
14
  // oxlint-disable unicorn/prefer-code-point
@@ -383,7 +387,7 @@ export function createAjv(opt?: Options): Ajv {
383
387
  modifying: false,
384
388
  errors: true,
385
389
  schemaType: 'boolean',
386
- validate: function validate(_opt: true, data: string, _schema, ctx) {
390
+ validate: function validate(_opt: JsonSchemaIsoMonthOptions, data: string, _schema, ctx) {
387
391
  const isValid = isIsoDateTimeValid(data)
388
392
  if (isValid) return true
389
393
  ;(validate as any).errors = [
@@ -396,6 +400,25 @@ export function createAjv(opt?: Options): Ajv {
396
400
  },
397
401
  })
398
402
 
403
+ ajv.addKeyword({
404
+ keyword: 'IsoMonth',
405
+ type: 'string',
406
+ modifying: false,
407
+ errors: true,
408
+ schemaType: 'object',
409
+ validate: function validate(_opt: true, data: string, _schema, ctx) {
410
+ const isValid = isIsoMonthValid(data)
411
+ if (isValid) return true
412
+ ;(validate as any).errors = [
413
+ {
414
+ instancePath: ctx?.instancePath ?? '',
415
+ message: `is an invalid IsoMonth`,
416
+ },
417
+ ]
418
+ return false
419
+ },
420
+ })
421
+
399
422
  ajv.addKeyword({
400
423
  keyword: 'errorMessages',
401
424
  schemaType: 'object',
@@ -577,3 +600,24 @@ function isIsoTimezoneValid(s: string): boolean {
577
600
 
578
601
  return true
579
602
  }
603
+
604
+ /**
605
+ * This is a performance optimized correct validation
606
+ * for ISO month formatted as "YYYY-MM".
607
+ */
608
+ function isIsoMonthValid(s: string): boolean {
609
+ // must be exactly "YYYY-MM"
610
+ if (s.length !== 7) return false
611
+ if (s.charCodeAt(4) !== DASH_CODE) return false
612
+
613
+ // fast parse numbers without substrings/Number()
614
+ const year =
615
+ (s.charCodeAt(0) - ZERO_CODE) * 1000 +
616
+ (s.charCodeAt(1) - ZERO_CODE) * 100 +
617
+ (s.charCodeAt(2) - ZERO_CODE) * 10 +
618
+ (s.charCodeAt(3) - ZERO_CODE)
619
+
620
+ const month = (s.charCodeAt(5) - ZERO_CODE) * 10 + (s.charCodeAt(6) - ZERO_CODE)
621
+
622
+ return _isBetween(year, 1900, 2500, '[]') && _isBetween(month, 1, 12, '[]')
623
+ }
@@ -19,6 +19,7 @@ import {
19
19
  type Inclusiveness,
20
20
  type IsoDate,
21
21
  type IsoDateTime,
22
+ type IsoMonth,
22
23
  JWT_REGEX,
23
24
  type NumberEnum,
24
25
  type StringEnum,
@@ -232,8 +233,16 @@ export class JsonSchemaTerminal<IN, OUT, Opt> {
232
233
  return jsonSchema
233
234
  }
234
235
 
235
- clone(): JsonSchemaAnyBuilder<IN, OUT, Opt> {
236
- return new JsonSchemaAnyBuilder<IN, OUT, Opt>(_deepCopy(this.schema))
236
+ clone(): this {
237
+ const cloned = Object.create(Object.getPrototypeOf(this))
238
+ cloned.schema = _deepCopy(this.schema)
239
+ return cloned
240
+ }
241
+
242
+ cloneAndUpdateSchema(schema: Partial<JsonSchema>): this {
243
+ const clone = this.clone()
244
+ _objectAssign(clone.schema, schema)
245
+ return clone
237
246
  }
238
247
 
239
248
  /**
@@ -241,6 +250,7 @@ export class JsonSchemaTerminal<IN, OUT, Opt> {
241
250
  */
242
251
  in!: IN
243
252
  out!: OUT
253
+ opt!: Opt
244
254
  }
245
255
 
246
256
  export class JsonSchemaAnyBuilder<IN, OUT, Opt> extends JsonSchemaTerminal<IN, OUT, Opt> {
@@ -267,58 +277,48 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> extends JsonSchemaTerminal<IN, O
267
277
  * ```
268
278
  */
269
279
  isOfType<ExpectedType>(): ExactMatch<ExpectedType, OUT> extends true ? this : never {
270
- _objectAssign(this.schema, { hasIsOfTypeCheck: true })
271
- return this as any
280
+ return this.cloneAndUpdateSchema({ hasIsOfTypeCheck: true }) as any
272
281
  }
273
282
 
274
283
  $schema($schema: string): this {
275
- _objectAssign(this.schema, { $schema })
276
- return this
284
+ return this.cloneAndUpdateSchema({ $schema })
277
285
  }
278
286
 
279
287
  $schemaDraft7(): this {
280
- this.$schema('http://json-schema.org/draft-07/schema#')
281
- return this
288
+ return this.$schema('http://json-schema.org/draft-07/schema#')
282
289
  }
283
290
 
284
291
  $id($id: string): this {
285
- _objectAssign(this.schema, { $id })
286
- return this
292
+ return this.cloneAndUpdateSchema({ $id })
287
293
  }
288
294
 
289
295
  title(title: string): this {
290
- _objectAssign(this.schema, { title })
291
- return this
296
+ return this.cloneAndUpdateSchema({ title })
292
297
  }
293
298
 
294
299
  description(description: string): this {
295
- _objectAssign(this.schema, { description })
296
- return this
300
+ return this.cloneAndUpdateSchema({ description })
297
301
  }
298
302
 
299
303
  deprecated(deprecated = true): this {
300
- _objectAssign(this.schema, { deprecated })
301
- return this
304
+ return this.cloneAndUpdateSchema({ deprecated })
302
305
  }
303
306
 
304
307
  type(type: string): this {
305
- _objectAssign(this.schema, { type })
306
- return this
308
+ return this.cloneAndUpdateSchema({ type })
307
309
  }
308
310
 
309
311
  default(v: any): this {
310
- _objectAssign(this.schema, { default: v })
311
- return this
312
+ return this.cloneAndUpdateSchema({ default: v })
312
313
  }
313
314
 
314
315
  instanceof(of: string): this {
315
- _objectAssign(this.schema, { type: 'object', instanceof: of })
316
- return this
316
+ return this.cloneAndUpdateSchema({ type: 'object', instanceof: of })
317
317
  }
318
318
 
319
319
  optional(): JsonSchemaAnyBuilder<IN | undefined, OUT | undefined, true> {
320
- this.schema.optionalField = true
321
- return this as unknown as JsonSchemaAnyBuilder<IN | undefined, OUT | undefined, true>
320
+ const clone = this.cloneAndUpdateSchema({ optionalField: true })
321
+ return clone as unknown as JsonSchemaAnyBuilder<IN | undefined, OUT | undefined, true>
322
322
  }
323
323
 
324
324
  nullable(): JsonSchemaAnyBuilder<IN | null, OUT | null, Opt> {
@@ -386,20 +386,18 @@ export class JsonSchemaStringBuilder<
386
386
  }
387
387
 
388
388
  pattern(pattern: string, opt?: JsonBuilderRuleOpt): this {
389
- if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
390
- if (opt?.msg) this.setErrorMessage('pattern', opt.msg)
391
- _objectAssign(this.schema, { pattern })
392
- return this
389
+ const clone = this.cloneAndUpdateSchema({ pattern })
390
+ if (opt?.name) clone.setErrorMessage('pattern', `is not a valid ${opt.name}`)
391
+ if (opt?.msg) clone.setErrorMessage('pattern', opt.msg)
392
+ return clone
393
393
  }
394
394
 
395
395
  minLength(minLength: number): this {
396
- _objectAssign(this.schema, { minLength })
397
- return this
396
+ return this.cloneAndUpdateSchema({ minLength })
398
397
  }
399
398
 
400
399
  maxLength(maxLength: number): this {
401
- _objectAssign(this.schema, { maxLength })
402
- return this
400
+ return this.cloneAndUpdateSchema({ maxLength })
403
401
  }
404
402
 
405
403
  length(exactLength: number): this
@@ -411,28 +409,31 @@ export class JsonSchemaStringBuilder<
411
409
 
412
410
  email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
413
411
  const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
414
- _objectAssign(this.schema, { email: { ...defaultOptions, ...opt } })
415
- return this.trim().toLowerCase()
412
+ return this.cloneAndUpdateSchema({ email: { ...defaultOptions, ...opt } })
413
+ .trim()
414
+ .toLowerCase()
416
415
  }
417
416
 
418
417
  trim(): this {
419
- _objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } })
420
- return this
418
+ return this.cloneAndUpdateSchema({ transform: { ...this.schema.transform, trim: true } })
421
419
  }
422
420
 
423
421
  toLowerCase(): this {
424
- _objectAssign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } })
425
- return this
422
+ return this.cloneAndUpdateSchema({
423
+ transform: { ...this.schema.transform, toLowerCase: true },
424
+ })
426
425
  }
427
426
 
428
427
  toUpperCase(): this {
429
- _objectAssign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } })
430
- return this
428
+ return this.cloneAndUpdateSchema({
429
+ transform: { ...this.schema.transform, toUpperCase: true },
430
+ })
431
431
  }
432
432
 
433
433
  truncate(toLength: number): this {
434
- _objectAssign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } })
435
- return this
434
+ return this.cloneAndUpdateSchema({
435
+ transform: { ...this.schema.transform, truncate: toLength },
436
+ })
436
437
  }
437
438
 
438
439
  branded<B extends string>(): JsonSchemaStringBuilder<B, B, Opt> {
@@ -446,13 +447,15 @@ export class JsonSchemaStringBuilder<
446
447
  * because this call effectively starts a new schema chain.
447
448
  */
448
449
  isoDate(): JsonSchemaIsoDateBuilder {
449
- _objectAssign(this.schema, { IsoDate: {} })
450
450
  return new JsonSchemaIsoDateBuilder()
451
451
  }
452
452
 
453
453
  isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt> {
454
- _objectAssign(this.schema, { IsoDateTime: true })
455
- return this.branded<IsoDateTime>()
454
+ return this.cloneAndUpdateSchema({ IsoDateTime: true }).branded<IsoDateTime>()
455
+ }
456
+
457
+ isoMonth(): JsonSchemaIsoMonthBuilder {
458
+ return new JsonSchemaIsoMonthBuilder()
456
459
  }
457
460
 
458
461
  /**
@@ -537,33 +540,31 @@ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonS
537
540
  }
538
541
 
539
542
  before(date: string): this {
540
- _objectAssign(this.schema.IsoDate!, { before: date })
541
- return this
543
+ return this.cloneAndUpdateSchema({ IsoDate: { before: date } })
542
544
  }
543
545
 
544
546
  sameOrBefore(date: string): this {
545
- _objectAssign(this.schema.IsoDate!, { sameOrBefore: date })
546
- return this
547
+ return this.cloneAndUpdateSchema({ IsoDate: { sameOrBefore: date } })
547
548
  }
548
549
 
549
550
  after(date: string): this {
550
- _objectAssign(this.schema.IsoDate!, { after: date })
551
- return this
551
+ return this.cloneAndUpdateSchema({ IsoDate: { after: date } })
552
552
  }
553
553
 
554
554
  sameOrAfter(date: string): this {
555
- _objectAssign(this.schema.IsoDate!, { sameOrAfter: date })
556
- return this
555
+ return this.cloneAndUpdateSchema({ IsoDate: { sameOrAfter: date } })
557
556
  }
558
557
 
559
558
  between(fromDate: string, toDate: string, incl: Inclusiveness): this {
559
+ let schemaPatch: Partial<JsonSchema> = {}
560
+
560
561
  if (incl === '[)') {
561
- _objectAssign(this.schema.IsoDate!, { sameOrAfter: fromDate, before: toDate })
562
+ schemaPatch = { IsoDate: { sameOrAfter: fromDate, before: toDate } }
562
563
  } else if (incl === '[]') {
563
- _objectAssign(this.schema.IsoDate!, { sameOrAfter: fromDate, sameOrBefore: toDate })
564
+ schemaPatch = { IsoDate: { sameOrAfter: fromDate, sameOrBefore: toDate } }
564
565
  }
565
566
 
566
- return this
567
+ return this.cloneAndUpdateSchema(schemaPatch)
567
568
  }
568
569
  }
569
570
 
@@ -574,6 +575,21 @@ export interface JsonSchemaIsoDateOptions {
574
575
  sameOrAfter?: string
575
576
  }
576
577
 
578
+ export class JsonSchemaIsoMonthBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<
579
+ string | IsoDate,
580
+ IsoMonth,
581
+ Opt
582
+ > {
583
+ constructor() {
584
+ super({
585
+ type: 'string',
586
+ IsoMonth: {},
587
+ })
588
+ }
589
+ }
590
+
591
+ export interface JsonSchemaIsoMonthOptions {}
592
+
577
593
  export class JsonSchemaNumberBuilder<
578
594
  IN extends number | undefined = number,
579
595
  OUT = IN,
@@ -591,6 +607,7 @@ export class JsonSchemaNumberBuilder<
591
607
  * This `optionalValues` feature only works when the current schema is nested in an object or array schema,
592
608
  * due to how mutability works in Ajv.
593
609
  */
610
+
594
611
  override optional(
595
612
  optionalValues?: number[],
596
613
  ): JsonSchemaNumberBuilder<IN | undefined, OUT | undefined, true> {
@@ -613,8 +630,7 @@ export class JsonSchemaNumberBuilder<
613
630
  }
614
631
 
615
632
  integer(): this {
616
- _objectAssign(this.schema, { type: 'integer' })
617
- return this
633
+ return this.cloneAndUpdateSchema({ type: 'integer' })
618
634
  }
619
635
 
620
636
  branded<B extends number>(): JsonSchemaNumberBuilder<B, B, Opt> {
@@ -622,28 +638,23 @@ export class JsonSchemaNumberBuilder<
622
638
  }
623
639
 
624
640
  multipleOf(multipleOf: number): this {
625
- _objectAssign(this.schema, { multipleOf })
626
- return this
641
+ return this.cloneAndUpdateSchema({ multipleOf })
627
642
  }
628
643
 
629
644
  min(minimum: number): this {
630
- _objectAssign(this.schema, { minimum })
631
- return this
645
+ return this.cloneAndUpdateSchema({ minimum })
632
646
  }
633
647
 
634
648
  exclusiveMin(exclusiveMinimum: number): this {
635
- _objectAssign(this.schema, { exclusiveMinimum })
636
- return this
649
+ return this.cloneAndUpdateSchema({ exclusiveMinimum })
637
650
  }
638
651
 
639
652
  max(maximum: number): this {
640
- _objectAssign(this.schema, { maximum })
641
- return this
653
+ return this.cloneAndUpdateSchema({ maximum })
642
654
  }
643
655
 
644
656
  exclusiveMax(exclusiveMaximum: number): this {
645
- _objectAssign(this.schema, { exclusiveMaximum })
646
- return this
657
+ return this.cloneAndUpdateSchema({ exclusiveMaximum })
647
658
  }
648
659
 
649
660
  lessThan(value: number): this {
@@ -812,21 +823,24 @@ export class JsonSchemaObjectBuilder<
812
823
  /**
813
824
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
814
825
  */
826
+
815
827
  allowAdditionalProperties(): this {
816
- _objectAssign(this.schema, { additionalProperties: true })
817
- return this
828
+ return this.cloneAndUpdateSchema({ additionalProperties: true })
818
829
  }
819
830
 
820
831
  extend<IN2 extends AnyObject>(
821
832
  props: AnyObject,
822
833
  ): JsonSchemaObjectBuilder<IN & IN2, OUT & IN2, Opt> {
823
- const newBuilder = new JsonSchemaObjectBuilder<IN & IN2, OUT & IN2, Opt>()
824
- _objectAssign(newBuilder.schema, _deepCopy(this.schema))
834
+ const clone = this.cloneAndUpdateSchema(_deepCopy(this.schema)) as JsonSchemaObjectBuilder<
835
+ IN & IN2,
836
+ OUT & IN2,
837
+ Opt
838
+ >
825
839
 
826
840
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder<IN2, IN2, false>(props)
827
- mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
841
+ mergeJsonSchemaObjects(clone.schema as any, incomingSchemaBuilder.schema as any)
828
842
 
829
- return newBuilder
843
+ return clone
830
844
  }
831
845
 
832
846
  /**
@@ -842,13 +856,11 @@ export class JsonSchemaObjectBuilder<
842
856
  }
843
857
 
844
858
  minProperties(minProperties: number): this {
845
- Object.assign(this.schema, { minProperties })
846
- return this
859
+ return this.cloneAndUpdateSchema({ minProperties })
847
860
  }
848
861
 
849
862
  maxProperties(maxProperties: number): this {
850
- Object.assign(this.schema, { maxProperties })
851
- return this
863
+ return this.cloneAndUpdateSchema({ maxProperties })
852
864
  }
853
865
  }
854
866
 
@@ -928,9 +940,9 @@ export class JsonSchemaObjectInferringBuilder<
928
940
  /**
929
941
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
930
942
  */
943
+
931
944
  allowAdditionalProperties(): this {
932
- _objectAssign(this.schema, { additionalProperties: true })
933
- return this
945
+ return this.cloneAndUpdateSchema({ additionalProperties: true })
934
946
  }
935
947
 
936
948
  extend<NEW_PROPS extends Record<string, JsonSchemaAnyBuilder<any, any, any>>>(
@@ -985,13 +997,11 @@ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<I
985
997
  }
986
998
 
987
999
  minLength(minItems: number): this {
988
- _objectAssign(this.schema, { minItems })
989
- return this
1000
+ return this.cloneAndUpdateSchema({ minItems })
990
1001
  }
991
1002
 
992
1003
  maxLength(maxItems: number): this {
993
- _objectAssign(this.schema, { maxItems })
994
- return this
1004
+ return this.cloneAndUpdateSchema({ maxItems })
995
1005
  }
996
1006
 
997
1007
  length(exactLength: number): this
@@ -1006,8 +1016,7 @@ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<I
1006
1016
  }
1007
1017
 
1008
1018
  unique(): this {
1009
- _objectAssign(this.schema, { uniqueItems: true })
1010
- return this
1019
+ return this.cloneAndUpdateSchema({ uniqueItems: true })
1011
1020
  }
1012
1021
  }
1013
1022
 
@@ -1024,13 +1033,11 @@ export class JsonSchemaSet2Builder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<
1024
1033
  }
1025
1034
 
1026
1035
  min(minItems: number): this {
1027
- _objectAssign(this.schema, { minItems })
1028
- return this
1036
+ return this.cloneAndUpdateSchema({ minItems })
1029
1037
  }
1030
1038
 
1031
1039
  max(maxItems: number): this {
1032
- _objectAssign(this.schema, { maxItems })
1033
- return this
1040
+ return this.cloneAndUpdateSchema({ maxItems })
1034
1041
  }
1035
1042
  }
1036
1043
 
@@ -1139,6 +1146,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
1139
1146
  Buffer?: true
1140
1147
  IsoDate?: JsonSchemaIsoDateOptions
1141
1148
  IsoDateTime?: true
1149
+ IsoMonth?: JsonSchemaIsoMonthOptions
1142
1150
  instanceof?: string | string[]
1143
1151
  transform?: { trim?: true; toLowerCase?: true; toUpperCase?: true; truncate?: number }
1144
1152
  errorMessages?: StringMap<string>