@naturalcycles/nodejs-lib 15.49.0 → 15.50.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.
@@ -0,0 +1,8 @@
1
+ import { type AnyObject } from '@naturalcycles/js-lib/types';
2
+ import type { JsonSchema } from '../jsonSchemaBuilder.js';
3
+ /**
4
+ * Each row must be an object (current limitation).
5
+ *
6
+ * `additionalProperties` is set to `true`, cause it's safer.
7
+ */
8
+ export declare function generateJsonSchemaFromData<T extends AnyObject = AnyObject>(rows: AnyObject[]): JsonSchema<T>;
@@ -0,0 +1,87 @@
1
+ import { _uniq } from '@naturalcycles/js-lib/array';
2
+ import { _stringMapEntries } from '@naturalcycles/js-lib/types';
3
+ /**
4
+ * Each row must be an object (current limitation).
5
+ *
6
+ * `additionalProperties` is set to `true`, cause it's safer.
7
+ */
8
+ export function generateJsonSchemaFromData(rows) {
9
+ return objectToJsonSchema(rows);
10
+ }
11
+ function objectToJsonSchema(rows) {
12
+ const typesByKey = {};
13
+ rows.forEach(r => {
14
+ Object.keys(r).forEach(key => {
15
+ typesByKey[key] ||= new Set();
16
+ typesByKey[key].add(getTypeOfValue(r[key]));
17
+ });
18
+ });
19
+ const s = {
20
+ type: 'object',
21
+ properties: {},
22
+ required: [],
23
+ additionalProperties: true,
24
+ };
25
+ _stringMapEntries(typesByKey).forEach(([key, types]) => {
26
+ const schema = mergeTypes([...types], rows.map(r => r[key]));
27
+ if (!schema)
28
+ return;
29
+ s.properties[key] = schema;
30
+ });
31
+ // console.log(typesByKey)
32
+ return s;
33
+ }
34
+ function mergeTypes(types, samples) {
35
+ // skip "undefined" types
36
+ types = types.filter(t => t !== 'undefined');
37
+ if (!types.length)
38
+ return undefined;
39
+ if (types.length > 1) {
40
+ // oneOf
41
+ const s = {
42
+ oneOf: types.map(type => mergeTypes([type], samples)),
43
+ };
44
+ return s;
45
+ }
46
+ const type = types[0];
47
+ if (type === 'null') {
48
+ return {
49
+ type: 'null',
50
+ };
51
+ }
52
+ if (type === 'boolean') {
53
+ return {
54
+ type: 'boolean',
55
+ };
56
+ }
57
+ if (type === 'string') {
58
+ return {
59
+ type: 'string',
60
+ };
61
+ }
62
+ if (type === 'number') {
63
+ return {
64
+ type: 'number',
65
+ };
66
+ }
67
+ if (type === 'object') {
68
+ return objectToJsonSchema(samples.filter((r) => r && typeof r === 'object'));
69
+ }
70
+ if (type === 'array') {
71
+ // possible feature: detect if it's a tuple
72
+ // currently assume no-tuple
73
+ const items = samples.filter(r => Array.isArray(r)).flat();
74
+ const itemTypes = _uniq(items.map(i => getTypeOfValue(i)));
75
+ return {
76
+ type: 'array',
77
+ items: mergeTypes(itemTypes, items),
78
+ };
79
+ }
80
+ }
81
+ function getTypeOfValue(v) {
82
+ if (v === null)
83
+ return 'null';
84
+ if (Array.isArray(v))
85
+ return 'array';
86
+ return typeof v;
87
+ }
@@ -220,23 +220,42 @@ export function createAjv(opt) {
220
220
  ajv.addKeyword({
221
221
  keyword: 'email',
222
222
  type: 'string',
223
- modifying: false,
223
+ modifying: true,
224
224
  errors: true,
225
225
  schemaType: 'object',
226
226
  validate: function validate(opt, data, _schema, ctx) {
227
227
  const { checkTLD } = opt;
228
- if (!checkTLD)
229
- return true;
230
- const tld = _substringAfterLast(data, '.');
231
- if (validTLDs.has(tld))
232
- return true;
233
- validate.errors = [
234
- {
235
- instancePath: ctx?.instancePath ?? '',
236
- message: `has an invalid TLD`,
237
- },
238
- ];
239
- return false;
228
+ const cleanData = data.trim();
229
+ // from `ajv-formats`
230
+ const EMAIL_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;
231
+ const result = cleanData.match(EMAIL_REGEX);
232
+ if (!result) {
233
+ ;
234
+ validate.errors = [
235
+ {
236
+ instancePath: ctx?.instancePath ?? '',
237
+ message: `is not a valid email address`,
238
+ },
239
+ ];
240
+ return false;
241
+ }
242
+ if (checkTLD) {
243
+ const tld = _substringAfterLast(cleanData, '.');
244
+ if (!validTLDs.has(tld)) {
245
+ ;
246
+ validate.errors = [
247
+ {
248
+ instancePath: ctx?.instancePath ?? '',
249
+ message: `has an invalid TLD`,
250
+ },
251
+ ];
252
+ return false;
253
+ }
254
+ }
255
+ if (ctx?.parentData && ctx.parentDataProperty) {
256
+ ctx.parentData[ctx.parentDataProperty] = cleanData;
257
+ }
258
+ return true;
240
259
  },
241
260
  });
242
261
  ajv.addKeyword({
@@ -1,6 +1,7 @@
1
1
  import Ajv from 'ajv';
2
2
  export * from './ajvSchema.js';
3
3
  export * from './ajvValidationError.js';
4
+ export * from './from-data/generateJsonSchemaFromData.js';
4
5
  export * from './getAjv.js';
5
6
  export * from './jsonSchemaBuilder.js';
6
7
  export { Ajv };
@@ -1,6 +1,7 @@
1
1
  import Ajv from 'ajv';
2
2
  export * from './ajvSchema.js';
3
3
  export * from './ajvValidationError.js';
4
+ export * from './from-data/generateJsonSchemaFromData.js';
4
5
  export * from './getAjv.js';
5
6
  export * from './jsonSchemaBuilder.js';
6
7
  export { Ajv };
@@ -217,9 +217,7 @@ export class JsonSchemaStringBuilder extends JsonSchemaAnyBuilder {
217
217
  email(opt) {
218
218
  const defaultOptions = { checkTLD: true };
219
219
  _objectAssign(this.schema, { email: { ...defaultOptions, ...opt } });
220
- // from `ajv-formats`
221
- 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;
222
- return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase();
220
+ return this.trim().toLowerCase();
223
221
  }
224
222
  trim() {
225
223
  _objectAssign(this.schema, { transform: { ...this.schema.transform, trim: true } });
@@ -496,7 +494,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
496
494
  /**
497
495
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
498
496
  */
499
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
497
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
500
498
  dbEntity() {
501
499
  return this.extend({
502
500
  id: j.string(),
@@ -558,7 +556,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
558
556
  /**
559
557
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
560
558
  */
561
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
559
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
562
560
  dbEntity() {
563
561
  return this.extend({
564
562
  id: j.string(),
@@ -5,7 +5,7 @@ import { hideBin } from 'yargs/helpers';
5
5
  * Quick yargs helper to make it work in esm.
6
6
  * It also allows to not have yargs and `@types/yargs` to be declared as dependencies.
7
7
  */
8
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
8
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
9
9
  export function _yargs() {
10
10
  return yargs(hideBin(process.argv));
11
11
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/nodejs-lib",
3
3
  "type": "module",
4
- "version": "15.49.0",
4
+ "version": "15.50.1",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -0,0 +1,112 @@
1
+ import { _uniq } from '@naturalcycles/js-lib/array'
2
+ import { _stringMapEntries, type AnyObject, type StringMap } from '@naturalcycles/js-lib/types'
3
+ import type { JsonSchema } from '../jsonSchemaBuilder.js'
4
+
5
+ type PrimitiveType = 'undefined' | 'null' | 'boolean' | 'string' | 'number'
6
+ type Type = PrimitiveType | 'array' | 'object'
7
+
8
+ /**
9
+ * Each row must be an object (current limitation).
10
+ *
11
+ * `additionalProperties` is set to `true`, cause it's safer.
12
+ */
13
+ export function generateJsonSchemaFromData<T extends AnyObject = AnyObject>(
14
+ rows: AnyObject[],
15
+ ): JsonSchema<T> {
16
+ return objectToJsonSchema<T>(rows as any)
17
+ }
18
+
19
+ function objectToJsonSchema<T extends AnyObject>(rows: AnyObject[]): JsonSchema<T> {
20
+ const typesByKey: StringMap<Set<Type>> = {}
21
+
22
+ rows.forEach(r => {
23
+ Object.keys(r).forEach(key => {
24
+ typesByKey[key] ||= new Set<Type>()
25
+ typesByKey[key].add(getTypeOfValue(r[key]))
26
+ })
27
+ })
28
+
29
+ const s: JsonSchema<T> = {
30
+ type: 'object',
31
+ properties: {} as any,
32
+ required: [],
33
+ additionalProperties: true,
34
+ }
35
+
36
+ _stringMapEntries(typesByKey).forEach(([key, types]) => {
37
+ const schema = mergeTypes(
38
+ [...types],
39
+ rows.map(r => r[key]),
40
+ )
41
+ if (!schema) return
42
+ s.properties![key as keyof T] = schema as any
43
+ })
44
+
45
+ // console.log(typesByKey)
46
+
47
+ return s
48
+ }
49
+
50
+ function mergeTypes(types: Type[], samples: any[]): JsonSchema | undefined {
51
+ // skip "undefined" types
52
+ types = types.filter(t => t !== 'undefined')
53
+
54
+ if (!types.length) return undefined
55
+
56
+ if (types.length > 1) {
57
+ // oneOf
58
+ const s: JsonSchema = {
59
+ oneOf: types.map(type => mergeTypes([type], samples)!),
60
+ }
61
+
62
+ return s
63
+ }
64
+
65
+ const type = types[0]!
66
+
67
+ if (type === 'null') {
68
+ return {
69
+ type: 'null',
70
+ } as JsonSchema
71
+ }
72
+
73
+ if (type === 'boolean') {
74
+ return {
75
+ type: 'boolean',
76
+ } as JsonSchema
77
+ }
78
+
79
+ if (type === 'string') {
80
+ return {
81
+ type: 'string',
82
+ } as JsonSchema
83
+ }
84
+
85
+ if (type === 'number') {
86
+ return {
87
+ type: 'number',
88
+ } as JsonSchema
89
+ }
90
+
91
+ if (type === 'object') {
92
+ return objectToJsonSchema(samples.filter((r: any) => r && typeof r === 'object'))
93
+ }
94
+
95
+ if (type === 'array') {
96
+ // possible feature: detect if it's a tuple
97
+ // currently assume no-tuple
98
+ const items = samples.filter(r => Array.isArray(r)).flat()
99
+ const itemTypes = _uniq(items.map(i => getTypeOfValue(i)))
100
+
101
+ return {
102
+ type: 'array',
103
+ items: mergeTypes(itemTypes, items),
104
+ } as JsonSchema
105
+ }
106
+ }
107
+
108
+ function getTypeOfValue(v: any): Type {
109
+ if (v === null) return 'null'
110
+ if (Array.isArray(v)) return 'array'
111
+ return typeof v as Type
112
+ }
@@ -244,22 +244,46 @@ export function createAjv(opt?: Options): Ajv {
244
244
  ajv.addKeyword({
245
245
  keyword: 'email',
246
246
  type: 'string',
247
- modifying: false,
247
+ modifying: true,
248
248
  errors: true,
249
249
  schemaType: 'object',
250
250
  validate: function validate(opt: JsonSchemaStringEmailOptions, data: string, _schema, ctx) {
251
251
  const { checkTLD } = opt
252
- if (!checkTLD) return true
252
+ const cleanData = data.trim()
253
253
 
254
- const tld = _substringAfterLast(data, '.')
255
- if (validTLDs.has(tld)) return true
256
- ;(validate as any).errors = [
257
- {
258
- instancePath: ctx?.instancePath ?? '',
259
- message: `has an invalid TLD`,
260
- },
261
- ]
262
- return false
254
+ // from `ajv-formats`
255
+ const EMAIL_REGEX =
256
+ /^[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
257
+ const result = cleanData.match(EMAIL_REGEX)
258
+
259
+ if (!result) {
260
+ ;(validate as any).errors = [
261
+ {
262
+ instancePath: ctx?.instancePath ?? '',
263
+ message: `is not a valid email address`,
264
+ },
265
+ ]
266
+ return false
267
+ }
268
+
269
+ if (checkTLD) {
270
+ const tld = _substringAfterLast(cleanData, '.')
271
+ if (!validTLDs.has(tld)) {
272
+ ;(validate as any).errors = [
273
+ {
274
+ instancePath: ctx?.instancePath ?? '',
275
+ message: `has an invalid TLD`,
276
+ },
277
+ ]
278
+ return false
279
+ }
280
+ }
281
+
282
+ if (ctx?.parentData && ctx.parentDataProperty) {
283
+ ctx.parentData[ctx.parentDataProperty] = cleanData
284
+ }
285
+
286
+ return true
263
287
  },
264
288
  })
265
289
 
@@ -2,6 +2,7 @@ import Ajv from 'ajv'
2
2
 
3
3
  export * from './ajvSchema.js'
4
4
  export * from './ajvValidationError.js'
5
+ export * from './from-data/generateJsonSchemaFromData.js'
5
6
  export * from './getAjv.js'
6
7
  export * from './jsonSchemaBuilder.js'
7
8
 
@@ -302,11 +302,7 @@ export class JsonSchemaStringBuilder<
302
302
  email(opt?: Partial<JsonSchemaStringEmailOptions>): this {
303
303
  const defaultOptions: JsonSchemaStringEmailOptions = { checkTLD: true }
304
304
  _objectAssign(this.schema, { email: { ...defaultOptions, ...opt } })
305
-
306
- // from `ajv-formats`
307
- const regex =
308
- /^[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
309
- return this.regex(regex, { msg: 'is not a valid email address' }).trim().toLowerCase()
305
+ return this.trim().toLowerCase()
310
306
  }
311
307
 
312
308
  trim(): this {
@@ -677,7 +673,7 @@ export class JsonSchemaObjectBuilder<
677
673
  /**
678
674
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
679
675
  */
680
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
676
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
681
677
  dbEntity() {
682
678
  return this.extend({
683
679
  id: j.string(),
@@ -805,7 +801,7 @@ export class JsonSchemaObjectInferringBuilder<
805
801
  /**
806
802
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
807
803
  */
808
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
804
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
809
805
  dbEntity() {
810
806
  return this.extend({
811
807
  id: j.string(),
@@ -6,7 +6,7 @@ import { hideBin } from 'yargs/helpers'
6
6
  * Quick yargs helper to make it work in esm.
7
7
  * It also allows to not have yargs and `@types/yargs` to be declared as dependencies.
8
8
  */
9
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
9
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
10
10
  export function _yargs() {
11
11
  return yargs(hideBin(process.argv))
12
12
  }