@naturalcycles/nodejs-lib 15.46.3 → 15.47.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.
@@ -233,17 +233,67 @@ export function createAjv(opt) {
233
233
  type: 'string',
234
234
  modifying: false,
235
235
  errors: true,
236
- schemaType: 'boolean',
237
- validate: function validate(_opt, data, _schema, ctx) {
236
+ schemaType: 'object',
237
+ validate: function validate(opt, data, _schema, ctx) {
238
+ const hasOptions = Object.keys(opt).length > 0;
238
239
  const isValid = isIsoDateValid(data);
239
- if (isValid)
240
+ if (!isValid) {
241
+ ;
242
+ validate.errors = [
243
+ {
244
+ instancePath: ctx?.instancePath ?? '',
245
+ message: `is an invalid IsoDate`,
246
+ },
247
+ ];
248
+ return false;
249
+ }
250
+ if (!hasOptions)
240
251
  return true;
241
- validate.errors = [
242
- {
243
- instancePath: ctx?.instancePath ?? '',
244
- message: `is an invalid IsoDate`,
245
- },
246
- ];
252
+ const { before, sameOrBefore, after, sameOrAfter } = opt;
253
+ const errors = [];
254
+ if (before) {
255
+ const isParamValid = isIsoDateValid(before);
256
+ const isRuleValid = isParamValid && before > data;
257
+ if (!isRuleValid) {
258
+ errors.push({
259
+ instancePath: ctx?.instancePath ?? '',
260
+ message: `is not before ${before}`,
261
+ });
262
+ }
263
+ }
264
+ if (sameOrBefore) {
265
+ const isParamValid = isIsoDateValid(sameOrBefore);
266
+ const isRuleValid = isParamValid && sameOrBefore >= data;
267
+ if (!isRuleValid) {
268
+ errors.push({
269
+ instancePath: ctx?.instancePath ?? '',
270
+ message: `is not the same or before ${sameOrBefore}`,
271
+ });
272
+ }
273
+ }
274
+ if (after) {
275
+ const isParamValid = isIsoDateValid(after);
276
+ const isRuleValid = isParamValid && after < data;
277
+ if (!isRuleValid) {
278
+ errors.push({
279
+ instancePath: ctx?.instancePath ?? '',
280
+ message: `is not after ${after}`,
281
+ });
282
+ }
283
+ }
284
+ if (sameOrAfter) {
285
+ const isParamValid = isIsoDateValid(sameOrAfter);
286
+ const isRuleValid = isParamValid && sameOrAfter <= data;
287
+ if (!isRuleValid) {
288
+ errors.push({
289
+ instancePath: ctx?.instancePath ?? '',
290
+ message: `is not the same or after ${sameOrAfter}`,
291
+ });
292
+ }
293
+ }
294
+ if (errors.length === 0)
295
+ return true;
296
+ validate.errors = errors;
247
297
  return false;
248
298
  },
249
299
  });
@@ -75,7 +75,13 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
75
75
  toUpperCase(): this;
76
76
  truncate(toLength: number): this;
77
77
  branded<B extends string>(): JsonSchemaStringBuilder<B, B, Opt>;
78
- isoDate(): JsonSchemaStringBuilder<IsoDate | IN, IsoDate, Opt>;
78
+ /**
79
+ * Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
80
+ *
81
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
82
+ * because this call effectively starts a new schema chain.
83
+ */
84
+ isoDate(): JsonSchemaIsoDateBuilder;
79
85
  isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt>;
