@ganju/utils 0.0.16 → 0.0.18
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 +9 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +26 -2
- package/dist/customCodeToken.d.ts +15 -0
- package/dist/customCodeToken.d.ts.map +1 -1
- package/dist/customCodeToken.js +49 -1
- package/dist/data.d.ts +6 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +53 -1
- package/dist/dataApi.d.ts +200 -1
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +106 -1
- package/dist/dataCompiler.d.ts.map +1 -1
- package/dist/dataCompiler.js +5 -1
- package/dist/dataCsv.d.ts +20 -0
- package/dist/dataCsv.d.ts.map +1 -0
- package/dist/dataCsv.js +102 -0
- package/dist/dataDdl.d.ts.map +1 -1
- package/dist/dataDdl.js +3 -1
- package/dist/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +86 -4
- package/dist/dataImport.d.ts +2 -0
- package/dist/dataImport.d.ts.map +1 -1
- package/dist/dataImport.js +4 -1
- package/dist/dataQueries.d.ts +73 -0
- package/dist/dataQueries.d.ts.map +1 -0
- package/dist/dataQueries.js +513 -0
- package/dist/dataRpc.d.ts +44 -0
- package/dist/dataRpc.d.ts.map +1 -1
- package/dist/dataTools.d.ts +40 -0
- package/dist/dataTools.d.ts.map +1 -0
- package/dist/dataTools.js +280 -0
- package/dist/dataTriggers.d.ts +203 -0
- package/dist/dataTriggers.d.ts.map +1 -0
- package/dist/dataTriggers.js +760 -0
- package/dist/dataTypes.d.ts +3 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/index.d.ts +201 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/localizeDataError.d.ts +18 -0
- package/dist/localizeDataError.d.ts.map +1 -0
- package/dist/localizeDataError.js +359 -0
- package/dist/schema.d.ts +201 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +149 -1
- package/dist/sdkConstants.d.ts +3 -0
- package/dist/sdkConstants.d.ts.map +1 -1
- package/dist/sdkConstants.js +4 -1
- package/package.json +1 -1
- package/src/constants.ts +30 -2
- package/src/customCodeToken.ts +87 -0
- package/src/data.ts +74 -0
- package/src/dataApi.ts +129 -2
- package/src/dataCompiler.ts +5 -1
- package/src/dataCsv.ts +99 -0
- package/src/dataDdl.ts +3 -1
- package/src/dataEngine.ts +94 -4
- package/src/dataImport.ts +3 -0
- package/src/dataQueries.ts +730 -0
- package/src/dataRpc.ts +26 -0
- package/src/dataTools.ts +338 -0
- package/src/dataTriggers.ts +1134 -0
- package/src/dataTypes.ts +9 -0
- package/src/index.ts +11 -1
- package/src/localizeDataError.ts +593 -0
- package/src/schema.ts +158 -1
- package/src/sdkConstants.ts +3 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runQuery = exports.queriesFor = exports.dropQuery = exports.saveQuery = exports.listQueries = exports.checkQueryPlan = exports.compileQuerySql = exports.DATA_DEFAULT_QUERY_ROWS = exports.DATA_MAX_QUERY_PARAMS = exports.DATA_MAX_QUERY_SQL = exports.DATA_MAX_QUERIES = exports.DATA_QUERY_PARAM_TYPES = void 0;
|
|
4
|
+
// Named queries: a SELECT the owner writes once, with named parameters, for
|
|
5
|
+
// reports the structured query can't express. Functions run them by name, and
|
|
6
|
+
// so may the assistant when the owner allows it; nobody sends SQL at run time.
|
|
7
|
+
//
|
|
8
|
+
// A Durable Object refuses `PRAGMA query_only`, so nothing in SQLite stops a
|
|
9
|
+
// statement from writing. The check when a query is saved is the whole guard,
|
|
10
|
+
// in two layers:
|
|
11
|
+
// - our tokenizer: one statement, starting with SELECT or WITH, naming none
|
|
12
|
+
// of SQLite's or our own tables, with named parameters only. A Durable
|
|
13
|
+
// Object's exec runs every statement in a string ("SELECT 1; DELETE FROM
|
|
14
|
+
// orders" deletes), so the single statement is checked here, on tokens.
|
|
15
|
+
// - SQLite's own parser: the statement is compiled with EXPLAIN, never run,
|
|
16
|
+
// and refused if its program opens anything for writing or reads a table
|
|
17
|
+
// that isn't one of the owner's.
|
|
18
|
+
const dataIdentifiers_1 = require("./dataIdentifiers");
|
|
19
|
+
const dataCompiler_1 = require("./dataCompiler");
|
|
20
|
+
const dataTypes_1 = require("./dataTypes");
|
|
21
|
+
exports.DATA_QUERY_PARAM_TYPES = [
|
|
22
|
+
'text',
|
|
23
|
+
'integer',
|
|
24
|
+
'decimal',
|
|
25
|
+
'boolean',
|
|
26
|
+
'date',
|
|
27
|
+
'datetime'
|
|
28
|
+
];
|
|
29
|
+
exports.DATA_MAX_QUERIES = 100;
|
|
30
|
+
exports.DATA_MAX_QUERY_SQL = 10_000;
|
|
31
|
+
exports.DATA_MAX_QUERY_PARAMS = 20;
|
|
32
|
+
exports.DATA_DEFAULT_QUERY_ROWS = 100;
|
|
33
|
+
const WORD_START = /[\p{L}_]/u;
|
|
34
|
+
const WORD_PART = /[\p{L}\p{N}_$]/u;
|
|
35
|
+
const unterminated = (what) => new dataTypes_1.DataError(`The SQL has an unterminated ${what}.`);
|
|
36
|
+
// SQLite's lexical rules, as far as telling statements, names, literals and
|
|
37
|
+
// parameters apart needs: comments and whitespace are dropped, everything
|
|
38
|
+
// else becomes a token that keeps its place in the text.
|
|
39
|
+
const tokenize = (sql) => {
|
|
40
|
+
const tokens = [];
|
|
41
|
+
let i = 0;
|
|
42
|
+
const push = (kind, start, end) => tokens.push({ kind, text: sql.slice(start, end), start, end });
|
|
43
|
+
while (i < sql.length) {
|
|
44
|
+
const ch = sql[i];
|
|
45
|
+
if (/\s/.test(ch)) {
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else if (ch === '-' && sql[i + 1] === '-') {
|
|
49
|
+
const end = sql.indexOf('\n', i);
|
|
50
|
+
i = end === -1 ? sql.length : end + 1;
|
|
51
|
+
}
|
|
52
|
+
else if (ch === '/' && sql[i + 1] === '*') {
|
|
53
|
+
const end = sql.indexOf('*/', i + 2);
|
|
54
|
+
if (end === -1)
|
|
55
|
+
throw unterminated('comment');
|
|
56
|
+
i = end + 2;
|
|
57
|
+
}
|
|
58
|
+
else if (ch === "'" || ch === '"' || ch === '`' || ch === '[') {
|
|
59
|
+
const close = ch === '[' ? ']' : ch;
|
|
60
|
+
let j = i + 1;
|
|
61
|
+
for (;;) {
|
|
62
|
+
if (j >= sql.length) {
|
|
63
|
+
throw unterminated(ch === "'" ? 'string' : 'quoted name');
|
|
64
|
+
}
|
|
65
|
+
if (sql[j] === close) {
|
|
66
|
+
// A doubled quote is the quote itself, except in [brackets].
|
|
67
|
+
if (close !== ']' && sql[j + 1] === close) {
|
|
68
|
+
j += 2;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
j++;
|
|
74
|
+
}
|
|
75
|
+
push(ch === "'" ? 'string' : 'quoted', i, j + 1);
|
|
76
|
+
i = j + 1;
|
|
77
|
+
}
|
|
78
|
+
else if (ch === ':' || ch === '@' || ch === '$') {
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < sql.length && WORD_PART.test(sql[j]))
|
|
81
|
+
j++;
|
|
82
|
+
if (j === i + 1) {
|
|
83
|
+
push('other', i, j);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
push('param', i, j);
|
|
87
|
+
}
|
|
88
|
+
i = j;
|
|
89
|
+
}
|
|
90
|
+
else if (ch === '?') {
|
|
91
|
+
let j = i + 1;
|
|
92
|
+
while (j < sql.length && /\d/.test(sql[j]))
|
|
93
|
+
j++;
|
|
94
|
+
push('positional', i, j);
|
|
95
|
+
i = j;
|
|
96
|
+
}
|
|
97
|
+
else if (ch === ';') {
|
|
98
|
+
push('semicolon', i, i + 1);
|
|
99
|
+
i++;
|
|
100
|
+
}
|
|
101
|
+
else if (/\d/.test(ch) || (ch === '.' && /\d/.test(sql[i + 1] ?? ''))) {
|
|
102
|
+
let j = i + 1;
|
|
103
|
+
while (j < sql.length && /[\w.]/.test(sql[j]))
|
|
104
|
+
j++;
|
|
105
|
+
push('number', i, j);
|
|
106
|
+
i = j;
|
|
107
|
+
}
|
|
108
|
+
else if (WORD_START.test(ch)) {
|
|
109
|
+
let j = i + 1;
|
|
110
|
+
while (j < sql.length && WORD_PART.test(sql[j]))
|
|
111
|
+
j++;
|
|
112
|
+
push('word', i, j);
|
|
113
|
+
i = j;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
push('other', i, i + 1);
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return tokens;
|
|
121
|
+
};
|
|
122
|
+
// A quoted name's text without its quotes, the way SQLite reads it.
|
|
123
|
+
const nameOf = (token) => {
|
|
124
|
+
if (token.kind !== 'quoted')
|
|
125
|
+
return token.text.toLowerCase();
|
|
126
|
+
const inner = token.text.slice(1, -1);
|
|
127
|
+
const quote = token.text[0];
|
|
128
|
+
return (quote === '[' ? inner : inner.split(quote + quote).join(quote)).toLowerCase();
|
|
129
|
+
};
|
|
130
|
+
const FORBIDDEN_WORDS = new Set(['pragma', 'attach', 'detach', 'vacuum']);
|
|
131
|
+
const reservedName = (name) => name.startsWith('_ganju_') ||
|
|
132
|
+
name.startsWith('sqlite_') ||
|
|
133
|
+
name.startsWith('pragma_') ||
|
|
134
|
+
name.includes('__');
|
|
135
|
+
/**
|
|
136
|
+
* Checks a named query's text and compiles its parameters to positions. Pure:
|
|
137
|
+
* what only SQLite can tell (that it reads, and what) is checked when it is
|
|
138
|
+
* saved, by checkQueryPlan.
|
|
139
|
+
*/
|
|
140
|
+
const compileQuerySql = (sql) => {
|
|
141
|
+
if (typeof sql !== 'string' || !sql.trim()) {
|
|
142
|
+
throw new dataTypes_1.DataError('"sql" is the SELECT to run.');
|
|
143
|
+
}
|
|
144
|
+
if (sql.length > exports.DATA_MAX_QUERY_SQL) {
|
|
145
|
+
throw new dataTypes_1.DataError(`A named query is up to ${exports.DATA_MAX_QUERY_SQL.toLocaleString('en-US')} characters.`);
|
|
146
|
+
}
|
|
147
|
+
const tokens = tokenize(sql);
|
|
148
|
+
// Trailing semicolons are harmless; any other ends the statement early, and
|
|
149
|
+
// what follows would run as a second one.
|
|
150
|
+
while (tokens.length && tokens[tokens.length - 1].kind === 'semicolon') {
|
|
151
|
+
tokens.pop();
|
|
152
|
+
}
|
|
153
|
+
if (tokens.some(t => t.kind === 'semicolon')) {
|
|
154
|
+
throw new dataTypes_1.DataError('A named query is one statement: remove the ";" inside it.');
|
|
155
|
+
}
|
|
156
|
+
const head = tokens[0];
|
|
157
|
+
if (!head ||
|
|
158
|
+
head.kind !== 'word' ||
|
|
159
|
+
!['select', 'with'].includes(head.text.toLowerCase())) {
|
|
160
|
+
throw new dataTypes_1.DataError('A named query starts with SELECT or WITH; it only reads.');
|
|
161
|
+
}
|
|
162
|
+
const params = [];
|
|
163
|
+
let out = '';
|
|
164
|
+
let last = 0;
|
|
165
|
+
for (const token of tokens) {
|
|
166
|
+
if (token.kind === 'positional') {
|
|
167
|
+
throw new dataTypes_1.DataError('Use named parameters (:from, :to) instead of "?", so each one can be described.');
|
|
168
|
+
}
|
|
169
|
+
if (token.kind === 'word' &&
|
|
170
|
+
FORBIDDEN_WORDS.has(token.text.toLowerCase())) {
|
|
171
|
+
throw new dataTypes_1.DataError(`${token.text.toUpperCase()} isn't allowed in a named query.`);
|
|
172
|
+
}
|
|
173
|
+
if ((token.kind === 'word' || token.kind === 'quoted') &&
|
|
174
|
+
reservedName(nameOf(token))) {
|
|
175
|
+
throw new dataTypes_1.DataError(`"${nameOf(token)}" isn't one of this database's tables or columns; named queries read the tables you defined.`);
|
|
176
|
+
}
|
|
177
|
+
if (token.kind === 'param') {
|
|
178
|
+
const name = token.text.slice(1);
|
|
179
|
+
if (!dataIdentifiers_1.DATA_IDENTIFIER_PATTERN.test(name)) {
|
|
180
|
+
throw new dataTypes_1.DataError(`Parameter ${token.text}: use lowercase letters, digits and _, starting with a letter.`);
|
|
181
|
+
}
|
|
182
|
+
let index = params.indexOf(name);
|
|
183
|
+
if (index === -1)
|
|
184
|
+
index = params.push(name) - 1;
|
|
185
|
+
out += `${sql.slice(last, token.start)}?${index + 1}`;
|
|
186
|
+
last = token.end;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
out += sql.slice(last, tokens.length ? tokens[tokens.length - 1].end : 0);
|
|
190
|
+
if (params.length > exports.DATA_MAX_QUERY_PARAMS) {
|
|
191
|
+
throw new dataTypes_1.DataError(`A named query takes up to ${exports.DATA_MAX_QUERY_PARAMS} parameters.`);
|
|
192
|
+
}
|
|
193
|
+
return { sql: out, params };
|
|
194
|
+
};
|
|
195
|
+
exports.compileQuerySql = compileQuerySql;
|
|
196
|
+
// What SQLite's program does
|
|
197
|
+
// Opcodes that only appear in a program that changes the database file. A
|
|
198
|
+
// SELECT's own scratch tables (ORDER BY, DISTINCT, IN lists) use ephemeral
|
|
199
|
+
// cursors and never these.
|
|
200
|
+
const WRITE_OPCODES = new Set([
|
|
201
|
+
'OpenWrite',
|
|
202
|
+
'Clear',
|
|
203
|
+
'Destroy',
|
|
204
|
+
'CreateBtree',
|
|
205
|
+
'ParseSchema',
|
|
206
|
+
'DropTable',
|
|
207
|
+
'DropIndex',
|
|
208
|
+
'DropTrigger',
|
|
209
|
+
'VUpdate',
|
|
210
|
+
'VCreate',
|
|
211
|
+
'VDestroy',
|
|
212
|
+
'SqlExec',
|
|
213
|
+
'Vacuum',
|
|
214
|
+
'IncrVacuum',
|
|
215
|
+
'SetCookie',
|
|
216
|
+
'JournalMode'
|
|
217
|
+
]);
|
|
218
|
+
// Virtual tables are opened with VOpen, not OpenRead, and the program names
|
|
219
|
+
// no module. What it does give is the virtual table's address, and the
|
|
220
|
+
// built-in ones (json_each, dbstat) keep one per connection: so json_each's
|
|
221
|
+
// and json_tree's are read off a query of their own, and any other is
|
|
222
|
+
// refused. dbstat, for one, lists every table with its size.
|
|
223
|
+
const ALLOWED_VIRTUAL_TABLES = [
|
|
224
|
+
"SELECT 1 FROM json_each('[]')",
|
|
225
|
+
"SELECT 1 FROM json_tree('[]')"
|
|
226
|
+
];
|
|
227
|
+
const virtualTablesOf = (program) => program.filter(step => String(step.opcode) === 'VOpen').map(step => step.p4);
|
|
228
|
+
// SQLite's own error, without the code and offset it appends.
|
|
229
|
+
const sqliteMessage = (error) => (error instanceof Error ? error.message : String(error))
|
|
230
|
+
.replace(/\s*at offset \d+/, '')
|
|
231
|
+
.replace(/:\s*SQLITE_\w+$/, '')
|
|
232
|
+
.trim();
|
|
233
|
+
const placeholders = (count) => Array(count).fill(null);
|
|
234
|
+
/**
|
|
235
|
+
* Compiles the statement with EXPLAIN (nothing runs) and checks its program:
|
|
236
|
+
* it may not open anything for writing or start a write transaction, and every
|
|
237
|
+
* table it reads must be one of `schema`'s. Returns those tables and the plan.
|
|
238
|
+
*/
|
|
239
|
+
const checkQueryPlan = (runner, schema, compiled) => {
|
|
240
|
+
let program;
|
|
241
|
+
try {
|
|
242
|
+
program = runner.all(`EXPLAIN ${compiled.sql}`, placeholders(compiled.params.length));
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
throw new dataTypes_1.DataError(`The SQL doesn't compile: ${sqliteMessage(error)}.`);
|
|
246
|
+
}
|
|
247
|
+
const roots = new Map();
|
|
248
|
+
for (const row of runner.all("SELECT rootpage, tbl_name FROM sqlite_schema WHERE type IN ('table', 'index')")) {
|
|
249
|
+
roots.set(Number(row.rootpage), String(row.tbl_name));
|
|
250
|
+
}
|
|
251
|
+
const owned = new Set(schema.tables.map(t => t.name));
|
|
252
|
+
const opened = virtualTablesOf(program);
|
|
253
|
+
if (opened.length > 0) {
|
|
254
|
+
const allowed = new Set(ALLOWED_VIRTUAL_TABLES.flatMap(sql => virtualTablesOf(runner.all(`EXPLAIN ${sql}`))));
|
|
255
|
+
if (opened.some(vtab => typeof vtab !== 'string' || !allowed.has(vtab))) {
|
|
256
|
+
throw new dataTypes_1.DataError("A named query reads only this database's tables, with json_each and json_tree for JSON; not other virtual tables such as dbstat.");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const tables = new Set();
|
|
260
|
+
for (const step of program) {
|
|
261
|
+
const opcode = String(step.opcode);
|
|
262
|
+
if (WRITE_OPCODES.has(opcode) ||
|
|
263
|
+
(opcode === 'Transaction' && Number(step.p2) !== 0)) {
|
|
264
|
+
throw new dataTypes_1.DataError('A named query only reads: this one would change the database.');
|
|
265
|
+
}
|
|
266
|
+
if (opcode === 'OpenRead') {
|
|
267
|
+
const table = roots.get(Number(step.p2));
|
|
268
|
+
if (!table || !owned.has(table)) {
|
|
269
|
+
throw new dataTypes_1.DataError(`A named query reads only this database's tables${table ? `, not "${table}"` : ''}.`);
|
|
270
|
+
}
|
|
271
|
+
tables.add(table);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const plan = runner
|
|
275
|
+
.all(`EXPLAIN QUERY PLAN ${compiled.sql}`, placeholders(compiled.params.length))
|
|
276
|
+
.map(row => String(row.detail));
|
|
277
|
+
return { tables: [...tables].sort(), plan };
|
|
278
|
+
};
|
|
279
|
+
exports.checkQueryPlan = checkQueryPlan;
|
|
280
|
+
const parseList = (raw) => {
|
|
281
|
+
try {
|
|
282
|
+
const value = JSON.parse(String(raw));
|
|
283
|
+
return Array.isArray(value) ? value : [];
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const fromRow = (row) => {
|
|
290
|
+
const stored = row;
|
|
291
|
+
return {
|
|
292
|
+
name: stored.name,
|
|
293
|
+
description: stored.description,
|
|
294
|
+
sql: stored.sql,
|
|
295
|
+
params: parseList(stored.params),
|
|
296
|
+
assistant: Boolean(stored.assistant),
|
|
297
|
+
tables: parseList(stored.tables),
|
|
298
|
+
plan: parseList(stored.plan),
|
|
299
|
+
updatedAt: stored.updated_at,
|
|
300
|
+
updatedBy: stored.updated_by
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const listQueries = (runner) => runner.all('SELECT * FROM _ganju_queries ORDER BY name').map(fromRow);
|
|
304
|
+
exports.listQueries = listQueries;
|
|
305
|
+
const findQuery = (runner, name) => {
|
|
306
|
+
const row = typeof name === 'string'
|
|
307
|
+
? runner.all('SELECT * FROM _ganju_queries WHERE name = ?', [name])[0]
|
|
308
|
+
: undefined;
|
|
309
|
+
if (!row) {
|
|
310
|
+
const names = (0, exports.listQueries)(runner).map(query => query.name);
|
|
311
|
+
throw new dataTypes_1.DataError(`There is no named query "${String(name)}". Named queries: ${names.join(', ') || '(none)'}.`, 'not_found');
|
|
312
|
+
}
|
|
313
|
+
return fromRow(row);
|
|
314
|
+
};
|
|
315
|
+
const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
316
|
+
const normaliseParams = (raw, used) => {
|
|
317
|
+
const list = raw === undefined || raw === null ? [] : raw;
|
|
318
|
+
if (!Array.isArray(list)) {
|
|
319
|
+
throw new dataTypes_1.DataError('"params" is a list of { name, type, description }.');
|
|
320
|
+
}
|
|
321
|
+
const params = list.map((entry, i) => {
|
|
322
|
+
if (!isPlainObject(entry) || typeof entry.name !== 'string') {
|
|
323
|
+
throw new dataTypes_1.DataError(`params[${i}] needs a name.`);
|
|
324
|
+
}
|
|
325
|
+
if (!exports.DATA_QUERY_PARAM_TYPES.includes(entry.type)) {
|
|
326
|
+
throw new dataTypes_1.DataError(`Parameter ${entry.name}: "type" is one of ${exports.DATA_QUERY_PARAM_TYPES.join(', ')}.`);
|
|
327
|
+
}
|
|
328
|
+
const description = typeof entry.description === 'string' && entry.description.trim()
|
|
329
|
+
? entry.description.trim().slice(0, dataTypes_1.DATA_MAX_DESCRIPTION)
|
|
330
|
+
: null;
|
|
331
|
+
return {
|
|
332
|
+
name: entry.name,
|
|
333
|
+
type: entry.type,
|
|
334
|
+
...(description ? { description } : {}),
|
|
335
|
+
...(entry.required === false ? { required: false } : {})
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
const declared = params.map(p => p.name);
|
|
339
|
+
const undeclared = used.filter(name => !declared.includes(name));
|
|
340
|
+
if (undeclared.length) {
|
|
341
|
+
throw new dataTypes_1.DataError(`Declare the type of ${undeclared.map(n => `:${n}`).join(', ')} in "params".`);
|
|
342
|
+
}
|
|
343
|
+
const unused = declared.filter(name => !used.includes(name));
|
|
344
|
+
if (unused.length) {
|
|
345
|
+
throw new dataTypes_1.DataError(`The SQL doesn't use ${unused.map(n => `:${n}`).join(', ')}.`);
|
|
346
|
+
}
|
|
347
|
+
return used.map(name => params.find(p => p.name === name));
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Saves a named query, replacing one by the same name. Refused unless it
|
|
351
|
+
* passes both checks; the tables it reads and its plan are kept with it.
|
|
352
|
+
*/
|
|
353
|
+
const saveQuery = (runner, schema, caller, input) => {
|
|
354
|
+
if (!isPlainObject(input))
|
|
355
|
+
throw new dataTypes_1.DataError('A named query is an object.');
|
|
356
|
+
const name = input.name;
|
|
357
|
+
if (typeof name !== 'string' || !dataIdentifiers_1.DATA_IDENTIFIER_PATTERN.test(name)) {
|
|
358
|
+
throw new dataTypes_1.DataError('A named query needs a name: lowercase letters, digits and _, starting with a letter.');
|
|
359
|
+
}
|
|
360
|
+
const compiled = (0, exports.compileQuerySql)(input.sql);
|
|
361
|
+
const params = normaliseParams(input.params, compiled.params);
|
|
362
|
+
const { tables, plan } = (0, exports.checkQueryPlan)(runner, schema, compiled);
|
|
363
|
+
const description = typeof input.description === 'string' && input.description.trim()
|
|
364
|
+
? input.description.trim().slice(0, dataTypes_1.DATA_MAX_DESCRIPTION)
|
|
365
|
+
: null;
|
|
366
|
+
const exists = runner.all('SELECT 1 FROM _ganju_queries WHERE name = ?', [name]).length >
|
|
367
|
+
0;
|
|
368
|
+
if (!exists) {
|
|
369
|
+
const count = Number(runner.all('SELECT count(*) AS n FROM _ganju_queries')[0]?.n ?? 0);
|
|
370
|
+
if (count >= exports.DATA_MAX_QUERIES) {
|
|
371
|
+
throw new dataTypes_1.DataError(`A database holds up to ${exports.DATA_MAX_QUERIES} named queries.`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
runner.all(`INSERT INTO _ganju_queries (name, description, sql, params, assistant, tables, plan, updated_at, updated_by)
|
|
375
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), ?)
|
|
376
|
+
ON CONFLICT(name) DO UPDATE SET description = excluded.description, sql = excluded.sql,
|
|
377
|
+
params = excluded.params, assistant = excluded.assistant, tables = excluded.tables,
|
|
378
|
+
plan = excluded.plan, updated_at = excluded.updated_at, updated_by = excluded.updated_by`, [
|
|
379
|
+
name,
|
|
380
|
+
description,
|
|
381
|
+
input.sql.trim(),
|
|
382
|
+
JSON.stringify(params),
|
|
383
|
+
input.assistant === true ? 1 : 0,
|
|
384
|
+
JSON.stringify(tables),
|
|
385
|
+
JSON.stringify(plan),
|
|
386
|
+
caller.actor
|
|
387
|
+
]);
|
|
388
|
+
return findQuery(runner, name);
|
|
389
|
+
};
|
|
390
|
+
exports.saveQuery = saveQuery;
|
|
391
|
+
const dropQuery = (runner, name) => {
|
|
392
|
+
const query = findQuery(runner, name);
|
|
393
|
+
runner.all('DELETE FROM _ganju_queries WHERE name = ?', [query.name]);
|
|
394
|
+
return { dropped: query.name };
|
|
395
|
+
};
|
|
396
|
+
exports.dropQuery = dropQuery;
|
|
397
|
+
// The queries a caller may run: all of them, or for a channel bot those the
|
|
398
|
+
// owner let the assistant run that read only tables it may read, none of
|
|
399
|
+
// them scoped to a customer. A named query's SQL can't be scoped, so it could
|
|
400
|
+
// read every customer's rows.
|
|
401
|
+
const mayRun = (schema, caller, query) => {
|
|
402
|
+
if (caller.tables &&
|
|
403
|
+
!query.tables.every(name => caller.tables?.includes(name))) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (!caller.restricted)
|
|
407
|
+
return true;
|
|
408
|
+
if (!query.assistant)
|
|
409
|
+
return false;
|
|
410
|
+
return query.tables.every(name => {
|
|
411
|
+
const table = schema.tables.find(t => t.name === name);
|
|
412
|
+
return Boolean(table && dataTypes_1.DATA_ACCESS_RANK[table.access] >= 1 && !(0, dataCompiler_1.isScoped)(caller, table));
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const queriesFor = (runner, schema, caller) => (0, exports.listQueries)(runner).filter(query => mayRun(schema, caller, query));
|
|
416
|
+
exports.queriesFor = queriesFor;
|
|
417
|
+
// A parameter's value as the column of its type would store it.
|
|
418
|
+
const bindParam = (param, value, timeZone) => {
|
|
419
|
+
const column = {
|
|
420
|
+
name: param.name,
|
|
421
|
+
type: param.type,
|
|
422
|
+
required: param.required !== false
|
|
423
|
+
};
|
|
424
|
+
try {
|
|
425
|
+
return (0, dataCompiler_1.toStored)({ name: 'params' }, column, value, timeZone);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
if (error instanceof dataTypes_1.DataError) {
|
|
429
|
+
throw new dataTypes_1.DataError(error.message.replace(/^params\./, 'Parameter '));
|
|
430
|
+
}
|
|
431
|
+
throw error;
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
/**
|
|
435
|
+
* Runs a named query with `params`, up to `limit` rows (100 by default, 500 at
|
|
436
|
+
* most) and 1 MB. A query that would scan a table over 200,000 rows is refused
|
|
437
|
+
* before it runs: the database serves one request at a time.
|
|
438
|
+
*/
|
|
439
|
+
const runQuery = (runner, schema, caller, args) => {
|
|
440
|
+
if (!isPlainObject(args))
|
|
441
|
+
throw new dataTypes_1.DataError('Arguments are an object.');
|
|
442
|
+
const query = findQuery(runner, args.name ?? args.query);
|
|
443
|
+
if (!mayRun(schema, caller, query)) {
|
|
444
|
+
throw new dataTypes_1.DataError(`There is no named query "${query.name}" the assistant may run.`, 'not_found');
|
|
445
|
+
}
|
|
446
|
+
const given = args.params === undefined || args.params === null ? {} : args.params;
|
|
447
|
+
if (!isPlainObject(given)) {
|
|
448
|
+
throw new dataTypes_1.DataError('"params" is an object, e.g. { "from": "2026-10-01" }.');
|
|
449
|
+
}
|
|
450
|
+
const unknown = Object.keys(given).filter(key => !query.params.some(p => p.name === key));
|
|
451
|
+
if (unknown.length) {
|
|
452
|
+
throw new dataTypes_1.DataError(`${query.name} takes ${query.params.map(p => p.name).join(', ') || 'no parameters'}; not ${unknown.join(', ')}.`);
|
|
453
|
+
}
|
|
454
|
+
const limit = args.limit === undefined || args.limit === null
|
|
455
|
+
? exports.DATA_DEFAULT_QUERY_ROWS
|
|
456
|
+
: args.limit;
|
|
457
|
+
if (typeof limit !== 'number' ||
|
|
458
|
+
!Number.isInteger(limit) ||
|
|
459
|
+
limit < 1 ||
|
|
460
|
+
limit > dataTypes_1.DATA_MAX_PAGE) {
|
|
461
|
+
throw new dataTypes_1.DataError(`"limit" is 1 to ${dataTypes_1.DATA_MAX_PAGE}.`);
|
|
462
|
+
}
|
|
463
|
+
const values = query.params.map(param => bindParam(param, given[param.name], caller.timeZone));
|
|
464
|
+
const compiled = (0, exports.compileQuerySql)(query.sql);
|
|
465
|
+
// A full scan shows in the plan as "SCAN x", with or without "USING
|
|
466
|
+
// (COVERING) INDEX": either way it reads every row, and on a large table
|
|
467
|
+
// that is the request everyone else waits behind.
|
|
468
|
+
const scans = query.plan.some(line => {
|
|
469
|
+
const detail = line.trim();
|
|
470
|
+
return (detail.startsWith('SCAN ') &&
|
|
471
|
+
!detail.startsWith('SCAN CONSTANT ROW') &&
|
|
472
|
+
!detail.includes('VIRTUAL TABLE'));
|
|
473
|
+
});
|
|
474
|
+
if (scans) {
|
|
475
|
+
for (const name of query.tables) {
|
|
476
|
+
const n = Number(runner.all(`SELECT max(id) AS n FROM ${(0, dataIdentifiers_1.quoteIdentifier)(name)}`)[0]?.n ?? 0);
|
|
477
|
+
if (n > dataTypes_1.DATA_LARGE_TABLE_ROWS) {
|
|
478
|
+
throw new dataTypes_1.DataError(`${query.name} reads all of ${name}, which has about ${n.toLocaleString('en-US')} rows; filter on an indexed column (id, or a column marked filterable, unique or a relation).`, 'too_large');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const rows = [];
|
|
483
|
+
let bytes = 0;
|
|
484
|
+
let truncated = false;
|
|
485
|
+
try {
|
|
486
|
+
for (const row of runner.iterate(compiled.sql, values)) {
|
|
487
|
+
if (rows.length === limit) {
|
|
488
|
+
truncated = true;
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
const size = JSON.stringify(row).length;
|
|
492
|
+
if (rows.length > 0 && bytes + size > dataTypes_1.DATA_MAX_RESULT_BYTES) {
|
|
493
|
+
truncated = true;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
bytes += size;
|
|
497
|
+
rows.push({ ...row });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
throw new dataTypes_1.DataError(`${query.name} failed: ${sqliteMessage(error)}.`);
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
query: query.name,
|
|
505
|
+
rows,
|
|
506
|
+
...(truncated
|
|
507
|
+
? {
|
|
508
|
+
truncated: `stopped at ${rows.length} rows; narrow the parameters or ask the owner for a query that returns fewer`
|
|
509
|
+
}
|
|
510
|
+
: {})
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
exports.runQuery = runQuery;
|
package/dist/dataRpc.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DataCaller, DataDeleteImpact, DataResult, DataSchema, DataTable, DataTableCopy } from './dataTypes';
|
|
2
|
+
import type { DataQuery } from './dataQueries';
|
|
3
|
+
import type { DataTrigger, DataTriggerWithSecret, SavedTrigger } from './dataTriggers';
|
|
2
4
|
export type ExportFormat = 'jsonl' | 'csv';
|
|
3
5
|
export interface ExportFile {
|
|
4
6
|
table: string | null;
|
|
@@ -36,6 +38,7 @@ export interface DatabaseInfo {
|
|
|
36
38
|
initializedAt: string | null;
|
|
37
39
|
deletedAt: string | null;
|
|
38
40
|
firstWriteAt: string | null;
|
|
41
|
+
timeZone: string | null;
|
|
39
42
|
schemaVersion: number;
|
|
40
43
|
size: number;
|
|
41
44
|
tables: {
|
|
@@ -62,6 +65,9 @@ export interface ProjectDatabaseRpc {
|
|
|
62
65
|
size(): R<{
|
|
63
66
|
bytes: number;
|
|
64
67
|
}>;
|
|
68
|
+
setTimeZone(caller: DataCaller, args: {
|
|
69
|
+
timeZone: string | null;
|
|
70
|
+
}): R<DatabaseInfo>;
|
|
65
71
|
markDeleted(caller: DataCaller): R<{
|
|
66
72
|
deletedAt: string | null;
|
|
67
73
|
bookmark: string;
|
|
@@ -117,6 +123,44 @@ export interface ProjectDatabaseRpc {
|
|
|
117
123
|
limit?: number;
|
|
118
124
|
cursor?: number;
|
|
119
125
|
}): R<unknown>;
|
|
126
|
+
listQueries(caller: DataCaller): R<DataQuery[]>;
|
|
127
|
+
saveQuery(caller: DataCaller, input: unknown): R<DataQuery>;
|
|
128
|
+
dropQuery(caller: DataCaller, name: string): R<{
|
|
129
|
+
dropped: string;
|
|
130
|
+
}>;
|
|
131
|
+
runQuery(caller: DataCaller, args: {
|
|
132
|
+
name: string;
|
|
133
|
+
params?: Record<string, unknown>;
|
|
134
|
+
limit?: number;
|
|
135
|
+
}): R<{
|
|
136
|
+
query: string;
|
|
137
|
+
rows: Record<string, unknown>[];
|
|
138
|
+
truncated?: string;
|
|
139
|
+
}>;
|
|
140
|
+
listTriggers(caller: DataCaller): R<{
|
|
141
|
+
triggers: DataTrigger[];
|
|
142
|
+
pending: number;
|
|
143
|
+
}>;
|
|
144
|
+
getTrigger(nameOrId: string): R<DataTriggerWithSecret>;
|
|
145
|
+
saveTrigger(caller: DataCaller, input: unknown): R<SavedTrigger>;
|
|
146
|
+
dropTrigger(caller: DataCaller, name: string): R<{
|
|
147
|
+
dropped: string;
|
|
148
|
+
}>;
|
|
149
|
+
setTriggerEnabled(caller: DataCaller, args: {
|
|
150
|
+
name: string;
|
|
151
|
+
enabled: boolean;
|
|
152
|
+
reason?: string | null;
|
|
153
|
+
}): R<DataTrigger>;
|
|
154
|
+
replaceManagedTriggers(caller: DataCaller, inputs: unknown): R<{
|
|
155
|
+
triggers: DataTrigger[];
|
|
156
|
+
removed: string[];
|
|
157
|
+
secrets: Record<string, string>;
|
|
158
|
+
}>;
|
|
159
|
+
clearOutbox(args: {
|
|
160
|
+
before: number;
|
|
161
|
+
}): R<{
|
|
162
|
+
cleared: number;
|
|
163
|
+
}>;
|
|
120
164
|
exportStart(caller: DataCaller, args?: {
|
|
121
165
|
format?: ExportFormat;
|
|
122
166
|
}): R<{
|
package/dist/dataRpc.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataRpc.d.ts","sourceRoot":"","sources":["../src/dataRpc.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,UAAU,EACV,UAAU,EACV,SAAS,EACT,aAAa,EACd,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"dataRpc.d.ts","sourceRoot":"","sources":["../src/dataRpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DataCaller, DataSchema } from './dataTypes';
|
|
2
|
+
import type { DataQuery } from './dataQueries';
|
|
3
|
+
export declare const DATA_TOOL_DESCRIBE = "data-describe";
|
|
4
|
+
export declare const DATA_TOOL_QUERY = "data-query";
|
|
5
|
+
export declare const DATA_TOOL_WRITE = "data-write";
|
|
6
|
+
export declare const DATA_TOOL_ADMIN = "data-admin";
|
|
7
|
+
export declare const DATA_TOOL_NAMES: readonly ["data-describe", "data-query", "data-write", "data-admin"];
|
|
8
|
+
export declare const DATA_TOOL_TITLES: Record<string, string>;
|
|
9
|
+
export declare const DATA_TOOL_MAX_LIMIT = 100;
|
|
10
|
+
export interface DataToolDefinition {
|
|
11
|
+
name: string;
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
inputSchema: Record<string, unknown>;
|
|
15
|
+
annotations: {
|
|
16
|
+
readOnlyHint: boolean;
|
|
17
|
+
destructiveHint: boolean;
|
|
18
|
+
openWorldHint: boolean;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Whether the caller may use the data tools at all: it sees a table. */
|
|
22
|
+
export declare const hasDataToolTables: (schema: DataSchema, caller: DataCaller) => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* data-describe, data-query and, when the caller may add rows somewhere,
|
|
25
|
+
* data-write. Only tables the caller may see are named, so a hidden table
|
|
26
|
+
* doesn't exist for the model. `queries` are the named queries it may run.
|
|
27
|
+
*/
|
|
28
|
+
export declare const dataToolDefinitions: (schema: DataSchema, caller: DataCaller, options?: {
|
|
29
|
+
queries?: DataQuery[];
|
|
30
|
+
}) => DataToolDefinition[];
|
|
31
|
+
export declare const DATA_ADMIN_ACTIONS: readonly ["schema", "create_table", "update_table", "drop_table", "add_column", "update_column", "drop_column", "save_query", "drop_query", "import", "export"];
|
|
32
|
+
export type DataAdminAction = (typeof DATA_ADMIN_ACTIONS)[number];
|
|
33
|
+
/**
|
|
34
|
+
* data-admin: the owner's own MCP client managing the database's structure.
|
|
35
|
+
* One tool, so it costs one slot in the session it exists in; destructive, so
|
|
36
|
+
* every call goes through confirmation and, after outside content, the
|
|
37
|
+
* approval link.
|
|
38
|
+
*/
|
|
39
|
+
export declare const dataAdminToolDefinition: () => DataToolDefinition;
|
|
40
|
+
//# sourceMappingURL=dataTools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataTools.d.ts","sourceRoot":"","sources":["../src/dataTools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EAEX,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAClD,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,eAAe,eAAe,CAAC;AAC5C,eAAO,MAAM,eAAe,sEAKlB,CAAC;AAIX,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKnD,CAAC;AAIF,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,WAAW,EAAE;QACX,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,OAAO,CAAC;QACzB,aAAa,EAAE,OAAO,CAAC;KACxB,CAAC;CACH;AA2DD,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,GAAI,QAAQ,UAAU,EAAE,QAAQ,UAAU,YACpC,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,UAAU,EAClB,QAAQ,UAAU,EAClB,UAAS;IAAE,OAAO,CAAC,EAAE,SAAS,EAAE,CAAA;CAAO,KACtC,kBAAkB,EA6JpB,CAAC;AAEF,eAAO,MAAM,kBAAkB,iKAYrB,CAAC;AACX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,kBAsCzC,CAAC"}
|