@rebasepro/inference 0.23.0 → 0.24.1-canary.g19596ef
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/README.md +13 -8
- package/dist/collection_builder.d.ts +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +141 -25
- package/dist/index.es.js.map +1 -1
- package/dist/numbers.d.ts +14 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rebasepro/inference
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Infer Rebase collection property schemas from sample data: field types, enums, `required`, `email` / `url`, storage fields, and nested maps and arrays.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ of it resolves too: Node has supported `require(esm)` since 22.12.
|
|
|
14
14
|
|
|
15
15
|
## What This Package Does
|
|
16
16
|
|
|
17
|
-
`@rebasepro/inference` examines arrays of data objects and produces `Properties` definitions compatible with the Rebase collection schema. It
|
|
17
|
+
`@rebasepro/inference` examines arrays of data objects and produces `Properties` definitions compatible with the Rebase collection schema. It counts the types each field holds to pick one: a column that mixes numbers with text that is not exactly a number is a string. It also detects enum values and builds nested map/array structures. A `"reference"` property is built only when your `getType` returns `"reference"`; the default `inferTypeFromValue` never does. The admin's "create a collection from a file" import is its consumer in Rebase; database introspection does not use it.
|
|
18
18
|
|
|
19
19
|
## Key Exports
|
|
20
20
|
|
|
@@ -22,10 +22,10 @@ of it resolves too: Node has supported `require(esm)` since 22.12.
|
|
|
22
22
|
|
|
23
23
|
| Export | Type | Description |
|
|
24
24
|
|---|---|---|
|
|
25
|
-
| `buildEntityPropertiesFromData` | `(data: object[], getType: InferenceTypeBuilder) => Promise<Properties>` | Main entry — infer full property schema from data |
|
|
25
|
+
| `buildEntityPropertiesFromData` | `(data: object[], getType: InferenceTypeBuilder, options?: { ignoreKey?: (key: string) => boolean }) => Promise<Properties>` | Main entry — infer full property schema from data. `ignoreKey` leaves top-level keys out (none by default) |
|
|
26
26
|
| `buildPropertyFromData` | `(data: unknown[], property: Property, getType: InferenceTypeBuilder) => Property` | Refine an existing property with new sample data |
|
|
27
27
|
| `buildPropertiesOrder` | `(properties: Properties, propertiesOrder?: string[], priorityKeys?: string[]) => string[]` | Sort property keys (title/name first, then images, then alphabetical) |
|
|
28
|
-
| `inferTypeFromValue` | `(value: unknown) => DataType` | Default type inference: string, number, boolean, array, map, vector |
|
|
28
|
+
| `inferTypeFromValue` | `(value: unknown) => DataType` | Default type inference: string, number, boolean, date, array, map, vector |
|
|
29
29
|
| `InferenceTypeBuilder` | Type | `(value: unknown) => DataType` — pluggable type detector |
|
|
30
30
|
|
|
31
31
|
### String Utilities
|
|
@@ -60,6 +60,10 @@ const sampleData = [
|
|
|
60
60
|
{ name: "Alice", age: 30, active: true, role: "admin" },
|
|
61
61
|
{ name: "Bob", age: 25, active: false, role: "editor" },
|
|
62
62
|
{ name: "Carol", age: 28, active: true, role: "admin" },
|
|
63
|
+
{ name: "Dan", age: 41, active: true, role: "editor" },
|
|
64
|
+
{ name: "Eve", age: 35, active: false, role: "admin" },
|
|
65
|
+
{ name: "Frank", age: 52, active: true, role: "editor" },
|
|
66
|
+
{ name: "Grace", age: 23, active: true, role: "admin" },
|
|
63
67
|
];
|
|
64
68
|
|
|
65
69
|
const properties = await buildEntityPropertiesFromData(
|
|
@@ -67,11 +71,12 @@ const properties = await buildEntityPropertiesFromData(
|
|
|
67
71
|
inferTypeFromValue
|
|
68
72
|
);
|
|
69
73
|
// properties = {
|
|
70
|
-
// name: { type: "string", name: "Name",
|
|
71
|
-
// age: { type: "number", name: "Age",
|
|
72
|
-
// active: { type: "boolean", name: "Active",
|
|
73
|
-
// role: { type: "string", name: "Role", enum: [...],
|
|
74
|
+
// name: { type: "string", name: "Name", validation: { required: true } },
|
|
75
|
+
// age: { type: "number", name: "Age", validation: { required: true } },
|
|
76
|
+
// active: { type: "boolean", name: "Active", validation: { required: true } },
|
|
77
|
+
// role: { type: "string", name: "Role", enum: [...], validation: { required: true } }
|
|
74
78
|
// }
|
|
79
|
+
// An enum is inferred when a column has fewer distinct values than a third of its values.
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
## Related Packages
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { DataType, Properties, Property } from "@rebasepro/types";
|
|
2
2
|
export type InferenceTypeBuilder = (value: unknown) => DataType;
|
|
3
|
-
export
|
|
3
|
+
export type BuildEntityPropertiesOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Top-level keys to leave out of the inferred properties — Firestore
|
|
6
|
+
* metadata such as `__name__`, say. Nothing is left out by default: a
|
|
7
|
+
* column is data, `_id` in a Mongo export included.
|
|
8
|
+
*/
|
|
9
|
+
ignoreKey?: (key: string) => boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function buildEntityPropertiesFromData(data: object[], getType: InferenceTypeBuilder, options?: BuildEntityPropertiesOptions): Promise<Properties>;
|
|
4
12
|
export declare function buildPropertyFromData(data: unknown[], property: Property, getType: InferenceTypeBuilder): Property;
|
|
5
13
|
export declare function buildPropertiesOrder(properties: Properties, propertiesOrder?: string[], priorityKeys?: string[]): string[];
|
|
6
14
|
export declare function inferTypeFromValue(value: unknown): DataType;
|
package/dist/index.d.ts
CHANGED
package/dist/index.es.js
CHANGED
|
@@ -74,13 +74,18 @@ function removeTrailingSlash(s) {
|
|
|
74
74
|
*/
|
|
75
75
|
function extractEnumFromValues(values) {
|
|
76
76
|
if (!Array.isArray(values)) return [];
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
const seen = /* @__PURE__ */ new Set();
|
|
78
|
+
const enumValues = [];
|
|
79
|
+
for (const value of values) {
|
|
80
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") continue;
|
|
81
|
+
const id = String(value);
|
|
82
|
+
if (seen.has(id)) continue;
|
|
83
|
+
seen.add(id);
|
|
84
|
+
enumValues.push({
|
|
85
|
+
id,
|
|
86
|
+
label: typeof value === "string" ? unslugify$1(value) : id
|
|
87
|
+
});
|
|
88
|
+
}
|
|
84
89
|
enumValues.sort((a, b) => a.label.localeCompare(b.label));
|
|
85
90
|
return enumValues;
|
|
86
91
|
}
|
|
@@ -110,6 +115,14 @@ var AUDIO_EXTENSIONS = [
|
|
|
110
115
|
];
|
|
111
116
|
var VIDEO_EXTENSIONS = [".avi", ".mp4"];
|
|
112
117
|
var 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])?)*$/;
|
|
118
|
+
var EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
119
|
+
function isAbsoluteUrl(value) {
|
|
120
|
+
try {
|
|
121
|
+
return Boolean(new URL(value).protocol);
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
113
126
|
function buildStringProperty({ name, totalDocsCount, valuesResult }) {
|
|
114
127
|
let stringProperty = {
|
|
115
128
|
name: name ?? "",
|
|
@@ -119,9 +132,11 @@ function buildStringProperty({ name, totalDocsCount, valuesResult }) {
|
|
|
119
132
|
const totalEntriesCount = valuesResult.values.length;
|
|
120
133
|
const totalValues = Array.from(valuesResult.valuesCount.keys()).length;
|
|
121
134
|
const config = {};
|
|
122
|
-
const
|
|
135
|
+
const nonBlank = valuesResult.values.filter((value) => value !== "" && value !== null && value !== void 0);
|
|
136
|
+
const everyValue = (test) => nonBlank.length > 0 && nonBlank.every((value) => typeof value === "string" && test(value));
|
|
137
|
+
const probablyAURL = everyValue((value) => value.startsWith("http") && isAbsoluteUrl(value));
|
|
123
138
|
if (probablyAURL) config.url = true;
|
|
124
|
-
const probablyAnEmail =
|
|
139
|
+
const probablyAnEmail = everyValue((value) => emailRegEx.test(value) && EMAIL_SHAPE.test(value));
|
|
125
140
|
if (probablyAnEmail) config.email = true;
|
|
126
141
|
const probablyUserIds = valuesResult.values.filter((value) => typeof value === "string" && value.length === 28 && !value.includes(" ")).length > totalDocsCount / 3 * 2;
|
|
127
142
|
if (!probablyAnEmail && !probablyAURL && !probablyUserIds && !probablyAURL && totalValues < totalEntriesCount / 3) {
|
|
@@ -179,6 +194,42 @@ function buildReferenceProperty({ name, totalDocsCount, valuesResult }) {
|
|
|
179
194
|
};
|
|
180
195
|
}
|
|
181
196
|
//#endregion
|
|
197
|
+
//#region src/numbers.ts
|
|
198
|
+
/**
|
|
199
|
+
* A double holds 15 significant decimal digits exactly; a 16th may already be
|
|
200
|
+
* rounded (`9007199254740993` reads back as …992).
|
|
201
|
+
*/
|
|
202
|
+
var MAX_EXACT_SIGNIFICANT_DIGITS = 15;
|
|
203
|
+
/**
|
|
204
|
+
* JSON's number grammar: an optional minus, an integer part with no leading
|
|
205
|
+
* zero, an optional fraction and an optional exponent. No `+`, no thousands
|
|
206
|
+
* separator, no currency or percent sign, no hexadecimal, no `Infinity`.
|
|
207
|
+
*/
|
|
208
|
+
var NUMBER_SPELLING = /^-?(0|[1-9]\d*)(\.(\d+))?([eE][+-]?\d+)?$/;
|
|
209
|
+
/**
|
|
210
|
+
* The number a text spells, when it spells one that a number gives back with
|
|
211
|
+
* nothing lost — otherwise `undefined`.
|
|
212
|
+
*
|
|
213
|
+
* This is the one answer to "is this text a number" for both the inference
|
|
214
|
+
* that picks a column's type and the import that converts its cells, so the
|
|
215
|
+
* two cannot disagree. It refuses what `Number()` would forgive and silently
|
|
216
|
+
* change: a leading zero (`02134` is a zip code, `00123` a code — as numbers
|
|
217
|
+
* they lose the zero), more significant digits than a double holds exactly (a
|
|
218
|
+
* 20-digit SKU), and anything that is not a plain decimal (`1,234`, `$1.00`,
|
|
219
|
+
* `12%`, `+15551234567`, `N/A`). Formatting a number does not carry —
|
|
220
|
+
* `10.00` and `1e3` — is allowed: their value is exact.
|
|
221
|
+
*/
|
|
222
|
+
function readNumberExactly(text) {
|
|
223
|
+
const trimmed = text.trim();
|
|
224
|
+
const match = NUMBER_SPELLING.exec(trimmed);
|
|
225
|
+
if (!match) return void 0;
|
|
226
|
+
const significant = (match[1] + (match[3] ?? "").replace(/0+$/, "")).replace(/^0+/, "");
|
|
227
|
+
if (significant.length > MAX_EXACT_SIGNIFICANT_DIGITS) return void 0;
|
|
228
|
+
const value = Number(trimmed);
|
|
229
|
+
if (!Number.isFinite(value) || value === 0 && significant !== "") return void 0;
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
182
233
|
//#region src/collection_builder.ts
|
|
183
234
|
/**
|
|
184
235
|
* The entry a count record holds for a column, if it holds one.
|
|
@@ -192,12 +243,12 @@ function buildReferenceProperty({ name, totalDocsCount, valuesResult }) {
|
|
|
192
243
|
function ownEntry(record, key) {
|
|
193
244
|
return Object.hasOwn(record, key) ? record[key] : void 0;
|
|
194
245
|
}
|
|
195
|
-
async function buildEntityPropertiesFromData(data, getType) {
|
|
246
|
+
async function buildEntityPropertiesFromData(data, getType, options = {}) {
|
|
196
247
|
const typesCount = {};
|
|
197
248
|
const valuesCount = {};
|
|
198
249
|
if (data) data.forEach((entry) => {
|
|
199
250
|
if (entry) Object.entries(entry).forEach(([key, value]) => {
|
|
200
|
-
if (
|
|
251
|
+
if (options.ignoreKey?.(key)) return;
|
|
201
252
|
increaseMapTypeCount(typesCount, key, value, getType);
|
|
202
253
|
increaseValuesCount(valuesCount, key, value, getType);
|
|
203
254
|
});
|
|
@@ -280,17 +331,24 @@ function increaseTypeCount(type, typesCount, fieldValue, getType) {
|
|
|
280
331
|
} else if (!typesCount[type]) typesCount[type] = 1;
|
|
281
332
|
else typesCount[type] = Number(typesCount[type]) + 1;
|
|
282
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* A cell nobody filled in. A CSV reader hands over `""` for it where a
|
|
336
|
+
* spreadsheet reader hands over nothing, and the import writes neither — so it
|
|
337
|
+
* is no value here either: it casts no vote for the column's type and does not
|
|
338
|
+
* count towards `required` or an enum.
|
|
339
|
+
*/
|
|
340
|
+
function isBlank(value) {
|
|
341
|
+
return value === null || value === void 0 || value === "";
|
|
342
|
+
}
|
|
283
343
|
function increaseMapTypeCount(typesCountRecord, key, fieldValue, getType) {
|
|
284
|
-
if (key.startsWith("_")) return;
|
|
285
344
|
let typesCount = ownEntry(typesCountRecord, key);
|
|
286
345
|
if (!typesCount) {
|
|
287
346
|
typesCount = {};
|
|
288
347
|
typesCountRecord[key] = typesCount;
|
|
289
348
|
}
|
|
290
|
-
if (fieldValue
|
|
349
|
+
if (!isBlank(fieldValue)) increaseTypeCount(getType(fieldValue), typesCount, fieldValue, getType);
|
|
291
350
|
}
|
|
292
351
|
function increaseValuesCount(typeValuesRecord, key, fieldValue, getType) {
|
|
293
|
-
if (key.startsWith("_")) return;
|
|
294
352
|
const type = getType(fieldValue);
|
|
295
353
|
const valuesRecord = ownEntry(typeValuesRecord, key) ?? {
|
|
296
354
|
values: [],
|
|
@@ -298,18 +356,19 @@ function increaseValuesCount(typeValuesRecord, key, fieldValue, getType) {
|
|
|
298
356
|
};
|
|
299
357
|
typeValuesRecord[key] = valuesRecord;
|
|
300
358
|
if (type === "map") {
|
|
301
|
-
let mapValuesRecord = valuesRecord.
|
|
359
|
+
let mapValuesRecord = valuesRecord.mapValues;
|
|
302
360
|
if (!mapValuesRecord) {
|
|
303
361
|
mapValuesRecord = {};
|
|
304
|
-
valuesRecord.
|
|
362
|
+
valuesRecord.mapValues = mapValuesRecord;
|
|
305
363
|
}
|
|
306
364
|
if (fieldValue) Object.entries(fieldValue).forEach(([subKey, value]) => increaseValuesCount(mapValuesRecord, subKey, value, getType));
|
|
307
365
|
} else if (type === "array") {
|
|
308
366
|
if (Array.isArray(fieldValue)) fieldValue.forEach((value) => {
|
|
367
|
+
if (isBlank(value)) return;
|
|
309
368
|
valuesRecord.values.push(value);
|
|
310
369
|
valuesRecord.valuesCount.set(value, (valuesRecord.valuesCount.get(value) ?? 0) + 1);
|
|
311
370
|
});
|
|
312
|
-
} else if (fieldValue
|
|
371
|
+
} else if (!isBlank(fieldValue)) {
|
|
313
372
|
valuesRecord.values.push(fieldValue);
|
|
314
373
|
valuesRecord.valuesCount.set(fieldValue, (valuesRecord.valuesCount.get(fieldValue) ?? 0) + 1);
|
|
315
374
|
}
|
|
@@ -328,7 +387,33 @@ function getHighestTypesCount(typesCount) {
|
|
|
328
387
|
function getHighestRecordCount(record) {
|
|
329
388
|
return Object.entries(record).map(([key, typesCount]) => getHighestTypesCount(typesCount)).reduce((a, b) => Math.max(a, b), 0);
|
|
330
389
|
}
|
|
331
|
-
|
|
390
|
+
/**
|
|
391
|
+
* The types a cell can be converted between: the import turns any of them into
|
|
392
|
+
* text without loss, and text back into one of them only when the text spells
|
|
393
|
+
* it.
|
|
394
|
+
*/
|
|
395
|
+
var SCALAR_TYPES = /* @__PURE__ */ new Set([
|
|
396
|
+
"string",
|
|
397
|
+
"number",
|
|
398
|
+
"boolean",
|
|
399
|
+
"date"
|
|
400
|
+
]);
|
|
401
|
+
/**
|
|
402
|
+
* A column's type, from the votes its values cast.
|
|
403
|
+
*
|
|
404
|
+
* When the votes mix scalar types — numbers beside text, booleans beside
|
|
405
|
+
* numbers — the majority is not an answer: converting the minority to it
|
|
406
|
+
* changes them (`02134` → 2134, a 20-digit SKU rounded, `N/A` → nothing,
|
|
407
|
+
* 2 → `false`). Such a column is text, which holds every one of them as
|
|
408
|
+
* written. The one exception is numbers beside text that spells numbers
|
|
409
|
+
* exactly (`10.00`, `1e3`): that column is a number, and nothing is lost.
|
|
410
|
+
*
|
|
411
|
+
* @param values every non-blank value the votes were cast for, when known;
|
|
412
|
+
* without them, a mix of numbers and text is text.
|
|
413
|
+
*/
|
|
414
|
+
function getMostProbableType(typesCount, values) {
|
|
415
|
+
const scalarTypes = Object.entries(typesCount).filter(([type, count]) => SCALAR_TYPES.has(type) && Number(count) > 0).map(([type]) => type);
|
|
416
|
+
if (scalarTypes.length > 1) return scalarTypes.every((type) => type === "number" || type === "string") && values !== void 0 && values.every((value) => typeof value !== "string" || readNumberExactly(value) !== void 0) ? "number" : "string";
|
|
332
417
|
let highestCount = -1;
|
|
333
418
|
let probableType = "string";
|
|
334
419
|
Object.entries(typesCount).forEach(([type, count]) => {
|
|
@@ -362,7 +447,7 @@ function buildPropertyFromCount(key, totalDocsCount, mostProbableType, typesCoun
|
|
|
362
447
|
};
|
|
363
448
|
} else if (mostProbableType === "array") {
|
|
364
449
|
const arrayTypesCount = typesCount.array;
|
|
365
|
-
const of = buildPropertyFromCount(key, totalDocsCount, getMostProbableType(arrayTypesCount), arrayTypesCount, valuesResult);
|
|
450
|
+
const of = buildPropertyFromCount(key, totalDocsCount, getMostProbableType(arrayTypesCount, valuesResult?.values), arrayTypesCount, valuesResult);
|
|
366
451
|
result = {
|
|
367
452
|
type: "array",
|
|
368
453
|
name: title ?? key ?? "",
|
|
@@ -375,7 +460,8 @@ function buildPropertyFromCount(key, totalDocsCount, mostProbableType, typesCoun
|
|
|
375
460
|
totalDocsCount,
|
|
376
461
|
valuesResult
|
|
377
462
|
};
|
|
378
|
-
if (mostProbableType === "string") result =
|
|
463
|
+
if ((mostProbableType === "number" || mostProbableType === "string") && isEpochColumn(key, valuesResult?.values)) result = { type: "date" };
|
|
464
|
+
else if (mostProbableType === "string") result = buildStringProperty(propertyProps);
|
|
379
465
|
else if (mostProbableType === "reference") result = buildReferenceProperty(propertyProps);
|
|
380
466
|
else result = { type: mostProbableType };
|
|
381
467
|
if (title) result.name = title;
|
|
@@ -384,11 +470,40 @@ function buildPropertyFromCount(key, totalDocsCount, mostProbableType, typesCoun
|
|
|
384
470
|
}
|
|
385
471
|
return result;
|
|
386
472
|
}
|
|
473
|
+
/** The words that end a column name holding a moment: `created_at`, `updatedOn`, `birth_date`, `timestamp`. */
|
|
474
|
+
var MOMENT_NAME_ENDINGS = /* @__PURE__ */ new Set([
|
|
475
|
+
"at",
|
|
476
|
+
"on",
|
|
477
|
+
"date",
|
|
478
|
+
"time",
|
|
479
|
+
"datetime",
|
|
480
|
+
"timestamp",
|
|
481
|
+
"ts"
|
|
482
|
+
]);
|
|
483
|
+
/** An epoch in seconds from 2001-09-09 to 2103, or in milliseconds over the same span. */
|
|
484
|
+
function isEpochNumber(value) {
|
|
485
|
+
return Number.isInteger(value) && (value >= 1e9 && value < 42e8 || value >= 0xe8d4a51000 && value < 42e11);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Whether a column holds epoch timestamps: its name ends in a word that says
|
|
489
|
+
* it holds a moment, and every value it has is a whole number of seconds or
|
|
490
|
+
* milliseconds since 1970 (written as a number, or as the digits a CSV
|
|
491
|
+
* writes) in the range a timestamp of this century falls in. Both, because
|
|
492
|
+
* either alone is an order number or a `response_time` in milliseconds. The
|
|
493
|
+
* import reads such a value into a date as soon as the column is one.
|
|
494
|
+
*/
|
|
495
|
+
function isEpochColumn(key, values) {
|
|
496
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
497
|
+
if (words.length === 0 || !MOMENT_NAME_ENDINGS.has(words[words.length - 1])) return false;
|
|
498
|
+
const present = (values ?? []).filter((value) => !isBlank(value));
|
|
499
|
+
return present.length > 0 && present.every((value) => typeof value === "number" ? isEpochNumber(value) : typeof value === "string" && /^\d+$/.test(value.trim()) && isEpochNumber(Number(value.trim())));
|
|
500
|
+
}
|
|
387
501
|
function buildPropertiesFromCount(totalDocsCount, typesCountRecord, valuesCountRecord) {
|
|
388
502
|
const res = {};
|
|
389
503
|
Object.entries(typesCountRecord).forEach(([key, typesCount]) => {
|
|
390
|
-
const
|
|
391
|
-
|
|
504
|
+
const valuesEntry = valuesCountRecord ? ownEntry(valuesCountRecord, key) : void 0;
|
|
505
|
+
const mostProbableType = getMostProbableType(typesCount, valuesEntry?.values);
|
|
506
|
+
res[key] = buildPropertyFromCount(key, totalDocsCount, mostProbableType, typesCount, valuesEntry);
|
|
392
507
|
});
|
|
393
508
|
return res;
|
|
394
509
|
}
|
|
@@ -403,10 +518,10 @@ function countMaxDocumentsUnder(typesCount) {
|
|
|
403
518
|
function getMostProbableTypeInArray(array, getType) {
|
|
404
519
|
const typesCount = {};
|
|
405
520
|
array.forEach((value) => {
|
|
406
|
-
if (value
|
|
521
|
+
if (isBlank(value)) return;
|
|
407
522
|
increaseTypeCount(getType(value), typesCount, value, getType);
|
|
408
523
|
});
|
|
409
|
-
return getMostProbableType(typesCount);
|
|
524
|
+
return getMostProbableType(typesCount, array.filter((value) => !isBlank(value)));
|
|
410
525
|
}
|
|
411
526
|
function checkTypesCountHighVariability(typesCount) {
|
|
412
527
|
const maxCount = countMaxDocumentsUnder(typesCount);
|
|
@@ -422,11 +537,12 @@ function inferTypeFromValue(value) {
|
|
|
422
537
|
if (typeof value === "string") return "string";
|
|
423
538
|
if (typeof value === "number") return "number";
|
|
424
539
|
if (typeof value === "boolean") return "boolean";
|
|
540
|
+
if (value instanceof Date) return "date";
|
|
425
541
|
if (Array.isArray(value)) return "array";
|
|
426
542
|
if (typeof value === "object") return "map";
|
|
427
543
|
return "string";
|
|
428
544
|
}
|
|
429
545
|
//#endregion
|
|
430
|
-
export { buildEntityPropertiesFromData, buildPropertiesOrder, buildPropertyFromData, extractEnumFromValues, findCommonInitialStringInPath, inferTypeFromValue, isObject, isPlainObject, looksLikeReference, mergeDeep, parseReferenceString, prettifyIdentifier, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveEnumValues, unslugify };
|
|
546
|
+
export { buildEntityPropertiesFromData, buildPropertiesOrder, buildPropertyFromData, extractEnumFromValues, findCommonInitialStringInPath, inferTypeFromValue, isObject, isPlainObject, looksLikeReference, mergeDeep, parseReferenceString, prettifyIdentifier, readNumberExactly, removeInitialAndTrailingSlashes, removeInitialSlash, removeTrailingSlash, resolveEnumValues, unslugify };
|
|
431
547
|
|
|
432
548
|
//# sourceMappingURL=index.es.js.map
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":[],"sources":["../src/strings.ts","../src/util.ts","../src/builders/string_property_builder.ts","../src/builders/validation_builder.ts","../src/builders/reference_property_builder.ts","../src/collection_builder.ts"],"sourcesContent":["import { ValuesCountEntry } from \"./types\";\n\n/**\n * Parse a reference string value which can be in the format:\n * - Simple: \"path/entityId\"\n * - With database: \"database_name:::path/entityId\"\n * Returns the path and database (undefined if not specified or if \"(default)\")\n */\nexport function parseReferenceString(value: string): { path: string; database?: string } | null {\n if (!value) return null;\n\n let database: string | undefined = undefined;\n let fullPath = value;\n\n // Parse the new format: database_name:::path/entityId\n if (value.includes(\":::\")) {\n const [dbName, pathPart] = value.split(\":::\");\n if (dbName && dbName !== \"(default)\") {\n database = dbName;\n }\n fullPath = pathPart;\n }\n\n // Check if it looks like a path (contains at least one slash)\n if (!fullPath || !fullPath.includes(\"/\")) {\n return null;\n }\n\n // Extract the collection path (everything before the last slash)\n const path = fullPath.substring(0, fullPath.lastIndexOf(\"/\"));\n\n return { path,\ndatabase };\n}\n\n/**\n * Check if a string value looks like a reference\n */\nexport function looksLikeReference(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n return parseReferenceString(value) !== null;\n}\n\nexport function findCommonInitialStringInPath(valuesCount?: ValuesCountEntry) {\n\n if (!valuesCount) return undefined;\n\n function getPath(value: unknown): string | undefined {\n let pathString: string | undefined;\n\n if (typeof value === \"string\") {\n pathString = value;\n } else if (value && typeof value === \"object\" && \"slug\" in value && typeof (value as Record<string, unknown>).slug === \"string\") {\n pathString = (value as Record<string, unknown>).slug as string;\n } else {\n console.warn(\"findCommonInitialStringInPath: value is not a string or document with path\", value);\n return undefined;\n }\n\n if (!pathString) return undefined;\n\n // Parse the new format: database_name:::path/entityId\n // Extract just the path portion for comparison\n if (pathString.includes(\":::\")) {\n const [, pathPart] = pathString.split(\":::\");\n pathString = pathPart;\n }\n\n return pathString;\n }\n\n const strings: string[] = valuesCount.values.map((v) => getPath(v)).filter(v => !!v) as string[];\n const pathWithSlash = strings.find((s) => s.includes(\"/\"));\n if (!pathWithSlash)\n return undefined;\n\n const searchedPath = pathWithSlash.substring(0, pathWithSlash.lastIndexOf(\"/\"));\n\n const yep = valuesCount.values\n .filter((value) => {\n const path = getPath(value);\n if (!path) return false;\n return path.startsWith(searchedPath)\n }).length > valuesCount.values.length / 3 * 2;\n\n return yep ? searchedPath : undefined;\n\n}\n\nexport function removeInitialAndTrailingSlashes(s: string): string {\n return removeInitialSlash(removeTrailingSlash(s));\n}\n\nexport function removeInitialSlash(s: string) {\n if (s.startsWith(\"/\"))\n return s.slice(1);\n else return s;\n}\n\nexport function removeTrailingSlash(s: string) {\n if (s.endsWith(\"/\"))\n return s.slice(0, -1);\n else return s;\n}\n","import { EnumValueConfig, EnumValues } from \"@rebasepro/types\";\nimport { unslugify } from \"@rebasepro/utils\";\n\n// Canonical utility functions — single source of truth in @rebasepro/utils\nexport { unslugify, prettifyIdentifier, isObject, isPlainObject, mergeDeep } from \"@rebasepro/utils\";\n\n/**\n * Extract enum values from a list of sample values.\n * This is inference-specific logic (not a general utility).\n */\nexport function extractEnumFromValues(values: unknown[]) {\n if (!Array.isArray(values)) {\n return [];\n }\n const enumValues = values\n .map((value) => {\n if (typeof value === \"string\") {\n return ({\n id: value,\n label: unslugify(value)\n });\n } else\n return null;\n }).filter(Boolean) as Array<{ id: string, label: string }>;\n enumValues.sort((a, b) => a.label.localeCompare(b.label));\n return enumValues;\n}\n\nexport function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined {\n // Check Array.isArray first since typeof [] === \"object\" is true in JavaScript\n if (Array.isArray(input)) {\n return input as EnumValueConfig[];\n } else if (typeof input === \"object\" && input !== null) {\n return Object.entries(input).map(([id, value]) =>\n (typeof value === \"string\"\n ? {\n id,\n label: value\n }\n : value));\n } else {\n return undefined;\n }\n}\n","import { findCommonInitialStringInPath } from \"../strings\";\nimport { extractEnumFromValues } from \"../util\";\nimport { FileType, Property, StringProperty } from \"@rebasepro/types\";\nimport { InferencePropertyBuilderProps, ValuesCountEntry } from \"../types\";\n\nconst IMAGE_EXTENSIONS = [\".jpg\", \".jpeg\", \".png\", \".webp\", \".gif\", \".avif\"];\nconst AUDIO_EXTENSIONS = [\".mp3\", \".ogg\", \".opus\", \".aac\"];\nconst VIDEO_EXTENSIONS = [\".avi\", \".mp4\"];\n\nconst 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])?)*$/;\n\nexport function buildStringProperty({\n name,\n totalDocsCount,\n valuesResult\n}: InferencePropertyBuilderProps): Property {\n\n let stringProperty: Property = {\n name: name ?? \"\",\n type: \"string\"\n\n };\n\n if (valuesResult) {\n\n const totalEntriesCount = valuesResult.values.length;\n const totalValues = Array.from(valuesResult.valuesCount.keys()).length;\n\n const config: Partial<StringProperty> = {};\n\n const probablyAURL = valuesResult.values\n .filter((value) => typeof value === \"string\" &&\n value.toString().startsWith(\"http\")).length > totalDocsCount / 3 * 2;\n if (probablyAURL) {\n config.url = true;\n }\n\n const probablyAnEmail = valuesResult.values\n .filter((value) => typeof value === \"string\" &&\n emailRegEx.test(value)).length > totalDocsCount / 3 * 2;\n if (probablyAnEmail) {\n config.email = true;\n }\n\n const probablyUserIds = valuesResult.values\n .filter((value) => typeof value === \"string\" && value.length === 28 && !value.includes(\" \"))\n .length > totalDocsCount / 3 * 2;\n // No `admin.readOnly` here. This package is core — the field does not exist in\n // `@rebasepro/types`, because `@rebasepro/cms-types` declares it — and a\n // guess about whether a form should be editable is presentation, not schema.\n // (It also would not have enforced anything: nothing on the server reads\n // `readOnly`.) The inferred `isId` below is the part that carries meaning.\n\n if (!probablyAnEmail &&\n !probablyAURL &&\n !probablyUserIds &&\n !probablyAURL &&\n totalValues < totalEntriesCount / 3\n ) {\n const enumValues = extractEnumFromValues(Array.from(valuesResult.valuesCount.keys()));\n\n if (Object.keys(enumValues).length > 1)\n config.enum = enumValues;\n }\n\n // regular string\n if (!probablyAnEmail &&\n !probablyAURL &&\n !probablyUserIds &&\n !probablyAURL &&\n !config.enum) {\n const fileType = probableFileType(valuesResult, totalDocsCount);\n if (fileType) {\n config.storage = {\n acceptedFiles: fileType as FileType[],\n storagePath: findCommonInitialStringInPath(valuesResult) ?? \"/\"\n };\n }\n }\n\n if (Object.keys(config).length > 0)\n stringProperty = {\n ...stringProperty,\n ...config\n } as StringProperty;\n }\n\n return stringProperty;\n}\n\nfunction probableFileType(valuesCount: ValuesCountEntry, totalDocsCount: number): false | FileType[] {\n const isImage = (value: string) => IMAGE_EXTENSIONS.some((extension) => value.toString().endsWith(extension));\n const isAudio = (value: string) => AUDIO_EXTENSIONS.some((extension) => value.toString().endsWith(extension));\n const isVideo = (value: string) => VIDEO_EXTENSIONS.some((extension) => value.toString().endsWith(extension));\n\n const stringValues = valuesCount.values.filter((v): v is string => typeof v === \"string\");\n\n let imageCount = 0;\n let audioCount = 0;\n let videoCount = 0;\n\n for (const value of stringValues) {\n if (isImage(value)) imageCount++;\n else if (isAudio(value)) audioCount++;\n else if (isVideo(value)) videoCount++;\n }\n\n const totalMediaCount = imageCount + audioCount + videoCount;\n if (totalMediaCount > (totalDocsCount * 2) / 3) {\n const fileTypes: FileType[] = [];\n if (imageCount > 0) fileTypes.push(\"image/*\");\n if (audioCount > 0) fileTypes.push(\"audio/*\");\n if (videoCount > 0) fileTypes.push(\"video/*\");\n return fileTypes.length > 0 ? fileTypes : false;\n }\n\n return false;\n}\n","import { PropertyValidationSchema } from \"@rebasepro/types\";\nimport { InferencePropertyBuilderProps } from \"../types\";\n\nexport function buildValidation({\n totalDocsCount,\n valuesResult\n}: InferencePropertyBuilderProps): PropertyValidationSchema | undefined {\n\n if (valuesResult) {\n const totalEntriesCount = valuesResult.values.length;\n if (totalDocsCount === totalEntriesCount)\n return {\n required: true\n }\n }\n\n return undefined;\n}\n","import { findCommonInitialStringInPath } from \"../strings\";\nimport { InferencePropertyBuilderProps } from \"../types\";\nimport { Property } from \"@rebasepro/types\";\n\nexport function buildReferenceProperty({\n name,\n totalDocsCount,\n valuesResult\n}: InferencePropertyBuilderProps): Property {\n\n const property: Property = {\n name: name ?? \"\",\n type: \"reference\",\n path: findCommonInitialStringInPath(valuesResult) ?? \"!!!FIX_ME!!!\"\n };\n\n return property;\n}\n","import {\n InferencePropertyBuilderProps,\n TypesCount,\n TypesCountRecord,\n ValuesCountEntry,\n ValuesCountRecord\n} from \"./types\";\nimport { buildStringProperty } from \"./builders/string_property_builder\";\nimport { buildValidation } from \"./builders/validation_builder\";\nimport { buildReferenceProperty } from \"./builders/reference_property_builder\";\nimport { extractEnumFromValues, mergeDeep, prettifyIdentifier, resolveEnumValues } from \"./util\";\nimport { DataType, EnumValues, Properties, Property, StringProperty, Vector } from \"@rebasepro/types\";\n\nexport type InferenceTypeBuilder = (value: unknown) => DataType;\n\n/**\n * The entry a count record holds for a column, if it holds one.\n *\n * The records are keyed by column names out of the data, and a plain object\n * answers `record[\"constructor\"]` or `record[\"toString\"]` with what it\n * inherits: the count was written onto the global `Object`, and the values\n * list was `Object.values`, so importing a file with a `constructor` column\n * threw. Only an own entry is one this record counted.\n */\nfunction ownEntry<T>(record: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(record, key) ? record[key] : undefined;\n}\n\nexport async function buildEntityPropertiesFromData(\n data: object[],\n getType: InferenceTypeBuilder\n): Promise<Properties> {\n const typesCount: TypesCountRecord = {};\n const valuesCount: ValuesCountRecord = {};\n if (data) {\n data.forEach((entry) => {\n if (entry) {\n Object.entries(entry).forEach(([key, value]) => {\n if (key.startsWith(\"_\")) return; // Ignore properties starting with _\n increaseMapTypeCount(typesCount, key, value, getType);\n increaseValuesCount(valuesCount, key, value, getType);\n });\n }\n });\n }\n return buildPropertiesFromCount(data.length, typesCount, valuesCount);\n}\n\nexport function buildPropertyFromData(\n data: unknown[],\n property: Property,\n getType: InferenceTypeBuilder\n): Property {\n const typesCount = {};\n const valuesCount: ValuesCountRecord = {};\n if (data) {\n data.forEach((entry) => {\n increaseTypeCount(property.type, typesCount, entry, getType);\n increaseValuesCount(valuesCount, \"inferred_prop\", entry, getType);\n });\n }\n const enumValues = \"enum\" in property ? resolveEnumValues(property[\"enum\"] as EnumValues) : undefined;\n if (enumValues) {\n const newEnumValues = extractEnumFromValues(Array.from(valuesCount[\"inferred_prop\"].valuesCount.keys()));\n return {\n ...property,\n enum: [...newEnumValues, ...enumValues]\n } as StringProperty;\n }\n const generatedProperty = buildPropertyFromCount(\n \"inferred_prop\",\n data.length,\n property.type,\n typesCount,\n valuesCount[\"inferred_prop\"]\n );\n return mergeDeep(generatedProperty, property);\n}\n\nexport function buildPropertiesOrder(\n properties: Properties,\n propertiesOrder?: string[],\n priorityKeys?: string[]\n): string[] {\n const lowerCasePriorityKeys = (priorityKeys ?? []).map((key) => key.toLowerCase());\n\n function propOrder(s: string) {\n const k = s.toLowerCase();\n if (lowerCasePriorityKeys.includes(k)) return 4;\n if (k === \"title\" || k === \"name\") return 3;\n if (k.includes(\"title\") || k.includes(\"name\")) return 2;\n if (k.includes(\"image\") || k.includes(\"picture\")) return 1;\n return 0;\n }\n\n function deriveOrder(keys: string[]): string[] {\n return [...keys]\n .sort() // alphabetically\n .sort((a, b) => propOrder(b) - propOrder(a));\n }\n\n // A caller-supplied order is an answer, not an input. It used to be handed\n // to the same alphabetical + priority sort as a derived order, which threw\n // it away entirely — and because `Array.prototype.sort` sorts in place, the\n // caller's own array was silently reordered as a side effect.\n if (propertiesOrder) {\n const placed = new Set(propertiesOrder);\n // Properties the caller did not place — a newly inferred column, say —\n // still have to appear, so they follow in derived order.\n const rest = Object.keys(properties).filter(key => !placed.has(key));\n return [...propertiesOrder, ...deriveOrder(rest)];\n }\n\n return deriveOrder(Object.keys(properties));\n}\n\n/**\n * @param type\n * @param typesCount\n * @param fieldValue\n * @param getType\n */\nfunction increaseTypeCount(\n type: DataType,\n typesCount: TypesCount,\n fieldValue: unknown,\n getType: InferenceTypeBuilder\n) {\n if (type === \"map\") {\n if (fieldValue) {\n let mapTypesCount = typesCount[type];\n if (!mapTypesCount) {\n mapTypesCount = {};\n typesCount[type] = mapTypesCount;\n }\n Object.entries(fieldValue as Record<string, unknown>).forEach(([key, value]) => {\n increaseMapTypeCount(mapTypesCount as TypesCountRecord, key, value, getType);\n });\n }\n } else if (type === \"array\") {\n let arrayTypesCount = typesCount[type];\n if (!arrayTypesCount) {\n arrayTypesCount = {};\n typesCount[type] = arrayTypesCount;\n }\n if (fieldValue && Array.isArray(fieldValue) && fieldValue.length > 0) {\n const arrayType = getMostProbableTypeInArray(fieldValue, getType);\n if (arrayType === \"map\") {\n let mapTypesCount = arrayTypesCount[arrayType];\n if (!mapTypesCount) {\n mapTypesCount = {};\n }\n fieldValue.forEach((value) => {\n if (value && typeof value === \"object\" && !Array.isArray(value)) { // Ensure value is an object for Object.entries\n Object.entries(value).forEach(([key, v]) =>\n increaseMapTypeCount(mapTypesCount, key, v, getType)\n );\n }\n });\n arrayTypesCount[arrayType] = mapTypesCount;\n } else {\n if (!arrayTypesCount[arrayType]) arrayTypesCount[arrayType] = 1;\n else arrayTypesCount[arrayType] = Number(arrayTypesCount[arrayType]) + 1;\n }\n }\n } else {\n if (!typesCount[type]) typesCount[type] = 1;\n else typesCount[type] = Number(typesCount[type]) + 1;\n }\n}\n\nfunction increaseMapTypeCount(\n typesCountRecord: TypesCountRecord,\n key: string,\n fieldValue: unknown,\n getType: InferenceTypeBuilder\n) {\n if (key.startsWith(\"_\")) return; // Ignore properties starting with _\n\n let typesCount: TypesCount | undefined = ownEntry(typesCountRecord, key);\n if (!typesCount) {\n typesCount = {};\n typesCountRecord[key] = typesCount;\n }\n\n if (fieldValue != null) {\n // Check that fieldValue is not null or undefined before proceeding\n const type = getType(fieldValue);\n increaseTypeCount(type, typesCount, fieldValue, getType);\n }\n}\n\nfunction increaseValuesCount(\n typeValuesRecord: ValuesCountRecord,\n key: string,\n fieldValue: unknown,\n getType: InferenceTypeBuilder\n) {\n if (key.startsWith(\"_\")) return; // Ignore properties starting with _\n\n const type = getType(fieldValue);\n\n const valuesRecord: {\n values: unknown[];\n valuesCount: Map<unknown, number>;\n map?: ValuesCountRecord;\n } = ownEntry(typeValuesRecord, key) ?? {\n values: [],\n valuesCount: new Map()\n };\n typeValuesRecord[key] = valuesRecord;\n\n if (type === \"map\") {\n let mapValuesRecord: ValuesCountRecord | undefined = valuesRecord.map;\n if (!mapValuesRecord) {\n mapValuesRecord = {};\n valuesRecord.map = mapValuesRecord;\n }\n if (fieldValue)\n Object.entries(fieldValue as Record<string, unknown>).forEach(([subKey, value]) =>\n increaseValuesCount(mapValuesRecord as ValuesCountRecord, subKey, value, getType)\n );\n } else if (type === \"array\") {\n if (Array.isArray(fieldValue)) {\n fieldValue.forEach((value) => {\n valuesRecord.values.push(value);\n valuesRecord.valuesCount.set(value, (valuesRecord.valuesCount.get(value) ?? 0) + 1);\n });\n }\n } else {\n if (fieldValue !== null && fieldValue !== undefined) {\n valuesRecord.values.push(fieldValue);\n valuesRecord.valuesCount.set(fieldValue, (valuesRecord.valuesCount.get(fieldValue) ?? 0) + 1);\n }\n }\n}\n\nfunction getHighestTypesCount(typesCount: TypesCount): number {\n let highestCount = 0;\n Object.entries(typesCount).forEach(([type, count]) => {\n let countValue = 0;\n if (type === \"map\") {\n countValue = getHighestRecordCount(count as TypesCountRecord);\n } else if (type === \"array\") {\n countValue = getHighestTypesCount(count as TypesCount);\n } else {\n countValue = Number(count);\n }\n if (countValue > highestCount) {\n highestCount = countValue;\n }\n });\n\n return highestCount;\n}\n\nfunction getHighestRecordCount(record: TypesCountRecord): number {\n return Object.entries(record)\n .map(([key, typesCount]) => getHighestTypesCount(typesCount))\n .reduce((a, b) => Math.max(a, b), 0);\n}\n\nfunction getMostProbableType(typesCount: TypesCount): DataType {\n let highestCount = -1;\n let probableType: DataType = \"string\"; // default\n Object.entries(typesCount).forEach(([type, count]) => {\n let countValue;\n if (type === \"map\") {\n countValue = getHighestRecordCount(count as TypesCountRecord);\n } else if (type === \"array\") {\n countValue = getHighestTypesCount(count as TypesCount);\n } else {\n countValue = Number(count);\n }\n if (countValue > highestCount) {\n highestCount = countValue;\n probableType = type as DataType;\n }\n });\n return probableType;\n}\n\nfunction buildPropertyFromCount(\n key: string,\n totalDocsCount: number,\n mostProbableType: DataType,\n typesCount: TypesCount,\n valuesResult?: ValuesCountEntry\n): Property {\n let title: string | undefined;\n\n if (key) {\n title = prettifyIdentifier(key);\n }\n\n let result: Property | undefined = undefined;\n if (mostProbableType === \"map\") {\n const highVariability = checkTypesCountHighVariability(typesCount);\n if (highVariability) {\n result = {\n type: \"map\",\n name: title ?? key ?? \"\",\n keyValue: true,\n properties: {}\n };\n }\n const properties = buildPropertiesFromCount(\n totalDocsCount,\n typesCount.map as TypesCountRecord,\n valuesResult ? valuesResult.mapValues : undefined\n );\n result = {\n type: \"map\",\n name: title ?? key ?? \"\",\n properties\n };\n } else if (mostProbableType === \"array\") {\n const arrayTypesCount = typesCount.array as TypesCount;\n const arrayMostProbableType = getMostProbableType(arrayTypesCount);\n const of = buildPropertyFromCount(\n key,\n totalDocsCount,\n arrayMostProbableType,\n arrayTypesCount,\n valuesResult\n );\n result = {\n type: \"array\",\n name: title ?? key ?? \"\",\n of\n };\n }\n\n if (!result) {\n const propertyProps: InferencePropertyBuilderProps = {\n name: key,\n totalDocsCount,\n valuesResult\n };\n if (mostProbableType === \"string\") {\n result = buildStringProperty(propertyProps);\n } else if (mostProbableType === \"reference\") {\n result = buildReferenceProperty(propertyProps);\n } else {\n result = {\n type: mostProbableType\n } as Property;\n }\n\n if (title) {\n result.name = title;\n }\n\n const validation = buildValidation(propertyProps);\n if (validation) {\n result.validation = validation;\n }\n }\n\n return result;\n}\n\nfunction buildPropertiesFromCount(\n totalDocsCount: number,\n typesCountRecord: TypesCountRecord,\n valuesCountRecord?: ValuesCountRecord\n): Properties {\n const res: Properties = {};\n Object.entries(typesCountRecord).forEach(([key, typesCount]) => {\n const mostProbableType = getMostProbableType(typesCount);\n res[key] = buildPropertyFromCount(\n key,\n totalDocsCount,\n mostProbableType,\n typesCount,\n valuesCountRecord ? ownEntry(valuesCountRecord, key) : undefined\n );\n });\n return res;\n}\n\nfunction countMaxDocumentsUnder(typesCount: TypesCount) {\n let count = 0;\n Object.entries(typesCount).forEach(([type, value]) => {\n if (typeof value === \"object\") {\n count = Math.max(count, countMaxDocumentsUnder(value as TypesCountRecord));\n } else {\n count = Math.max(count, Number(value));\n }\n });\n return count;\n}\n\nfunction getMostProbableTypeInArray(\n array: unknown[],\n getType: InferenceTypeBuilder\n): DataType {\n const typesCount: TypesCount = {};\n array.forEach((value) => {\n // A hole in the array says nothing about what the array holds, so it\n // gets no vote. This used to fall out of `getType` answering \"map\" for\n // null and `increaseTypeCount` then skipping falsy map values; now that\n // null infers as \"string\" the skip has to be explicit, or every array\n // with a gap in it would drift towards \"string\".\n if (value === null || value === undefined) return;\n increaseTypeCount(getType(value), typesCount, value, getType);\n });\n return getMostProbableType(typesCount);\n}\n\nfunction checkTypesCountHighVariability(typesCount: TypesCount) {\n const maxCount = countMaxDocumentsUnder(typesCount);\n let keysWithFewValues = 0;\n Object.entries(typesCount.map ?? {}).forEach(([key, value]) => {\n const count = countMaxDocumentsUnder(value);\n if (count < maxCount / 3) {\n keysWithFewValues++;\n }\n });\n return keysWithFewValues / Object.entries(typesCount.map ?? {}).length > 0.5;\n}\n\n\nexport function inferTypeFromValue(value: unknown): DataType {\n if (value === null || value === undefined) return \"string\";\n if (value instanceof Vector || (value && typeof value === \"object\" && \"__type\" in value && (value as Record<string, unknown>).__type === \"Vector\")) return \"vector\";\n if (typeof value === \"string\") return \"string\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (Array.isArray(value)) return \"array\";\n if (typeof value === \"object\") return \"map\";\n return \"string\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/strings.ts","../src/util.ts","../src/builders/string_property_builder.ts","../src/builders/validation_builder.ts","../src/builders/reference_property_builder.ts","../src/numbers.ts","../src/collection_builder.ts"],"sourcesContent":["import { ValuesCountEntry } from \"./types\";\n\n/**\n * Parse a reference string value which can be in the format:\n * - Simple: \"path/entityId\"\n * - With database: \"database_name:::path/entityId\"\n * Returns the path and database (undefined if not specified or if \"(default)\")\n */\nexport function parseReferenceString(value: string): { path: string; database?: string } | null {\n if (!value) return null;\n\n let database: string | undefined = undefined;\n let fullPath = value;\n\n // Parse the new format: database_name:::path/entityId\n if (value.includes(\":::\")) {\n const [dbName, pathPart] = value.split(\":::\");\n if (dbName && dbName !== \"(default)\") {\n database = dbName;\n }\n fullPath = pathPart;\n }\n\n // Check if it looks like a path (contains at least one slash)\n if (!fullPath || !fullPath.includes(\"/\")) {\n return null;\n }\n\n // Extract the collection path (everything before the last slash)\n const path = fullPath.substring(0, fullPath.lastIndexOf(\"/\"));\n\n return { path,\ndatabase };\n}\n\n/**\n * Check if a string value looks like a reference\n */\nexport function looksLikeReference(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n return parseReferenceString(value) !== null;\n}\n\nexport function findCommonInitialStringInPath(valuesCount?: ValuesCountEntry) {\n\n if (!valuesCount) return undefined;\n\n function getPath(value: unknown): string | undefined {\n let pathString: string | undefined;\n\n if (typeof value === \"string\") {\n pathString = value;\n } else if (value && typeof value === \"object\" && \"slug\" in value && typeof (value as Record<string, unknown>).slug === \"string\") {\n pathString = (value as Record<string, unknown>).slug as string;\n } else {\n console.warn(\"findCommonInitialStringInPath: value is not a string or document with path\", value);\n return undefined;\n }\n\n if (!pathString) return undefined;\n\n // Parse the new format: database_name:::path/entityId\n // Extract just the path portion for comparison\n if (pathString.includes(\":::\")) {\n const [, pathPart] = pathString.split(\":::\");\n pathString = pathPart;\n }\n\n return pathString;\n }\n\n const strings: string[] = valuesCount.values.map((v) => getPath(v)).filter(v => !!v) as string[];\n const pathWithSlash = strings.find((s) => s.includes(\"/\"));\n if (!pathWithSlash)\n return undefined;\n\n const searchedPath = pathWithSlash.substring(0, pathWithSlash.lastIndexOf(\"/\"));\n\n const yep = valuesCount.values\n .filter((value) => {\n const path = getPath(value);\n if (!path) return false;\n return path.startsWith(searchedPath)\n }).length > valuesCount.values.length / 3 * 2;\n\n return yep ? searchedPath : undefined;\n\n}\n\nexport function removeInitialAndTrailingSlashes(s: string): string {\n return removeInitialSlash(removeTrailingSlash(s));\n}\n\nexport function removeInitialSlash(s: string) {\n if (s.startsWith(\"/\"))\n return s.slice(1);\n else return s;\n}\n\nexport function removeTrailingSlash(s: string) {\n if (s.endsWith(\"/\"))\n return s.slice(0, -1);\n else return s;\n}\n","import { EnumValueConfig, EnumValues } from \"@rebasepro/types\";\nimport { unslugify } from \"@rebasepro/utils\";\n\n// Canonical utility functions — single source of truth in @rebasepro/utils\nexport { unslugify, prettifyIdentifier, isObject, isPlainObject, mergeDeep } from \"@rebasepro/utils\";\n\n/**\n * Extract enum values from a list of sample values.\n * This is inference-specific logic (not a general utility).\n */\nexport function extractEnumFromValues(values: unknown[]) {\n if (!Array.isArray(values)) {\n return [];\n }\n // A number or a boolean in a text column is one of its options too (`10`\n // and `12` beside `S`, `M`, `L`): left out, the enum refused the rows it\n // was inferred from. Its id is its text, which is what the import writes.\n const seen = new Set<string>();\n const enumValues: Array<{ id: string, label: string }> = [];\n for (const value of values) {\n if (typeof value !== \"string\" && typeof value !== \"number\" && typeof value !== \"boolean\") continue;\n const id = String(value);\n if (seen.has(id)) continue;\n seen.add(id);\n enumValues.push({ id, label: typeof value === \"string\" ? unslugify(value) : id });\n }\n enumValues.sort((a, b) => a.label.localeCompare(b.label));\n return enumValues;\n}\n\nexport function resolveEnumValues(input: EnumValues): EnumValueConfig[] | undefined {\n // Check Array.isArray first since typeof [] === \"object\" is true in JavaScript\n if (Array.isArray(input)) {\n return input as EnumValueConfig[];\n } else if (typeof input === \"object\" && input !== null) {\n return Object.entries(input).map(([id, value]) =>\n (typeof value === \"string\"\n ? {\n id,\n label: value\n }\n : value));\n } else {\n return undefined;\n }\n}\n","import { findCommonInitialStringInPath } from \"../strings\";\nimport { extractEnumFromValues } from \"../util\";\nimport { FileType, Property, StringProperty } from \"@rebasepro/types\";\nimport { InferencePropertyBuilderProps, ValuesCountEntry } from \"../types\";\n\nconst IMAGE_EXTENSIONS = [\".jpg\", \".jpeg\", \".png\", \".webp\", \".gif\", \".avif\"];\nconst AUDIO_EXTENSIONS = [\".mp3\", \".ogg\", \".opus\", \".aac\"];\nconst VIDEO_EXTENSIONS = [\".avi\", \".mp4\"];\n\nconst 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])?)*$/;\n\n// The shape the server checks an `email` property against\n// (`write-validation.ts`): the regex above alone accepts `a@localhost`.\nconst EMAIL_SHAPE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nfunction isAbsoluteUrl(value: string): boolean {\n try {\n return Boolean(new URL(value).protocol);\n } catch {\n return false;\n }\n}\n\nexport function buildStringProperty({\n name,\n totalDocsCount,\n valuesResult\n}: InferencePropertyBuilderProps): Property {\n\n let stringProperty: Property = {\n name: name ?? \"\",\n type: \"string\"\n\n };\n\n if (valuesResult) {\n\n const totalEntriesCount = valuesResult.values.length;\n const totalValues = Array.from(valuesResult.valuesCount.keys()).length;\n\n const config: Partial<StringProperty> = {};\n\n // `url` and `email` are constraints a write is checked against, so\n // they hold only when every value the column has meets them: set at\n // two thirds, the import refused the rest of the rows it was inferred\n // from. A blank is no value.\n const nonBlank = valuesResult.values.filter((value) => value !== \"\" && value !== null && value !== undefined);\n const everyValue = (test: (value: string) => boolean) =>\n nonBlank.length > 0 && nonBlank.every((value) => typeof value === \"string\" && test(value));\n\n const probablyAURL = everyValue((value) => value.startsWith(\"http\") && isAbsoluteUrl(value));\n if (probablyAURL) {\n config.url = true;\n }\n\n const probablyAnEmail = everyValue((value) => emailRegEx.test(value) && EMAIL_SHAPE.test(value));\n if (probablyAnEmail) {\n config.email = true;\n }\n\n const probablyUserIds = valuesResult.values\n .filter((value) => typeof value === \"string\" && value.length === 28 && !value.includes(\" \"))\n .length > totalDocsCount / 3 * 2;\n // No `admin.readOnly` here. This package is core — the field does not exist in\n // `@rebasepro/types`, because `@rebasepro/cms-types` declares it — and a\n // guess about whether a form should be editable is presentation, not schema.\n // (It also would not have enforced anything: nothing on the server reads\n // `readOnly`.) The inferred `isId` below is the part that carries meaning.\n\n if (!probablyAnEmail &&\n !probablyAURL &&\n !probablyUserIds &&\n !probablyAURL &&\n totalValues < totalEntriesCount / 3\n ) {\n const enumValues = extractEnumFromValues(Array.from(valuesResult.valuesCount.keys()));\n\n if (Object.keys(enumValues).length > 1)\n config.enum = enumValues;\n }\n\n // regular string\n if (!probablyAnEmail &&\n !probablyAURL &&\n !probablyUserIds &&\n !probablyAURL &&\n !config.enum) {\n const fileType = probableFileType(valuesResult, totalDocsCount);\n if (fileType) {\n config.storage = {\n acceptedFiles: fileType as FileType[],\n storagePath: findCommonInitialStringInPath(valuesResult) ?? \"/\"\n };\n }\n }\n\n if (Object.keys(config).length > 0)\n stringProperty = {\n ...stringProperty,\n ...config\n } as StringProperty;\n }\n\n return stringProperty;\n}\n\nfunction probableFileType(valuesCount: ValuesCountEntry, totalDocsCount: number): false | FileType[] {\n const isImage = (value: string) => IMAGE_EXTENSIONS.some((extension) => value.toString().endsWith(extension));\n const isAudio = (value: string) => AUDIO_EXTENSIONS.some((extension) => value.toString().endsWith(extension));\n const isVideo = (value: string) => VIDEO_EXTENSIONS.some((extension) => value.toString().endsWith(extension));\n\n const stringValues = valuesCount.values.filter((v): v is string => typeof v === \"string\");\n\n let imageCount = 0;\n let audioCount = 0;\n let videoCount = 0;\n\n for (const value of stringValues) {\n if (isImage(value)) imageCount++;\n else if (isAudio(value)) audioCount++;\n else if (isVideo(value)) videoCount++;\n }\n\n const totalMediaCount = imageCount + audioCount + videoCount;\n if (totalMediaCount > (totalDocsCount * 2) / 3) {\n const fileTypes: FileType[] = [];\n if (imageCount > 0) fileTypes.push(\"image/*\");\n if (audioCount > 0) fileTypes.push(\"audio/*\");\n if (videoCount > 0) fileTypes.push(\"video/*\");\n return fileTypes.length > 0 ? fileTypes : false;\n }\n\n return false;\n}\n","import { PropertyValidationSchema } from \"@rebasepro/types\";\nimport { InferencePropertyBuilderProps } from \"../types\";\n\nexport function buildValidation({\n totalDocsCount,\n valuesResult\n}: InferencePropertyBuilderProps): PropertyValidationSchema | undefined {\n\n if (valuesResult) {\n const totalEntriesCount = valuesResult.values.length;\n if (totalDocsCount === totalEntriesCount)\n return {\n required: true\n }\n }\n\n return undefined;\n}\n","import { findCommonInitialStringInPath } from \"../strings\";\nimport { InferencePropertyBuilderProps } from \"../types\";\nimport { Property } from \"@rebasepro/types\";\n\nexport function buildReferenceProperty({\n name,\n totalDocsCount,\n valuesResult\n}: InferencePropertyBuilderProps): Property {\n\n const property: Property = {\n name: name ?? \"\",\n type: \"reference\",\n path: findCommonInitialStringInPath(valuesResult) ?? \"!!!FIX_ME!!!\"\n };\n\n return property;\n}\n","/**\n * A double holds 15 significant decimal digits exactly; a 16th may already be\n * rounded (`9007199254740993` reads back as …992).\n */\nconst MAX_EXACT_SIGNIFICANT_DIGITS = 15;\n\n/**\n * JSON's number grammar: an optional minus, an integer part with no leading\n * zero, an optional fraction and an optional exponent. No `+`, no thousands\n * separator, no currency or percent sign, no hexadecimal, no `Infinity`.\n */\nconst NUMBER_SPELLING = /^-?(0|[1-9]\\d*)(\\.(\\d+))?([eE][+-]?\\d+)?$/;\n\n/**\n * The number a text spells, when it spells one that a number gives back with\n * nothing lost — otherwise `undefined`.\n *\n * This is the one answer to \"is this text a number\" for both the inference\n * that picks a column's type and the import that converts its cells, so the\n * two cannot disagree. It refuses what `Number()` would forgive and silently\n * change: a leading zero (`02134` is a zip code, `00123` a code — as numbers\n * they lose the zero), more significant digits than a double holds exactly (a\n * 20-digit SKU), and anything that is not a plain decimal (`1,234`, `$1.00`,\n * `12%`, `+15551234567`, `N/A`). Formatting a number does not carry —\n * `10.00` and `1e3` — is allowed: their value is exact.\n */\nexport function readNumberExactly(text: string): number | undefined {\n const trimmed = text.trim();\n const match = NUMBER_SPELLING.exec(trimmed);\n if (!match) return undefined;\n const integerPart = match[1];\n const fraction = (match[3] ?? \"\").replace(/0+$/, \"\");\n const significant = (integerPart + fraction).replace(/^0+/, \"\");\n if (significant.length > MAX_EXACT_SIGNIFICANT_DIGITS) return undefined;\n const value = Number(trimmed);\n // `1e400` overflows to Infinity and `1e-400` underflows to 0.\n if (!Number.isFinite(value) || (value === 0 && significant !== \"\")) return undefined;\n return value;\n}\n","import {\n InferencePropertyBuilderProps,\n TypesCount,\n TypesCountRecord,\n ValuesCountEntry,\n ValuesCountRecord\n} from \"./types\";\nimport { buildStringProperty } from \"./builders/string_property_builder\";\nimport { buildValidation } from \"./builders/validation_builder\";\nimport { buildReferenceProperty } from \"./builders/reference_property_builder\";\nimport { extractEnumFromValues, mergeDeep, prettifyIdentifier, resolveEnumValues } from \"./util\";\nimport { readNumberExactly } from \"./numbers\";\nimport { DataType, EnumValues, Properties, Property, StringProperty, Vector } from \"@rebasepro/types\";\n\nexport type InferenceTypeBuilder = (value: unknown) => DataType;\n\n/**\n * The entry a count record holds for a column, if it holds one.\n *\n * The records are keyed by column names out of the data, and a plain object\n * answers `record[\"constructor\"]` or `record[\"toString\"]` with what it\n * inherits: the count was written onto the global `Object`, and the values\n * list was `Object.values`, so importing a file with a `constructor` column\n * threw. Only an own entry is one this record counted.\n */\nfunction ownEntry<T>(record: Record<string, T>, key: string): T | undefined {\n return Object.hasOwn(record, key) ? record[key] : undefined;\n}\n\nexport type BuildEntityPropertiesOptions = {\n /**\n * Top-level keys to leave out of the inferred properties — Firestore\n * metadata such as `__name__`, say. Nothing is left out by default: a\n * column is data, `_id` in a Mongo export included.\n */\n ignoreKey?: (key: string) => boolean;\n};\n\nexport async function buildEntityPropertiesFromData(\n data: object[],\n getType: InferenceTypeBuilder,\n options: BuildEntityPropertiesOptions = {}\n): Promise<Properties> {\n const typesCount: TypesCountRecord = {};\n const valuesCount: ValuesCountRecord = {};\n if (data) {\n data.forEach((entry) => {\n if (entry) {\n Object.entries(entry).forEach(([key, value]) => {\n if (options.ignoreKey?.(key)) return;\n increaseMapTypeCount(typesCount, key, value, getType);\n increaseValuesCount(valuesCount, key, value, getType);\n });\n }\n });\n }\n return buildPropertiesFromCount(data.length, typesCount, valuesCount);\n}\n\nexport function buildPropertyFromData(\n data: unknown[],\n property: Property,\n getType: InferenceTypeBuilder\n): Property {\n const typesCount = {};\n const valuesCount: ValuesCountRecord = {};\n if (data) {\n data.forEach((entry) => {\n increaseTypeCount(property.type, typesCount, entry, getType);\n increaseValuesCount(valuesCount, \"inferred_prop\", entry, getType);\n });\n }\n const enumValues = \"enum\" in property ? resolveEnumValues(property[\"enum\"] as EnumValues) : undefined;\n if (enumValues) {\n const newEnumValues = extractEnumFromValues(Array.from(valuesCount[\"inferred_prop\"].valuesCount.keys()));\n return {\n ...property,\n enum: [...newEnumValues, ...enumValues]\n } as StringProperty;\n }\n const generatedProperty = buildPropertyFromCount(\n \"inferred_prop\",\n data.length,\n property.type,\n typesCount,\n valuesCount[\"inferred_prop\"]\n );\n return mergeDeep(generatedProperty, property);\n}\n\nexport function buildPropertiesOrder(\n properties: Properties,\n propertiesOrder?: string[],\n priorityKeys?: string[]\n): string[] {\n const lowerCasePriorityKeys = (priorityKeys ?? []).map((key) => key.toLowerCase());\n\n function propOrder(s: string) {\n const k = s.toLowerCase();\n if (lowerCasePriorityKeys.includes(k)) return 4;\n if (k === \"title\" || k === \"name\") return 3;\n if (k.includes(\"title\") || k.includes(\"name\")) return 2;\n if (k.includes(\"image\") || k.includes(\"picture\")) return 1;\n return 0;\n }\n\n function deriveOrder(keys: string[]): string[] {\n return [...keys]\n .sort() // alphabetically\n .sort((a, b) => propOrder(b) - propOrder(a));\n }\n\n // A caller-supplied order is an answer, not an input. It used to be handed\n // to the same alphabetical + priority sort as a derived order, which threw\n // it away entirely — and because `Array.prototype.sort` sorts in place, the\n // caller's own array was silently reordered as a side effect.\n if (propertiesOrder) {\n const placed = new Set(propertiesOrder);\n // Properties the caller did not place — a newly inferred column, say —\n // still have to appear, so they follow in derived order.\n const rest = Object.keys(properties).filter(key => !placed.has(key));\n return [...propertiesOrder, ...deriveOrder(rest)];\n }\n\n return deriveOrder(Object.keys(properties));\n}\n\n/**\n * @param type\n * @param typesCount\n * @param fieldValue\n * @param getType\n */\nfunction increaseTypeCount(\n type: DataType,\n typesCount: TypesCount,\n fieldValue: unknown,\n getType: InferenceTypeBuilder\n) {\n if (type === \"map\") {\n if (fieldValue) {\n let mapTypesCount = typesCount[type];\n if (!mapTypesCount) {\n mapTypesCount = {};\n typesCount[type] = mapTypesCount;\n }\n Object.entries(fieldValue as Record<string, unknown>).forEach(([key, value]) => {\n increaseMapTypeCount(mapTypesCount as TypesCountRecord, key, value, getType);\n });\n }\n } else if (type === \"array\") {\n let arrayTypesCount = typesCount[type];\n if (!arrayTypesCount) {\n arrayTypesCount = {};\n typesCount[type] = arrayTypesCount;\n }\n if (fieldValue && Array.isArray(fieldValue) && fieldValue.length > 0) {\n const arrayType = getMostProbableTypeInArray(fieldValue, getType);\n if (arrayType === \"map\") {\n let mapTypesCount = arrayTypesCount[arrayType];\n if (!mapTypesCount) {\n mapTypesCount = {};\n }\n fieldValue.forEach((value) => {\n if (value && typeof value === \"object\" && !Array.isArray(value)) { // Ensure value is an object for Object.entries\n Object.entries(value).forEach(([key, v]) =>\n increaseMapTypeCount(mapTypesCount, key, v, getType)\n );\n }\n });\n arrayTypesCount[arrayType] = mapTypesCount;\n } else {\n if (!arrayTypesCount[arrayType]) arrayTypesCount[arrayType] = 1;\n else arrayTypesCount[arrayType] = Number(arrayTypesCount[arrayType]) + 1;\n }\n }\n } else {\n if (!typesCount[type]) typesCount[type] = 1;\n else typesCount[type] = Number(typesCount[type]) + 1;\n }\n}\n\n/**\n * A cell nobody filled in. A CSV reader hands over `\"\"` for it where a\n * spreadsheet reader hands over nothing, and the import writes neither — so it\n * is no value here either: it casts no vote for the column's type and does not\n * count towards `required` or an enum.\n */\nfunction isBlank(value: unknown): boolean {\n return value === null || value === undefined || value === \"\";\n}\n\nfunction increaseMapTypeCount(\n typesCountRecord: TypesCountRecord,\n key: string,\n fieldValue: unknown,\n getType: InferenceTypeBuilder\n) {\n let typesCount: TypesCount | undefined = ownEntry(typesCountRecord, key);\n if (!typesCount) {\n typesCount = {};\n typesCountRecord[key] = typesCount;\n }\n\n if (!isBlank(fieldValue)) {\n const type = getType(fieldValue);\n increaseTypeCount(type, typesCount, fieldValue, getType);\n }\n}\n\nfunction increaseValuesCount(\n typeValuesRecord: ValuesCountRecord,\n key: string,\n fieldValue: unknown,\n getType: InferenceTypeBuilder\n) {\n const type = getType(fieldValue);\n\n const valuesRecord: ValuesCountEntry = ownEntry(typeValuesRecord, key) ?? {\n values: [],\n valuesCount: new Map()\n };\n typeValuesRecord[key] = valuesRecord;\n\n if (type === \"map\") {\n // Under `mapValues`, the field `buildPropertyFromCount` reads. Stored\n // under `map`, a nested field's values were never seen: no enum, no\n // `required`, and no number-from-text decision for it.\n let mapValuesRecord: ValuesCountRecord | undefined = valuesRecord.mapValues;\n if (!mapValuesRecord) {\n mapValuesRecord = {};\n valuesRecord.mapValues = mapValuesRecord;\n }\n if (fieldValue)\n Object.entries(fieldValue as Record<string, unknown>).forEach(([subKey, value]) =>\n increaseValuesCount(mapValuesRecord as ValuesCountRecord, subKey, value, getType)\n );\n } else if (type === \"array\") {\n if (Array.isArray(fieldValue)) {\n fieldValue.forEach((value) => {\n if (isBlank(value)) return;\n valuesRecord.values.push(value);\n valuesRecord.valuesCount.set(value, (valuesRecord.valuesCount.get(value) ?? 0) + 1);\n });\n }\n } else {\n if (!isBlank(fieldValue)) {\n valuesRecord.values.push(fieldValue);\n valuesRecord.valuesCount.set(fieldValue, (valuesRecord.valuesCount.get(fieldValue) ?? 0) + 1);\n }\n }\n}\n\nfunction getHighestTypesCount(typesCount: TypesCount): number {\n let highestCount = 0;\n Object.entries(typesCount).forEach(([type, count]) => {\n let countValue = 0;\n if (type === \"map\") {\n countValue = getHighestRecordCount(count as TypesCountRecord);\n } else if (type === \"array\") {\n countValue = getHighestTypesCount(count as TypesCount);\n } else {\n countValue = Number(count);\n }\n if (countValue > highestCount) {\n highestCount = countValue;\n }\n });\n\n return highestCount;\n}\n\nfunction getHighestRecordCount(record: TypesCountRecord): number {\n return Object.entries(record)\n .map(([key, typesCount]) => getHighestTypesCount(typesCount))\n .reduce((a, b) => Math.max(a, b), 0);\n}\n\n/**\n * The types a cell can be converted between: the import turns any of them into\n * text without loss, and text back into one of them only when the text spells\n * it.\n */\nconst SCALAR_TYPES: ReadonlySet<string> = new Set([\"string\", \"number\", \"boolean\", \"date\"]);\n\n/**\n * A column's type, from the votes its values cast.\n *\n * When the votes mix scalar types — numbers beside text, booleans beside\n * numbers — the majority is not an answer: converting the minority to it\n * changes them (`02134` → 2134, a 20-digit SKU rounded, `N/A` → nothing,\n * 2 → `false`). Such a column is text, which holds every one of them as\n * written. The one exception is numbers beside text that spells numbers\n * exactly (`10.00`, `1e3`): that column is a number, and nothing is lost.\n *\n * @param values every non-blank value the votes were cast for, when known;\n * without them, a mix of numbers and text is text.\n */\nfunction getMostProbableType(typesCount: TypesCount, values?: unknown[]): DataType {\n const scalarTypes = Object.entries(typesCount)\n .filter(([type, count]) => SCALAR_TYPES.has(type) && Number(count) > 0)\n .map(([type]) => type);\n if (scalarTypes.length > 1) {\n const numbersBesideNumericText = scalarTypes.every(type => type === \"number\" || type === \"string\")\n && values !== undefined\n && values.every(value => typeof value !== \"string\" || readNumberExactly(value) !== undefined);\n return numbersBesideNumericText ? \"number\" : \"string\";\n }\n let highestCount = -1;\n let probableType: DataType = \"string\"; // default\n Object.entries(typesCount).forEach(([type, count]) => {\n let countValue;\n if (type === \"map\") {\n countValue = getHighestRecordCount(count as TypesCountRecord);\n } else if (type === \"array\") {\n countValue = getHighestTypesCount(count as TypesCount);\n } else {\n countValue = Number(count);\n }\n if (countValue > highestCount) {\n highestCount = countValue;\n probableType = type as DataType;\n }\n });\n return probableType;\n}\n\nfunction buildPropertyFromCount(\n key: string,\n totalDocsCount: number,\n mostProbableType: DataType,\n typesCount: TypesCount,\n valuesResult?: ValuesCountEntry\n): Property {\n let title: string | undefined;\n\n if (key) {\n title = prettifyIdentifier(key);\n }\n\n let result: Property | undefined = undefined;\n if (mostProbableType === \"map\") {\n const highVariability = checkTypesCountHighVariability(typesCount);\n if (highVariability) {\n result = {\n type: \"map\",\n name: title ?? key ?? \"\",\n keyValue: true,\n properties: {}\n };\n }\n const properties = buildPropertiesFromCount(\n totalDocsCount,\n typesCount.map as TypesCountRecord,\n valuesResult ? valuesResult.mapValues : undefined\n );\n result = {\n type: \"map\",\n name: title ?? key ?? \"\",\n properties\n };\n } else if (mostProbableType === \"array\") {\n const arrayTypesCount = typesCount.array as TypesCount;\n // The values recorded for an array column are its items.\n const arrayMostProbableType = getMostProbableType(arrayTypesCount, valuesResult?.values);\n const of = buildPropertyFromCount(\n key,\n totalDocsCount,\n arrayMostProbableType,\n arrayTypesCount,\n valuesResult\n );\n result = {\n type: \"array\",\n name: title ?? key ?? \"\",\n of\n };\n }\n\n if (!result) {\n const propertyProps: InferencePropertyBuilderProps = {\n name: key,\n totalDocsCount,\n valuesResult\n };\n if ((mostProbableType === \"number\" || mostProbableType === \"string\") && isEpochColumn(key, valuesResult?.values)) {\n result = { type: \"date\" } as Property;\n } else if (mostProbableType === \"string\") {\n result = buildStringProperty(propertyProps);\n } else if (mostProbableType === \"reference\") {\n result = buildReferenceProperty(propertyProps);\n } else {\n result = {\n type: mostProbableType\n } as Property;\n }\n\n if (title) {\n result.name = title;\n }\n\n const validation = buildValidation(propertyProps);\n if (validation) {\n result.validation = validation;\n }\n }\n\n return result;\n}\n\n/** The words that end a column name holding a moment: `created_at`, `updatedOn`, `birth_date`, `timestamp`. */\nconst MOMENT_NAME_ENDINGS = new Set([\"at\", \"on\", \"date\", \"time\", \"datetime\", \"timestamp\", \"ts\"]);\n\n/** An epoch in seconds from 2001-09-09 to 2103, or in milliseconds over the same span. */\nfunction isEpochNumber(value: number): boolean {\n return Number.isInteger(value)\n && ((value >= 1e9 && value < 4.2e9) || (value >= 1e12 && value < 4.2e12));\n}\n\n/**\n * Whether a column holds epoch timestamps: its name ends in a word that says\n * it holds a moment, and every value it has is a whole number of seconds or\n * milliseconds since 1970 (written as a number, or as the digits a CSV\n * writes) in the range a timestamp of this century falls in. Both, because\n * either alone is an order number or a `response_time` in milliseconds. The\n * import reads such a value into a date as soon as the column is one.\n */\nfunction isEpochColumn(key: string, values: unknown[] | undefined): boolean {\n const words = key.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);\n if (words.length === 0 || !MOMENT_NAME_ENDINGS.has(words[words.length - 1])) return false;\n const present = (values ?? []).filter(value => !isBlank(value));\n return present.length > 0 && present.every(value =>\n typeof value === \"number\"\n ? isEpochNumber(value)\n : typeof value === \"string\" && /^\\d+$/.test(value.trim()) && isEpochNumber(Number(value.trim())));\n}\n\nfunction buildPropertiesFromCount(\n totalDocsCount: number,\n typesCountRecord: TypesCountRecord,\n valuesCountRecord?: ValuesCountRecord\n): Properties {\n const res: Properties = {};\n Object.entries(typesCountRecord).forEach(([key, typesCount]) => {\n const valuesEntry = valuesCountRecord ? ownEntry(valuesCountRecord, key) : undefined;\n const mostProbableType = getMostProbableType(typesCount, valuesEntry?.values);\n res[key] = buildPropertyFromCount(\n key,\n totalDocsCount,\n mostProbableType,\n typesCount,\n valuesEntry\n );\n });\n return res;\n}\n\nfunction countMaxDocumentsUnder(typesCount: TypesCount) {\n let count = 0;\n Object.entries(typesCount).forEach(([type, value]) => {\n if (typeof value === \"object\") {\n count = Math.max(count, countMaxDocumentsUnder(value as TypesCountRecord));\n } else {\n count = Math.max(count, Number(value));\n }\n });\n return count;\n}\n\nfunction getMostProbableTypeInArray(\n array: unknown[],\n getType: InferenceTypeBuilder\n): DataType {\n const typesCount: TypesCount = {};\n array.forEach((value) => {\n // A hole in the array says nothing about what the array holds, so it\n // gets no vote. This used to fall out of `getType` answering \"map\" for\n // null and `increaseTypeCount` then skipping falsy map values; now that\n // null infers as \"string\" the skip has to be explicit, or every array\n // with a gap in it would drift towards \"string\".\n if (isBlank(value)) return;\n increaseTypeCount(getType(value), typesCount, value, getType);\n });\n return getMostProbableType(typesCount, array.filter(value => !isBlank(value)));\n}\n\nfunction checkTypesCountHighVariability(typesCount: TypesCount) {\n const maxCount = countMaxDocumentsUnder(typesCount);\n let keysWithFewValues = 0;\n Object.entries(typesCount.map ?? {}).forEach(([key, value]) => {\n const count = countMaxDocumentsUnder(value);\n if (count < maxCount / 3) {\n keysWithFewValues++;\n }\n });\n return keysWithFewValues / Object.entries(typesCount.map ?? {}).length > 0.5;\n}\n\n\nexport function inferTypeFromValue(value: unknown): DataType {\n if (value === null || value === undefined) return \"string\";\n if (value instanceof Vector || (value && typeof value === \"object\" && \"__type\" in value && (value as Record<string, unknown>).__type === \"Vector\")) return \"vector\";\n if (typeof value === \"string\") return \"string\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"boolean\") return \"boolean\";\n // A Date is an object, so it read as a map — while the CMS's own detector\n // (`getInferenceType`) calls it a date. The two defaults agree now.\n if (value instanceof Date) return \"date\";\n if (Array.isArray(value)) return \"array\";\n if (typeof value === \"object\") return \"map\";\n return \"string\";\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The number a text spells, when it spells one that a number gives back with
|
|
3
|
+
* nothing lost — otherwise `undefined`.
|
|
4
|
+
*
|
|
5
|
+
* This is the one answer to "is this text a number" for both the inference
|
|
6
|
+
* that picks a column's type and the import that converts its cells, so the
|
|
7
|
+
* two cannot disagree. It refuses what `Number()` would forgive and silently
|
|
8
|
+
* change: a leading zero (`02134` is a zip code, `00123` a code — as numbers
|
|
9
|
+
* they lose the zero), more significant digits than a double holds exactly (a
|
|
10
|
+
* 20-digit SKU), and anything that is not a plain decimal (`1,234`, `$1.00`,
|
|
11
|
+
* `12%`, `+15551234567`, `N/A`). Formatting a number does not carry —
|
|
12
|
+
* `10.00` and `1e3` — is allowed: their value is exact.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readNumberExactly(text: string): number | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/inference",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1-canary.g19596ef",
|
|
4
4
|
"description": "Infer a Rebase collection schema from existing data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rebase",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"module": "./dist/index.es.js",
|
|
30
30
|
"types": "dist/index.d.ts",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@rebasepro/types": "0.
|
|
33
|
-
"@rebasepro/utils": "0.
|
|
32
|
+
"@rebasepro/types": "0.24.1-canary.g19596ef",
|
|
33
|
+
"@rebasepro/utils": "0.24.1-canary.g19596ef"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/jest": "^30.0.0",
|