@naturalcycles/nodejs-lib 15.47.0 → 15.48.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.
@@ -13,6 +13,13 @@ export declare const getAjv: any;
13
13
  * and are not interested in transforming the data.
14
14
  */
15
15
  export declare const getNonMutatingAjv: any;
16
+ /**
17
+ * Returns cached instance of Ajv, which is coercing data.
18
+ *
19
+ * To be used in places where we know that we are going to receive data with the wrong type,
20
+ * typically: request path params and request query params.
21
+ */
22
+ export declare const getCoercingAjv: any;
16
23
  /**
17
24
  * Create Ajv with modified defaults.
18
25
  *
@@ -19,6 +19,10 @@ const AJV_NON_MUTATING_OPTIONS = {
19
19
  removeAdditional: false,
20
20
  useDefaults: false,
21
21
  };
22
+ const AJV_MUTATING_COERCING_OPTIONS = {
23
+ ...AJV_OPTIONS,
24
+ coerceTypes: true,
25
+ };
22
26
  /**
23
27
  * Return cached instance of Ajv with default (recommended) options.
24
28
  *
@@ -33,6 +37,13 @@ export const getAjv = _lazyValue(createAjv);
33
37
  * and are not interested in transforming the data.
34
38
  */
35
39
  export const getNonMutatingAjv = _lazyValue(() => createAjv(AJV_NON_MUTATING_OPTIONS));
40
+ /**
41
+ * Returns cached instance of Ajv, which is coercing data.
42
+ *
43
+ * To be used in places where we know that we are going to receive data with the wrong type,
44
+ * typically: request path params and request query params.
45
+ */
46
+ export const getCoercingAjv = _lazyValue(() => createAjv(AJV_MUTATING_COERCING_OPTIONS));
36
47
  /**
37
48
  * Create Ajv with modified defaults.
38
49
  *
@@ -163,6 +163,8 @@ export declare class JsonSchemaObjectBuilder<IN extends AnyObject, OUT extends A
163
163
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
164
164
  */
165
165
  dbEntity(): JsonSchemaObjectBuilder<any, any, Opt>;
166
+ minProperties(minProperties: number): this;
167
+ maxProperties(maxProperties: number): this;
166
168
  }
167
169
  export declare class JsonSchemaObjectInferringBuilder<PROPS extends Record<string, JsonSchemaAnyBuilder<any, any, any>>, Opt extends boolean = false> extends JsonSchemaAnyBuilder<Expand<{
168
170
  [K in keyof PROPS as PROPS[K] extends JsonSchemaAnyBuilder<any, any, infer IsOpt> ? IsOpt extends true ? never : K : never]: PROPS[K] extends JsonSchemaAnyBuilder<infer IN, any, any> ? IN : never;
@@ -208,9 +210,10 @@ export declare class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder<string
208
210
  constructor();
209
211
  }
210
212
  export declare class JsonSchemaEnumBuilder<IN extends string | number | boolean | null, OUT extends IN = IN, Opt extends boolean = false> extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
211
- constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt);
213
+ constructor(enumValues: readonly IN[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt);
212
214
  branded<B extends IN>(): JsonSchemaEnumBuilder<B | IN, B, Opt>;
213
215
  }
