@ganju/utils 0.0.20 → 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/constants.d.ts +16 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +43 -0
- package/dist/data.d.ts +7 -5
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -3
- package/dist/dataApi.d.ts +82 -6
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +47 -4
- package/dist/dataAttachment.d.ts +8 -0
- package/dist/dataAttachment.d.ts.map +1 -0
- package/dist/dataAttachment.js +19 -0
- package/dist/dataCompiler.d.ts +4 -0
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +27 -2
- package/dist/dataDdl.d.ts.map +1 -1
- package/dist/dataDdl.js +5 -0
- package/dist/dataEngine.d.ts +25 -3
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +508 -14
- package/dist/dataImport.d.ts +3 -0
- package/dist/dataImport.d.ts.map +1 -1
- package/dist/dataImport.js +12 -2
- package/dist/dataIndexAdvice.d.ts +36 -0
- package/dist/dataIndexAdvice.d.ts.map +1 -0
- package/dist/dataIndexAdvice.js +89 -0
- package/dist/dataRpc.d.ts +20 -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/dataTools.js +3 -3
- package/dist/dataTriggers.d.ts +66 -4
- package/dist/dataTriggers.d.ts.map +1 -1
- package/dist/dataTriggers.js +446 -20
- package/dist/dataTypes.d.ts +69 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/dataTypes.js +21 -1
- package/dist/index.d.ts +91 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/localizeDataError.d.ts.map +1 -1
- package/dist/localizeDataError.js +109 -3
- package/dist/schema.d.ts +67 -6
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +57 -2
- package/dist/untrustedContent.d.ts +7 -0
- package/dist/untrustedContent.d.ts.map +1 -1
- package/dist/untrustedContent.js +13 -1
- package/dist/whatsappWebhook.d.ts +28 -0
- package/dist/whatsappWebhook.d.ts.map +1 -0
- package/dist/whatsappWebhook.js +30 -0
- package/package.json +1 -1
- package/src/constants.ts +46 -0
- package/src/data.ts +33 -3
- package/src/dataApi.ts +54 -4
- package/src/dataAttachment.ts +23 -0
- package/src/dataCompiler.ts +41 -1
- package/src/dataDdl.ts +7 -0
- package/src/dataEngine.ts +680 -15
- package/src/dataImport.ts +16 -1
- package/src/dataIndexAdvice.ts +127 -0
- package/src/dataRpc.ts +27 -2
- package/src/dataSheet.ts +147 -0
- package/src/dataTools.ts +3 -3
- package/src/dataTriggers.ts +599 -20
- package/src/dataTypes.ts +108 -0
- package/src/index.ts +15 -0
- package/src/localizeDataError.ts +183 -4
- package/src/schema.ts +60 -2
- package/src/untrustedContent.ts +15 -0
- package/src/whatsappWebhook.ts +55 -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';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Which columns of a database are worth an index, and which indexes nobody
|
|
2
|
+
// uses, from the API's daily query stats and the database's schema. Pure, so
|
|
3
|
+
// the daily job and the tests apply the same rule.
|
|
4
|
+
import { constants } from './constants';
|
|
5
|
+
import { isIndexedColumn } from './dataDdl';
|
|
6
|
+
import type { DataSchema } from './dataTypes';
|
|
7
|
+
|
|
8
|
+
// A column's (or, with column '', a table's) sums over a window.
|
|
9
|
+
export interface IndexAdviceStat {
|
|
10
|
+
tableName: string;
|
|
11
|
+
columnName: string;
|
|
12
|
+
queries: number;
|
|
13
|
+
rowsRead: number;
|
|
14
|
+
rowsReturned: number;
|
|
15
|
+
rowsWritten: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IndexAdviceCandidate {
|
|
19
|
+
tableName: string;
|
|
20
|
+
columnName: string;
|
|
21
|
+
kind: 'add' | 'unused';
|
|
22
|
+
queries: number;
|
|
23
|
+
avgRowsRead: number;
|
|
24
|
+
avgRowsReturned: number;
|
|
25
|
+
rowsWritten: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IndexAdviceInput {
|
|
29
|
+
schema: DataSchema;
|
|
30
|
+
// Sums over the last 7 days, and over the last 30.
|
|
31
|
+
week: IndexAdviceStat[];
|
|
32
|
+
month: IndexAdviceStat[];
|
|
33
|
+
// Whether stats go back the whole 30 days, so "no query used it" means it.
|
|
34
|
+
monthCovered: boolean;
|
|
35
|
+
// "table.column" made filterable, or "table" created, in the last 30 days.
|
|
36
|
+
recentlyIndexed: Set<string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Indexes the owner can add or remove: a column marked filterable. Unique and
|
|
40
|
+
// relation columns are indexed for what they are, and stay.
|
|
41
|
+
const canIndex = (type: string) => type !== 'json' && type !== 'long_text';
|
|
42
|
+
|
|
43
|
+
export const adviseIndexes = (
|
|
44
|
+
input: IndexAdviceInput
|
|
45
|
+
): IndexAdviceCandidate[] => {
|
|
46
|
+
const out: IndexAdviceCandidate[] = [];
|
|
47
|
+
const tables = new Map(input.schema.tables.map(t => [t.name, t]));
|
|
48
|
+
|
|
49
|
+
for (const stat of input.week) {
|
|
50
|
+
if (!stat.columnName) continue;
|
|
51
|
+
const table = tables.get(stat.tableName);
|
|
52
|
+
const column = table?.columns.find(c => c.name === stat.columnName);
|
|
53
|
+
if (!column || isIndexedColumn(column) || !canIndex(column.type)) continue;
|
|
54
|
+
if (stat.queries < constants.DATA_INDEX_ADVICE_MIN_QUERIES) continue;
|
|
55
|
+
const avgRead = stat.rowsRead / stat.queries;
|
|
56
|
+
const avgReturned = stat.rowsReturned / stat.queries;
|
|
57
|
+
if (avgRead < constants.DATA_INDEX_ADVICE_MIN_ROWS_READ) continue;
|
|
58
|
+
// Scanning, not just a small table: many rows read for each one returned.
|
|
59
|
+
if (
|
|
60
|
+
stat.rowsRead <
|
|
61
|
+
constants.DATA_INDEX_ADVICE_MIN_READ_RATIO * stat.rowsReturned
|
|
62
|
+
) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out.push({
|
|
66
|
+
tableName: stat.tableName,
|
|
67
|
+
columnName: stat.columnName,
|
|
68
|
+
kind: constants.DATA_INDEX_ADVICE_ADD,
|
|
69
|
+
queries: stat.queries,
|
|
70
|
+
avgRowsRead: Math.round(avgRead),
|
|
71
|
+
avgRowsReturned: Math.round(avgReturned),
|
|
72
|
+
rowsWritten: 0
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (input.monthCovered) {
|
|
77
|
+
const used = new Set(
|
|
78
|
+
input.month
|
|
79
|
+
.filter(s => s.columnName && s.queries > 0)
|
|
80
|
+
.map(s => `${s.tableName}.${s.columnName}`)
|
|
81
|
+
);
|
|
82
|
+
for (const table of input.schema.tables) {
|
|
83
|
+
const totals = input.month.find(
|
|
84
|
+
s => s.tableName === table.name && s.columnName === ''
|
|
85
|
+
);
|
|
86
|
+
const written = totals?.rowsWritten ?? 0;
|
|
87
|
+
if (written < constants.DATA_INDEX_UNUSED_MIN_ROWS_WRITTEN) continue;
|
|
88
|
+
if (written <= (totals?.queries ?? 0)) continue;
|
|
89
|
+
if (input.recentlyIndexed.has(table.name)) continue;
|
|
90
|
+
for (const column of table.columns) {
|
|
91
|
+
if (!column.filterable || column.unique) continue;
|
|
92
|
+
if (column.type === 'relation' || column.type === 'file') continue;
|
|
93
|
+
const key = `${table.name}.${column.name}`;
|
|
94
|
+
if (used.has(key) || input.recentlyIndexed.has(key)) continue;
|
|
95
|
+
out.push({
|
|
96
|
+
tableName: table.name,
|
|
97
|
+
columnName: column.name,
|
|
98
|
+
kind: constants.DATA_INDEX_ADVICE_UNUSED,
|
|
99
|
+
queries: 0,
|
|
100
|
+
avgRowsRead: 0,
|
|
101
|
+
avgRowsReturned: 0,
|
|
102
|
+
rowsWritten: written
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// A dismissed suggestion comes back once its numbers have doubled.
|
|
111
|
+
export const adviceDoubled = (
|
|
112
|
+
current: { queries: number; avgRowsRead: number; rowsWritten: number },
|
|
113
|
+
dismissed: {
|
|
114
|
+
queries: number | null;
|
|
115
|
+
avgRowsRead: number | null;
|
|
116
|
+
rowsWritten: number | null;
|
|
117
|
+
}
|
|
118
|
+
): boolean =>
|
|
119
|
+
(dismissed.queries !== null &&
|
|
120
|
+
dismissed.queries > 0 &&
|
|
121
|
+
current.queries >= 2 * dismissed.queries) ||
|
|
122
|
+
(dismissed.avgRowsRead !== null &&
|
|
123
|
+
dismissed.avgRowsRead > 0 &&
|
|
124
|
+
current.avgRowsRead >= 2 * dismissed.avgRowsRead) ||
|
|
125
|
+
(dismissed.rowsWritten !== null &&
|
|
126
|
+
dismissed.rowsWritten > 0 &&
|
|
127
|
+
current.rowsWritten >= 2 * dismissed.rowsWritten);
|
package/src/dataRpc.ts
CHANGED
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
import type {
|
|
5
5
|
DataCaller,
|
|
6
6
|
DataDeleteImpact,
|
|
7
|
+
DataFileLinkChange,
|
|
8
|
+
DataFileLinks,
|
|
9
|
+
DataFileLinksReplaced,
|
|
7
10
|
DataResult,
|
|
8
11
|
DataSchema,
|
|
9
12
|
DataTable,
|
|
10
|
-
DataTableCopy
|
|
13
|
+
DataTableCopy,
|
|
14
|
+
DataTableSync
|
|
11
15
|
} from './dataTypes';
|
|
16
|
+
import type { SheetSyncArgs } from './dataEngine';
|
|
12
17
|
import type { DataQuery } from './dataQueries';
|
|
13
18
|
import type { DataTrigger, DataTriggerWithSecret, SavedTrigger } from './dataTriggers';
|
|
14
19
|
|
|
@@ -56,7 +61,8 @@ export interface DatabaseInfo {
|
|
|
56
61
|
timeZone: string | null;
|
|
57
62
|
schemaVersion: number;
|
|
58
63
|
size: number;
|
|
59
|
-
|
|
64
|
+
// `rows` is about how many: the highest id, read from the index.
|
|
65
|
+
tables: { name: string; label: string | null; columns: number; rows: number }[];
|
|
60
66
|
// A column change copying a large table in the background: table changes,
|
|
61
67
|
// and writes that would change its rows, wait while it runs.
|
|
62
68
|
copy: DataTableCopy | null;
|
|
@@ -106,6 +112,12 @@ export interface ProjectDatabaseRpc {
|
|
|
106
112
|
dismissCopy(caller: DataCaller): R<{ copy: null }>;
|
|
107
113
|
describe(caller: DataCaller, args?: unknown): R<unknown>;
|
|
108
114
|
query(caller: DataCaller, args: unknown): R<unknown>;
|
|
115
|
+
// A row as a trigger event carries it (`id`, or the newest): for a test
|
|
116
|
+
// send and for a run whose event was too large to carry its row.
|
|
117
|
+
eventRow(
|
|
118
|
+
caller: DataCaller,
|
|
119
|
+
args: { table: string; id?: number }
|
|
120
|
+
): R<Record<string, unknown> | null>;
|
|
109
121
|
write(
|
|
110
122
|
caller: DataCaller,
|
|
111
123
|
args: unknown
|
|
@@ -114,6 +126,19 @@ export interface ProjectDatabaseRpc {
|
|
|
114
126
|
caller: DataCaller,
|
|
115
127
|
args: { table: string; ids?: number[] }
|
|
116
128
|
): R<DataDeleteImpact>;
|
|
129
|
+
// The rows that link knowledge items, and moving or clearing those links.
|
|
130
|
+
fileLinks(caller: DataCaller, args: { uris: string[] }): R<DataFileLinks>;
|
|
131
|
+
replaceFileLinks(
|
|
132
|
+
caller: DataCaller,
|
|
133
|
+
args: { changes: DataFileLinkChange[] }
|
|
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>;
|
|
117
142
|
history(
|
|
118
143
|
caller: DataCaller,
|
|
119
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
|
+
};
|
package/src/dataTools.ts
CHANGED
|
@@ -61,7 +61,7 @@ const CHANGES = {
|
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
const WHERE_HELP =
|
|
64
|
-
'Filter object. { "column": value } matches exactly; { "column": { "op": value } } with op one of eq, ne, gt, gte, lt, lte, in, notIn (lists), contains, startsWith (text, case-insensitive), isNull (true/false). Several keys must all match; { "or": [ {...}, {...} ] } matches any. A relation column takes the related row\'s id, or filter on a column of the related table with "relation.column", e.g. { "product.name": "Margherita" }. Datetimes are ISO 8601; include the offset for local times, e.g. "2026-10-19T00:00:00-05:00".';
|
|
64
|
+
'Filter object. { "column": value } matches exactly; { "column": { "op": value } } with op one of eq, ne, gt, gte, lt, lte, in, notIn (lists), contains, startsWith (text, case-insensitive), isNull (true/false). Several keys must all match; { "or": [ {...}, {...} ] } matches any. A relation column takes the related row\'s id, or filter on a column of the related table with "relation.column", e.g. { "product.name": "Margherita" }. A file column matches the item\'s uri. Datetimes are ISO 8601; include the offset for local times, e.g. "2026-10-19T00:00:00-05:00".';
|
|
65
65
|
|
|
66
66
|
const accessOf = (caller: DataCaller, table: DataTable) =>
|
|
67
67
|
caller.restricted ? table.access : 'full';
|
|
@@ -145,7 +145,7 @@ export const dataToolDefinitions = (
|
|
|
145
145
|
title: DATA_TOOL_TITLES[DATA_TOOL_QUERY],
|
|
146
146
|
description: [
|
|
147
147
|
"Read rows from the project's database: structured records such as products, prices, stock, orders, bookings or customers. Use it for anything with fields and numbers; use the knowledge/resource tools for documents and pages.",
|
|
148
|
-
'Returns matching rows (relation columns come back as { id, label }), the total that match, and nextCursor when there are more. With "aggregate" it returns counts, sums, averages, minimums or maximums instead of rows, optionally per group: use that for "how many" and "how much" rather than adding rows up yourself.',
|
|
148
|
+
'Returns matching rows (relation columns come back as { id, label }; file columns as { uri, title, mimeType }, or { uri, missing: true } when the item is gone: deliver one with send-resource and its uri, or read a document with read-resource), the total that match, and nextCursor when there are more. With "aggregate" it returns counts, sums, averages, minimums or maximums instead of rows, optionally per group: use that for "how many" and "how much" rather than adding rows up yourself.',
|
|
149
149
|
"The database holds only what these tables and columns hold. When a question needs something they don't have (opening hours, parking, calories, reviews, forecasts), look in the knowledge tools if you have them; if nothing has it, say you don't have that information. Never guess it.",
|
|
150
150
|
`Every table also has id, created_at and updated_at. Datetimes come back in the project's time zone (${timeZone}) with their offset; read them as local times.`,
|
|
151
151
|
'Tables:',
|
|
@@ -242,7 +242,7 @@ export const dataToolDefinitions = (
|
|
|
242
242
|
title: DATA_TOOL_TITLES[DATA_TOOL_WRITE],
|
|
243
243
|
description: [
|
|
244
244
|
"Add, change or delete rows in the project's database. All operations in one call succeed together or none is applied, so put everything that belongs together (an order, its lines and the stock they use) in one call.",
|
|
245
|
-
'- insert: { "op": "insert", "table": "...", "values": { column: value } }. A relation column takes the related row\'s id; to point at a row inserted earlier in the same call use { "$ref": n }, n being that operation\'s position (0-based).',
|
|
245
|
+
'- insert: { "op": "insert", "table": "...", "values": { column: value } }. A relation column takes the related row\'s id; to point at a row inserted earlier in the same call use { "$ref": n }, n being that operation\'s position (0-based). A file column takes the uri of an item in the project\'s knowledge (from list-resources or search-resources), or null.',
|
|
246
246
|
'- update: { "op": "update", "table": "...", "id": 12, "set": { column: value } }. For numbers, { "decrement": 2 } or { "increment": 2 } changes the value relative to what is stored. Instead of id you can give "where" (same format as data-query).',
|
|
247
247
|
'- delete: { "op": "delete", "table": "...", "id": 12 } (or "where").',
|
|
248
248
|
'Guards: an update or delete also takes "where" conditions that must hold, e.g. "where": { "stock": { "gte": 2 } }, and "expect", how many rows must change (1 by default when id is given). If a guard fails, nothing in the call is written and the error says why (e.g. how much stock is left).',
|