@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.
- package/dist/validation/ajv/from-data/generateJsonSchemaFromData.d.ts +8 -0
- package/dist/validation/ajv/from-data/generateJsonSchemaFromData.js +87 -0
- package/dist/validation/ajv/getAjv.js +32 -13
- package/dist/validation/ajv/index.d.ts +1 -0
- package/dist/validation/ajv/index.js +1 -0
- package/dist/validation/ajv/jsonSchemaBuilder.js +3 -5
- package/dist/yargs/yargs.util.js +1 -1
- package/package.json +1 -1
- package/src/validation/ajv/from-data/generateJsonSchemaFromData.ts +112 -0
- package/src/validation/ajv/getAjv.ts +35 -11
- package/src/validation/ajv/index.ts +1 -0
- package/src/validation/ajv/jsonSchemaBuilder.ts +3 -7
- package/src/yargs/yargs.util.ts +1 -1
|
@@ -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:
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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({
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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(),
|
package/dist/yargs/yargs.util.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
@@ -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:
|
|
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
|
-
|
|
252
|
+
const cleanData = data.trim()
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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(),
|
package/src/yargs/yargs.util.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
}
|