@ganju/utils 0.0.15 → 0.0.17

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.
Files changed (77) hide show
  1. package/dist/constants.d.ts +26 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +107 -9
  4. package/dist/customCodeToken.d.ts +11 -0
  5. package/dist/customCodeToken.d.ts.map +1 -1
  6. package/dist/customCodeToken.js +41 -1
  7. package/dist/data.d.ts +13 -0
  8. package/dist/data.d.ts.map +1 -0
  9. package/dist/data.js +103 -0
  10. package/dist/dataApi.d.ts +578 -0
  11. package/dist/dataApi.d.ts.map +1 -0
  12. package/dist/dataApi.js +399 -0
  13. package/dist/dataCompiler.d.ts +68 -0
  14. package/dist/dataCompiler.d.ts.map +1 -0
  15. package/dist/dataCompiler.js +730 -0
  16. package/dist/dataCsv.d.ts +20 -0
  17. package/dist/dataCsv.d.ts.map +1 -0
  18. package/dist/dataCsv.js +102 -0
  19. package/dist/dataDdl.d.ts +29 -0
  20. package/dist/dataDdl.d.ts.map +1 -0
  21. package/dist/dataDdl.js +354 -0
  22. package/dist/dataEngine.d.ts +143 -0
  23. package/dist/dataEngine.d.ts.map +1 -0
  24. package/dist/dataEngine.js +2042 -0
  25. package/dist/dataIdentifiers.d.ts +8 -0
  26. package/dist/dataIdentifiers.d.ts.map +1 -0
  27. package/dist/dataIdentifiers.js +55 -0
  28. package/dist/dataImport.d.ts +79 -0
  29. package/dist/dataImport.d.ts.map +1 -0
  30. package/dist/dataImport.js +453 -0
  31. package/dist/dataQueries.d.ts +73 -0
  32. package/dist/dataQueries.d.ts.map +1 -0
  33. package/dist/dataQueries.js +513 -0
  34. package/dist/dataRpc.d.ts +150 -0
  35. package/dist/dataRpc.d.ts.map +1 -0
  36. package/dist/dataRpc.js +2 -0
  37. package/dist/dataTools.d.ts +40 -0
  38. package/dist/dataTools.d.ts.map +1 -0
  39. package/dist/dataTools.js +280 -0
  40. package/dist/dataTypes.d.ts +159 -0
  41. package/dist/dataTypes.d.ts.map +1 -0
  42. package/dist/dataTypes.js +160 -0
  43. package/dist/index.d.ts +394 -8
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -0
  46. package/dist/oauth.d.ts +14 -0
  47. package/dist/oauth.d.ts.map +1 -1
  48. package/dist/oauth.js +42 -2
  49. package/dist/resourceRerank.d.ts +3 -2
  50. package/dist/resourceRerank.d.ts.map +1 -1
  51. package/dist/resourceRerank.js +5 -2
  52. package/dist/schema.d.ts +376 -4
  53. package/dist/schema.d.ts.map +1 -1
  54. package/dist/schema.js +268 -1
  55. package/dist/sdkConstants.d.ts +1 -0
  56. package/dist/sdkConstants.d.ts.map +1 -1
  57. package/dist/sdkConstants.js +2 -1
  58. package/package.json +17 -2
  59. package/src/constants.ts +121 -9
  60. package/src/customCodeToken.ts +74 -0
  61. package/src/data.ts +108 -0
  62. package/src/dataApi.ts +479 -0
  63. package/src/dataCompiler.ts +952 -0
  64. package/src/dataCsv.ts +99 -0
  65. package/src/dataDdl.ts +419 -0
  66. package/src/dataEngine.ts +2773 -0
  67. package/src/dataIdentifiers.ts +63 -0
  68. package/src/dataImport.ts +557 -0
  69. package/src/dataQueries.ts +730 -0
  70. package/src/dataRpc.ts +132 -0
  71. package/src/dataTools.ts +338 -0
  72. package/src/dataTypes.ts +318 -0
  73. package/src/index.ts +13 -1
  74. package/src/oauth.ts +42 -1
  75. package/src/resourceRerank.ts +5 -2
  76. package/src/schema.ts +315 -1
  77. package/src/sdkConstants.ts +1 -0
