@rebasepro/inference 0.17.3-canary.gdd23447 → 0.18.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/LICENSE +0 -1
- package/README.md +4 -0
- package/package.json +26 -12
- package/src/builders/reference_property_builder.ts +0 -18
- package/src/builders/string_property_builder.ts +0 -118
- package/src/builders/validation_builder.ts +0 -18
- package/src/collection_builder.ts +0 -424
- package/src/index.ts +0 -3
- package/src/strings.ts +0 -104
- package/src/test_schemas/pop_products.json +0 -948
- package/src/types.ts +0 -43
- package/src/util.ts +0 -44
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ Automatically infer Rebase collection property schemas from sample data — anal
|
|
|
8
8
|
pnpm add @rebasepro/inference
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
ESM-only: `"type": "module"` with no CommonJS build, so it is loaded with
|
|
12
|
+
`import`. `require()` of it resolves only on Node 22.12+, which supports
|
|
13
|
+
`require(esm)`.
|
|
14
|
+
|
|
11
15
|
## What This Package Does
|
|
12
16
|
|
|
13
17
|
`@rebasepro/inference` examines arrays of data objects and produces `Properties` definitions compatible with the Rebase collection schema. It uses statistical analysis to determine the most probable type for each field, detect enum values, identify reference patterns, and build nested map/array structures. Used by the Rebase introspection pipeline and data import tools.
|
package/package.json
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/inference",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
|
+
"description": "Infer a Rebase collection schema from existing data.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rebase",
|
|
7
|
+
"schema",
|
|
8
|
+
"inference",
|
|
9
|
+
"codegen"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://rebase.pro",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/rebasepro/rebase/issues"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/rebasepro/rebase.git",
|
|
18
|
+
"directory": "packages/inference"
|
|
19
|
+
},
|
|
4
20
|
"license": "MIT",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=22.22.0"
|
|
23
|
+
},
|
|
5
24
|
"type": "module",
|
|
6
25
|
"publishConfig": {
|
|
7
26
|
"access": "public"
|
|
@@ -9,10 +28,9 @@
|
|
|
9
28
|
"main": "./dist/index.es.js",
|
|
10
29
|
"module": "./dist/index.es.js",
|
|
11
30
|
"types": "dist/index.d.ts",
|
|
12
|
-
"source": "src/index.ts",
|
|
13
31
|
"dependencies": {
|
|
14
|
-
"@rebasepro/types": "0.
|
|
15
|
-
"@rebasepro/utils": "0.
|
|
32
|
+
"@rebasepro/types": "0.18.0",
|
|
33
|
+
"@rebasepro/utils": "0.18.0"
|
|
16
34
|
},
|
|
17
35
|
"devDependencies": {
|
|
18
36
|
"@types/jest": "^30.0.0",
|
|
@@ -26,24 +44,20 @@
|
|
|
26
44
|
".": {
|
|
27
45
|
"types": "./dist/index.d.ts",
|
|
28
46
|
"development": "./dist/index.es.js",
|
|
29
|
-
"import": "./dist/index.es.js"
|
|
47
|
+
"import": "./dist/index.es.js",
|
|
48
|
+
"default": "./dist/index.es.js"
|
|
30
49
|
},
|
|
31
50
|
"./package.json": "./package.json"
|
|
32
51
|
},
|
|
33
52
|
"files": [
|
|
34
|
-
"dist"
|
|
35
|
-
"src"
|
|
53
|
+
"dist"
|
|
36
54
|
],
|
|
37
55
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "https://github.com/rebasepro/rebase.git",
|
|
41
|
-
"directory": "packages/inference"
|
|
42
|
-
},
|
|
43
56
|
"scripts": {
|
|
44
57
|
"dev": "vite",
|
|
45
58
|
"build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
|
|
46
59
|
"test": "jest --passWithNoTests",
|
|
60
|
+
"test:watch": "jest --watch",
|
|
47
61
|
"clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { findCommonInitialStringInPath } from "../strings";
|
|
2
|
-
import { InferencePropertyBuilderProps } from "../types";
|
|
3
|
-
import { Property } from "@rebasepro/types";
|
|
4
|
-
|
|
5
|
-
export function buildReferenceProperty({
|
|
6
|
-
name,
|
|
7
|
-
totalDocsCount,
|
|
8
|
-
valuesResult
|
|
9
|
-
}: InferencePropertyBuilderProps): Property {
|
|
10
|
-
|
|
11
|
-
const property: Property = {
|
|
12
|
-
name: name ?? "",
|
|
13
|
-
type: "reference",
|
|
14
|
-
path: findCommonInitialStringInPath(valuesResult) ?? "!!!FIX_ME!!!"
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
return property;
|
|
18
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { findCommonInitialStringInPath } from "../strings";
|
|
2
|
-
import { extractEnumFromValues } from "../util";
|
|
3
|
-
import { FileType, Property, StringProperty } from "@rebasepro/types";
|
|
4
|
-
import { InferencePropertyBuilderProps, ValuesCountEntry } from "../types";
|
|
5
|
-
|
|
6
|
-
const IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif"];
|
|
7
|
-
const AUDIO_EXTENSIONS = [".mp3", ".ogg", ".opus", ".aac"];
|
|
8
|
-
const VIDEO_EXTENSIONS = [".avi", ".mp4"];
|
|
9
|
-
|
|
10
|
-
const emailRegEx = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
11
|
-
|
|
12
|
-
export function buildStringProperty({
|
|
13
|
-
name,
|
|
14
|
-
totalDocsCount,
|
|
15
|
-
valuesResult
|
|
16
|
-
}: InferencePropertyBuilderProps): Property {
|
|
17
|
-
|
|
18
|
-
let stringProperty: Property = {
|
|
19
|
-
name: name ?? "",
|
|
20
|
-
type: "string"
|
|
21
|
-
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
if (valuesResult) {
|
|
25
|
-
|
|
26
|
-
const totalEntriesCount = valuesResult.values.length;
|
|
27
|
-
const totalValues = Array.from(valuesResult.valuesCount.keys()).length;
|
|
28
|
-
|
|
29
|
-
const config: Partial<StringProperty> = {};
|
|
30
|
-
|
|
31
|
-
const probablyAURL = valuesResult.values
|
|
32
|
-
.filter((value) => typeof value === "string" &&
|
|
33
|
-
value.toString().startsWith("http")).length > totalDocsCount / 3 * 2;
|
|
34
|
-
if (probablyAURL) {
|
|
35
|
-
config.url = true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const probablyAnEmail = valuesResult.values
|
|
39
|
-
.filter((value) => typeof value === "string" &&
|
|
40
|
-
emailRegEx.test(value)).length > totalDocsCount / 3 * 2;
|
|
41
|
-
if (probablyAnEmail) {
|
|
42
|
-
config.email = true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const probablyUserIds = valuesResult.values
|
|
46
|
-
.filter((value) => typeof value === "string" && value.length === 28 && !value.includes(" "))
|
|
47
|
-
.length > totalDocsCount / 3 * 2;
|
|
48
|
-
// No `admin.readOnly` here. This package is core — the field does not exist in
|
|
49
|
-
// `@rebasepro/types`, because `@rebasepro/cms-types` declares it — and a
|
|
50
|
-
// guess about whether a form should be editable is presentation, not schema.
|
|
51
|
-
// (It also would not have enforced anything: nothing on the server reads
|
|
52
|
-
// `readOnly`.) The inferred `isId` below is the part that carries meaning.
|
|
53
|
-
|
|
54
|
-
if (!probablyAnEmail &&
|
|
55
|
-
!probablyAURL &&
|
|
56
|
-
!probablyUserIds &&
|
|
57
|
-
!probablyAURL &&
|
|
58
|
-
totalValues < totalEntriesCount / 3
|
|
59
|
-
) {
|
|
60
|
-
const enumValues = extractEnumFromValues(Array.from(valuesResult.valuesCount.keys()));
|
|
61
|
-
|
|
62
|
-
if (Object.keys(enumValues).length > 1)
|
|
63
|
-
config.enum = enumValues;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// regular string
|
|
67
|
-
if (!probablyAnEmail &&
|
|
68
|
-
!probablyAURL &&
|
|
69
|
-
!probablyUserIds &&
|
|
70
|
-
!probablyAURL &&
|
|
71
|
-
!config.enum) {
|
|
72
|
-
const fileType = probableFileType(valuesResult, totalDocsCount);
|
|
73
|
-
if (fileType) {
|
|
74
|
-
config.storage = {
|
|
75
|
-
acceptedFiles: fileType as FileType[],
|
|
76
|
-
storagePath: findCommonInitialStringInPath(valuesResult) ?? "/"
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (Object.keys(config).length > 0)
|
|
82
|
-
stringProperty = {
|
|
83
|
-
...stringProperty,
|
|
84
|
-
...config
|
|
85
|
-
} as StringProperty;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return stringProperty;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function probableFileType(valuesCount: ValuesCountEntry, totalDocsCount: number): false | FileType[] {
|
|
92
|
-
const isImage = (value: string) => IMAGE_EXTENSIONS.some((extension) => value.toString().endsWith(extension));
|
|
93
|
-
const isAudio = (value: string) => AUDIO_EXTENSIONS.some((extension) => value.toString().endsWith(extension));
|
|
94
|
-
const isVideo = (value: string) => VIDEO_EXTENSIONS.some((extension) => value.toString().endsWith(extension));
|
|
95
|
-
|
|
96
|
-
const stringValues = valuesCount.values.filter((v): v is string => typeof v === "string");
|
|
97
|
-
|
|
98
|
-
let imageCount = 0;
|
|
99
|
-
let audioCount = 0;
|
|
100
|
-
let videoCount = 0;
|
|
101
|
-
|
|
102
|
-
for (const value of stringValues) {
|
|
103
|
-
if (isImage(value)) imageCount++;
|
|
104
|
-
else if (isAudio(value)) audioCount++;
|
|
105
|
-
else if (isVideo(value)) videoCount++;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const totalMediaCount = imageCount + audioCount + videoCount;
|
|
109
|
-
if (totalMediaCount > (totalDocsCount * 2) / 3) {
|
|
110
|
-
const fileTypes: FileType[] = [];
|
|
111
|
-
if (imageCount > 0) fileTypes.push("image/*");
|
|
112
|
-
if (audioCount > 0) fileTypes.push("audio/*");
|
|
113
|
-
if (videoCount > 0) fileTypes.push("video/*");
|
|
114
|
-
return fileTypes.length > 0 ? fileTypes : false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { PropertyValidationSchema } from "@rebasepro/types";
|
|
2
|
-
import { InferencePropertyBuilderProps } from "../types";
|
|
3
|
-
|
|
4
|
-
export function buildValidation({
|
|
5
|
-
totalDocsCount,
|
|
6
|
-
valuesResult
|
|
7
|
-
}: InferencePropertyBuilderProps): PropertyValidationSchema | undefined {
|
|
8
|
-
|
|
9
|
-
if (valuesResult) {
|
|
10
|
-
const totalEntriesCount = valuesResult.values.length;
|
|
11
|
-
if (totalDocsCount === totalEntriesCount)
|
|
12
|
-
return {
|
|
13
|
-
required: true
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
InferencePropertyBuilderProps,
|
|
3
|
-
TypesCount,
|
|
4
|
-
TypesCountRecord,
|
|
5
|
-
ValuesCountEntry,
|
|
6
|
-
ValuesCountRecord
|
|
7
|
-
} from "./types";
|
|
8
|
-
import { buildStringProperty } from "./builders/string_property_builder";
|
|
9
|
-
import { buildValidation } from "./builders/validation_builder";
|
|
10
|
-
import { buildReferenceProperty } from "./builders/reference_property_builder";
|
|
11
|
-
import { extractEnumFromValues, mergeDeep, prettifyIdentifier, resolveEnumValues } from "./util";
|
|
12
|
-
import { DataType, EnumValues, Properties, Property, StringProperty, Vector } from "@rebasepro/types";
|
|
13
|
-
|
|
14
|
-
export type InferenceTypeBuilder = (value: unknown) => DataType;
|
|
15
|
-
|
|
16
|
-
export async function buildEntityPropertiesFromData(
|
|
17
|
-
data: object[],
|
|
18
|
-
getType: InferenceTypeBuilder
|
|
19
|
-
): Promise<Properties> {
|
|
20
|
-
const typesCount: TypesCountRecord = {};
|
|
21
|
-
const valuesCount: ValuesCountRecord = {};
|
|
22
|
-
if (data) {
|
|
23
|
-
data.forEach((entry) => {
|
|
24
|
-
if (entry) {
|
|
25
|
-
Object.entries(entry).forEach(([key, value]) => {
|
|
26
|
-
if (key.startsWith("_")) return; // Ignore properties starting with _
|
|
27
|
-
increaseMapTypeCount(typesCount, key, value, getType);
|
|
28
|
-
increaseValuesCount(valuesCount, key, value, getType);
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
return buildPropertiesFromCount(data.length, typesCount, valuesCount);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function buildPropertyFromData(
|
|
37
|
-
data: unknown[],
|
|
38
|
-
property: Property,
|
|
39
|
-
getType: InferenceTypeBuilder
|
|
40
|
-
): Property {
|
|
41
|
-
const typesCount = {};
|
|
42
|
-
const valuesCount: ValuesCountRecord = {};
|
|
43
|
-
if (data) {
|
|
44
|
-
data.forEach((entry) => {
|
|
45
|
-
increaseTypeCount(property.type, typesCount, entry, getType);
|
|
46
|
-
increaseValuesCount(valuesCount, "inferred_prop", entry, getType);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
const enumValues = "enum" in property ? resolveEnumValues(property["enum"] as EnumValues) : undefined;
|
|
50
|
-
if (enumValues) {
|
|
51
|
-
const newEnumValues = extractEnumFromValues(Array.from(valuesCount["inferred_prop"].valuesCount.keys()));
|
|
52
|
-
return {
|
|
53
|
-
...property,
|
|
54
|
-
enum: [...newEnumValues, ...enumValues]
|
|
55
|
-
} as StringProperty;
|
|
56
|
-
}
|
|
57
|
-
const generatedProperty = buildPropertyFromCount(
|
|
58
|
-
"inferred_prop",
|
|
59
|
-
data.length,
|
|
60
|
-
property.type,
|
|
61
|
-
typesCount,
|
|
62
|
-
valuesCount["inferred_prop"]
|
|
63
|
-
);
|
|
64
|
-
return mergeDeep(generatedProperty, property);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function buildPropertiesOrder(
|
|
68
|
-
properties: Properties,
|
|
69
|
-
propertiesOrder?: string[],
|
|
70
|
-
priorityKeys?: string[]
|
|
71
|
-
): string[] {
|
|
72
|
-
const lowerCasePriorityKeys = (priorityKeys ?? []).map((key) => key.toLowerCase());
|
|
73
|
-
|
|
74
|
-
function propOrder(s: string) {
|
|
75
|
-
const k = s.toLowerCase();
|
|
76
|
-
if (lowerCasePriorityKeys.includes(k)) return 4;
|
|
77
|
-
if (k === "title" || k === "name") return 3;
|
|
78
|
-
if (k.includes("title") || k.includes("name")) return 2;
|
|
79
|
-
if (k.includes("image") || k.includes("picture")) return 1;
|
|
80
|
-
return 0;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function deriveOrder(keys: string[]): string[] {
|
|
84
|
-
return [...keys]
|
|
85
|
-
.sort() // alphabetically
|
|
86
|
-
.sort((a, b) => propOrder(b) - propOrder(a));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// A caller-supplied order is an answer, not an input. It used to be handed
|
|
90
|
-
// to the same alphabetical + priority sort as a derived order, which threw
|
|
91
|
-
// it away entirely — and because `Array.prototype.sort` sorts in place, the
|
|
92
|
-
// caller's own array was silently reordered as a side effect.
|
|
93
|
-
if (propertiesOrder) {
|
|
94
|
-
const placed = new Set(propertiesOrder);
|
|
95
|
-
// Properties the caller did not place — a newly inferred column, say —
|
|
96
|
-
// still have to appear, so they follow in derived order.
|
|
97
|
-
const rest = Object.keys(properties).filter(key => !placed.has(key));
|
|
98
|
-
return [...propertiesOrder, ...deriveOrder(rest)];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return deriveOrder(Object.keys(properties));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* @param type
|
|
106
|
-
* @param typesCount
|
|
107
|
-
* @param fieldValue
|
|
108
|
-
* @param getType
|
|
109
|
-
*/
|
|
110
|
-
function increaseTypeCount(
|
|
111
|
-
type: DataType,
|
|
112
|
-
typesCount: TypesCount,
|
|
113
|
-
fieldValue: unknown,
|
|
114
|
-
getType: InferenceTypeBuilder
|
|
115
|
-
) {
|
|
116
|
-
if (type === "map") {
|
|
117
|
-
if (fieldValue) {
|
|
118
|
-
let mapTypesCount = typesCount[type];
|
|
119
|
-
if (!mapTypesCount) {
|
|
120
|
-
mapTypesCount = {};
|
|
121
|
-
typesCount[type] = mapTypesCount;
|
|
122
|
-
}
|
|
123
|
-
Object.entries(fieldValue as Record<string, unknown>).forEach(([key, value]) => {
|
|
124
|
-
increaseMapTypeCount(mapTypesCount as TypesCountRecord, key, value, getType);
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
} else if (type === "array") {
|
|
128
|
-
let arrayTypesCount = typesCount[type];
|
|
129
|
-
if (!arrayTypesCount) {
|
|
130
|
-
arrayTypesCount = {};
|
|
131
|
-
typesCount[type] = arrayTypesCount;
|
|
132
|
-
}
|
|
133
|
-
if (fieldValue && Array.isArray(fieldValue) && fieldValue.length > 0) {
|
|
134
|
-
const arrayType = getMostProbableTypeInArray(fieldValue, getType);
|
|
135
|
-
if (arrayType === "map") {
|
|
136
|
-
let mapTypesCount = arrayTypesCount[arrayType];
|
|
137
|
-
if (!mapTypesCount) {
|
|
138
|
-
mapTypesCount = {};
|
|
139
|
-
}
|
|
140
|
-
fieldValue.forEach((value) => {
|
|
141
|
-
if (value && typeof value === "object" && !Array.isArray(value)) { // Ensure value is an object for Object.entries
|
|
142
|
-
Object.entries(value).forEach(([key, v]) =>
|
|
143
|
-
increaseMapTypeCount(mapTypesCount, key, v, getType)
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
arrayTypesCount[arrayType] = mapTypesCount;
|
|
148
|
-
} else {
|
|
149
|
-
if (!arrayTypesCount[arrayType]) arrayTypesCount[arrayType] = 1;
|
|
150
|
-
else arrayTypesCount[arrayType] = Number(arrayTypesCount[arrayType]) + 1;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
if (!typesCount[type]) typesCount[type] = 1;
|
|
155
|
-
else typesCount[type] = Number(typesCount[type]) + 1;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function increaseMapTypeCount(
|
|
160
|
-
typesCountRecord: TypesCountRecord,
|
|
161
|
-
key: string,
|
|
162
|
-
fieldValue: unknown,
|
|
163
|
-
getType: InferenceTypeBuilder
|
|
164
|
-
) {
|
|
165
|
-
if (key.startsWith("_")) return; // Ignore properties starting with _
|
|
166
|
-
|
|
167
|
-
let typesCount: TypesCount = typesCountRecord[key];
|
|
168
|
-
if (!typesCount) {
|
|
169
|
-
typesCount = {};
|
|
170
|
-
typesCountRecord[key] = typesCount;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (fieldValue != null) {
|
|
174
|
-
// Check that fieldValue is not null or undefined before proceeding
|
|
175
|
-
const type = getType(fieldValue);
|
|
176
|
-
increaseTypeCount(type, typesCount, fieldValue, getType);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function increaseValuesCount(
|
|
181
|
-
typeValuesRecord: ValuesCountRecord,
|
|
182
|
-
key: string,
|
|
183
|
-
fieldValue: unknown,
|
|
184
|
-
getType: InferenceTypeBuilder
|
|
185
|
-
) {
|
|
186
|
-
if (key.startsWith("_")) return; // Ignore properties starting with _
|
|
187
|
-
|
|
188
|
-
const type = getType(fieldValue);
|
|
189
|
-
|
|
190
|
-
let valuesRecord: {
|
|
191
|
-
values: unknown[];
|
|
192
|
-
valuesCount: Map<unknown, number>;
|
|
193
|
-
map?: ValuesCountRecord;
|
|
194
|
-
} = typeValuesRecord[key];
|
|
195
|
-
|
|
196
|
-
if (!valuesRecord) {
|
|
197
|
-
valuesRecord = {
|
|
198
|
-
values: [],
|
|
199
|
-
valuesCount: new Map()
|
|
200
|
-
};
|
|
201
|
-
typeValuesRecord[key] = valuesRecord;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (type === "map") {
|
|
205
|
-
let mapValuesRecord: ValuesCountRecord | undefined = valuesRecord.map;
|
|
206
|
-
if (!mapValuesRecord) {
|
|
207
|
-
mapValuesRecord = {};
|
|
208
|
-
valuesRecord.map = mapValuesRecord;
|
|
209
|
-
}
|
|
210
|
-
if (fieldValue)
|
|
211
|
-
Object.entries(fieldValue as Record<string, unknown>).forEach(([subKey, value]) =>
|
|
212
|
-
increaseValuesCount(mapValuesRecord as ValuesCountRecord, subKey, value, getType)
|
|
213
|
-
);
|
|
214
|
-
} else if (type === "array") {
|
|
215
|
-
if (Array.isArray(fieldValue)) {
|
|
216
|
-
fieldValue.forEach((value) => {
|
|
217
|
-
valuesRecord.values.push(value);
|
|
218
|
-
valuesRecord.valuesCount.set(value, (valuesRecord.valuesCount.get(value) ?? 0) + 1);
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
if (fieldValue !== null && fieldValue !== undefined) {
|
|
223
|
-
valuesRecord.values.push(fieldValue);
|
|
224
|
-
valuesRecord.valuesCount.set(fieldValue, (valuesRecord.valuesCount.get(fieldValue) ?? 0) + 1);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function getHighestTypesCount(typesCount: TypesCount): number {
|
|
230
|
-
let highestCount = 0;
|
|
231
|
-
Object.entries(typesCount).forEach(([type, count]) => {
|
|
232
|
-
let countValue = 0;
|
|
233
|
-
if (type === "map") {
|
|
234
|
-
countValue = getHighestRecordCount(count as TypesCountRecord);
|
|
235
|
-
} else if (type === "array") {
|
|
236
|
-
countValue = getHighestTypesCount(count as TypesCount);
|
|
237
|
-
} else {
|
|
238
|
-
countValue = Number(count);
|
|
239
|
-
}
|
|
240
|
-
if (countValue > highestCount) {
|
|
241
|
-
highestCount = countValue;
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
return highestCount;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function getHighestRecordCount(record: TypesCountRecord): number {
|
|
249
|
-
return Object.entries(record)
|
|
250
|
-
.map(([key, typesCount]) => getHighestTypesCount(typesCount))
|
|
251
|
-
.reduce((a, b) => Math.max(a, b), 0);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function getMostProbableType(typesCount: TypesCount): DataType {
|
|
255
|
-
let highestCount = -1;
|
|
256
|
-
let probableType: DataType = "string"; // default
|
|
257
|
-
Object.entries(typesCount).forEach(([type, count]) => {
|
|
258
|
-
let countValue;
|
|
259
|
-
if (type === "map") {
|
|
260
|
-
countValue = getHighestRecordCount(count as TypesCountRecord);
|
|
261
|
-
} else if (type === "array") {
|
|
262
|
-
countValue = getHighestTypesCount(count as TypesCount);
|
|
263
|
-
} else {
|
|
264
|
-
countValue = Number(count);
|
|
265
|
-
}
|
|
266
|
-
if (countValue > highestCount) {
|
|
267
|
-
highestCount = countValue;
|
|
268
|
-
probableType = type as DataType;
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
return probableType;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function buildPropertyFromCount(
|
|
275
|
-
key: string,
|
|
276
|
-
totalDocsCount: number,
|
|
277
|
-
mostProbableType: DataType,
|
|
278
|
-
typesCount: TypesCount,
|
|
279
|
-
valuesResult?: ValuesCountEntry
|
|
280
|
-
): Property {
|
|
281
|
-
let title: string | undefined;
|
|
282
|
-
|
|
283
|
-
if (key) {
|
|
284
|
-
title = prettifyIdentifier(key);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
let result: Property | undefined = undefined;
|
|
288
|
-
if (mostProbableType === "map") {
|
|
289
|
-
const highVariability = checkTypesCountHighVariability(typesCount);
|
|
290
|
-
if (highVariability) {
|
|
291
|
-
result = {
|
|
292
|
-
type: "map",
|
|
293
|
-
name: title ?? key ?? "",
|
|
294
|
-
keyValue: true,
|
|
295
|
-
properties: {}
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
const properties = buildPropertiesFromCount(
|
|
299
|
-
totalDocsCount,
|
|
300
|
-
typesCount.map as TypesCountRecord,
|
|
301
|
-
valuesResult ? valuesResult.mapValues : undefined
|
|
302
|
-
);
|
|
303
|
-
result = {
|
|
304
|
-
type: "map",
|
|
305
|
-
name: title ?? key ?? "",
|
|
306
|
-
properties
|
|
307
|
-
};
|
|
308
|
-
} else if (mostProbableType === "array") {
|
|
309
|
-
const arrayTypesCount = typesCount.array as TypesCount;
|
|
310
|
-
const arrayMostProbableType = getMostProbableType(arrayTypesCount);
|
|
311
|
-
const of = buildPropertyFromCount(
|
|
312
|
-
key,
|
|
313
|
-
totalDocsCount,
|
|
314
|
-
arrayMostProbableType,
|
|
315
|
-
arrayTypesCount,
|
|
316
|
-
valuesResult
|
|
317
|
-
);
|
|
318
|
-
result = {
|
|
319
|
-
type: "array",
|
|
320
|
-
name: title ?? key ?? "",
|
|
321
|
-
of
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!result) {
|
|
326
|
-
const propertyProps: InferencePropertyBuilderProps = {
|
|
327
|
-
name: key,
|
|
328
|
-
totalDocsCount,
|
|
329
|
-
valuesResult
|
|
330
|
-
};
|
|
331
|
-
if (mostProbableType === "string") {
|
|
332
|
-
result = buildStringProperty(propertyProps);
|
|
333
|
-
} else if (mostProbableType === "reference") {
|
|
334
|
-
result = buildReferenceProperty(propertyProps);
|
|
335
|
-
} else {
|
|
336
|
-
result = {
|
|
337
|
-
type: mostProbableType
|
|
338
|
-
} as Property;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (title) {
|
|
342
|
-
result.name = title;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const validation = buildValidation(propertyProps);
|
|
346
|
-
if (validation) {
|
|
347
|
-
result.validation = validation;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return result;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function buildPropertiesFromCount(
|
|
355
|
-
totalDocsCount: number,
|
|
356
|
-
typesCountRecord: TypesCountRecord,
|
|
357
|
-
valuesCountRecord?: ValuesCountRecord
|
|
358
|
-
): Properties {
|
|
359
|
-
const res: Properties = {};
|
|
360
|
-
Object.entries(typesCountRecord).forEach(([key, typesCount]) => {
|
|
361
|
-
const mostProbableType = getMostProbableType(typesCount);
|
|
362
|
-
res[key] = buildPropertyFromCount(
|
|
363
|
-
key,
|
|
364
|
-
totalDocsCount,
|
|
365
|
-
mostProbableType,
|
|
366
|
-
typesCount,
|
|
367
|
-
valuesCountRecord ? valuesCountRecord[key] : undefined
|
|
368
|
-
);
|
|
369
|
-
});
|
|
370
|
-
return res;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function countMaxDocumentsUnder(typesCount: TypesCount) {
|
|
374
|
-
let count = 0;
|
|
375
|
-
Object.entries(typesCount).forEach(([type, value]) => {
|
|
376
|
-
if (typeof value === "object") {
|
|
377
|
-
count = Math.max(count, countMaxDocumentsUnder(value as TypesCountRecord));
|
|
378
|
-
} else {
|
|
379
|
-
count = Math.max(count, Number(value));
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
return count;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function getMostProbableTypeInArray(
|
|
386
|
-
array: unknown[],
|
|
387
|
-
getType: InferenceTypeBuilder
|
|
388
|
-
): DataType {
|
|
389
|
-
const typesCount: TypesCount = {};
|
|
390
|
-
array.forEach((value) => {
|
|
391
|
-
// A hole in the array says nothing about what the array holds, so it
|
|
392
|
-
// gets no vote. This used to fall out of `getType` answering "map" for
|
|
393
|
-
// null and `increaseTypeCount` then skipping falsy map values; now that
|
|
394
|
-
// null infers as "string" the skip has to be explicit, or every array
|
|
395
|
-
// with a gap in it would drift towards "string".
|
|
396
|
-
if (value === null || value === undefined) return;
|
|
397
|
-
increaseTypeCount(getType(value), typesCount, value, getType);
|
|
398
|
-
});
|
|
399
|
-
return getMostProbableType(typesCount);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function checkTypesCountHighVariability(typesCount: TypesCount) {
|
|
403
|
-
const maxCount = countMaxDocumentsUnder(typesCount);
|
|
404
|
-
let keysWithFewValues = 0;
|
|
405
|
-
Object.entries(typesCount.map ?? {}).forEach(([key, value]) => {
|
|
406
|
-
const count = countMaxDocumentsUnder(value);
|
|
407
|
-
if (count < maxCount / 3) {
|
|
408
|
-
keysWithFewValues++;
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
return keysWithFewValues / Object.entries(typesCount.map ?? {}).length > 0.5;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
export function inferTypeFromValue(value: unknown): DataType {
|
|
416
|
-
if (value === null || value === undefined) return "string";
|
|
417
|
-
if (value instanceof Vector || (value && typeof value === "object" && "__type" in value && (value as Record<string, unknown>).__type === "Vector")) return "vector";
|
|
418
|
-
if (typeof value === "string") return "string";
|
|
419
|
-
if (typeof value === "number") return "number";
|
|
420
|
-
if (typeof value === "boolean") return "boolean";
|
|
421
|
-
if (Array.isArray(value)) return "array";
|
|
422
|
-
if (typeof value === "object") return "map";
|
|
423
|
-
return "string";
|
|
424
|
-
}
|
package/src/index.ts
DELETED