@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.
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -1
- package/dist/customCodeToken.d.ts +11 -0
- package/dist/customCodeToken.d.ts.map +1 -1
- package/dist/customCodeToken.js +41 -1
- package/dist/data.d.ts +4 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +26 -1
- package/dist/dataApi.d.ts +68 -1
- package/dist/dataApi.d.ts.map +1 -1
- package/dist/dataApi.js +45 -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/dataEngine.d.ts.map +1 -1
- package/dist/dataEngine.js +24 -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 +19 -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/dataTypes.d.ts +2 -0
- package/dist/dataTypes.d.ts.map +1 -1
- package/dist/index.d.ts +86 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/schema.d.ts +91 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +65 -1
- package/dist/sdkConstants.d.ts +1 -0
- package/dist/sdkConstants.d.ts.map +1 -1
- package/dist/sdkConstants.js +2 -1
- package/package.json +1 -1
- package/src/constants.ts +13 -1
- package/src/customCodeToken.ts +74 -0
- package/src/data.ts +34 -0
- package/src/dataApi.ts +53 -2
- package/src/dataCompiler.ts +5 -1
- package/src/dataCsv.ts +99 -0
- package/src/dataEngine.ts +24 -4
- package/src/dataImport.ts +3 -0
- package/src/dataQueries.ts +730 -0
- package/src/dataRpc.ts +11 -0
- package/src/dataTools.ts +338 -0
- package/src/dataTypes.ts +6 -0
- package/src/index.ts +9 -1
- package/src/schema.ts +69 -1
- 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 }
|
package/src/dataTools.ts
ADDED
|
@@ -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 {
|
|
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,
|
package/src/sdkConstants.ts
CHANGED
|
@@ -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
|