@@ -0,0 +1,8 @@
1
+ export declare const DATA_IDENTIFIER_PATTERN: RegExp;
2
+ export declare const DATA_SYSTEM_COLUMNS: readonly ["id", "created_at", "updated_at"];
3
+ export declare const DATA_HIDDEN_COLUMNS: readonly ["version", "created_by", "updated_by", "participant_id"];
4
+ export type IdentifierKind = 'table' | 'column';
5
+ export declare const identifierIssue: (name: unknown, kind: IdentifierKind) => string | null;
6
+ export declare const isValidIdentifier: (name: unknown, kind: IdentifierKind) => boolean;
7
+ export declare const quoteIdentifier: (name: string) => string;
8
+ //# sourceMappingURL=dataIdentifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataIdentifiers.d.ts","sourceRoot":"","sources":["../src/dataIdentifiers.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,uBAAuB,QAA2B,CAAC;AAGhE,eAAO,MAAM,mBAAmB,6CAA8C,CAAC;AAE/E,eAAO,MAAM,mBAAmB,oEAKtB,CAAC;AAKX,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAC;AAGhD,eAAO,MAAM,eAAe,GAC1B,MAAM,OAAO,EACb,MAAM,cAAc,KACnB,MAAM,GAAG,IA6BX,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,OAAO,EAAE,MAAM,cAAc,YAC/B,CAAC;AAIvC,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAAqB,CAAC"}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ // Table and column names an owner may use. Every name reaches SQL quoted, so
3
+ // SQL keywords are fine ("order" is a natural column); what is refused is
4
+ // what would collide with SQLite's own names or ours: a table name is also the
5
+ // prefix of its search table, and `__` is kept for those.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.quoteIdentifier = exports.isValidIdentifier = exports.identifierIssue = exports.DATA_HIDDEN_COLUMNS = exports.DATA_SYSTEM_COLUMNS = exports.DATA_IDENTIFIER_PATTERN = void 0;
8
+ exports.DATA_IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
9
+ // Columns every table has, set by the database, never by a caller.
10
+ exports.DATA_SYSTEM_COLUMNS = ['id', 'created_at', 'updated_at'];
11
+ // Kept on every row and never shown to the assistant.
12
+ exports.DATA_HIDDEN_COLUMNS = [
13
+ 'version',
14
+ 'created_by',
15
+ 'updated_by',
16
+ 'participant_id'
17
+ ];
18
+ // Names SQLite gives a meaning of its own.
19
+ const SQLITE_ALIASES = new Set(['rowid', 'oid', '_rowid_']);
20
+ // Why `name` can't be used, or null when it can.
21
+ const identifierIssue = (name, kind) => {
22
+ if (typeof name !== 'string' || name.length === 0) {
23
+ return `A ${kind} needs a name.`;
24
+ }
25
+ if (!exports.DATA_IDENTIFIER_PATTERN.test(name)) {
26
+ return `"${name}" isn't a valid ${kind} name: use lowercase letters, digits and _, starting with a letter, up to 63 characters.`;
27
+ }
28
+ if (name.includes('__')) {
29
+ return `"${name}": two underscores in a row are reserved.`;
30
+ }
31
+ if (name.startsWith('sqlite_') || name.startsWith('_ganju_')) {
32
+ return `"${name}" is a reserved name.`;
33
+ }
34
+ // Tables share one namespace with indexes and triggers, which we name
35
+ // ix_<table>_<column>, ux_… and tr_….
36
+ if (kind === 'table' && /^(ix|ux|tr)_\d/.test(name)) {
37
+ return `"${name}" looks like one of the database's own index names; pick another table name.`;
38
+ }
39
+ if (SQLITE_ALIASES.has(name)) {
40
+ return `"${name}" is reserved by SQLite; pick another ${kind} name (e.g. "${name}_ref").`;
41
+ }
42
+ if (kind === 'column' &&
43
+ (exports.DATA_SYSTEM_COLUMNS.includes(name) ||
44
+ exports.DATA_HIDDEN_COLUMNS.includes(name))) {
45
+ return `"${name}" is a column every table already has.`;
46
+ }
47
+ return null;
48
+ };
49
+ exports.identifierIssue = identifierIssue;
50
+ const isValidIdentifier = (name, kind) => (0, exports.identifierIssue)(name, kind) === null;
51
+ exports.isValidIdentifier = isValidIdentifier;
52
+ // Only ever called with a name that passed identifierIssue, or with one of
53
+ // our own (`_ganju_*`, system columns); the quotes keep keywords inert.
54
+ const quoteIdentifier = (name) => `"${name}"`;
55
+ exports.quoteIdentifier = quoteIdentifier;
@@ -0,0 +1,79 @@
1
+ import { type DataColumn, type DataColumnInput, type DataColumnType, type DataTable } from './dataTypes';
2
+ export { identifierIssue, isValidIdentifier } from './dataIdentifiers';
3
+ export { toTableInput } from './dataTypes';
4
+ export type ImportCell = string | number | boolean | null | undefined;
5
+ export type ImportRow = ImportCell[];
6
+ export declare const DATA_IMPORT_SAMPLE_ROWS = 200;
7
+ export declare const DATA_IMPORT_BATCH = 500;
8
+ export type DecimalMark = '.' | ',';
9
+ /**
10
+ * The decimal mark a file's numbers use, from the values that can only be
11
+ * read one way ("12,5", "1.234.567", "1,234.50"). Undefined when none of
12
+ * them say, or they disagree evenly: each "28.000" is then flagged.
13
+ */
14
+ export declare const detectDecimalMark: (rows: ImportRow[]) => DecimalMark | undefined;
15
+ export interface ParsedNumber {
16
+ value: number;
17
+ ambiguous: boolean;
18
+ }
19
+ /**
20
+ * A number as a sheet writes it, with its thousands separators:
21
+ * "28.000" → 28000 and "1.234,50" → 1234.5 with a "," mark. A value is
22
+ * thousands-separated when the same separator repeats, when both appear (the
23
+ * last is the decimal), or when it has three digits after its only one and
24
+ * the file's mark is the other character. Null when it isn't a number.
25
+ */
26
+ export declare const parseNumber: (value: string, mark?: DecimalMark) => ParsedNumber | null;
27
+ export declare const normaliseHeader: (header: string) => string;
28
+ export declare const columnNameFromHeader: (header: string, index: number) => string;
29
+ export interface GuessOptions {
30
+ decimalMark?: DecimalMark;
31
+ header?: string;
32
+ }
33
+ export declare const guessColumnType: (values: ImportCell[], options?: GuessOptions) => {
34
+ type: DataColumnType;
35
+ values?: string[];
36
+ };
37
+ export declare const guessColumns: (headers: string[], rows: ImportRow[], options?: {
38
+ decimalMark?: DecimalMark;
39
+ }) => DataColumnInput[];
40
+ export declare const matchHeaders: (headers: string[], table: Pick<DataTable, "columns">) => (string | null)[];
41
+ export type RelationLookup = Map<string, number>;
42
+ export declare const relationKey: (value: ImportCell) => string;
43
+ export interface NumberGuess {
44
+ column: string;
45
+ text: string;
46
+ read: number;
47
+ alternative: number;
48
+ }
49
+ export type CellResult = {
50
+ ok: true;
51
+ value: unknown;
52
+ warning?: NumberGuess;
53
+ } | {
54
+ ok: false;
55
+ error: string;
56
+ };
57
+ export interface CellOptions {
58
+ relation?: RelationLookup;
59
+ timeZone?: string;
60
+ decimalMark?: DecimalMark;
61
+ }
62
+ export declare const importCell: (table: Pick<DataTable, "name">, column: DataColumn, cell: ImportCell, options?: CellOptions) => CellResult;
63
+ export interface ImportedRow {
64
+ index: number;
65
+ line: number;
66
+ values: Record<string, unknown>;
67
+ errors: string[];
68
+ warnings: NumberGuess[];
69
+ }
70
+ export declare const importRows: (table: Pick<DataTable, "name" | "columns">, mapping: (string | null)[], rows: ImportRow[], options?: {
71
+ relations?: Record<string, RelationLookup>;
72
+ timeZone?: string;
73
+ decimalMark?: DecimalMark;
74
+ lines?: number[];
75
+ }) => ImportedRow[];
76
+ export declare const csvLine: (cells: ImportCell[]) => string;
77
+ export { detectSeparator, parseCsv } from './dataCsv';
78
+ export type { ParsedCsv } from './dataCsv';
79
+ //# sourceMappingURL=dataImport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataImport.d.ts","sourceRoot":"","sources":["../src/dataImport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,453 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCsv = exports.detectSeparator = exports.csvLine = exports.importRows = exports.importCell = exports.relationKey = exports.matchHeaders = exports.guessColumns = exports.guessColumnType = exports.columnNameFromHeader = exports.normaliseHeader = exports.parseNumber = exports.detectDecimalMark = exports.DATA_IMPORT_BATCH = exports.DATA_IMPORT_SAMPLE_ROWS = exports.toTableInput = exports.isValidIdentifier = exports.identifierIssue = void 0;
4
+ // Reading a spreadsheet into a table: the column types guessed for a new
5
+ // table, the headers matched to an existing one's columns, and each cell
6
+ // checked the way the database will check it. The dashboard's import and
7
+ // `ganju data import` both use these, so a file lands the same way from
8
+ // either. Entry point `@ganju/utils/dataImport`.
9
+ const dataCompiler_1 = require("./dataCompiler");
10
+ const dataIdentifiers_1 = require("./dataIdentifiers");
11
+ const dataTypes_1 = require("./dataTypes");
12
+ var dataIdentifiers_2 = require("./dataIdentifiers");
13
+ Object.defineProperty(exports, "identifierIssue", { enumerable: true, get: function () { return dataIdentifiers_2.identifierIssue; } });
14
+ Object.defineProperty(exports, "isValidIdentifier", { enumerable: true, get: function () { return dataIdentifiers_2.isValidIdentifier; } });
15
+ // The CLI reads tables through this entry point too, and writes them back
16
+ // out the way an export's schema.json does.
17
+ var dataTypes_2 = require("./dataTypes");
18
+ Object.defineProperty(exports, "toTableInput", { enumerable: true, get: function () { return dataTypes_2.toTableInput; } });
19
+ // How many rows the type guess reads.
20
+ exports.DATA_IMPORT_SAMPLE_ROWS = 200;
21
+ // Rows sent per request, the API's own limit.
22
+ exports.DATA_IMPORT_BATCH = 500;
23
+ // Up to this many distinct values, each seen at least twice on average, a
24
+ // text column reads as a list of options.
25
+ const ENUM_MAX_DISTINCT = 12;
26
+ const LONG_TEXT_LENGTH = 200;
27
+ const TRUE_WORDS = new Set(['true', 'yes', 'y', 'si', 'sí', 'verdadero', '1']);
28
+ const FALSE_WORDS = new Set(['false', 'no', 'n', 'falso', '0']);
29
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
30
+ const DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
31
+ const EMAIL = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
32
+ const URL = /^https?:\/\/\S+$/i;
33
+ // Digits with the usual separators, long enough not to be a quantity.
34
+ const PHONE = /^\+?[\d\s().-]{7,25}$/;
35
+ // A header that names a phone column: its numbers are phones even when the
36
+ // sheet stored them as plain numbers.
37
+ const PHONE_HEADER = /phone|tel|telefono|celular|movil|mobile|whatsapp/;
38
+ // Digits with "." or "," between them, nothing else.
39
+ const NUMBER_TEXT = /^-?\d+([.,]\d+)*$/;
40
+ // A code, not a quantity: "05001" loses its zero as a number.
41
+ const LEADING_ZERO = /^-?0\d/;
42
+ const isBlank = (cell) => cell === null || cell === undefined || String(cell).trim() === '';
43
+ const text = (cell) => String(cell ?? '').trim();
44
+ const otherMark = (mark) => (mark === '.' ? ',' : '.');
45
+ // What one value says about the file's decimal mark on its own, or null when
46
+ // it could be either: "28.000" is 28 thousand or 28.
47
+ const markOf = (value) => {
48
+ if (!NUMBER_TEXT.test(value))
49
+ return null;
50
+ const dot = value.lastIndexOf('.');
51
+ const comma = value.lastIndexOf(',');
52
+ if (dot >= 0 && comma >= 0)
53
+ return dot > comma ? '.' : ',';
54
+ const mark = dot >= 0 ? '.' : ',';
55
+ const parts = value.replace(/^-/, '').split(mark);
56
+ if (parts.length === 1)
57
+ return null;
58
+ // Used twice, it can only group thousands.
59
+ if (parts.length > 2)
60
+ return otherMark(mark);
61
+ const [whole, fraction] = parts;
62
+ if (fraction.length !== 3 || whole.length > 3 || /^0+$/.test(whole))
63
+ return mark;
64
+ return null;
65
+ };
66
+ /**
67
+ * The decimal mark a file's numbers use, from the values that can only be
68
+ * read one way ("12,5", "1.234.567", "1,234.50"). Undefined when none of
69
+ * them say, or they disagree evenly: each "28.000" is then flagged.
70
+ */
71
+ const detectDecimalMark = (rows) => {
72
+ let dot = 0;
73
+ let comma = 0;
74
+ for (const row of rows) {
75
+ for (const cell of row) {
76
+ if (typeof cell !== 'string')
77
+ continue;
78
+ const mark = markOf(cell.trim());
79
+ if (mark === '.')
80
+ dot++;
81
+ else if (mark === ',')
82
+ comma++;
83
+ }
84
+ }
85
+ if (dot === comma)
86
+ return undefined;
87
+ return dot > comma ? '.' : ',';
88
+ };
89
+ exports.detectDecimalMark = detectDecimalMark;
90
+ /**
91
+ * A number as a sheet writes it, with its thousands separators:
92
+ * "28.000" → 28000 and "1.234,50" → 1234.5 with a "," mark. A value is
93
+ * thousands-separated when the same separator repeats, when both appear (the
94
+ * last is the decimal), or when it has three digits after its only one and
95
+ * the file's mark is the other character. Null when it isn't a number.
96
+ */
97
+ const parseNumber = (value, mark) => {
98
+ const raw = value.trim();
99
+ if (!NUMBER_TEXT.test(raw))
100
+ return null;
101
+ const negative = raw.startsWith('-');
102
+ const digits = negative ? raw.slice(1) : raw;
103
+ const dot = digits.lastIndexOf('.');
104
+ const comma = digits.lastIndexOf(',');
105
+ let decimal;
106
+ let ambiguous = false;
107
+ if (dot < 0 && comma < 0) {
108
+ decimal = null;
109
+ }
110
+ else if (dot >= 0 && comma >= 0) {
111
+ decimal = dot > comma ? '.' : ',';
112
+ }
113
+ else {
114
+ const sep = dot >= 0 ? '.' : ',';
115
+ const own = markOf(digits);
116
+ if (own !== null) {
117
+ decimal = own === sep ? sep : null;
118
+ }
119
+ else if (mark) {
120
+ decimal = mark === sep ? sep : null;
121
+ }
122
+ else {
123
+ decimal = null;
124
+ ambiguous = true;
125
+ }
126
+ }
127
+ const [whole, fraction = '', ...rest] = decimal === null ? [digits] : digits.split(decimal);
128
+ if (rest.length)
129
+ return null;
130
+ const groups = whole.split(/[.,]/);
131
+ // Thousands come in threes after a first group of one to three digits.
132
+ if (groups.length > 1 &&
133
+ (groups[0].length > 3 ||
134
+ groups.slice(1).some(g => g.length !== 3) ||
135
+ new Set(whole.replace(/\d/g, '')).size > 1)) {
136
+ return null;
137
+ }
138
+ if (fraction && /[.,]/.test(fraction))
139
+ return null;
140
+ const n = Number(`${negative ? '-' : ''}${groups.join('')}${fraction ? `.${fraction}` : ''}`);
141
+ return Number.isFinite(n) ? { value: n, ambiguous } : null;
142
+ };
143
+ exports.parseNumber = parseNumber;
144
+ // Lowercase, no accents, nothing but letters and digits: how a header is
145
+ // compared with a column's name or label.
146
+ const normaliseHeader = (header) => header
147
+ .normalize('NFKD')
148
+ .replace(/[̀-ͯ]/g, '')
149
+ .toLowerCase()
150
+ .replace(/[^a-z0-9]/g, '');
151
+ exports.normaliseHeader = normaliseHeader;
152
+ // A header as a column name the database accepts: "Precio (COP)" →
153
+ // "precio_cop". A name every table already has gets a prefix, so a sheet's
154
+ // own "id" column is kept as "source_id".
155
+ const columnNameFromHeader = (header, index) => {
156
+ let name = header
157
+ .normalize('NFKD')
158
+ .replace(/[̀-ͯ]/g, '')
159
+ .toLowerCase()
160
+ .replace(/[^a-z0-9]+/g, '_')
161
+ .replace(/^_+|_+$/g, '')
162
+ .replace(/_{2,}/g, '_')
163
+ .slice(0, 63);
164
+ if (!name)
165
+ name = `column_${index + 1}`;
166
+ if (!/^[a-z]/.test(name))
167
+ name = `c_${name}`.slice(0, 63);
168
+ if (dataIdentifiers_1.DATA_SYSTEM_COLUMNS.includes(name) ||
169
+ dataIdentifiers_1.DATA_HIDDEN_COLUMNS.includes(name) ||
170
+ (0, dataIdentifiers_1.identifierIssue)(name, 'column') !== null) {
171
+ name = `source_${name}`.slice(0, 63);
172
+ }
173
+ return (0, dataIdentifiers_1.identifierIssue)(name, 'column') === null ? name : `column_${index + 1}`;
174
+ };
175
+ exports.columnNameFromHeader = columnNameFromHeader;
176
+ // How many values in a sample may disagree with a column's type and still
177
+ // have it guessed: a typo in one cell shouldn't turn a price column into
178
+ // text. Those rows are flagged in the preview instead.
179
+ const tolerated = (count) => count < 3 ? 0 : Math.max(1, Math.floor(count * 0.1));
180
+ // The type the values in one column read as, from the first sample rows.
181
+ const guessColumnType = (values, options = {}) => {
182
+ const cells = values
183
+ .slice(0, exports.DATA_IMPORT_SAMPLE_ROWS)
184
+ .filter(cell => !isBlank(cell));
185
+ if (cells.length === 0)
186
+ return { type: 'text' };
187
+ const count = (test) => cells.filter(test).length;
188
+ const mostly = (matches) => matches > 0 && cells.length - matches <= tolerated(cells.length);
189
+ if (mostly(count(cell => typeof cell === 'boolean')))
190
+ return { type: 'boolean' };
191
+ if (options.header &&
192
+ PHONE_HEADER.test((0, exports.normaliseHeader)(options.header)) &&
193
+ mostly(count(cell => {
194
+ const value = text(cell);
195
+ return PHONE.test(value) && value.replace(/\D/g, '').length >= 7;
196
+ }))) {
197
+ return { type: 'phone' };
198
+ }
199
+ // Codes with a leading zero stay text, so "05001" keeps its zero.
200
+ if (count(cell => typeof cell === 'string' && LEADING_ZERO.test(cell.trim()))) {
201
+ return { type: 'text' };
202
+ }
203
+ const numberOf = (cell) => typeof cell === 'number'
204
+ ? cell
205
+ : (0, exports.parseNumber)(text(cell), options.decimalMark)?.value;
206
+ const integers = count(cell => {
207
+ const n = numberOf(cell);
208
+ return n !== undefined && Number.isSafeInteger(n);
209
+ });
210
+ const decimals = count(cell => {
211
+ const n = numberOf(cell);
212
+ return n !== undefined && Number.isFinite(n);
213
+ });
214
+ // 0/1 alone is still a number column: a quantity of 1 is common. A
215
+ // column with any fractions is decimal, so none of them is refused.
216
+ if (decimals > integers && mostly(decimals))
217
+ return { type: 'decimal' };
218
+ if (mostly(integers))
219
+ return { type: 'integer' };
220
+ if (mostly(count(cell => {
221
+ const word = text(cell).toLowerCase();
222
+ return TRUE_WORDS.has(word) || FALSE_WORDS.has(word);
223
+ }))) {
224
+ return { type: 'boolean' };
225
+ }
226
+ if (mostly(count(cell => DATE.test(text(cell)))))
227
+ return { type: 'date' };
228
+ if (mostly(count(cell => DATETIME.test(text(cell)))))
229
+ return { type: 'datetime' };
230
+ if (mostly(count(cell => EMAIL.test(text(cell)))))
231
+ return { type: 'email' };
232
+ if (mostly(count(cell => URL.test(text(cell)))))
233
+ return { type: 'url' };
234
+ if (mostly(count(cell => PHONE.test(text(cell)) && text(cell).replace(/\D/g, '').length >= 7))) {
235
+ return { type: 'phone' };
236
+ }
237
+ if (cells.some(cell => text(cell).length > LONG_TEXT_LENGTH)) {
238
+ return { type: 'long_text' };
239
+ }
240
+ const distinct = new Map();
241
+ for (const cell of cells) {
242
+ const value = text(cell);
243
+ if (value.length > 100) {
244
+ distinct.clear();
245
+ break;
246
+ }
247
+ const key = value.toLowerCase();
248
+ if (!distinct.has(key))
249
+ distinct.set(key, value);
250
+ }
251
+ if (distinct.size > 0 &&
252
+ distinct.size <= ENUM_MAX_DISTINCT &&
253
+ distinct.size <= dataTypes_1.DATA_MAX_ENUM_VALUES &&
254
+ cells.length >= distinct.size * 2) {
255
+ return { type: 'enum', values: [...distinct.values()] };
256
+ }
257
+ return { type: 'text' };
258
+ };
259
+ exports.guessColumnType = guessColumnType;
260
+ // The columns of a new table made from a file: one per header, named from
261
+ // it, typed from its values.
262
+ const guessColumns = (headers, rows, options = {}) => {
263
+ const used = new Set();
264
+ return headers.slice(0, dataTypes_1.DATA_MAX_COLUMNS).map((header, index) => {
265
+ let name = (0, exports.columnNameFromHeader)(header, index);
266
+ for (let n = 2; used.has(name); n++) {
267
+ name = `${(0, exports.columnNameFromHeader)(header, index).slice(0, 60)}_${n}`;
268
+ }
269
+ used.add(name);
270
+ const guess = (0, exports.guessColumnType)(rows.map(row => row[index]), { decimalMark: options.decimalMark, header });
271
+ const label = header.trim();
272
+ return {
273
+ name,
274
+ type: guess.type,
275
+ ...(label && label !== name ? { label: label.slice(0, 100) } : {}),
276
+ ...(guess.values ? { values: guess.values } : {})
277
+ };
278
+ });
279
+ };
280
+ exports.guessColumns = guessColumns;
281
+ // Which column each header fills: matched by name or label, ignoring case,
282
+ // accents and punctuation. `null` for a header that matches nothing; the
283
+ // owner maps those by hand.
284
+ const matchHeaders = (headers, table) => {
285
+ const taken = new Set();
286
+ return headers.map(header => {
287
+ const key = (0, exports.normaliseHeader)(header);
288
+ if (!key)
289
+ return null;
290
+ const column = table.columns.find(c => !taken.has(c.name) &&
291
+ ((0, exports.normaliseHeader)(c.name) === key ||
292
+ (c.label !== null && (0, exports.normaliseHeader)(c.label) === key)));
293
+ if (!column)
294
+ return null;
295
+ taken.add(column.name);
296
+ return column.name;
297
+ });
298
+ };
299
+ exports.matchHeaders = matchHeaders;
300
+ const relationKey = (value) => text(value)
301
+ .normalize('NFKD')
302
+ .replace(/[\u0300-\u036f]/g, '')
303
+ .toLowerCase();
304
+ exports.relationKey = relationKey;
305
+ // One cell as the column will store it, or why it won't fit. The check is
306
+ // the database's own, so a row that passes here is refused only for what
307
+ // needs the stored rows (a duplicate in a unique column).
308
+ const importCell = (table, column, cell, options = {}) => {
309
+ if (isBlank(cell)) {
310
+ return column.required
311
+ ? { ok: false, error: `${column.name} is required.` }
312
+ : { ok: true, value: null };
313
+ }
314
+ let value = typeof cell === 'string' ? cell.trim() : cell;
315
+ let warning;
316
+ if ((column.type === 'integer' || column.type === 'decimal') &&
317
+ typeof value === 'string') {
318
+ const parsed = (0, exports.parseNumber)(value, options.decimalMark);
319
+ if (parsed) {
320
+ if (parsed.ambiguous) {
321
+ warning = {
322
+ column: column.name,
323
+ text: value,
324
+ read: parsed.value,
325
+ alternative: Number(value.replace(',', '.'))
326
+ };
327
+ }
328
+ value = parsed.value;
329
+ }
330
+ }
331
+ else if (column.type === 'relation') {
332
+ const asId = typeof value === 'number' ? value : Number(text(cell));
333
+ const found = options.relation?.get((0, exports.relationKey)(cell));
334
+ if (found !== undefined)
335
+ value = found;
336
+ else if (Number.isSafeInteger(asId) && asId > 0)
337
+ value = asId;
338
+ else {
339
+ return {
340
+ ok: false,
341
+ error: `${column.name}: "${text(cell)}" matches no row in ${column.table}.`
342
+ };
343
+ }
344
+ }
345
+ else if (column.type === 'boolean' && typeof value === 'string') {
346
+ const word = value.toLowerCase();
347
+ if (TRUE_WORDS.has(word))
348
+ value = true;
349
+ else if (FALSE_WORDS.has(word))
350
+ value = false;
351
+ }
352
+ else if (column.type === 'json' && typeof value === 'string') {
353
+ try {
354
+ value = JSON.parse(value);
355
+ }
356
+ catch {
357
+ return { ok: false, error: `${column.name} is not valid JSON.` };
358
+ }
359
+ }
360
+ else if (column.type === 'date' && typeof value === 'string') {
361
+ // A spreadsheet's date with a midnight time.
362
+ value = value.replace(/[T ]00:00(:00(\.0+)?)?Z?$/, '');
363
+ }
364
+ try {
365
+ const stored = (0, dataCompiler_1.toStored)(table, column, value, options.timeZone);
366
+ // The database refuses these too, but only once a whole batch is sent.
367
+ if (column.min !== null &&
368
+ column.min !== undefined &&
369
+ typeof stored === 'number' &&
370
+ stored < column.min) {
371
+ return {
372
+ ok: false,
373
+ error: `${table.name}.${column.name} can't go below ${column.min}.`
374
+ };
375
+ }
376
+ if (column.type === 'boolean')
377
+ return { ok: true, value: stored === 1 };
378
+ if (column.type === 'json')
379
+ return { ok: true, value };
380
+ return warning
381
+ ? { ok: true, value: stored, warning }
382
+ : { ok: true, value: stored };
383
+ }
384
+ catch (error) {
385
+ return {
386
+ ok: false,
387
+ error: error instanceof Error ? error.message : String(error)
388
+ };
389
+ }
390
+ };
391
+ exports.importCell = importCell;
392
+ // Every row of the file as the values it will insert, with each problem
393
+ // found. `mapping[i]` is the column header i fills, or null to skip it.
394
+ // `lines[i]` is row i's line in the file, when blank rows were left out
395
+ // before; otherwise rows follow the header one per line.
396
+ const importRows = (table, mapping, rows, options = {}) => {
397
+ const columns = new Map(table.columns.map(c => [c.name, c]));
398
+ const mapped = new Set(mapping.filter((m) => m !== null));
399
+ const missing = table.columns.filter(c => c.required && c.default === undefined && !mapped.has(c.name));
400
+ // A unique column's values seen so far, with the line they came from: the
401
+ // database refuses a file that repeats one, and can't say which row did.
402
+ const seen = new Map();
403
+ return rows.map((row, index) => {
404
+ const line = options.lines?.[index] ?? index + 2;
405
+ const values = {};
406
+ const errors = missing.map(c => `${c.name} is required.`);
407
+ const warnings = [];
408
+ mapping.forEach((name, i) => {
409
+ if (name === null)
410
+ return;
411
+ const column = columns.get(name);
412
+ if (!column)
413
+ return;
414
+ const result = (0, exports.importCell)(table, column, row[i], {
415
+ relation: options.relations?.[name],
416
+ timeZone: options.timeZone,
417
+ decimalMark: options.decimalMark
418
+ });
419
+ if (!result.ok) {
420
+ errors.push(result.error);
421
+ return;
422
+ }
423
+ if (result.value !== null)
424
+ values[name] = result.value;
425
+ if (result.warning)
426
+ warnings.push(result.warning);
427
+ if (column.unique && result.value !== null) {
428
+ const key = JSON.stringify(result.value);
429
+ const lines = seen.get(name) ?? new Map();
430
+ seen.set(name, lines);
431
+ const first = lines.get(key);
432
+ if (first === undefined)
433
+ lines.set(key, line);
434
+ else {
435
+ errors.push(`${column.name}: "${text(row[i])}" is already on line ${first}, and this column takes each value once.`);
436
+ }
437
+ }
438
+ });
439
+ return { index, line, values, errors, warnings };
440
+ });
441
+ };
442
+ exports.importRows = importRows;
443
+ // A CSV line, quoted where it needs to be: for the file of refused rows.
444
+ const csvLine = (cells) => cells
445
+ .map(cell => {
446
+ const value = cell === null || cell === undefined ? '' : String(cell);
447
+ return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
448
+ })
449
+ .join(',');
450
+ exports.csvLine = csvLine;
451
+ var dataCsv_1 = require("./dataCsv");
452
+ Object.defineProperty(exports, "detectSeparator", { enumerable: true, get: function () { return dataCsv_1.detectSeparator; } });
453
+ Object.defineProperty(exports, "parseCsv", { enumerable: true, get: function () { return dataCsv_1.parseCsv; } });