@ganju/utils 0.0.16 → 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 (57) hide show
  1. package/dist/constants.d.ts +3 -0
  2. package/dist/constants.d.ts.map +1 -1
  3. package/dist/constants.js +12 -1
  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 +4 -0
  8. package/dist/data.d.ts.map +1 -1
  9. package/dist/data.js +26 -1
  10. package/dist/dataApi.d.ts +68 -1
  11. package/dist/dataApi.d.ts.map +1 -1
  12. package/dist/dataApi.js +45 -1
  13. package/dist/dataCompiler.d.ts.map +1 -1
  14. package/dist/dataCompiler.js +5 -1
  15. package/dist/dataCsv.d.ts +20 -0
  16. package/dist/dataCsv.d.ts.map +1 -0
  17. package/dist/dataCsv.js +102 -0
  18. package/dist/dataEngine.d.ts.map +1 -1
  19. package/dist/dataEngine.js +24 -4
  20. package/dist/dataImport.d.ts +2 -0
  21. package/dist/dataImport.d.ts.map +1 -1
  22. package/dist/dataImport.js +4 -1
  23. package/dist/dataQueries.d.ts +73 -0
  24. package/dist/dataQueries.d.ts.map +1 -0
  25. package/dist/dataQueries.js +513 -0
  26. package/dist/dataRpc.d.ts +19 -0
  27. package/dist/dataRpc.d.ts.map +1 -1
  28. package/dist/dataTools.d.ts +40 -0
  29. package/dist/dataTools.d.ts.map +1 -0
  30. package/dist/dataTools.js +280 -0
  31. package/dist/dataTypes.d.ts +2 -0
  32. package/dist/dataTypes.d.ts.map +1 -1
  33. package/dist/index.d.ts +86 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/schema.d.ts +91 -0
  37. package/dist/schema.d.ts.map +1 -1
  38. package/dist/schema.js +65 -1
  39. package/dist/sdkConstants.d.ts +1 -0
  40. package/dist/sdkConstants.d.ts.map +1 -1
  41. package/dist/sdkConstants.js +2 -1
  42. package/package.json +1 -1
  43. package/src/constants.ts +13 -1
  44. package/src/customCodeToken.ts +74 -0
  45. package/src/data.ts +34 -0
  46. package/src/dataApi.ts +53 -2
  47. package/src/dataCompiler.ts +5 -1
  48. package/src/dataCsv.ts +99 -0
  49. package/src/dataEngine.ts +24 -4
  50. package/src/dataImport.ts +3 -0
  51. package/src/dataQueries.ts +730 -0
  52. package/src/dataRpc.ts +11 -0
  53. package/src/dataTools.ts +338 -0
  54. package/src/dataTypes.ts +6 -0
  55. package/src/index.ts +9 -1
  56. package/src/schema.ts +69 -1
  57. package/src/sdkConstants.ts +1 -0
@@ -74,4 +74,6 @@ export declare const importRows: (table: Pick<DataTable, "name" | "columns">, ma
74
74
  lines?: number[];
75
75
  }) => ImportedRow[];
76
76
  export declare const csvLine: (cells: ImportCell[]) => string;
77
+ export { detectSeparator, parseCsv } from './dataCsv';
78
+ export type { ParsedCsv } from './dataCsv';
77
79
  //# sourceMappingURL=dataImport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dataImport.d.ts","sourceRoot":"","sources":["../src/dataImport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dataImport.d.ts","sourceRoot":"","sources":["../src/dataImport.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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;
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
4
  // Reading a spreadsheet into a table: the column types guessed for a new
5
5
  // table, the headers matched to an existing one's columns, and each cell
6
6
  // checked the way the database will check it. The dashboard's import and
@@ -448,3 +448,6 @@ const csvLine = (cells) => cells
448
448
  })
449
449
  .join(',');