80
86
  /**
81
87
  * Validates the string format to be JWT.
@@ -102,6 +108,20 @@ export declare class JsonSchemaStringBuilder<IN extends string = string, OUT = I
102
108
  export interface JsonSchemaStringEmailOptions {
103
109
  checkTLD: boolean;
104
110
  }
111
+ export declare class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<string | IsoDate, IsoDate, Opt> {
112
+ constructor();
113
+ before(date: string): this;
114
+ sameOrBefore(date: string): this;
115
+ after(date: string): this;
116
+ sameOrAfter(date: string): this;
117
+ between(fromDate: string, toDate: string, incl: Inclusiveness): this;
118
+ }
119
+ export interface JsonSchemaIsoDateOptions {
120
+ before?: string;
121
+ sameOrBefore?: string;
122
+ after?: string;
123
+ sameOrAfter?: string;
124
+ }
105
125
  export declare class JsonSchemaNumberBuilder<IN extends number = number, OUT = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
106
126
  constructor();
107
127
  integer(): this;
@@ -177,7 +197,7 @@ export declare class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyB
177
197
  maxLength(maxItems: number): this;
178
198
  length(minItems: number, maxItems: number): this;
179
199
  exactLength(length: number): this;
180
- unique(uniqueItems: number): this;
200
+ unique(): this;
181
201
  }
182
202
  export declare class JsonSchemaSet2Builder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<Iterable<IN>, Set2<OUT>, Opt> {
183
203
  constructor(itemsSchema: JsonSchemaAnyBuilder<IN, OUT, Opt>);
@@ -194,7 +214,7 @@ export declare class JsonSchemaEnumBuilder<IN extends string | number | boolean
194
214
  export interface JsonSchema<IN = unknown, OUT = IN> {
195
215
  readonly in?: IN;
196
216
  readonly out?: OUT;
197
- $schema?: AnyObject;
217
+ $schema?: string;
198
218
  $id?: string;
199
219
  title?: string;
200
220
  description?: string;
@@ -232,10 +252,14 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
232
252
  exclusiveMinimum?: number;
233
253
  maximum?: number;
234
254
  exclusiveMaximum?: number;
255
+ minItems?: number;
256
+ maxItems?: number;
257
+ uniqueItems?: boolean;
235
258
  enum?: any;
259
+ email?: JsonSchemaStringEmailOptions;
236
260
  Set2?: JsonSchema;
237
261
  Buffer?: true;
238
- IsoDate?: true;
262
+ IsoDate?: JsonSchemaIsoDateOptions;
239
263
  IsoDateTime?: true;
240
264
  instanceof?: string | string[];
241
265
  transform?: {
@@ -4,7 +4,7 @@ import { _isUndefined, _numberEnumValues, _stringEnumValues, getEnumType, } from
4
4
  import { _uniq } from '@naturalcycles/js-lib/array';
5
5
  import { _assert } from '@naturalcycles/js-lib/error';
6
6
  import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object';
7
- import { JWT_REGEX, } from '@naturalcycles/js-lib/types';
7
+ import { _objectAssign, JWT_REGEX, } from '@naturalcycles/js-lib/types';
8
8
  import { TIMEZONES } from '../timezones.js';
9
9
  import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js';
10
10
  export const j = {
@@ -94,14 +94,14 @@ export class JsonSchemaAnyBuilder {
94
94
  * ```
95
95
  */
96
96
  isOfType() {
97
- Object.assign(this.schema, { hasIsOfTypeCheck: true });
97
+ _objectAssign(this.schema, { hasIsOfTypeCheck: true });
98
98
  return this;
99
99
  }
100
100
  getSchema() {
101
101
  return this.schema;
102
102
  }
103
103
  $schema($schema) {
104
- Object.assign(this.schema, { $schema });
104
+ _objectAssign(this.schema, { $schema });
105
105
  return this;
106
106
  }
107
107
  $schemaDraft7() {
@@ -109,31 +109,31 @@ export class JsonSchemaAnyBuilder {
109
109
  return this;
110
110
  }
111
111
  $id($id) {
112
- Object.assign(this.schema, { $id });
112
+ _objectAssign(this.schema, { $id });
113
113
  return this;
114
114
  }
115
115
  title(title) {
116
- Object.assign(this.schema, { title });
116
+ _objectAssign(this.schema, { title });
117
117
  return this;
118
118
  }
119
119
  description(description) {
120
- Object.assign(this.schema, { description });
120
+ _objectAssign(this.schema, { description });
121
121
  return this;
122
122
  }
123
123
  deprecated(deprecated = true) {
124
- Object.assign(this.schema, { deprecated });
124
+ _objectAssign(this.schema, { deprecated });
125
125
  return this;
126
126
  }
127
127
  type(type) {
128
- Object.assign(this.schema, { type });
128
+ _objectAssign(this.schema, { type });
129
129
  return this;
130
130
  }
131
131
  default(v) {
132
- Object.assign(this.schema, { default: v });
132
+ _objectAssign(this.schema, { default: v });
133
133
  return this;
134
134
  }
135
135
  instanceof(of) {
136
- Object.assign(this.schema, { type: 'object', instanceof: of });
136
+ _objectAssign(this.schema, { type: 'object', instanceof: of });
137
137
  return this;
138
138
  }
139
139
  optional() {
@@ -182,53 +182,59 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
182
182
  this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
183
183
  if (opt?.msg)
184
184
  this.setErrorMessage('pattern', opt.msg);
185
- Object.assign(this.schema, { pattern });
185
+ _objectAssign(this.schema, { pattern });
186
186
  return this;
187
187
  }
188
188
  minLength(minLength) {
189
- Object.assign(this.schema, { minLength });
189
+ _objectAssign(this.schema, { minLength });
190
190
  return this;
191
191
  }
192
192
  maxLength(maxLength) {
193
- Object.assign(this.schema, { maxLength });
193
+ _objectAssign(this.schema, { maxLength });
194
194
  return this;
195
195
  }
196
196
  length(minLength, maxLength) {
197
- Object.assign(this.schema, { minLength, maxLength });
197
+ _objectAssign(this.schema, { minLength, maxLength });
198
198
  return this;
199
199
  }
200
200
  email(opt) {
201
201
  const defaultOptions = { checkTLD: true };
202
- Object.assign(this.schema, { email: { ...defaultOptions, ...opt } });
202
+ _objectAssign(this.schema, { email: { ...defaultOptions, ...opt } });
203
203
  // from `ajv-formats`
204
204
  const regex = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
205
205
  return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase();
206
206
  }
207
207
  trim() {
208
- Object.assign(this.schema, { transform: { ...this.schema.transform, trim: true } });
208
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } });
209
209
  return this;
