@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
package/src/dataRpc.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  DataTable,
10
10
  DataTableCopy
11
11
  } from './dataTypes';
12
+ import type { DataQuery } from './dataQueries';
12
13
 
13
14
  export type ExportFormat = 'jsonl' | 'csv';
14
15
 
@@ -50,6 +51,8 @@ export interface DatabaseInfo {
50
51
  initializedAt: string | null;
51
52
  deletedAt: string | null;
52
53
  firstWriteAt: string | null;
54
+ // The zone set for the database, or null for UTC.
55
+ timeZone: string | null;
53
56
  schemaVersion: number;
54
57
  size: number;
55
58
  tables: { name: string; label: string | null; columns: number }[];
@@ -70,6 +73,7 @@ export interface ProjectDatabaseRpc {
70
73
  info(): R<DatabaseInfo>;
71
74
  rename(args: { name: string }): R<DatabaseInfo>;
72
75
  size(): R<{ bytes: number }>;
76
+ setTimeZone(caller: DataCaller, args: { timeZone: string | null }): R<DatabaseInfo>;
73
77
  markDeleted(
74
78
  caller: DataCaller
75
79
  ): R<{ deletedAt: string | null; bookmark: string }>;
@@ -113,6 +117,13 @@ export interface ProjectDatabaseRpc {
113
117
  caller: DataCaller,
114
118
  args: { table: string; id: number; limit?: number; cursor?: number }
115
119
  ): R<unknown>;
120
+ listQueries(caller: DataCaller): R<DataQuery[]>;
121
+ saveQuery(caller: DataCaller, input: unknown): R<DataQuery>;
122
+ dropQuery(caller: DataCaller, name: string): R<{ dropped: string }>;
123
+ runQuery(
124
+ caller: DataCaller,
125
+ args: { name: string; params?: Record<string, unknown>; limit?: number }
126
+ ): R<{ query: string; rows: Record<string, unknown>[]; truncated?: string }>;
116
127
  exportStart(
117
128
  caller: DataCaller,
118
129
  args?: { format?: ExportFormat }
@@ -0,0 +1,338 @@
1
+ // The data tools as a model sees them: names, descriptions and input schemas,
2
+ // built from the database's schema for one caller. The MCP worker registers
3
+ // them and the tool-design eval measures them, so the text the eval scored is
4
+ // the text that ships.
5
+ import { DATA_ACCESS_RANK } from './dataTypes';
6
+ import type {
7
+ DataCaller,
8
+ DataColumn,
9
+ DataSchema,
10
+ DataTable
11
+ } from './dataTypes';
12
+ import type { DataQuery } from './dataQueries';
13
+
14
+ export const DATA_TOOL_DESCRIBE = 'data-describe';
15
+ export const DATA_TOOL_QUERY = 'data-query';
16
+ export const DATA_TOOL_WRITE = 'data-write';
17
+ export const DATA_TOOL_ADMIN = 'data-admin';
18
+ export const DATA_TOOL_NAMES = [
19
+ DATA_TOOL_DESCRIBE,
20
+ DATA_TOOL_QUERY,
21
+ DATA_TOOL_WRITE,
22
+ DATA_TOOL_ADMIN
23
+ ] as const;
24
+
25
+ // How Tool health names them: they have no install row, and aren't a name
26
+ // the model made up.
27
+ export const DATA_TOOL_TITLES: Record<string, string> = {
28
+ [DATA_TOOL_DESCRIBE]: 'Describe the database',
29
+ [DATA_TOOL_QUERY]: 'Query the database',
30
+ [DATA_TOOL_WRITE]: 'Change rows in the database',
31
+ [DATA_TOOL_ADMIN]: 'Manage the database'
32
+ };
33
+
34
+ // How many rows data-query returns at most. The engine pages up to 500; a
35
+ // model reading 500 rows answers worse, not better.
36
+ export const DATA_TOOL_MAX_LIMIT = 100;
37
+
38
+ export interface DataToolDefinition {
39
+ name: string;
40
+ title: string;
41
+ description: string;
42
+ inputSchema: Record<string, unknown>;
43
+ annotations: {
44
+ readOnlyHint: boolean;
45
+ destructiveHint: boolean;
46
+ openWorldHint: boolean;
47
+ };
48
+ }
49
+
50
+ const READS = {
51
+ readOnlyHint: true,
52
+ destructiveHint: false,
53
+ openWorldHint: false
54
+ };
55
+ // One annotation per tool: a write may delete, so the tool says it is
56
+ // destructive, and the runner and the MCP confirmation decide per call.
57
+ const CHANGES = {
58
+ readOnlyHint: false,
59
+ destructiveHint: true,
60
+ openWorldHint: false
61
+ };
62
+
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".';
65
+
66
+ const accessOf = (caller: DataCaller, table: DataTable) =>
67
+ caller.restricted ? table.access : 'full';
68
+
69
+ const visible = (schema: DataSchema, caller: DataCaller) =>
70
+ schema.tables.filter(table => DATA_ACCESS_RANK[accessOf(caller, table)] >= 1);
71
+
72
+ // One column as data-query's description lists it: the type when it isn't
73
+ // plain text, so the model can write filters without guessing names.
74
+ const columnHint = (column: DataColumn) =>
75
+ column.type === 'relation'
76
+ ? `${column.name} (→ ${column.table})`
77
+ : column.type === 'enum'
78
+ ? `${column.name} (${(column.values ?? []).join('|')})`
79
+ : column.type === 'text' || column.type === 'long_text'
80
+ ? column.name
81
+ : `${column.name} (${column.type}${column.min !== null ? `, ≥ ${column.min}` : ''})`;
82
+
83
+ const tableLine = (caller: DataCaller, table: DataTable) => {
84
+ const access = accessOf(caller, table);
85
+ const description = table.description?.trim().replace(/\.$/, '');
86
+ return `- ${table.name}${description ? `: ${description}.` : '.'}${
87
+ caller.restricted && table.customerScoped
88
+ ? " Only this customer's own rows are visible."
89
+ : ''
90
+ }${access === 'read' ? ' Read only.' : access === 'add' ? ' Rows can be added, not changed.' : ''} Columns: ${table.columns
91
+ .map(columnHint)
92
+ .join(', ')}.`;
93
+ };
94
+
95
+ const queryLine = (query: DataQuery) =>
96
+ `- ${query.name}${query.description ? `: ${query.description.replace(/\.$/, '')}.` : '.'}${
97
+ query.params.length
98
+ ? ` Params: ${query.params
99
+ .map(
100
+ p =>
101
+ `${p.name} (${p.type}${p.required === false ? ', optional' : ''})`
102
+ )
103
+ .join(', ')}.`
104
+ : ''
105
+ }`;
106
+
107
+ /** Whether the caller may use the data tools at all: it sees a table. */
108
+ export const hasDataToolTables = (schema: DataSchema, caller: DataCaller) =>
109
+ visible(schema, caller).length > 0;
110
+
111
+ /**
112
+ * data-describe, data-query and, when the caller may add rows somewhere,
113
+ * data-write. Only tables the caller may see are named, so a hidden table
114
+ * doesn't exist for the model. `queries` are the named queries it may run.
115
+ */
116
+ export const dataToolDefinitions = (
117
+ schema: DataSchema,
118
+ caller: DataCaller,
119
+ options: { queries?: DataQuery[] } = {}
120
+ ): DataToolDefinition[] => {
121
+ const tables = visible(schema, caller);
122
+ if (tables.length === 0) return [];
123
+ const queries = options.queries ?? [];
124
+ const timeZone = caller.timeZone || 'UTC';
125
+ const tools: DataToolDefinition[] = [
126
+ {
127
+ name: DATA_TOOL_DESCRIBE,
128
+ title: DATA_TOOL_TITLES[DATA_TOOL_DESCRIBE],
129
+ description:
130
+ "Describe the project's database: its tables, their columns (type, allowed values, which table a relation points to, the owner's description of each), row counts, which columns full-text search covers, and what the assistant may do with each table (read, add, or full). Call it before querying a table whose columns you haven't seen in this conversation.",
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ table: {
135
+ type: 'string',
136
+ description:
137
+ 'One table to describe. Leave out to describe all of them.'
138
+ }
139
+ }
140
+ },
141
+ annotations: READS
142
+ },
143
+ {
144
+ name: DATA_TOOL_QUERY,
145
+ title: DATA_TOOL_TITLES[DATA_TOOL_QUERY],
146
+ description: [
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.',
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
+ `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
+ 'Tables:',
152
+ tables.map(table => tableLine(caller, table)).join('\n'),
153
+ ...(queries.length
154
+ ? [
155
+ 'Saved reports: instead of "table", pass "query" with its name and "params"; they return exactly what the report computes.',
156
+ queries.map(queryLine).join('\n')
157
+ ]
158
+ : [])
159
+ ].join('\n'),
160
+ inputSchema: {
161
+ type: 'object',
162
+ properties: {
163
+ table: { type: 'string', description: 'Table to read.' },
164
+ where: { type: 'object', description: WHERE_HELP },
165
+ search: {
166
+ type: 'string',
167
+ description:
168
+ "Words to look for in the table's searchable text columns (accents and case ignored, ranked by relevance). Use it when the user names something loosely; use where for exact values."
169
+ },
170
+ sort: {
171
+ type: 'array',
172
+ items: { type: 'string' },
173
+ description:
174
+ 'Columns to sort by, "-" first for descending, e.g. ["-created_at"]. Relation paths allowed.'
175
+ },
176
+ limit: {
177
+ type: 'integer',
178
+ description: `Rows to return, 1 to ${DATA_TOOL_MAX_LIMIT}. Default 20.`
179
+ },
180
+ cursor: {
181
+ type: 'string',
182
+ description:
183
+ 'nextCursor from the previous call, to get the next page.'
184
+ },
185
+ include: {
186
+ type: 'array',
187
+ items: { type: 'string' },
188
+ description:
189
+ 'Relation columns to expand into the full related row, e.g. ["customer"].'
190
+ },
191
+ aggregate: {
192
+ type: 'array',
193
+ description:
194
+ 'Compute instead of listing rows. Each item is { "fn": "count" } or { "fn": "sum"|"avg"|"min"|"max", "column": "..." } (relation paths allowed, e.g. "order.total"). Add "times": "<number column>" to aggregate a product, e.g. { "fn": "sum", "column": "stock", "times": "unit_cost" } for stock value.',
195
+ items: {
196
+ type: 'object',
197
+ properties: {
198
+ fn: {
199
+ type: 'string',
200
+ enum: ['count', 'sum', 'avg', 'min', 'max']
201
+ },
202
+ column: { type: 'string' },
203
+ times: { type: 'string' }
204
+ },
205
+ required: ['fn']
206
+ }
207
+ },
208
+ groupBy: {
209
+ type: 'array',
210
+ items: { type: 'string' },
211
+ description:
212
+ 'With aggregate: columns to group by, e.g. ["status"] or ["product"]. Groups come back largest first.'
213
+ },
214
+ ...(queries.length
215
+ ? {
216
+ query: {
217
+ type: 'string',
218
+ enum: queries.map(q => q.name),
219
+ description:
220
+ 'A saved report to run instead of reading a table.'
221
+ },
222
+ params: {
223
+ type: 'object',
224
+ description:
225
+ 'The saved report\'s parameters, e.g. { "from": "2026-10-01" }.'
226
+ }
227
+ }
228
+ : {})
229
+ },
230
+ ...(queries.length ? {} : { required: ['table'] })
231
+ },
232
+ annotations: READS
233
+ }
234
+ ];
235
+ if (
236
+ tables.some(
237
+ table => DATA_ACCESS_RANK[accessOf(caller, table)] >= DATA_ACCESS_RANK.add
238
+ )
239
+ ) {
240
+ tools.push({
241
+ name: DATA_TOOL_WRITE,
242
+ title: DATA_TOOL_TITLES[DATA_TOOL_WRITE],
243
+ description: [
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).',
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
+ '- delete: { "op": "delete", "table": "...", "id": 12 } (or "where").',
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).',
249
+ 'Only use ids you got from data-query or from an earlier insert; never guess them.'
250
+ ].join('\n'),
251
+ inputSchema: {
252
+ type: 'object',
253
+ properties: {
254
+ operations: {
255
+ type: 'array',
256
+ items: {
257
+ type: 'object',
258
+ properties: {
259
+ op: { type: 'string', enum: ['insert', 'update', 'delete'] },
260
+ table: { type: 'string' },
261
+ id: { type: 'integer' },
262
+ values: { type: 'object' },
263
+ set: { type: 'object' },
264
+ where: { type: 'object' },
265
+ expect: { type: 'integer' }
266
+ },
267
+ required: ['op', 'table']
268
+ }
269
+ }
270
+ },
271
+ required: ['operations']
272
+ },
273
+ annotations: CHANGES
274
+ });
275
+ }
276
+ return tools;
277
+ };
278
+
279
+ export const DATA_ADMIN_ACTIONS = [
280
+ 'schema',
281
+ 'create_table',
282
+ 'update_table',
283
+ 'drop_table',
284
+ 'add_column',
285
+ 'update_column',
286
+ 'drop_column',
287
+ 'save_query',
288
+ 'drop_query',
289
+ 'import',
290
+ 'export'
291
+ ] as const;
292
+ export type DataAdminAction = (typeof DATA_ADMIN_ACTIONS)[number];
293
+
294
+ /**
295
+ * data-admin: the owner's own MCP client managing the database's structure.
296
+ * One tool, so it costs one slot in the session it exists in; destructive, so
297
+ * every call goes through confirmation and, after outside content, the
298
+ * approval link.
299
+ */
300
+ export const dataAdminToolDefinition = (): DataToolDefinition => ({
301
+ name: DATA_TOOL_ADMIN,
302
+ title: DATA_TOOL_TITLES[DATA_TOOL_ADMIN],
303
+ description: [
304
+ "Manage the project's database structure as its owner: tables, columns, indexes, descriptions, saved reports, imports and exports. To read or change rows use data-query and data-write.",
305
+ '- schema: every table as it would be written again (columns with type, required, unique, filterable, searchable, description, enum values, relation target and minimum), plus the saved reports.',
306
+ '- create_table: "input" is { name, description, displayColumn, access (none|read|add|full: what channel bots may do), customerScoped, columns: [{ name, type, ... }] }. Types: text, long_text, integer, decimal, boolean, date, datetime, enum (with "values"), json, email, url, phone, relation (with "table" and "onDelete": restrict|cascade|set_null), file.',
307
+ '- update_table: "table" and "input" with what changes: name, description, displayColumn, access, customerScoped, history.',
308
+ '- drop_table: "table". It can be undone with a restore from the dashboard.',
309
+ '- add_column: "table" and "input" as one column of create_table.',
310
+ '- update_column: "table", "column" and "input" with what changes: name, type, description, required, unique, filterable (adds an index), searchable, values, min, default. A type change copies the table and refuses if a value doesn\'t fit.',
311
+ '- drop_column: "table" and "column".',
312
+ '- save_query: "input" is { name, description, sql, params: [{ name, type: text|integer|decimal|boolean|date|datetime, description }], assistant }. sql is one SELECT with named parameters (:from); it is checked to only read. assistant: true lets channel bots run it.',
313
+ '- drop_query: "name".',
314
+ '- import: "table" and "resource", the uri of a CSV or Excel file in the project\'s knowledge ("sheet" picks one, the first by default). Headers are matched to columns by name or label, and relation columns by the related row\'s name; rows are added, not replaced. Up to 5 MB.',
315
+ '- export: start a copy of the whole database (format jsonl or csv); download it from the Data page.'
316
+ ].join('\n'),
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {
320
+ action: { type: 'string', enum: [...DATA_ADMIN_ACTIONS] },
321
+ table: { type: 'string' },
322
+ column: { type: 'string' },
323
+ name: { type: 'string', description: 'drop_query: the saved report.' },
324
+ resource: {
325
+ type: 'string',
326
+ description: 'import: the CSV or Excel resource uri.'
327
+ },
328
+ sheet: {
329
+ type: 'string',
330
+ description: 'import: the Excel sheet; the first by default.'
331
+ },
332
+ format: { type: 'string', enum: ['jsonl', 'csv'] },
333
+ input: { type: 'object' }
334
+ },
335
+ required: ['action']
336
+ },
337
+ annotations: CHANGES
338
+ });
package/src/dataTypes.ts CHANGED
@@ -104,6 +104,9 @@ export interface DataTable {
104
104
 
105
105
  export interface DataSchema {
106
106
  tables: DataTable[];
107
+ // The zone the owner set for the database: datetimes come back in it for
108
+ // callers that send none. Null when unset (UTC).
109
+ timeZone?: string | null;
107
110
  }
108
111
 
109
112
  // Who is calling and under which rules. `actor` is recorded on every row a
@@ -116,6 +119,9 @@ export interface DataCaller {
116
119
  restricted?: boolean;
117
120
  // Customer-scoped tables: fill and filter `participant_id` with this.
118
121
  participantId?: string;
122
+ // Only these tables exist for the caller (a function's `data.tables`):
123
+ // every other one is hidden, through relations and filters too.
124
+ tables?: string[];
119
125
  // Datetimes come back in this zone, with the offset.
120
126
  timeZone?: string;
121
127
  limits?: DataCallerLimits;
package/src/index.ts CHANGED
@@ -109,12 +109,17 @@ import {
109
109
  import {
110
110
  mintCustomCodeToken,
111
111
  verifyCustomCodeToken,
112
+ mintCustomCodeCallToken,
113
+ verifyCustomCodeCallToken,
112
114
  customCodeScriptName,
113
115
  customCodePreviewScriptName,
114
116
  customCodeUploadName,
115
117
  customCodePreviewUploadName
116
118
  } from './customCodeToken';
117
- import type { CustomCodeTokenPayload } from './customCodeToken';
119
+ import type {
120
+ CustomCodeCallPayload,
121
+ CustomCodeTokenPayload
122
+ } from './customCodeToken';
118
123
  import {
119
124
  mintContainmentToken,
120
125
  verifyContainmentToken
@@ -411,6 +416,8 @@ export const utils = {
411
416
  listResourcesPage,
412
417
  mintCustomCodeToken,
413
418
  verifyCustomCodeToken,
419
+ mintCustomCodeCallToken,
420
+ verifyCustomCodeCallToken,
414
421
  customCodeScriptName,
415
422
  customCodePreviewScriptName,
416
423
  customCodeUploadName,
@@ -657,6 +664,7 @@ export type {
657
664
  CustomCodeManifest,
658
665
  CustomCodeSendFile,
659
666
  CustomCodeCreateResource,
667
+ CustomCodeCallPayload,
660
668
  CustomCodeTokenPayload,
661
669
  ContainmentTokenPayload,
662
670
  OAuthProviderConfig,
package/src/schema.ts CHANGED
@@ -707,6 +707,23 @@ const DATA_RESTORE_POINT_BODY = z.union([
707
707
  z.strictObject({ bookmark: z.string().min(1).max(200) })
708
708
  ]);
709
709
 
710
+ // The database's own settings. `timeZone` is an IANA zone (America/Bogota),
711
+ // or null for UTC: datetimes come back in it for callers that send none.
712
+ const DATA_DATABASE_PATCH = z.strictObject({
713
+ timeZone: z
714
+ .string()
715
+ .max(100)
716
+ .refine(zone => {
717
+ try {
718
+ new Intl.DateTimeFormat('en-US', { timeZone: zone });
719
+ return true;
720
+ } catch {
721
+ return false;
722
+ }
723
+ }, 'Use an IANA time zone, such as America/Bogota.')
724
+ .nullable()
725
+ });
726
+
710
727
  const DATA_EXPORT_BODY = z.strictObject({
711
728
  format: z.enum(['jsonl', 'csv']).optional()
712
729
  });
@@ -726,11 +743,36 @@ const DATA_CHANGE_LIST_QUERY = z.strictObject({
726
743
  cursor: z.uuid().optional()
727
744
  });
728
745
 
746
+ // Named queries: a SELECT the owner writes once, with named parameters
747
+ // (`:from`), checked to only read when it is saved.
748
+ const DATA_QUERY_PARAM = z.strictObject({
749
+ name: DATA_IDENTIFIER,
750
+ type: z.enum(['text', 'integer', 'decimal', 'boolean', 'date', 'datetime']),
751
+ description: z.string().max(300).nullish(),
752
+ required: z.boolean().optional()
753
+ });
754
+
755
+ const DATA_QUERY_INPUT = z.strictObject({
756
+ name: DATA_IDENTIFIER,
757
+ description: z.string().max(300).nullish(),
758
+ sql: z.string().min(1).max(10_000),
759
+ params: z.array(DATA_QUERY_PARAM).max(20).optional(),
760
+ // Channel bots may run it from data-query.
761
+ assistant: z.boolean().optional()
762
+ });
763
+
764
+ const DATA_QUERY_RUN_BODY = z.strictObject({
765
+ params: z.record(z.string(), z.unknown()).optional(),
766
+ limit: z.number().int().min(1).max(500).optional(),
767
+ timeZone: z.string().max(100).optional()
768
+ });
769
+
729
770
  const DATA_TABLE_PARAMS = DATA_DATABASE_GET.extend({ table: DATA_IDENTIFIER });
730
771
  const DATA_COLUMN_PARAMS = DATA_TABLE_PARAMS.extend({
731
772
  column: DATA_IDENTIFIER
732
773
  });
733
774
  const DATA_ROW_PARAMS = DATA_TABLE_PARAMS.extend({ id: DATA_ROW_ID });
775
+ const DATA_QUERY_PATH_PARAMS = DATA_DATABASE_GET.extend({ name: DATA_IDENTIFIER });
734
776
 
735
777
  const ARTIFACT_UPLOAD_RESOURCE_FILE = z.object({
736
778
  resourceId: z.uuid(),
@@ -1362,7 +1404,17 @@ const CUSTOM_CODE_TOOL = z.object({
1362
1404
  description: z.string().max(2000).optional(),
1363
1405
  inputSchema: SCHEMA_DEFINITION.default({ type: 'object', properties: {} }),
1364
1406
  outputSchema: SCHEMA_DEFINITION.optional(),
1365
- annotations: TOOL_ANNOTATIONS.optional()
1407
+ annotations: TOOL_ANNOTATIONS.optional(),
1408
+ // What this function may do through ctx.data. Absent means everything: it
1409
+ // is the owner's code. Narrowing matters most for a function a channel bot
1410
+ // calls after reading customer text, and the broker enforces it, since
1411
+ // anything in the script can be edited.
1412
+ data: z
1413
+ .strictObject({
1414
+ access: z.enum(['read', 'write']).optional(),
1415
+ tables: z.array(z.string().min(1).max(63)).max(50).optional()
1416
+ })
1417
+ .optional()
1366
1418
  });
1367
1419
 
1368
1420
  // The manifest a `ganju deploy` (or the dashboard) uploads alongside a bundle.
@@ -1611,6 +1663,16 @@ const CUSTOM_CODE_BROKER_SECRET = z.object({
1611
1663
  name: z.string().min(1).max(200)
1612
1664
  });
1613
1665
 
1666
+ // One ctx.data call. `call` is the token the dispatcher signed for the
1667
+ // function being run, which the script hands back: the broker reads the
1668
+ // function's name from it, never from the script, and applies that
1669
+ // function's `data` narrowing.
1670
+ const CUSTOM_CODE_BROKER_DATA = z.object({
1671
+ call: z.string().min(1).max(4000),
1672
+ op: z.enum(['describe', 'query', 'write', 'run']),
1673
+ args: z.record(z.string(), z.unknown()).default({})
1674
+ });
1675
+
1614
1676
  const CUSTOM_CODE_BROKER_RESOURCE_SEARCH = z.object({
1615
1677
  query: z.string().min(1).max(2000),
1616
1678
  limit: z.number().int().positive().max(20).default(5)
@@ -1971,6 +2033,11 @@ export const Schema = {
1971
2033
  DATA_EXPORT_FILE_QUERY,
1972
2034
  DATA_CHANGE_LIST_QUERY,
1973
2035
  DATA_TABLE_PARAMS,
2036
+ DATA_DATABASE_PATCH,
2037
+ DATA_QUERY_PARAM,
2038
+ DATA_QUERY_INPUT,
2039
+ DATA_QUERY_RUN_BODY,
2040
+ DATA_QUERY_PATH_PARAMS,
1974
2041
  DATA_COLUMN_PARAMS,
1975
2042
  DATA_ROW_PARAMS,
1976
2043
  ARTIFACT_REMOVE_RESOURCE,
@@ -2034,6 +2101,7 @@ export const Schema = {
2034
2101
  CUSTOM_CODE_INVOKE_RESPONSE,
2035
2102
  CUSTOM_CODE_BROKER_CONNECTION,
2036
2103
  CUSTOM_CODE_BROKER_SECRET,
2104
+ CUSTOM_CODE_BROKER_DATA,
2037
2105
  CUSTOM_CODE_BROKER_RESOURCE_SEARCH,
2038
2106
  CUSTOM_CODE_BROKER_RESOURCE_READ,
2039
2107
  CUSTOM_CODE_BROKER_RESOURCE_CREATE,
@@ -29,6 +29,7 @@ export const CUSTOM_CODE_BROKER_PATH_RESOURCES_LIST = '/resources/list';
29
29
  export const CUSTOM_CODE_BROKER_PATH_RESOURCES_CREATE = '/resources/create';
30
30
  export const CUSTOM_CODE_BROKER_PATH_RESOURCES_DELETE = '/resources/delete';
31
31
  export const CUSTOM_CODE_BROKER_PATH_SEND_FILE = '/send-file';
32
+ export const CUSTOM_CODE_BROKER_PATH_DATA = '/data';
32
33
 
33
34
  // Injected at upload time and echoed back by the health probe below. Uploading a
34
35
  // script into the dispatch namespace is not read-your-writes: dispatching to the