@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.
Files changed (51) hide show
  1. package/dist/data.d.ts +4 -4
  2. package/dist/data.d.ts.map +1 -1
  3. package/dist/data.js +16 -2
  4. package/dist/dataApi.d.ts +32 -2
  5. package/dist/dataApi.d.ts.map +1 -1
  6. package/dist/dataApi.js +11 -1
  7. package/dist/dataCompiler.d.ts +1 -0
  8. package/dist/dataCompiler.d.ts.map +1 -1
  9. package/dist/dataCompiler.js +3 -2
  10. package/dist/dataEngine.d.ts +17 -1
  11. package/dist/dataEngine.d.ts.map +1 -1
  12. package/dist/dataEngine.js +368 -10
  13. package/dist/dataImport.d.ts +3 -0
  14. package/dist/dataImport.d.ts.map +1 -1
  15. package/dist/dataImport.js +12 -2
  16. package/dist/dataRpc.d.ts +9 -1
  17. package/dist/dataRpc.d.ts.map +1 -1
  18. package/dist/dataSheet.d.ts +34 -0
  19. package/dist/dataSheet.d.ts.map +1 -0
  20. package/dist/dataSheet.js +100 -0
  21. package/dist/dataTriggers.d.ts +43 -4
  22. package/dist/dataTriggers.d.ts.map +1 -1
  23. package/dist/dataTriggers.js +401 -19
  24. package/dist/dataTypes.d.ts +23 -0
  25. package/dist/dataTypes.d.ts.map +1 -1
  26. package/dist/dataTypes.js +15 -1
  27. package/dist/index.d.ts +35 -4
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2 -0
  30. package/dist/localizeDataError.d.ts.map +1 -1
  31. package/dist/localizeDataError.js +45 -1
  32. package/dist/schema.d.ts +33 -4
  33. package/dist/schema.d.ts.map +1 -1
  34. package/dist/schema.js +26 -3
  35. package/dist/untrustedContent.d.ts +7 -0
  36. package/dist/untrustedContent.d.ts.map +1 -1
  37. package/dist/untrustedContent.js +13 -1
  38. package/package.json +1 -1
  39. package/src/data.ts +19 -3
  40. package/src/dataApi.ts +14 -2
  41. package/src/dataCompiler.ts +4 -1
  42. package/src/dataEngine.ts +485 -10
  43. package/src/dataImport.ts +16 -1
  44. package/src/dataRpc.ts +10 -1
  45. package/src/dataSheet.ts +147 -0
  46. package/src/dataTriggers.ts +528 -19
  47. package/src/dataTypes.ts +46 -0
  48. package/src/index.ts +4 -0
  49. package/src/localizeDataError.ts +82 -1
  50. package/src/schema.ts +27 -3
  51. 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 }
@@ -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
+ };