210
210
  }
211
211
  toLowerCase() {
212
- Object.assign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } });
212
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } });
213
213
  return this;
214
214
  }
215
215
  toUpperCase() {
216
- Object.assign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } });
216
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } });
217
217
  return this;
218
218
  }
219
219
  truncate(toLength) {
220
- Object.assign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } });
220
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } });
221
221
  return this;
222
222
  }
223
223
  branded() {
224
224
  return this;
225
225
  }
226
+ /**
227
+ * Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
228
+ *
229
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
230
+ * because this call effectively starts a new schema chain.
231
+ */
226
232
  isoDate() {
227
- Object.assign(this.schema, { IsoDate: true });
228
- return this.branded();
233
+ _objectAssign(this.schema, { IsoDate: {} });
234
+ return new JsonSchemaIsoDateBuilder();
229
235
  }
230
236
  isoDateTime() {
231
- Object.assign(this.schema, { IsoDateTime: true });
237
+ _objectAssign(this.schema, { IsoDateTime: true });
232
238
  return this.branded();
233
239
  }
234
240
  /**
@@ -289,6 +295,39 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
289
295
  return j.enum(TIMEZONES, { msg: 'is an invalid IANA timezone' }).branded();
290
296
  }
291
297
  }
298
+ export class JsonSchemaIsoDateBuilder extends JsonSchemaAnyBuilder {
299
+ constructor() {
300
+ super({
301
+ type: 'string',
302
+ IsoDate: {},
303
+ });
304
+ }
305
+ before(date) {
306
+ _objectAssign(this.schema.IsoDate, { before: date });
307
+ return this;
308
+ }
309
+ sameOrBefore(date) {
310
+ _objectAssign(this.schema.IsoDate, { sameOrBefore: date });
311
+ return this;
312
+ }
313
+ after(date) {
314
+ _objectAssign(this.schema.IsoDate, { after: date });
315
+ return this;
316
+ }
317
+ sameOrAfter(date) {
318
+ _objectAssign(this.schema.IsoDate, { sameOrAfter: date });
319
+ return this;
320
+ }
321
+ between(fromDate, toDate, incl) {
322
+ if (incl === '[)') {
323
+ _objectAssign(this.schema.IsoDate, { sameOrAfter: fromDate, before: toDate });
324
+ }
325
+ else if (incl === '[]') {
326
+ _objectAssign(this.schema.IsoDate, { sameOrAfter: fromDate, sameOrBefore: toDate });
327
+ }
328
+ return this;
329
+ }
330
+ }
292
331
  export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
293
332
  constructor() {
294
333
  super({
@@ -296,30 +335,30 @@ export class JsonSchemaNumberBuilder extends JsonSchemaAnyBuilder {
296
335
  });
297
336
  }
298
337
  integer() {
299
- Object.assign(this.schema, { type: 'integer' });
338
+ _objectAssign(this.schema, { type: 'integer' });
300
339
  return this;
301
340
  }
302
341
  branded() {
303
342
  return this;
304
343
  }
305
344
  multipleOf(multipleOf) {
306
- Object.assign(this.schema, { multipleOf });
345
+ _objectAssign(this.schema, { multipleOf });
307
346
  return this;
308
347
  }
309
348
  min(minimum) {
310
- Object.assign(this.schema, { minimum });
349
+ _objectAssign(this.schema, { minimum });
311
350
  return this;
312
351
  }
313
352
  exclusiveMin(exclusiveMinimum) {
314
- Object.assign(this.schema, { exclusiveMinimum });
353
+ _objectAssign(this.schema, { exclusiveMinimum });
315
354
  return this;
316
355
  }
317
356
  max(maximum) {
318
- Object.assign(this.schema, { maximum });
357
+ _objectAssign(this.schema, { maximum });
319
358
  return this;
320
359
  }
321
360
  exclusiveMax(exclusiveMaximum) {
322
- Object.assign(this.schema, { exclusiveMaximum });
361
+ _objectAssign(this.schema, { exclusiveMaximum });
323
362
  return this;
324
363
  }
325
364
  lessThan(value) {
@@ -427,12 +466,12 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
427
466
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
428
467
  */