450
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; } });
@@ -0,0 +1,73 @@
1
+ import type { DataCaller, DataSchema } from './dataTypes';
2
+ import type { SqlRow, SqlRunner } from './dataEngine';
3
+ export declare const DATA_QUERY_PARAM_TYPES: readonly ["text", "integer", "decimal", "boolean", "date", "datetime"];
4
+ export type DataQueryParamType = (typeof DATA_QUERY_PARAM_TYPES)[number];
5
+ export interface DataQueryParam {
6
+ name: string;
7
+ type: DataQueryParamType;
8
+ description?: string | null;
9
+ required?: boolean;
10
+ }
11
+ export interface DataQueryInput {
12
+ name: string;
13
+ description?: string | null;
14
+ sql: string;
15
+ params?: DataQueryParam[];
16
+ assistant?: boolean;
17
+ }
18
+ export interface DataQuery {
19
+ name: string;
20
+ description: string | null;
21
+ sql: string;
22
+ params: DataQueryParam[];
23
+ assistant: boolean;
24
+ tables: string[];
25
+ plan: string[];
26
+ updatedAt: string;
27
+ updatedBy: string | null;
28
+ }
29
+ export declare const DATA_MAX_QUERIES = 100;
30
+ export declare const DATA_MAX_QUERY_SQL = 10000;
31
+ export declare const DATA_MAX_QUERY_PARAMS = 20;
32
+ export declare const DATA_DEFAULT_QUERY_ROWS = 100;
33
+ export interface CompiledQuerySql {
34
+ sql: string;
35
+ params: string[];
36
+ }
37
+ /**
38
+ * Checks a named query's text and compiles its parameters to positions. Pure:
39
+ * what only SQLite can tell (that it reads, and what) is checked when it is
40
+ * saved, by checkQueryPlan.
41
+ */
42
+ export declare const compileQuerySql: (sql: unknown) => CompiledQuerySql;
43
+ export interface QueryPlanCheck {
44
+ tables: string[];
45
+ plan: string[];
46
+ }
47
+ /**
48
+ * Compiles the statement with EXPLAIN (nothing runs) and checks its program:
49
+ * it may not open anything for writing or start a write transaction, and every
50
+ * table it reads must be one of `schema`'s. Returns those tables and the plan.
51
+ */
52
+ export declare const checkQueryPlan: (runner: SqlRunner, schema: DataSchema, compiled: CompiledQuerySql) => QueryPlanCheck;
53
+ export declare const listQueries: (runner: SqlRunner) => DataQuery[];
54
+ /**
55
+ * Saves a named query, replacing one by the same name. Refused unless it
56
+ * passes both checks; the tables it reads and its plan are kept with it.
57
+ */
58
+ export declare const saveQuery: (runner: SqlRunner, schema: DataSchema, caller: DataCaller, input: unknown) => DataQuery;
59
+ export declare const dropQuery: (runner: SqlRunner, name: unknown) => {
60
+ dropped: string;
61
+ };
62
+ export declare const queriesFor: (runner: SqlRunner, schema: DataSchema, caller: DataCaller) => DataQuery[];
63
+ /**
64
+ * Runs a named query with `params`, up to `limit` rows (100 by default, 500 at
65
+ * most) and 1 MB. A query that would scan a table over 200,000 rows is refused
66
+ * before it runs: the database serves one request at a time.
67
+ */
68
+ export declare const runQuery: (runner: SqlRunner, schema: DataSchema, caller: DataCaller, args: unknown) => {
69
+ truncated?: string | undefined;
70
+ query: string;
71
+ rows: SqlRow[];
72
+ };
73
+ //# sourceMappingURL=dataQueries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataQueries.d.ts","sourceRoot":"","sources":["../src/dataQueries.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EACV,UAAU,EAEV,UAAU,EAEX,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEtD,eAAO,MAAM,sBAAsB,wEAOzB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE5B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAID,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;IAE1B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IAEnB,MAAM,EAAE,MAAM,EAAE,CAAC;IAGjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAsH3C,MAAM,WAAW,gBAAgB;IAE/B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,OAAO,KAAG,gBA2E9C,CAAC;AAgDF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,SAAS,EACjB,QAAQ,UAAU,EAClB,UAAU,gBAAgB,KACzB,cA0DF,CAAC;AAwCF,eAAO,MAAM,WAAW,GAAI,QAAQ,SAAS,KAAG,SAAS,EACc,CAAC;AA6DxE;;;GAGG;AACH,eAAO,MAAM,SAAS,GACpB,QAAQ,SAAS,EACjB,QAAQ,UAAU,EAClB,QAAQ,UAAU,EAClB,OAAO,OAAO,KACb,SA8CF,CAAC;AAEF,eAAO,MAAM,SAAS,GACpB,QAAQ,SAAS,EACjB,MAAM,OAAO,KACZ;IAAE,OAAO,EAAE,MAAM,CAAA;CAInB,CAAC;AAuBF,eAAO,MAAM,UAAU,GACrB,QAAQ,SAAS,EACjB,QAAQ,UAAU,EAClB,QAAQ,UAAU,KACjB,SAAS,EACwD,CAAC;AAuBrE;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,QAAQ,SAAS,EACjB,QAAQ,UAAU,EAClB,QAAQ,UAAU,EAClB,MAAM,OAAO;;;;CAgGd,CAAC"}
@@ -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,5 @@
1
1
  import type { DataCaller, DataDeleteImpact, DataResult, DataSchema, DataTable, DataTableCopy } from './dataTypes';
2
+ import type { DataQuery } from './dataQueries';
2
3
  export type ExportFormat = 'jsonl' | 'csv';
3
4
  export interface ExportFile {
4
5
  table: string | null;
@@ -36,6 +37,7 @@ export interface DatabaseInfo {
36
37
  initializedAt: string | null;
37
38
  deletedAt: string | null;
38
39
  firstWriteAt: string | null;
40
+ timeZone: string | null;
39
41
  schemaVersion: number;
40
42
  size: number;
41
43
  tables: {
@@ -62,6 +64,9 @@ export interface ProjectDatabaseRpc {
62
64
  size(): R<{
63
65
  bytes: number;
64
66
  }>;
67
+ setTimeZone(caller: DataCaller, args: {
68
+ timeZone: string | null;
69
+ }): R<DatabaseInfo>;
65
70
  markDeleted(caller: DataCaller): R<{
66
71
  deletedAt: string | null;
67
72
  bookmark: string;
@@ -117,6 +122,20 @@ export interface ProjectDatabaseRpc {
117
122
  limit?: number;
118
123
  cursor?: number;
119
124
  }): R<unknown>;
125
+ listQueries(caller: DataCaller): R<DataQuery[]>;
126
+ saveQuery(caller: DataCaller, input: unknown): R<DataQuery>;
127
+ dropQuery(caller: DataCaller, name: string): R<{
128
+ dropped: string;
129
+ }>;
130
+ runQuery(caller: DataCaller, args: {
131
+ name: string;
132
+ params?: Record<string, unknown>;
133
+ limit?: number;
134
+ }): R<{
135
+ query: string;
136
+ rows: Record<string, unknown>[];
137
+ truncated?: string;
138
+ }>;
120
139
  exportStart(caller: DataCaller, args?: {
121
140
  format?: ExportFormat;
122
141
  }): R<{