216
+ type EnumBaseType = 'string' | 'number' | 'other';
214
217
  export interface JsonSchema<IN = unknown, OUT = IN> {
215
218
  readonly in?: IN;
216
219
  readonly out?: OUT;
@@ -6,7 +6,7 @@ import { _assert } from '@naturalcycles/js-lib/error';
6
6
  import { _deepCopy, _sortObject } from '@naturalcycles/js-lib/object';
7
7
  import { _objectAssign, JWT_REGEX, } from '@naturalcycles/js-lib/types';
8
8
  import { TIMEZONES } from '../timezones.js';
9
- import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js';
9
+ import { isEveryItemNumber, isEveryItemString, JSON_SCHEMA_ORDER, mergeJsonSchemaObjects, } from './jsonSchemaBuilder.util.js';
10
10
  export const j = {
11
11
  string() {
12
12
  return new JsonSchemaStringBuilder();
@@ -35,20 +35,29 @@ export const j = {
35
35
  },
36
36
  enum(input, opt) {
37
37
  let enumValues;
38
+ let baseType = 'other';
38
39
  if (Array.isArray(input)) {
39
40
  enumValues = input;
41
+ if (isEveryItemNumber(input)) {
42
+ baseType = 'number';
43
+ }
44
+ else if (isEveryItemString(input)) {
45
+ baseType = 'string';
46
+ }
40
47
  }
41
48
  else if (typeof input === 'object') {
42
49
  const enumType = getEnumType(input);
43
50
  if (enumType === 'NumberEnum') {
44
51
  enumValues = _numberEnumValues(input);
52
+ baseType = 'number';
45
53
  }
46
54
  else if (enumType === 'StringEnum') {
47
55
  enumValues = _stringEnumValues(input);
56
+ baseType = 'string';
48
57
  }
49
58
  }
50
59
  _assert(enumValues, 'Unsupported enum input');
51
- return new JsonSchemaEnumBuilder(enumValues, opt);
60
+ return new JsonSchemaEnumBuilder(enumValues, baseType, opt);
52
61
  },
53
62
  oneOf(items) {
54
63
  const schemas = items.map(b => b.build());
@@ -487,6 +496,14 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
487
496
  updated: j.number().unixTimestamp2000(),
488
497
  });
489
498
  }
499
+ minProperties(minProperties) {
500
+ Object.assign(this.schema, { minProperties });
501
+ return this;
502
+ }
503
+ maxProperties(maxProperties) {
504
+ Object.assign(this.schema, { maxProperties });
505
+ return this;
506
+ }
490
507
  }
491
508
  export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
492
509
  constructor(props) {
@@ -592,8 +609,15 @@ export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
592
609
  }
593
610
  }
594
611
  export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
