@naturalcycles/nodejs-lib 15.49.0 → 15.50.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.
@@ -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
+ }
@@ -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 };
@@ -496,7 +496,7 @@ export class JsonSchemaObjectBuilder extends JsonSchemaAnyBuilder {
496
496
  /**
497
497
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
498
498
  */
499
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
499
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
500
500
  dbEntity() {
501
501
  return this.extend({
502
502
  id: j.string(),
@@ -558,7 +558,7 @@ export class JsonSchemaObjectInferringBuilder extends JsonSchemaAnyBuilder {
558
558
  /**
559
559
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
560
560
  */
561
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
561
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
562
562
  dbEntity() {
563
563
  return this.extend({
564
564
  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.0",
5
5
  "dependencies": {
6
6
  "@naturalcycles/js-lib": "^15",
7
7
  "@types/js-yaml": "^4",
@@ -73,7 +73,7 @@ export class AjvSchema<IN = unknown, OUT = IN> {
73
73
  let jsonSchema: JsonSchema<IN, OUT>
74
74
 
75
75
  if (AjvSchema.isJsonSchemaBuilder(schema)) {
76
- jsonSchema = (schema as JsonSchemaTerminal<IN, OUT, any>).build()
76
+ jsonSchema = schema.build()
77
77
  AjvSchema.requireValidJsonSchema(jsonSchema)
78
78
  } else {
79
79
  jsonSchema = schema
@@ -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
+ }
@@ -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
 
@@ -677,7 +677,7 @@ export class JsonSchemaObjectBuilder<
677
677
  /**
678
678
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
679
679
  */
680
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
680
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
681
681
  dbEntity() {
682
682
  return this.extend({
683
683
  id: j.string(),
@@ -805,7 +805,7 @@ export class JsonSchemaObjectInferringBuilder<
805
805
  /**
806
806
  * Extends the current schema with `id`, `created` and `updated` according to NC DB conventions.
807
807
  */
808
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
808
+ // oxlint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
809
809
  dbEntity() {
810
810
  return this.extend({
811
811
  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
  }