429
468
  allowAdditionalProperties() {
430
- Object.assign(this.schema, { additionalProperties: true });
469
+ _objectAssign(this.schema, { additionalProperties: true });
431
470
  return this;
432
471
  }
433
472
  extend(props) {
434
473
  const newBuilder = new JsonSchemaObjectBuilder();
435
- Object.assign(newBuilder.schema, _deepCopy(this.schema));
474
+ _objectAssign(newBuilder.schema, _deepCopy(this.schema));
436
475
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder(props);
437
476
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
438
477
  return newBuilder;
@@ -481,12 +520,12 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
481
520
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
482
521
  */
483
522
  allowAdditionalProperties() {
484
- Object.assign(this.schema, { additionalProperties: true });
523
+ _objectAssign(this.schema, { additionalProperties: true });
485
524
  return this;
486
525
  }
487
526
  extend(props) {
488
527
  const newBuilder = new JsonSchemaObjectInferringBuilder();
489
- Object.assign(newBuilder.schema, _deepCopy(this.schema));
528
+ _objectAssign(newBuilder.schema, _deepCopy(this.schema));
490
529
  const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder(props);
491
530
  mergeJsonSchemaObjects(newBuilder.schema, incomingSchemaBuilder.schema);
492
531
  return newBuilder;
@@ -511,11 +550,11 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
511
550
  });
512
551
  }
513
552
  minLength(minItems) {
514
- Object.assign(this.schema, { minItems });
553
+ _objectAssign(this.schema, { minItems });
515
554
  return this;
516
555
  }
517
556
  maxLength(maxItems) {
518
- Object.assign(this.schema, { maxItems });
557
+ _objectAssign(this.schema, { maxItems });
519
558
  return this;
520
559
  }
521
560
  length(minItems, maxItems) {
@@ -524,8 +563,8 @@ export class JsonSchemaArrayBuilder extends JsonSchemaAnyBuilder {
524
563
  exactLength(length) {
525
564
  return this.minLength(length).maxLength(length);
526
565
  }
527
- unique(uniqueItems) {
528
- Object.assign(this.schema, { uniqueItems });
566
+ unique() {
567
+ _objectAssign(this.schema, { uniqueItems: true });
529
568
  return this;
530
569
  }
531
570
  }
@@ -537,11 +576,11 @@ export class JsonSchemaSet2Builder extends JsonSchemaAnyBuilder {
537
576
  });
538
577
  }
539
578
  min(minItems) {
540
- Object.assign(this.schema, { minItems });
579
+ _objectAssign(this.schema, { minItems });
541
580
  return this;
542
581
  }
543
582
  max(maxItems) {
544
- Object.assign(this.schema, { maxItems });
583
+ _objectAssign(this.schema, { maxItems });
545
584
  return this;
546
585
  }
547
586
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.46.3",
4
+ "version": "15.47.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -3,7 +3,7 @@ import { Set2 } from '@naturalcycles/js-lib/object'
3
3
  import { _substringAfterLast } from '@naturalcycles/js-lib/string'
4
4
  import { _, Ajv, type Options, type ValidateFunction } from 'ajv'
5
5
  import { validTLDs } from '../tlds.js'
