@naturalcycles/nodejs-lib 15.47.0 → 15.47.1

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
  *
@@ -208,9 +208,10 @@ export declare class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder<string
208
208
  constructor();
209
209
  }
210
210
  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);
211
+ constructor(enumValues: readonly IN[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt);
212
212
  branded<B extends IN>(): JsonSchemaEnumBuilder<B | IN, B, Opt>;
213
213
  }
214
+ type EnumBaseType = 'string' | 'number' | 'other';
214
215
  export interface JsonSchema<IN = unknown, OUT = IN> {
215
216
  readonly in?: IN;
216
217
  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());
@@ -592,8 +601,15 @@ export class JsonSchemaBufferBuilder extends JsonSchemaAnyBuilder {
592
601
  }
593
602
  }
594
603
  export class JsonSchemaEnumBuilder extends JsonSchemaAnyBuilder {
595
- constructor(enumValues, opt) {
596
- super({ enum: enumValues });
604
+ constructor(enumValues, baseType, opt) {
605
+ const jsonSchema = { enum: enumValues };
606
+ // Specifying the base type helps in cases when we ask Ajv to coerce the types.
607
+ // Having only the `enum` in the schema does not trigger a coercion in Ajv.
608
+ if (baseType === 'string')
609
+ jsonSchema.type = 'string';
610
+ if (baseType === 'number')
611
+ jsonSchema.type = 'number';
612
+ super(jsonSchema);
597
613
  if (opt?.name)
598
614
  this.setErrorMessage('pattern', `is not a valid ${opt.name}`);
599
615
  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.47.1",
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<
@@ -851,8 +864,15 @@ export class JsonSchemaEnumBuilder<
851
864
  OUT extends IN = IN,
852
865
  Opt extends boolean = false,
853
866
  > extends JsonSchemaAnyBuilder<IN, OUT, Opt> {
854
- constructor(enumValues: readonly IN[], opt?: JsonBuilderRuleOpt) {
855
- super({ enum: enumValues })
867
+ constructor(enumValues: readonly IN[], baseType: EnumBaseType, opt?: JsonBuilderRuleOpt) {
868
+ const jsonSchema: JsonSchema = { enum: enumValues }
869
+ // Specifying the base type helps in cases when we ask Ajv to coerce the types.
870
+ // Having only the `enum` in the schema does not trigger a coercion in Ajv.
871
+ if (baseType === 'string') jsonSchema.type = 'string'
872
+ if (baseType === 'number') jsonSchema.type = 'number'
873
+
874
+ super(jsonSchema)
875
+
856
876
  if (opt?.name) this.setErrorMessage('pattern', `is not a valid ${opt.name}`)
857
877
  if (opt?.msg) this.setErrorMessage('enum', opt.msg)
858
878
  }
@@ -862,6 +882,8 @@ export class JsonSchemaEnumBuilder<
862
882
  }
863
883
  }
864
884
 
885
+ type EnumBaseType = 'string' | 'number' | 'other'
886
+
865
887
  export interface JsonSchema<IN = unknown, OUT = IN> {
866
888
  readonly in?: IN
867
889
  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
+ }