@naturalcycles/db-lib 8.11.0 → 8.13.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.
- package/CHANGELOG.md +29 -0
- package/dist/adapter/cachedb/cache.db.d.ts +3 -3
- package/dist/adapter/cachedb/cache.db.js +4 -4
- package/dist/adapter/file/file.db.d.ts +2 -2
- package/dist/adapter/file/file.db.js +20 -18
- package/dist/adapter/file/inMemory.persistence.plugin.js +1 -1
- package/dist/adapter/file/localFile.persistence.plugin.js +9 -9
- package/dist/adapter/inmemory/inMemory.db.d.ts +3 -4
- package/dist/adapter/inmemory/inMemory.db.js +25 -23
- package/dist/adapter/inmemory/queryInMemory.js +1 -1
- package/dist/base.common.db.d.ts +3 -3
- package/dist/base.common.db.js +10 -4
- package/dist/common.db.d.ts +3 -3
- package/dist/commondao/common.dao.d.ts +7 -7
- package/dist/commondao/common.dao.js +56 -48
- package/dist/commondao/common.dao.model.d.ts +20 -27
- package/dist/db.model.js +2 -2
- package/dist/getDB.js +3 -3
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -5
- package/dist/kv/commonKeyValueDao.js +4 -4
- package/dist/model.util.js +2 -2
- package/dist/pipeline/dbPipelineBackup.d.ts +0 -1
- package/dist/pipeline/dbPipelineBackup.js +14 -25
- package/dist/pipeline/dbPipelineCopy.js +11 -11
- package/dist/pipeline/dbPipelineRestore.js +20 -20
- package/dist/query/dbQuery.js +2 -2
- package/dist/testing/daoTest.js +23 -23
- package/dist/testing/dbTest.js +17 -17
- package/dist/testing/keyValueDBTest.js +18 -14
- package/dist/testing/keyValueDaoTest.js +18 -14
- package/dist/testing/test.model.d.ts +4 -2
- package/dist/testing/test.model.js +39 -8
- package/dist/testing/timeSeriesTest.util.js +1 -1
- package/dist/transaction/dbTransaction.util.js +2 -2
- package/dist/validation/index.js +9 -9
- package/package.json +1 -1
- package/readme.md +5 -4
- package/src/adapter/cachedb/cache.db.ts +9 -5
- package/src/adapter/file/file.db.ts +7 -4
- package/src/adapter/inmemory/inMemory.db.ts +20 -7
- package/src/base.common.db.ts +10 -4
- package/src/common.db.ts +3 -3
- package/src/commondao/common.dao.model.ts +24 -29
- package/src/commondao/common.dao.ts +35 -24
- package/src/index.ts +0 -7
- package/src/pipeline/dbPipelineBackup.ts +2 -15
- package/src/pipeline/dbPipelineRestore.ts +1 -1
- package/src/testing/dbTest.ts +1 -1
- package/src/testing/keyValueDBTest.ts +10 -6
- package/src/testing/keyValueDaoTest.ts +10 -6
- package/src/testing/test.model.ts +38 -4
- package/dist/schema/common.schema.d.ts +0 -38
- package/dist/schema/common.schema.js +0 -18
- package/dist/schema/commonSchemaGenerator.d.ts +0 -35
- package/dist/schema/commonSchemaGenerator.js +0 -151
- package/src/schema/common.schema.ts +0 -49
- package/src/schema/commonSchemaGenerator.ts +0 -200
|
@@ -40,8 +40,10 @@ export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
|
40
40
|
|
|
41
41
|
test('streamIds limited', async () => {
|
|
42
42
|
const idsLimited = await readableToArray(dao.streamIds(2))
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Order is non-deterministic, so, cannot compare values
|
|
44
|
+
// idsLimited.sort()
|
|
45
|
+
// expect(idsLimited).toEqual(testIds.slice(0, 2))
|
|
46
|
+
expect(idsLimited.length).toBe(2)
|
|
45
47
|
})
|
|
46
48
|
|
|
47
49
|
test('streamValues', async () => {
|
|
@@ -52,8 +54,9 @@ export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
|
52
54
|
|
|
53
55
|
test('streamValues limited', async () => {
|
|
54
56
|
const valuesLimited = await readableToArray(dao.streamValues(2))
|
|
55
|
-
valuesLimited.sort()
|
|
56
|
-
expect(valuesLimited).toEqual(testEntries.map(e => e[1]).slice(0, 2))
|
|
57
|
+
// valuesLimited.sort()
|
|
58
|
+
// expect(valuesLimited).toEqual(testEntries.map(e => e[1]).slice(0, 2))
|
|
59
|
+
expect(valuesLimited.length).toBe(2)
|
|
57
60
|
})
|
|
58
61
|
|
|
59
62
|
test('streamEntries', async () => {
|
|
@@ -64,8 +67,9 @@ export function runCommonKeyValueDaoTest(dao: CommonKeyValueDao<Buffer>): void {
|
|
|
64
67
|
|
|
65
68
|
test('streamEntries limited', async () => {
|
|
66
69
|
const entriesLimited = await readableToArray(dao.streamEntries(2))
|
|
67
|
-
entriesLimited.sort()
|
|
68
|
-
expect(entriesLimited).toEqual(testEntries.slice(0, 2))
|
|
70
|
+
// entriesLimited.sort()
|
|
71
|
+
// expect(entriesLimited).toEqual(testEntries.slice(0, 2))
|
|
72
|
+
expect(entriesLimited.length).toBe(2)
|
|
69
73
|
})
|
|
70
74
|
|
|
71
75
|
test('deleteByIds should clear', async () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _range } from '@naturalcycles/js-lib'
|
|
1
|
+
import { jsonSchema, JsonSchemaObject, _range } from '@naturalcycles/js-lib'
|
|
2
2
|
import {
|
|
3
3
|
binarySchema,
|
|
4
4
|
booleanSchema,
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
objectSchema,
|
|
7
7
|
stringSchema,
|
|
8
8
|
} from '@naturalcycles/nodejs-lib'
|
|
9
|
-
import { CommonSchema, CommonSchemaGenerator } from '..'
|
|
10
9
|
import { BaseDBEntity, baseDBEntitySchema, Saved, savedDBEntitySchema } from '../db.model'
|
|
11
10
|
|
|
12
11
|
const MOCK_TS_2018_06_21 = 1529539200
|
|
@@ -49,6 +48,28 @@ export const testItemTMSchema = objectSchema<TestItemTM>({
|
|
|
49
48
|
even: booleanSchema.optional(),
|
|
50
49
|
})
|
|
51
50
|
|
|
51
|
+
export const testItemBMJsonSchema = jsonSchema
|
|
52
|
+
.rootObject<TestItemBM>({
|
|
53
|
+
k1: jsonSchema.string(),
|
|
54
|
+
k2: jsonSchema.string().optional(),
|
|
55
|
+
k3: jsonSchema.number().optional(),
|
|
56
|
+
even: jsonSchema.boolean().optional(),
|
|
57
|
+
b1: jsonSchema.buffer().optional(),
|
|
58
|
+
})
|
|
59
|
+
.baseDBEntity()
|
|
60
|
+
|
|
61
|
+
export const testItemDBMJsonSchema = jsonSchema.rootObject<TestItemDBM>({
|
|
62
|
+
// todo: figure out how to not copy-paste these 3 fields
|
|
63
|
+
id: jsonSchema.string(),
|
|
64
|
+
created: jsonSchema.unixTimestamp(),
|
|
65
|
+
updated: jsonSchema.unixTimestamp(),
|
|
66
|
+
k1: jsonSchema.string(),
|
|
67
|
+
k2: jsonSchema.string().optional(),
|
|
68
|
+
k3: jsonSchema.number().optional(),
|
|
69
|
+
even: jsonSchema.boolean().optional(),
|
|
70
|
+
b1: jsonSchema.buffer().optional(),
|
|
71
|
+
})
|
|
72
|
+
|
|
52
73
|
export function createTestItemDBM(num = 1): TestItemDBM {
|
|
53
74
|
return {
|
|
54
75
|
id: `id${num}`,
|
|
@@ -73,6 +94,19 @@ export function createTestItemsBM(count = 1): Saved<TestItemBM>[] {
|
|
|
73
94
|
return _range(1, count + 1).map(num => createTestItemBM(num))
|
|
74
95
|
}
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
97
|
+
const testItemJsonSchema = jsonSchema
|
|
98
|
+
.object<TestItemDBM>({
|
|
99
|
+
id: jsonSchema.string(),
|
|
100
|
+
k1: jsonSchema.string(),
|
|
101
|
+
k2: jsonSchema.string(),
|
|
102
|
+
k3: jsonSchema.number(),
|
|
103
|
+
even: jsonSchema.boolean(),
|
|
104
|
+
created: jsonSchema.unixTimestamp(),
|
|
105
|
+
updated: jsonSchema.unixTimestamp(),
|
|
106
|
+
})
|
|
107
|
+
.build()
|
|
108
|
+
|
|
109
|
+
export function getTestItemSchema(): JsonSchemaObject<TestItemDBM> {
|
|
110
|
+
// return CommonSchemaGenerator.generateFromRows({ table: TEST_TABLE }, createTestItemsDBM())
|
|
111
|
+
return testItemJsonSchema
|
|
78
112
|
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export interface CommonSchema<ROW = any> {
|
|
2
|
-
/**
|
|
3
|
-
* Name of the Table
|
|
4
|
-
*/
|
|
5
|
-
table: string;
|
|
6
|
-
fields: CommonSchemaField[];
|
|
7
|
-
}
|
|
8
|
-
export interface CommonSchemaField {
|
|
9
|
-
name: string;
|
|
10
|
-
type: DATA_TYPE;
|
|
11
|
-
/**
|
|
12
|
-
* If it's an Array - then parentType should tell "array of what?"
|
|
13
|
-
*/
|
|
14
|
-
arrayOf?: CommonSchemaField;
|
|
15
|
-
/**
|
|
16
|
-
* If it's an Object - defined the CommonSchemaField of that object
|
|
17
|
-
*/
|
|
18
|
-
objectFields?: CommonSchemaField[];
|
|
19
|
-
notNull?: boolean;
|
|
20
|
-
/**
|
|
21
|
-
* Applicable to certain fields, e.g String, to be able to autodetect limits for certain Databases
|
|
22
|
-
*/
|
|
23
|
-
maxLen?: number;
|
|
24
|
-
minLen?: number;
|
|
25
|
-
}
|
|
26
|
-
export declare enum DATA_TYPE {
|
|
27
|
-
UNKNOWN = "UNKNOWN",
|
|
28
|
-
NULL = "NULL",
|
|
29
|
-
STRING = "STRING",
|
|
30
|
-
INT = "INT",
|
|
31
|
-
FLOAT = "FLOAT",
|
|
32
|
-
BOOLEAN = "BOOLEAN",
|
|
33
|
-
BINARY = "BINARY",
|
|
34
|
-
LOCAL_DATE = "LOCAL_DATE",
|
|
35
|
-
TIMESTAMP = "TIMESTAMP",
|
|
36
|
-
ARRAY = "ARRAY",
|
|
37
|
-
OBJECT = "OBJECT"
|
|
38
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DATA_TYPE = void 0;
|
|
4
|
-
var DATA_TYPE;
|
|
5
|
-
(function (DATA_TYPE) {
|
|
6
|
-
DATA_TYPE["UNKNOWN"] = "UNKNOWN";
|
|
7
|
-
DATA_TYPE["NULL"] = "NULL";
|
|
8
|
-
DATA_TYPE["STRING"] = "STRING";
|
|
9
|
-
DATA_TYPE["INT"] = "INT";
|
|
10
|
-
DATA_TYPE["FLOAT"] = "FLOAT";
|
|
11
|
-
DATA_TYPE["BOOLEAN"] = "BOOLEAN";
|
|
12
|
-
DATA_TYPE["BINARY"] = "BINARY";
|
|
13
|
-
DATA_TYPE["LOCAL_DATE"] = "LOCAL_DATE";
|
|
14
|
-
DATA_TYPE["TIMESTAMP"] = "TIMESTAMP";
|
|
15
|
-
// Semi-structured
|
|
16
|
-
DATA_TYPE["ARRAY"] = "ARRAY";
|
|
17
|
-
DATA_TYPE["OBJECT"] = "OBJECT";
|
|
18
|
-
})(DATA_TYPE = exports.DATA_TYPE || (exports.DATA_TYPE = {}));
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { ErrorMode } from '@naturalcycles/js-lib';
|
|
2
|
-
import { CommonSchema } from './common.schema';
|
|
3
|
-
export interface CommonSchemaGeneratorCfg {
|
|
4
|
-
/**
|
|
5
|
-
* Name of the Table
|
|
6
|
-
*/
|
|
7
|
-
table: string;
|
|
8
|
-
/**
|
|
9
|
-
* @default SUPPRESS
|
|
10
|
-
*
|
|
11
|
-
* On error (field has multiple types):
|
|
12
|
-
* if ErrorMode.THROW_IMMEDIATE - will throw on .add()
|
|
13
|
-
* if ErrorMode.THROW_AGGREGATED - will throw on .generate()
|
|
14
|
-
*/
|
|
15
|
-
errorMode?: ErrorMode;
|
|
16
|
-
/**
|
|
17
|
-
* @default false
|
|
18
|
-
* If true - fields will be sorted by name alphabetically (also inside objects)
|
|
19
|
-
*/
|
|
20
|
-
sortedFields?: boolean;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Class that helps to generate CommonSchema by processing ALL rows through it.
|
|
24
|
-
*/
|
|
25
|
-
export declare class CommonSchemaGenerator<ROW = any> {
|
|
26
|
-
cfg: CommonSchemaGeneratorCfg;
|
|
27
|
-
constructor(cfg: CommonSchemaGeneratorCfg);
|
|
28
|
-
private fieldByName;
|
|
29
|
-
private nullableFields;
|
|
30
|
-
add(row: ROW): void;
|
|
31
|
-
private mergeFields;
|
|
32
|
-
private detectType;
|
|
33
|
-
generate(): CommonSchema<ROW>;
|
|
34
|
-
static generateFromRows<ROW>(cfg: CommonSchemaGeneratorCfg, rows?: ROW[]): CommonSchema<ROW>;
|
|
35
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CommonSchemaGenerator = void 0;
|
|
4
|
-
const js_lib_1 = require("@naturalcycles/js-lib");
|
|
5
|
-
const common_schema_1 = require("./common.schema");
|
|
6
|
-
const LOCAL_DATE_PATTERN = new RegExp(/[0-9]{4}-[01][0-9]-[0-3][0-9]/);
|
|
7
|
-
/**
|
|
8
|
-
* Class that helps to generate CommonSchema by processing ALL rows through it.
|
|
9
|
-
*/
|
|
10
|
-
class CommonSchemaGenerator {
|
|
11
|
-
constructor(cfg) {
|
|
12
|
-
this.cfg = cfg;
|
|
13
|
-
this.fieldByName = {};
|
|
14
|
-
this.nullableFields = new Set();
|
|
15
|
-
}
|
|
16
|
-
// private fieldsWithMultipleTypes: Record<string, DATA_TYPE[]> = {}
|
|
17
|
-
add(row) {
|
|
18
|
-
if (!row)
|
|
19
|
-
return; // safety
|
|
20
|
-
Object.entries(row).forEach(([fieldName, value]) => {
|
|
21
|
-
this.fieldByName[fieldName] = this.mergeFields(this.fieldByName[fieldName], this.detectType(fieldName, value));
|
|
22
|
-
if (this.fieldByName[fieldName].type === common_schema_1.DATA_TYPE.NULL) {
|
|
23
|
-
this.nullableFields.add(fieldName);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
// missing fields
|
|
27
|
-
Object.keys(this.fieldByName).forEach(fieldName => {
|
|
28
|
-
if (!(fieldName in row)) {
|
|
29
|
-
// missing field!
|
|
30
|
-
this.nullableFields.add(fieldName);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
mergeFields(existing, newField) {
|
|
35
|
-
if (!existing)
|
|
36
|
-
return newField;
|
|
37
|
-
if (newField.type === common_schema_1.DATA_TYPE.UNKNOWN || newField.type === common_schema_1.DATA_TYPE.NULL) {
|
|
38
|
-
return {
|
|
39
|
-
...existing,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
if (existing.type === common_schema_1.DATA_TYPE.UNKNOWN || existing.type === common_schema_1.DATA_TYPE.NULL) {
|
|
43
|
-
return {
|
|
44
|
-
...newField,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
let type = existing.type;
|
|
48
|
-
if (existing.type !== newField.type) {
|
|
49
|
-
const [type1, type2] = [existing.type, newField.type].sort();
|
|
50
|
-
if (type1 === common_schema_1.DATA_TYPE.FLOAT && type2 === common_schema_1.DATA_TYPE.INT) {
|
|
51
|
-
type = common_schema_1.DATA_TYPE.FLOAT;
|
|
52
|
-
}
|
|
53
|
-
else if (type1 === common_schema_1.DATA_TYPE.LOCAL_DATE && type2 === common_schema_1.DATA_TYPE.STRING) {
|
|
54
|
-
type = common_schema_1.DATA_TYPE.STRING;
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Type mismatch! Oj!
|
|
58
|
-
return newField; // currently just use "latest" type
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// Type is same
|
|
62
|
-
const minLen = existing.minLen !== undefined
|
|
63
|
-
? Math.min(...[existing.minLen, newField.minLen].filter(Boolean))
|
|
64
|
-
: undefined;
|
|
65
|
-
const maxLen = existing.maxLen !== undefined
|
|
66
|
-
? Math.max(...[existing.maxLen, newField.maxLen].filter(Boolean))
|
|
67
|
-
: undefined;
|
|
68
|
-
// todo: recursively merge/compare array/object schemas
|
|
69
|
-
return js_lib_1._filterNullishValues({
|
|
70
|
-
...newField,
|
|
71
|
-
type,
|
|
72
|
-
minLen,
|
|
73
|
-
maxLen,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
detectType(name, value, level = 1) {
|
|
77
|
-
// Null
|
|
78
|
-
if (value === undefined || value === null) {
|
|
79
|
-
return {
|
|
80
|
-
name,
|
|
81
|
-
type: common_schema_1.DATA_TYPE.NULL,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
// String
|
|
85
|
-
if (typeof value === 'string') {
|
|
86
|
-
// LocalDate
|
|
87
|
-
if (LOCAL_DATE_PATTERN.test(value)) {
|
|
88
|
-
return { name, type: common_schema_1.DATA_TYPE.LOCAL_DATE };
|
|
89
|
-
}
|
|
90
|
-
return { name, type: common_schema_1.DATA_TYPE.STRING, minLen: value.length, maxLen: value.length };
|
|
91
|
-
}
|
|
92
|
-
// Int, Float
|
|
93
|
-
if (typeof value === 'number') {
|
|
94
|
-
// Cannot really detect TIMESTAMP
|
|
95
|
-
return {
|
|
96
|
-
name,
|
|
97
|
-
type: Number.isInteger(value) ? common_schema_1.DATA_TYPE.INT : common_schema_1.DATA_TYPE.FLOAT,
|
|
98
|
-
minLen: value,
|
|
99
|
-
maxLen: value,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
// Boolean
|
|
103
|
-
if (typeof value === 'boolean') {
|
|
104
|
-
return { name, type: common_schema_1.DATA_TYPE.BOOLEAN };
|
|
105
|
-
}
|
|
106
|
-
// Binary
|
|
107
|
-
if (Buffer.isBuffer(value)) {
|
|
108
|
-
return { name, type: common_schema_1.DATA_TYPE.BINARY, minLen: value.length, maxLen: value.length };
|
|
109
|
-
}
|
|
110
|
-
// Array
|
|
111
|
-
if (Array.isArray(value)) {
|
|
112
|
-
return {
|
|
113
|
-
name,
|
|
114
|
-
type: common_schema_1.DATA_TYPE.ARRAY,
|
|
115
|
-
arrayOf: this.detectType('', value[0], level + 1),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
// Object
|
|
119
|
-
if (typeof value === 'object') {
|
|
120
|
-
return {
|
|
121
|
-
name,
|
|
122
|
-
type: common_schema_1.DATA_TYPE.OBJECT,
|
|
123
|
-
objectFields: Object.entries(value).map(([k, v]) => this.detectType(k, v, level + 1)),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
return { name, type: common_schema_1.DATA_TYPE.UNKNOWN };
|
|
127
|
-
}
|
|
128
|
-
generate() {
|
|
129
|
-
// set nullability
|
|
130
|
-
Object.keys(this.fieldByName).forEach(fieldName => {
|
|
131
|
-
if (!this.nullableFields.has(fieldName)) {
|
|
132
|
-
this.fieldByName[fieldName].notNull = true;
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
const { table, sortedFields } = this.cfg;
|
|
136
|
-
const fieldNames = Object.keys(this.fieldByName);
|
|
137
|
-
if (sortedFields)
|
|
138
|
-
fieldNames.sort(); // mutates
|
|
139
|
-
// todo: sort object fields too
|
|
140
|
-
return {
|
|
141
|
-
table,
|
|
142
|
-
fields: fieldNames.map(name => this.fieldByName[name]),
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
static generateFromRows(cfg, rows = []) {
|
|
146
|
-
const gen = new CommonSchemaGenerator(cfg);
|
|
147
|
-
rows.forEach(r => gen.add(r));
|
|
148
|
-
return gen.generate();
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
exports.CommonSchemaGenerator = CommonSchemaGenerator;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
2
|
-
export interface CommonSchema<ROW = any> {
|
|
3
|
-
/**
|
|
4
|
-
* Name of the Table
|
|
5
|
-
*/
|
|
6
|
-
table: string
|
|
7
|
-
|
|
8
|
-
fields: CommonSchemaField[]
|
|
9
|
-
// can possibly have `meta` with table meta-information
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface CommonSchemaField {
|
|
13
|
-
name: string
|
|
14
|
-
type: DATA_TYPE
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* If it's an Array - then parentType should tell "array of what?"
|
|
18
|
-
*/
|
|
19
|
-
arrayOf?: CommonSchemaField
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* If it's an Object - defined the CommonSchemaField of that object
|
|
23
|
-
*/
|
|
24
|
-
objectFields?: CommonSchemaField[]
|
|
25
|
-
|
|
26
|
-
notNull?: boolean
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Applicable to certain fields, e.g String, to be able to autodetect limits for certain Databases
|
|
30
|
-
*/
|
|
31
|
-
maxLen?: number
|
|
32
|
-
minLen?: number
|
|
33
|
-
// avgLen?: number
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export enum DATA_TYPE {
|
|
37
|
-
UNKNOWN = 'UNKNOWN',
|
|
38
|
-
NULL = 'NULL',
|
|
39
|
-
STRING = 'STRING',
|
|
40
|
-
INT = 'INT',
|
|
41
|
-
FLOAT = 'FLOAT',
|
|
42
|
-
BOOLEAN = 'BOOLEAN',
|
|
43
|
-
BINARY = 'BINARY',
|
|
44
|
-
LOCAL_DATE = 'LOCAL_DATE', // ISO
|
|
45
|
-
TIMESTAMP = 'TIMESTAMP', // unix timestamp
|
|
46
|
-
// Semi-structured
|
|
47
|
-
ARRAY = 'ARRAY',
|
|
48
|
-
OBJECT = 'OBJECT',
|
|
49
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { ErrorMode, _filterNullishValues } from '@naturalcycles/js-lib'
|
|
2
|
-
import { CommonSchema, CommonSchemaField, DATA_TYPE } from './common.schema'
|
|
3
|
-
|
|
4
|
-
export interface CommonSchemaGeneratorCfg {
|
|
5
|
-
/**
|
|
6
|
-
* Name of the Table
|
|
7
|
-
*/
|
|
8
|
-
table: string
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @default SUPPRESS
|
|
12
|
-
*
|
|
13
|
-
* On error (field has multiple types):
|
|
14
|
-
* if ErrorMode.THROW_IMMEDIATE - will throw on .add()
|
|
15
|
-
* if ErrorMode.THROW_AGGREGATED - will throw on .generate()
|
|
16
|
-
*/
|
|
17
|
-
errorMode?: ErrorMode
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @default false
|
|
21
|
-
* If true - fields will be sorted by name alphabetically (also inside objects)
|
|
22
|
-
*/
|
|
23
|
-
sortedFields?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const LOCAL_DATE_PATTERN = new RegExp(/[0-9]{4}-[01][0-9]-[0-3][0-9]/)
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Class that helps to generate CommonSchema by processing ALL rows through it.
|
|
30
|
-
*/
|
|
31
|
-
export class CommonSchemaGenerator<ROW = any> {
|
|
32
|
-
constructor(public cfg: CommonSchemaGeneratorCfg) {}
|
|
33
|
-
|
|
34
|
-
private fieldByName: Record<string, CommonSchemaField> = {}
|
|
35
|
-
private nullableFields = new Set<string>()
|
|
36
|
-
// private fieldsWithMultipleTypes: Record<string, DATA_TYPE[]> = {}
|
|
37
|
-
|
|
38
|
-
add(row: ROW): void {
|
|
39
|
-
if (!row) return // safety
|
|
40
|
-
|
|
41
|
-
Object.entries(row).forEach(([fieldName, value]) => {
|
|
42
|
-
this.fieldByName[fieldName] = this.mergeFields(
|
|
43
|
-
this.fieldByName[fieldName],
|
|
44
|
-
this.detectType(fieldName, value),
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
if (this.fieldByName[fieldName]!.type === DATA_TYPE.NULL) {
|
|
48
|
-
this.nullableFields.add(fieldName)
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
// missing fields
|
|
53
|
-
Object.keys(this.fieldByName).forEach(fieldName => {
|
|
54
|
-
if (!(fieldName in row)) {
|
|
55
|
-
// missing field!
|
|
56
|
-
this.nullableFields.add(fieldName)
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private mergeFields(
|
|
62
|
-
existing: CommonSchemaField | undefined,
|
|
63
|
-
newField: CommonSchemaField,
|
|
64
|
-
): CommonSchemaField {
|
|
65
|
-
if (!existing) return newField
|
|
66
|
-
|
|
67
|
-
if (newField.type === DATA_TYPE.UNKNOWN || newField.type === DATA_TYPE.NULL) {
|
|
68
|
-
return {
|
|
69
|
-
...existing,
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (existing.type === DATA_TYPE.UNKNOWN || existing.type === DATA_TYPE.NULL) {
|
|
74
|
-
return {
|
|
75
|
-
...newField,
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let type = existing.type
|
|
80
|
-
|
|
81
|
-
if (existing.type !== newField.type) {
|
|
82
|
-
const [type1, type2] = [existing.type, newField.type].sort()
|
|
83
|
-
if (type1 === DATA_TYPE.FLOAT && type2 === DATA_TYPE.INT) {
|
|
84
|
-
type = DATA_TYPE.FLOAT
|
|
85
|
-
} else if (type1 === DATA_TYPE.LOCAL_DATE && type2 === DATA_TYPE.STRING) {
|
|
86
|
-
type = DATA_TYPE.STRING
|
|
87
|
-
} else {
|
|
88
|
-
// Type mismatch! Oj!
|
|
89
|
-
return newField // currently just use "latest" type
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Type is same
|
|
94
|
-
const minLen =
|
|
95
|
-
existing.minLen !== undefined
|
|
96
|
-
? Math.min(...[existing.minLen, newField.minLen!].filter(Boolean))
|
|
97
|
-
: undefined
|
|
98
|
-
const maxLen =
|
|
99
|
-
existing.maxLen !== undefined
|
|
100
|
-
? Math.max(...[existing.maxLen, newField.maxLen!].filter(Boolean))
|
|
101
|
-
: undefined
|
|
102
|
-
|
|
103
|
-
// todo: recursively merge/compare array/object schemas
|
|
104
|
-
|
|
105
|
-
return _filterNullishValues({
|
|
106
|
-
...newField,
|
|
107
|
-
type,
|
|
108
|
-
minLen,
|
|
109
|
-
maxLen,
|
|
110
|
-
})
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private detectType(name: string, value: any, level = 1): CommonSchemaField {
|
|
114
|
-
// Null
|
|
115
|
-
if (value === undefined || value === null) {
|
|
116
|
-
return {
|
|
117
|
-
name,
|
|
118
|
-
type: DATA_TYPE.NULL,
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// String
|
|
123
|
-
if (typeof value === 'string') {
|
|
124
|
-
// LocalDate
|
|
125
|
-
if (LOCAL_DATE_PATTERN.test(value)) {
|
|
126
|
-
return { name, type: DATA_TYPE.LOCAL_DATE }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return { name, type: DATA_TYPE.STRING, minLen: value.length, maxLen: value.length }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Int, Float
|
|
133
|
-
if (typeof value === 'number') {
|
|
134
|
-
// Cannot really detect TIMESTAMP
|
|
135
|
-
return {
|
|
136
|
-
name,
|
|
137
|
-
type: Number.isInteger(value) ? DATA_TYPE.INT : DATA_TYPE.FLOAT,
|
|
138
|
-
minLen: value,
|
|
139
|
-
maxLen: value,
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Boolean
|
|
144
|
-
if (typeof value === 'boolean') {
|
|
145
|
-
return { name, type: DATA_TYPE.BOOLEAN }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Binary
|
|
149
|
-
if (Buffer.isBuffer(value)) {
|
|
150
|
-
return { name, type: DATA_TYPE.BINARY, minLen: value.length, maxLen: value.length }
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Array
|
|
154
|
-
if (Array.isArray(value)) {
|
|
155
|
-
return {
|
|
156
|
-
name,
|
|
157
|
-
type: DATA_TYPE.ARRAY,
|
|
158
|
-
arrayOf: this.detectType('', value[0], level + 1),
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Object
|
|
163
|
-
if (typeof value === 'object') {
|
|
164
|
-
return {
|
|
165
|
-
name,
|
|
166
|
-
type: DATA_TYPE.OBJECT,
|
|
167
|
-
objectFields: Object.entries(value).map(([k, v]) => this.detectType(k, v, level + 1)),
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return { name, type: DATA_TYPE.UNKNOWN }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
generate(): CommonSchema<ROW> {
|
|
175
|
-
// set nullability
|
|
176
|
-
Object.keys(this.fieldByName).forEach(fieldName => {
|
|
177
|
-
if (!this.nullableFields.has(fieldName)) {
|
|
178
|
-
this.fieldByName[fieldName]!.notNull = true
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
const { table, sortedFields } = this.cfg
|
|
183
|
-
|
|
184
|
-
const fieldNames = Object.keys(this.fieldByName)
|
|
185
|
-
if (sortedFields) fieldNames.sort() // mutates
|
|
186
|
-
|
|
187
|
-
// todo: sort object fields too
|
|
188
|
-
|
|
189
|
-
return {
|
|
190
|
-
table,
|
|
191
|
-
fields: fieldNames.map(name => this.fieldByName[name]!),
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
static generateFromRows<ROW>(cfg: CommonSchemaGeneratorCfg, rows: ROW[] = []): CommonSchema<ROW> {
|
|
196
|
-
const gen = new CommonSchemaGenerator(cfg)
|
|
197
|
-
rows.forEach(r => gen.add(r))
|
|
198
|
-
return gen.generate()
|
|
199
|
-
}
|
|
200
|
-
}
|