6
- import type { JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
6
+ import type { JsonSchemaIsoDateOptions, JsonSchemaStringEmailOptions } from './jsonSchemaBuilder.js'
7
7
 
8
8
  /* eslint-disable @typescript-eslint/prefer-string-starts-ends-with */
9
9
  // oxlint-disable unicorn/prefer-code-point
@@ -255,16 +255,77 @@ export function createAjv(opt?: Options): Ajv {
255
255
  type: 'string',
256
256
  modifying: false,
257
257
  errors: true,
258
- schemaType: 'boolean',
259
- validate: function validate(_opt: true, data: string, _schema, ctx) {
258
+ schemaType: 'object',
259
+ validate: function validate(opt: JsonSchemaIsoDateOptions, data: string, _schema, ctx) {
260
+ const hasOptions = Object.keys(opt).length > 0
261
+
260
262
  const isValid = isIsoDateValid(data)
261
- if (isValid) return true
262
- ;(validate as any).errors = [
263
- {
264
- instancePath: ctx?.instancePath ?? '',
265
- message: `is an invalid IsoDate`,
266
- },
267
- ]
263
+ if (!isValid) {
264
+ ;(validate as any).errors = [
265
+ {
266
+ instancePath: ctx?.instancePath ?? '',
267
+ message: `is an invalid IsoDate`,
268
+ },
269
+ ]
270
+
271
+ return false
272
+ }
273
+
274
+ if (!hasOptions) return true
275
+
276
+ const { before, sameOrBefore, after, sameOrAfter } = opt
277
+ const errors: any[] = []
278
+
279
+ if (before) {
280
+ const isParamValid = isIsoDateValid(before)
281
+ const isRuleValid = isParamValid && before > data
282
+
283
+ if (!isRuleValid) {
284
+ errors.push({
285
+ instancePath: ctx?.instancePath ?? '',
286
+ message: `is not before ${before}`,
287
+ })
288
+ }
289
+ }
290
+
291
+ if (sameOrBefore) {
292
+ const isParamValid = isIsoDateValid(sameOrBefore)
293
+ const isRuleValid = isParamValid && sameOrBefore >= data
294
+
295
+ if (!isRuleValid) {
296
+ errors.push({
297
+ instancePath: ctx?.instancePath ?? '',
298
+ message: `is not the same or before ${sameOrBefore}`,
299
+ })
300
+ }
301
+ }
302
+
303
+ if (after) {
304
+ const isParamValid = isIsoDateValid(after)
305
+ const isRuleValid = isParamValid && after < data
306
+
307
+ if (!isRuleValid) {
308
+ errors.push({
309
+ instancePath: ctx?.instancePath ?? '',
310
+ message: `is not after ${after}`,
311
+ })
312
+ }
313
+ }
314
+
315
+ if (sameOrAfter) {
316
+ const isParamValid = isIsoDateValid(sameOrAfter)
317
+ const isRuleValid = isParamValid && sameOrAfter <= data
318
+
319
+ if (!isRuleValid) {
320
+ errors.push({
321
+ instancePath: ctx?.instancePath ?? '',
322
+ message: `is not the same or after ${sameOrAfter}`,
323
+ })
324
+ }
325
+ }
326
+
327
+ if (errors.length === 0) return true
328
+ ;(validate as any).errors = errors
268
329
  return false
269
330
  },
270
331
  })
@@ -12,6 +12,7 @@ import { _assert } from '@naturalcycles/js-lib/error'
12
12
  import type { Set2 } from '@naturalcycles/js-lib/object'
13
13
  import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object'
14
14
  import {
15
+ _objectAssign,
15
16
  type AnyObject,
16
17
  type BaseDBEntity,
17
18
  type IANATimezone,
@@ -144,7 +145,7 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
144
145
  * ```
145
146
  */
146
147
  isOfType<ExpectedType>(): ExactMatch<ExpectedType, OUT> extends true ? this : never {
147
- Object.assign(this.schema, { hasIsOfTypeCheck: true })
148
+ _objectAssign(this.schema, { hasIsOfTypeCheck: true })
148
149
  return this as any
149
150
  }
150
151
 
@@ -153,7 +154,7 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
153
154
  }
154
155
 
155
156
  $schema($schema: string): this {
156
- Object.assign(this.schema, { $schema })
157
+ _objectAssign(this.schema, { $schema })
157
158
  return this
158
159
  }
159
160
 
@@ -163,37 +164,37 @@ export class JsonSchemaAnyBuilder<IN, OUT, Opt> {
163
164
  }
164
165
 
165
166
  $id($id: string): this {
166
- Object.assign(this.schema, { $id })
167
+ _objectAssign(this.schema, { $id })
167
168
  return this
168
169
  }
169
170
 
170
171
  title(title: string): this {
171
- Object.assign(this.schema, { title })
172
+ _objectAssign(this.schema, { title })
172
173
  return this
173
174
  }
174
175
 
175
176
  description(description: string): this {
176
- Object.assign(this.schema, { description })
177
+ _objectAssign(this.schema, { description })
177
178
  return this
178
179
  }
179
180
 
180
181
  deprecated(deprecated = true): this {
181
- Object.assign(this.schema, { deprecated })
182
+ _objectAssign(this.schema, { deprecated })
182
183
  return this
183
184
  }
184
185
 
185
186
  type(type: string): this {
186
- Object.assign(this.schema, { type })
187
+ _objectAssign(this.schema, { type })
187
188
  return this
188
189
  }
189
190
 
190
191
  default(v: any): this {
191
- Object.assign(this.schema, { default: v })
192
+ _objectAssign(this.schema, { default: v })
192
193
  return this
193
194
  }
194
195
 
195
196
  instanceof(of: string): this {
196
- Object.assign(this.schema, { type: 'object', instanceof: of })
197
+ _objectAssign(this.schema, { type: 'object', instanceof: of })
197
198
  return this
198
199
  }
199
200
 
@@ -253,28 +254,28 @@ export class JsonSchemaStringBuilder<
253
254
  pattern(pattern: string, opt?: JsonBuilderRuleOpt): this {
254
255
  if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
255
256
  if (opt?.msg) this.setErrorMessage('pattern', opt.msg)
256
- Object.assign(this.schema, { pattern })
257
+ _objectAssign(this.schema, { pattern })
257
258
  return this
258
259
  }
259
260
 
260
261
  minLength(minLength: number): this {
261
- Object.assign(this.schema, { minLength })
262
+ _objectAssign(this.schema, { minLength })
262
263
  return this
263
264
  }
264
265
 
265
266
  maxLength(maxLength: number): this {
266
- Object.assign(this.schema, { maxLength })
267
+ _objectAssign(this.schema, { maxLength })
267
268
  return this
268
269
  }
269
270
 
270
271
  length(minLength: number, maxLength: number): this {
271
- Object.assign(this.schema, { minLength, maxLength })
272
+ _objectAssign(this.schema, { minLength, maxLength })
272
273
  return this
273
274
  }
274
275
 
275
276
  email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
276
277
  const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
277
- Object.assign(this.schema, { email: { ...defaultOptions, ...opt } })
278
+ _objectAssign(this.schema, { email: { ...defaultOptions, ...opt } })
278
279
 
279
280
  // from `ajv-formats`
280
281
  const regex =
@@ -283,22 +284,22 @@ export class JsonSchemaStringBuilder<
283
284
  }
284
285
 
285
286
  trim(): this {
286
- Object.assign(this.schema, { transform: { ...this.schema.transform, trim: true } })
287
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } })
287
288
  return this
288
289
  }
289
290
 
290
291
  toLowerCase(): this {
291
- Object.assign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } })
292
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, toLowerCase: true } })
292
293
  return this
293
294
  }
294
295
 
295
296
  toUpperCase(): this {
296
- Object.assign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } })
297
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, toUpperCase: true } })
297
298
  return this
298
299
  }
299
300
 
300
301
  truncate(toLength: number): this {
301
- Object.assign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } })
302
+ _objectAssign(this.schema, { transform: { ...this.schema.transform, truncate: toLength } })
302
303
  return this
303
304
  }
304
305
 
@@ -306,13 +307,19 @@ export class JsonSchemaStringBuilder<
306
307
  return this as unknown as JsonSchemaStringBuilder<B, B, Opt>
307
308
  }
308
309
 
309
- isoDate(): JsonSchemaStringBuilder<IsoDate | IN, IsoDate, Opt> {
310
- Object.assign(this.schema, { IsoDate: true })
311
- return this.branded<IsoDate>()
310
+ /**
311
+ * Validates that the input is a fully-specified YYYY-MM-DD formatted valid IsoDate value.
312
+ *
313
+ * All previous expectations in the schema chain are dropped - including `.optional()` -
314
+ * because this call effectively starts a new schema chain.
315
+ */
316
+ isoDate(): JsonSchemaIsoDateBuilder {
317
+ _objectAssign(this.schema, { IsoDate: {} })
318
+ return new JsonSchemaIsoDateBuilder()
312
319
  }
313
320
 
314
321
  isoDateTime(): JsonSchemaStringBuilder<IsoDateTime | IN, IsoDateTime, Opt> {
315
- Object.assign(this.schema, { IsoDateTime: true })
322
+ _objectAssign(this.schema, { IsoDateTime: true })
316
323
  return this.branded<IsoDateTime>()
317
324
  }
318
325
 
@@ -392,6 +399,56 @@ export interface JsonSchemaStringEmailOptions {
392
399
  checkTLD: boolean
393
400
  }
394
401
 
402
+ export class JsonSchemaIsoDateBuilder<Opt extends boolean = false> extends JsonSchemaAnyBuilder<
403
+ string | IsoDate,
404
+ IsoDate,
405
+ Opt
406
+ > {
407
+ constructor() {
408
+ super({
409
+ type: 'string',
410
+ IsoDate: {},
411
+ })
412
+ }
413
+
414
+ before(date: string): this {
415
+ _objectAssign(this.schema.IsoDate!, { before: date })
416
+ return this
417
+ }
418
+
419
+ sameOrBefore(date: string): this {
420
+ _objectAssign(this.schema.IsoDate!, { sameOrBefore: date })
421
+ return this
422
+ }
423
+
424
+ after(date: string): this {
425
+ _objectAssign(this.schema.IsoDate!, { after: date })
426
+ return this
427
+ }
428
+
429
+ sameOrAfter(date: string): this {
430
+ _objectAssign(this.schema.IsoDate!, { sameOrAfter: date })
431
+ return this
432
+ }
433
+
434
+ between(fromDate: string, toDate: string, incl: Inclusiveness): this {
435
+ if (incl === '[)') {
436
+ _objectAssign(this.schema.IsoDate!, { sameOrAfter: fromDate, before: toDate })
437
+ } else if (incl === '[]') {
438
+ _objectAssign(this.schema.IsoDate!, { sameOrAfter: fromDate, sameOrBefore: toDate })
439
+ }
440
+
441
+ return this
442
+ }
443
+ }
444
+
445
+ export interface JsonSchemaIsoDateOptions {
446
+ before?: string
447
+ sameOrBefore?: string
448
+ after?: string
449
+ sameOrAfter?: string
450
+ }
451
+
395
452
  export class JsonSchemaNumberBuilder<
396
453
  IN extends number = number,
397
454
  OUT = IN,
@@ -404,7 +461,7 @@ export class JsonSchemaNumberBuilder<
404
461
  }
405
462
 
406
463
  integer(): this {
407
- Object.assign(this.schema, { type: 'integer' })
464
+ _objectAssign(this.schema, { type: 'integer' })
408
465
  return this
409
466
  }
410
467
 
@@ -413,27 +470,27 @@ export class JsonSchemaNumberBuilder<
413
470
  }
414
471
 
415
472
  multipleOf(multipleOf: number): this {
416
- Object.assign(this.schema, { multipleOf })
473
+ _objectAssign(this.schema, { multipleOf })
417
474
  return this
418
475
  }
419
476
 
420
477
  min(minimum: number): this {
421
- Object.assign(this.schema, { minimum })
478
+ _objectAssign(this.schema, { minimum })
422
479
  return this
423
480
  }
424
481
 
425
482
  exclusiveMin(exclusiveMinimum: number): this {
426
- Object.assign(this.schema, { exclusiveMinimum })
483
+ _objectAssign(this.schema, { exclusiveMinimum })
427
484
  return this
428
485
  }
429
486
 
430
487
  max(maximum: number): this {
431
- Object.assign(this.schema, { maximum })
488
+ _objectAssign(this.schema, { maximum })
432
489
  return this
433
490
  }
434
491
 
435
492
  exclusiveMax(exclusiveMaximum: number): this {
436
- Object.assign(this.schema, { exclusiveMaximum })
493
+ _objectAssign(this.schema, { exclusiveMaximum })
437
494
  return this
438
495
  }
439
496
 
@@ -575,7 +632,7 @@ export class JsonSchemaObjectBuilder<
575
632
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
576
633
  */
577
634
  allowAdditionalProperties(): this {
578
- Object.assign(this.schema, { additionalProperties: true })
635
+ _objectAssign(this.schema, { additionalProperties: true })
579
636
  return this
580
637
  }
581
638
 
@@ -583,7 +640,7 @@ export class JsonSchemaObjectBuilder<
583
640
  props: AnyObject,
584
641
  ): JsonSchemaObjectBuilder<IN & IN2, OUT & IN2, Opt> {
585
642
  const newBuilder = new JsonSchemaObjectBuilder<IN & IN2, OUT & IN2, Opt>()
586
- Object.assign(newBuilder.schema, _deepCopy(this.schema))
643
+ _objectAssign(newBuilder.schema, _deepCopy(this.schema))
587
644
 
588
645
  const incomingSchemaBuilder = new JsonSchemaObjectBuilder<IN2, IN2, false>(props)
589
646
  mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
@@ -675,7 +732,7 @@ export class JsonSchemaObjectInferringBuilder<
675
732
  * When set, the validation will not strip away properties that are not specified explicitly in the schema.
676
733
  */
677
734
  allowAdditionalProperties(): this {
678
- Object.assign(this.schema, { additionalProperties: true })
735
+ _objectAssign(this.schema, { additionalProperties: true })
679
736
  return this
680
737
  }
681
738
 
@@ -692,7 +749,7 @@ export class JsonSchemaObjectInferringBuilder<
692
749
  Opt
693
750
  > {
694
751
  const newBuilder = new JsonSchemaObjectInferringBuilder<PROPS, Opt>()
695
- Object.assign(newBuilder.schema, _deepCopy(this.schema))
752
+ _objectAssign(newBuilder.schema, _deepCopy(this.schema))
696
753
 
697
754
  const incomingSchemaBuilder = new JsonSchemaObjectInferringBuilder<NEW_PROPS, false>(props)
698
755
  mergeJsonSchemaObjects(newBuilder.schema as any, incomingSchemaBuilder.schema as any)
@@ -731,12 +788,12 @@ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<I
731
788
  }
732
789
 
733
790
  minLength(minItems: number): this {
734
- Object.assign(this.schema, { minItems })
791
+ _objectAssign(this.schema, { minItems })
735
792
  return this
736
793
  }
737
794
 
738
795
  maxLength(maxItems: number): this {
739
- Object.assign(this.schema, { maxItems })
796
+ _objectAssign(this.schema, { maxItems })
740
797
  return this
741
798
  }
742
799
 
@@ -748,8 +805,8 @@ export class JsonSchemaArrayBuilder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<I
748
805
  return this.minLength(length).maxLength(length)
749
806
  }
750
807
 
751
- unique(uniqueItems: number): this {
752
- Object.assign(this.schema, { uniqueItems })
808
+ unique(): this {
809
+ _objectAssign(this.schema, { uniqueItems: true })
753
810
  return this
754
811
  }
755
812
  }
@@ -767,12 +824,12 @@ export class JsonSchemaSet2Builder<IN, OUT, Opt> extends JsonSchemaAnyBuilder<
767
824
  }
768
825
 
769
826
  min(minItems: number): this {
770
- Object.assign(this.schema, { minItems })
827
+ _objectAssign(this.schema, { minItems })
771
828
  return this
772
829
  }
773
830
 
774
831
  max(maxItems: number): this {
775
- Object.assign(this.schema, { maxItems })
832
+ _objectAssign(this.schema, { maxItems })
776
833
  return this
777
834
  }
778
835
  }
@@ -809,7 +866,7 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
809
866
  readonly in?: IN
810
867
  readonly out?: OUT
811
868
 
812
- $schema?: AnyObject
869
+ $schema?: string
813
870
  $id?: string
814
871
  title?: string
815
872
  description?: string
@@ -857,14 +914,18 @@ export interface JsonSchema<IN = unknown, OUT = IN> {
857
914
  exclusiveMinimum?: number
858
915
  maximum?: number
859
916
  exclusiveMaximum?: number
917
+ minItems?: number
918
+ maxItems?: number
919
+ uniqueItems?: boolean
860
920
 
861
921
  enum?: any
862
922
 
863
923
  // Below we add custom Ajv keywords
864
924
 
925
+ email?: JsonSchemaStringEmailOptions
865
926
  Set2?: JsonSchema
866
927
  Buffer?: true
867
- IsoDate?: true
928
+ IsoDate?: JsonSchemaIsoDateOptions
868
929
  IsoDateTime?: true
869
930
  instanceof?: string | string[]
870
931
  transform?: { trim?: true; toLowerCase?: true; toUpperCase?: true; truncate?: number }