595
- constructor(enumValues, opt) {
596
- super({ enum: enumValues });
612
+ constructor(enumValues, baseType, opt) {
613
+ const jsonSchema = { enum: enumValues };
614
+ // Specifying the base type helps in cases when we ask Ajv to coerce the types.
615
+ // Having only the `enum` in the schema does not trigger a coercion in Ajv.
616
+ if (baseType === 'string')
617
+ jsonSchema.type = 'string';
618
+ if (baseType === 'number')
619
+ jsonSchema.type = 'number';
620
+ super(jsonSchema);
597
621
  if (opt?.name)
598
622
  this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
599
623
  if (opt?.msg)
@@ -7,3 +7,5 @@ export declare const JSON_SCHEMA_ORDER: string[];
7
7
  * API similar to Object.assign(s1, s2)
8
8
  */
9
9
  export declare function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObject>(schema1: JsonSchema<T1>, schema2: JsonSchema<T2>): JsonSchema<T1 & T2>;
10
+ export declare function isEveryItemString(arr: any[]): boolean;
11
+ export declare function isEveryItemNumber(arr: any[]): boolean;
@@ -63,3 +63,17 @@ export function mergeJsonSchemaObjects(schema1, schema2) {
63
63
  // `additionalProperties` remains the same
64
64
  return _filterNullishValues(s1, { mutate: true });
65
65
  }
66
+ export function isEveryItemString(arr) {
67
+ for (let i = 0; i <= arr.length; ++i) {
68
+ if (typeof arr[i] !== 'string')
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+ export function isEveryItemNumber(arr) {
74
+ for (let i = 0; i <= arr.length; ++i) {
75
+ if (typeof arr[i] !== 'number')
76
+ return false;
77
+ }
78
+ return true;
79
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.47.0",
4
+ "version": "15.48.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -24,6 +24,11 @@ const AJV_NON_MUTATING_OPTIONS: Options = {
24
24
  useDefaults: false,
25
25
  }
26
26
 
27
+ const AJV_MUTATING_COERCING_OPTIONS: Options = {
28
+ ...AJV_OPTIONS,
29
+ coerceTypes: true,
30
+ }
31
+
27
32
  /**
28
33
  * Return cached instance of Ajv with default (recommended) options.
29
34
  *
@@ -40,6 +45,14 @@ export const getAjv = _lazyValue(createAjv)
40
45
  */
41
46
  export const getNonMutatingAjv = _lazyValue(() => createAjv(AJV_NON_MUTATING_OPTIONS))
42
47
 
48
+ /**
49
+ * Returns cached instance of Ajv, which is coercing data.
50
+ *
51
+ * To be used in places where we know that we are going to receive data with the wrong type,
52
+ * typically: request path params and request query params.
53
+ */
54
+ export const getCoercingAjv = _lazyValue(() => createAjv(AJV_MUTATING_COERCING_OPTIONS))
55
+
43
56
  /**
44
57
  * Create Ajv with modified defaults.
45
58
  *
@@ -27,7 +27,12 @@ import {
27
27
  type UnixTimestampMillis,
28
28
  } from '@naturalcycles/js-lib/types'
29
29
  import { TIMEZONES } from '../timezones.js'
30
- import { JSON_SCHEMA_ORDER, mergeJsonSchemaObjects } from './jsonSchemaBuilder.util.js'
30
+ import {
31
+ isEveryItemNumber,
32
+ isEveryItemString,
33
+ JSON_SCHEMA_ORDER,
34
+ mergeJsonSchemaObjects,
35
+ } from './jsonSchemaBuilder.util.js'
31
36
 
32
37
  export const j = {
33
38
  string(): JsonSchemaStringBuilder<string, string, false> {
@@ -79,20 +84,28 @@ export const j = {
79
84
  : never
80
85
  > {
81
86
  let enumValues: readonly (string | number | boolean | null)[] | undefined
87
+ let baseType: EnumBaseType = 'other'
82
88
 
83
89
  if (Array.isArray(input)) {
84
90
  enumValues = input
91
+ if (isEveryItemNumber(input)) {
92
+ baseType = 'number'
93
+ } else if (isEveryItemString(input)) {
94
+ baseType = 'string'
95
+ }
85
96
  } else if (typeof input === 'object') {
86
97
  const enumType = getEnumType(input)
87
98
  if (enumType === 'NumberEnum') {
88
99
  enumValues = _numberEnumValues(input as NumberEnum)
100
+ baseType = 'number'
89
101
  } else if (enumType === 'StringEnum') {
90
102
  enumValues = _stringEnumValues(input as StringEnum)
103
+ baseType = 'string'
91
104
  }
92
105
  }
93
106
 
94
107
  _assert(enumValues, 'Unsupported enum input')
95
- return new JsonSchemaEnumBuilder(enumValues as any, opt)
108
+ return new JsonSchemaEnumBuilder(enumValues as any, baseType, opt)
96
109
  },
97
110
 
98
111
  oneOf<
@@ -659,6 +672,16 @@ export class JsonSchemaObjectBuilder<
659
672
  updated: j.number().unixTimestamp2000(),
660
673
  })
661
674
  }
675
+
676
+ minProperties(minProperties: number): this {
677
+ Object.assign(this.schema, { minProperties })
678
+ return this
679
+ }
680
+
681
+ maxProperties(maxProperties: number): this {
682
+ Object.assign(this.schema, { maxProperties })
683
+ return this
684
+ }
662
685
  }
663
686
 
664
687
  export class JsonSchemaObjectInferringBuilder<
@@ -851,8 +874,15 @@ export class JsonSchemaEnumBuilder<
851
874
  OUT extends IN = IN,
852
875
  Opt extends boolean = false,
853
876
  > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
854
- constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt) {
855
- super({ enum: enumValues })
877
+ constructor(enumValues: readonly IN[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt) {
878
+ const jsonSchema: JsonSchema = { enum: enumValues }
879
+ // Specifying the base type helps in cases when we ask Ajv to coerce the types.
880
+ // Having only the `enum` in the schema does not trigger a coercion in Ajv.
881
+ if (baseType === 'string') jsonSchema.type = 'string'
882
+ if (baseType === 'number') jsonSchema.type = 'number'
883
+
884
+ super(jsonSchema)
885
+
856
886
  if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
857
887
  if (opt?.msg) this.setErrorMessage('enum', opt.msg)
858
888
  }
@@ -862,6 +892,8 @@ export class JsonSchemaEnumBuilder<
862
892
  }
863
893
  }
864
894
 
895
+ type EnumBaseType = 'string' | 'number' | 'other'
896
+
865
897
  export interface JsonSchema<IN = unknown, OUT = IN> {
866
898
  readonly in?: IN
867
899
  readonly out?: OUT
@@ -76,3 +76,17 @@ export function mergeJsonSchemaObjects<T1 extends AnyObject, T2 extends AnyObjec
76
76
 
77
77
  return _filterNullishValues(s1, { mutate: true })
78
78
  }
79
+
80
+ export function isEveryItemString(arr: any[]): boolean {
81
+ for (let i = 0; i <= arr.length; ++i) {
82
+ if (typeof arr[i] !== 'string') return false
83
+ }
84
+ return true
85
+ }
86
+
87
+ export function isEveryItemNumber(arr: any[]): boolean {
88
+ for (let i = 0; i <= arr.length; ++i) {
89
+ if (typeof arr[i] !== 'number') return false
90
+ }
91
+ return true
92
+ }