@ganju/utils 0.0.21 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data.d.ts +4 -4
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +16 -2
- package/dist/dataApi.d.ts +32 -2
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +11 -1
- package/dist/dataCompiler.d.ts +1 -0
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +3 -2
- package/dist/dataEngine.d.ts +17 -1
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +368 -10
- package/dist/dataImport.d.ts +3 -0
- package/dist/dataImport.d.ts.map +1 -1
- package/dist/dataImport.js +12 -2
- package/dist/dataRpc.d.ts +9 -1
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataSheet.d.ts +34 -0
- package/dist/dataSheet.d.ts.map +1 -0
- package/dist/dataSheet.js +100 -0
- package/dist/dataTriggers.d.ts +43 -4
- package/dist/dataTriggers.d.ts.map +1 -1
- package/dist/dataTriggers.js +401 -19
- package/dist/dataTypes.d.ts +23 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/dataTypes.js +15 -1
- package/dist/index.d.ts +35 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/localizeDataError.d.ts.map +1 -1
- package/dist/localizeDataError.js +45 -1
- package/dist/schema.d.ts +33 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +26 -3
- package/dist/untrustedContent.d.ts +7 -0
- package/dist/untrustedContent.d.ts.map +1 -1
- package/dist/untrustedContent.js +13 -1
- package/package.json +1 -1
- package/src/data.ts +19 -3
- package/src/dataApi.ts +14 -2
- package/src/dataCompiler.ts +4 -1
- package/src/dataEngine.ts +485 -10
- package/src/dataImport.ts +16 -1
- package/src/dataRpc.ts +10 -1
- package/src/dataSheet.ts +147 -0
- package/src/dataTriggers.ts +528 -19
- package/src/dataTypes.ts +46 -0
- package/src/index.ts +4 -0
- package/src/localizeDataError.ts +82 -1
- package/src/schema.ts +27 -3
- package/src/untrustedContent.ts +15 -0
package/src/dataImport.ts
CHANGED
|
@@ -482,6 +482,9 @@ export interface ImportedRow {
|
|
|
482
482
|
errors: string[];
|
|
483
483
|
// Values read with a guess; the row is still imported.
|
|
484
484
|
warnings: NumberGuess[];
|
|
485
|
+
// The unique columns whose value an earlier line already has (each also
|
|
486
|
+
// one of `errors`): a sheet sync lets the database say so instead.
|
|
487
|
+
repeated: string[];
|
|
485
488
|
}
|
|
486
489
|
|
|
487
490
|
// Every row of the file as the values it will insert, with each problem
|
|
@@ -512,6 +515,7 @@ export const importRows = (
|
|
|
512
515
|
const values: Record<string, unknown> = {};
|
|
513
516
|
const errors = missing.map(c => `${c.name} is required.`);
|
|
514
517
|
const warnings: NumberGuess[] = [];
|
|
518
|
+
const repeated: string[] = [];
|
|
515
519
|
mapping.forEach((name, i) => {
|
|
516
520
|
if (name === null) return;
|
|
517
521
|
const column = columns.get(name);
|
|
@@ -534,13 +538,14 @@ export const importRows = (
|
|
|
534
538
|
const first = lines.get(key);
|
|
535
539
|
if (first === undefined) lines.set(key, line);
|
|
536
540
|
else {
|
|
541
|
+
repeated.push(column.name);
|
|
537
542
|
errors.push(
|
|
538
543
|
`${column.name}: "${text(row[i])}" is already on line ${first}, and this column takes each value once.`
|
|
539
544
|
);
|
|
540
545
|
}
|
|
541
546
|
}
|
|
542
547
|
});
|
|
543
|
-
return { index, line, values, errors, warnings };
|
|
548
|
+
return { index, line, values, errors, warnings, repeated };
|
|
544
549
|
});
|
|
545
550
|
};
|
|
546
551
|
|
|
@@ -555,3 +560,13 @@ export const csvLine = (cells: ImportCell[]) =>
|
|
|
555
560
|
|
|
556
561
|
export { detectSeparator, parseCsv } from './dataCsv';
|
|
557
562
|
export type { ParsedCsv } from './dataCsv';
|
|
563
|
+
export {
|
|
564
|
+
DATA_CSV_MIME,
|
|
565
|
+
DATA_SPREADSHEET_MIME,
|
|
566
|
+
cellFromSheet,
|
|
567
|
+
isTabularMimeType,
|
|
568
|
+
readCsvText,
|
|
569
|
+
readSheet,
|
|
570
|
+
readTabularFile
|
|
571
|
+
} from './dataSheet';
|
|
572
|
+
export type { SheetJs, TabularFile } from './dataSheet';
|
package/src/dataRpc.ts
CHANGED
|
@@ -10,8 +10,10 @@ import type {
|
|
|
10
10
|
DataResult,
|
|
11
11
|
DataSchema,
|
|
12
12
|
DataTable,
|
|
13
|
-
DataTableCopy
|
|
13
|
+
DataTableCopy,
|
|
14
|
+
DataTableSync
|
|
14
15
|
} from './dataTypes';
|
|
16
|
+
import type { SheetSyncArgs } from './dataEngine';
|
|
15
17
|
import type { DataQuery } from './dataQueries';
|
|
16
18
|
import type { DataTrigger, DataTriggerWithSecret, SavedTrigger } from './dataTriggers';
|
|
17
19
|
|
|
@@ -130,6 +132,13 @@ export interface ProjectDatabaseRpc {
|
|
|
130
132
|
caller: DataCaller,
|
|
131
133
|
args: { changes: DataFileLinkChange[] }
|
|
132
134
|
): R<DataFileLinksReplaced>;
|
|
135
|
+
// A synced table's rows made what its sheet says, all or nothing; and why
|
|
136
|
+
// a sync that never reached the rows was refused.
|
|
137
|
+
syncRows(caller: DataCaller, args: SheetSyncArgs): R<DataTableSync>;
|
|
138
|
+
recordSyncFailure(
|
|
139
|
+
caller: DataCaller,
|
|
140
|
+
args: { table: string; error: string; title?: string | null; version?: string | null }
|
|
141
|
+
): R<DataTableSync | null>;
|
|
133
142
|
history(
|
|
134
143
|
caller: DataCaller,
|
|
135
144
|
args: { table: string; id: number; limit?: number; cursor?: number }
|
package/src/dataSheet.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Reading a spreadsheet or CSV from the project's knowledge into header and
|
|
2
|
+
// rows, for data-admin's import and a table's sheet sync alike. SheetJS is
|
|
3
|
+
// passed in, so each worker loads it only when a file needs it.
|
|
4
|
+
import { parseCsv } from './dataCsv';
|
|
5
|
+
import { DATA_SHEET_MAX_BYTES, DataError } from './dataTypes';
|
|
6
|
+
import type { ImportCell } from './dataImport';
|
|
7
|
+
|
|
8
|
+
export const DATA_SPREADSHEET_MIME =
|
|
9
|
+
/spreadsheetml|ms-excel|opendocument\.spreadsheet/i;
|
|
10
|
+
export const DATA_CSV_MIME = /csv|text\/plain|tab-separated/i;
|
|
11
|
+
|
|
12
|
+
// Whether a knowledge item can be a table's source.
|
|
13
|
+
export const isTabularMimeType = (mimeType: string | null | undefined) =>
|
|
14
|
+
!!mimeType &&
|
|
15
|
+
(DATA_SPREADSHEET_MIME.test(mimeType) || DATA_CSV_MIME.test(mimeType));
|
|
16
|
+
|
|
17
|
+
// The parts of SheetJS used here, so this package doesn't depend on it.
|
|
18
|
+
export interface SheetJs {
|
|
19
|
+
read(
|
|
20
|
+
data: ArrayBuffer,
|
|
21
|
+
options: Record<string, unknown>
|
|
22
|
+
): {
|
|
23
|
+
SheetNames: string[];
|
|
24
|
+
Sheets: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
utils: {
|
|
27
|
+
sheet_to_json<T>(sheet: unknown, options: Record<string, unknown>): T[];
|
|
28
|
+
decode_range(ref: string): { s: { r: number } };
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TabularFile {
|
|
33
|
+
headers: string[];
|
|
34
|
+
rows: ImportCell[][];
|
|
35
|
+
// Each row's line in the file or sheet, counting the header.
|
|
36
|
+
lines: number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
40
|
+
|
|
41
|
+
// A spreadsheet's own date as the text the database reads, as the
|
|
42
|
+
// dashboard's import does: a date alone when it has no time, else a
|
|
43
|
+
// wall-clock time.
|
|
44
|
+
export const cellFromSheet = (cell: unknown): ImportCell => {
|
|
45
|
+
if (cell instanceof Date) {
|
|
46
|
+
const date = `${cell.getFullYear()}-${pad(cell.getMonth() + 1)}-${pad(cell.getDate())}`;
|
|
47
|
+
if (!cell.getHours() && !cell.getMinutes() && !cell.getSeconds()) {
|
|
48
|
+
return date;
|
|
49
|
+
}
|
|
50
|
+
return `${date}T${pad(cell.getHours())}:${pad(cell.getMinutes())}:${pad(cell.getSeconds())}`;
|
|
51
|
+
}
|
|
52
|
+
return typeof cell === 'string' ||
|
|
53
|
+
typeof cell === 'number' ||
|
|
54
|
+
typeof cell === 'boolean'
|
|
55
|
+
? cell
|
|
56
|
+
: null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// One sheet of an Excel or OpenDocument file: the first unless named.
|
|
60
|
+
// Blank lines are left out.
|
|
61
|
+
export const readSheet = (
|
|
62
|
+
XLSX: SheetJs,
|
|
63
|
+
bytes: ArrayBuffer,
|
|
64
|
+
uri: string,
|
|
65
|
+
sheetName?: string | null
|
|
66
|
+
): TabularFile => {
|
|
67
|
+
// Only the sheet being read is parsed: a workbook unpacks to many times
|
|
68
|
+
// its size, and a worker has 128 MiB.
|
|
69
|
+
const book = XLSX.read(bytes, {
|
|
70
|
+
cellDates: true,
|
|
71
|
+
dense: true,
|
|
72
|
+
sheets: sheetName ?? 0
|
|
73
|
+
});
|
|
74
|
+
const name = sheetName ?? book.SheetNames[0];
|
|
75
|
+
const sheet = name ? book.Sheets[name] : undefined;
|
|
76
|
+
if (!sheet) {
|
|
77
|
+
throw new DataError(
|
|
78
|
+
`${uri} has no sheet "${sheetName}". Sheets: ${book.SheetNames.join(', ')}.`,
|
|
79
|
+
'not_found'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const all = XLSX.utils.sheet_to_json<unknown[]>(sheet, {
|
|
83
|
+
header: 1,
|
|
84
|
+
raw: true,
|
|
85
|
+
defval: null,
|
|
86
|
+
blankrows: true
|
|
87
|
+
});
|
|
88
|
+
// The sheet's range may start below row 1.
|
|
89
|
+
const ref = (sheet as { '!ref'?: string })['!ref'];
|
|
90
|
+
const firstLine = ref ? XLSX.utils.decode_range(ref).s.r + 1 : 1;
|
|
91
|
+
const [header = [], ...body] = all;
|
|
92
|
+
const rows: ImportCell[][] = [];
|
|
93
|
+
const lines: number[] = [];
|
|
94
|
+
body.forEach((row, i) => {
|
|
95
|
+
const cells = row.map(cellFromSheet);
|
|
96
|
+
if (cells.every(c => c === null || c === '')) return;
|
|
97
|
+
rows.push(cells);
|
|
98
|
+
lines.push(firstLine + 1 + i);
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
headers: header.map(h => (h === null ? '' : String(h))),
|
|
102
|
+
rows,
|
|
103
|
+
lines
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// A CSV's text, with each row's line as a sheet's would be.
|
|
108
|
+
export const readCsvText = (text: string): TabularFile => {
|
|
109
|
+
const parsed = parseCsv(text);
|
|
110
|
+
return {
|
|
111
|
+
headers: parsed.headers,
|
|
112
|
+
rows: parsed.rows,
|
|
113
|
+
lines: parsed.lines
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// A stored file, read by its type: a spreadsheet through SheetJS (loaded
|
|
118
|
+
// only then), anything else as CSV text. Over DATA_SHEET_MAX_BYTES it isn't
|
|
119
|
+
// read at all.
|
|
120
|
+
export const readTabularFile = async (file: {
|
|
121
|
+
bytes: ArrayBuffer;
|
|
122
|
+
mimeType: string | null;
|
|
123
|
+
uri: string;
|
|
124
|
+
sheet?: string | null;
|
|
125
|
+
loadSheetJs: () => Promise<SheetJs>;
|
|
126
|
+
}): Promise<TabularFile> => {
|
|
127
|
+
if (file.bytes.byteLength > DATA_SHEET_MAX_BYTES) {
|
|
128
|
+
throw new DataError(
|
|
129
|
+
`${file.uri} is over ${DATA_SHEET_MAX_BYTES / (1024 * 1024)} MB.`,
|
|
130
|
+
'too_large'
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (file.mimeType && DATA_SPREADSHEET_MIME.test(file.mimeType)) {
|
|
134
|
+
return readSheet(
|
|
135
|
+
await file.loadSheetJs(),
|
|
136
|
+
file.bytes,
|
|
137
|
+
file.uri,
|
|
138
|
+
file.sheet
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (!file.mimeType || DATA_CSV_MIME.test(file.mimeType)) {
|
|
142
|
+
return readCsvText(new TextDecoder().decode(file.bytes));
|
|
143
|
+
}
|
|
144
|
+
throw new DataError(
|
|
145
|
+
`${file.uri} is ${file.mimeType}; a table reads CSV and Excel files.`
|
|
146
|
+
);
|
|
147
|
+
};
|