@mindstudio-ai/agent 0.1.102 → 0.1.104
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/cli.js +579 -32
- package/dist/index.d.ts +315 -64
- package/dist/index.js +536 -25
- package/dist/index.js.map +1 -1
- package/dist/postinstall.js +579 -32
- package/llms.txt +40 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -462,6 +462,59 @@ function buildSelect(table, options = {}) {
|
|
|
462
462
|
if (options.offset != null) sql += ` OFFSET ${options.offset}`;
|
|
463
463
|
return { sql, params: params.length > 0 ? params : void 0 };
|
|
464
464
|
}
|
|
465
|
+
var SQL_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
466
|
+
function assertSqlIdentifier(name, context) {
|
|
467
|
+
if (!SQL_IDENTIFIER_RE.test(name)) {
|
|
468
|
+
throw new MindStudioError(
|
|
469
|
+
`Invalid ${context}: "${name}" \u2014 must be a plain identifier (letters, digits, _ and $, not starting with a digit).`,
|
|
470
|
+
"invalid_identifier",
|
|
471
|
+
400
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function aggregateExpr(fn, column) {
|
|
476
|
+
switch (fn) {
|
|
477
|
+
case "count":
|
|
478
|
+
return "COUNT(*)";
|
|
479
|
+
case "sum":
|
|
480
|
+
return `TOTAL(${column})`;
|
|
481
|
+
case "avg":
|
|
482
|
+
return `AVG(${column})`;
|
|
483
|
+
case "min":
|
|
484
|
+
return `MIN(${column})`;
|
|
485
|
+
case "max":
|
|
486
|
+
return `MAX(${column})`;
|
|
487
|
+
case "countDistinct":
|
|
488
|
+
return `COUNT(DISTINCT ${column})`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function buildScalarAggregate(table, fn, column, where, whereParams) {
|
|
492
|
+
if (column != null) assertSqlIdentifier(column, "aggregate column");
|
|
493
|
+
let sql = `SELECT ${aggregateExpr(fn, column ?? void 0)} AS value FROM ${table}`;
|
|
494
|
+
if (where) sql += ` WHERE ${where}`;
|
|
495
|
+
return { sql, params: whereParams?.length ? whereParams : void 0 };
|
|
496
|
+
}
|
|
497
|
+
function buildGroupedAggregate(table, options) {
|
|
498
|
+
for (const col of options.by) assertSqlIdentifier(col, "group-by column");
|
|
499
|
+
for (const term of options.select) {
|
|
500
|
+
assertSqlIdentifier(term.alias, "aggregate alias");
|
|
501
|
+
if (term.column != null) assertSqlIdentifier(term.column, "aggregate column");
|
|
502
|
+
}
|
|
503
|
+
if (options.orderBy != null) assertSqlIdentifier(options.orderBy, "aggregate order key");
|
|
504
|
+
const selectList = [
|
|
505
|
+
...options.by,
|
|
506
|
+
...options.select.map((t) => `${aggregateExpr(t.fn, t.column)} AS ${t.alias}`)
|
|
507
|
+
].join(", ");
|
|
508
|
+
let sql = `SELECT ${selectList} FROM ${table}`;
|
|
509
|
+
if (options.where) sql += ` WHERE ${options.where}`;
|
|
510
|
+
if (options.by.length > 0) sql += ` GROUP BY ${options.by.join(", ")}`;
|
|
511
|
+
if (options.orderBy) sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
|
|
512
|
+
if (options.limit != null) sql += ` LIMIT ${options.limit}`;
|
|
513
|
+
return {
|
|
514
|
+
sql,
|
|
515
|
+
params: options.whereParams?.length ? options.whereParams : void 0
|
|
516
|
+
};
|
|
517
|
+
}
|
|
465
518
|
function buildExists(table, where, whereParams, negate) {
|
|
466
519
|
const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
|
|
467
520
|
const fn = negate ? "NOT EXISTS" : "EXISTS";
|
|
@@ -1119,6 +1172,187 @@ function isComparisonOp(value) {
|
|
|
1119
1172
|
return value in JS_TO_SQL_OP;
|
|
1120
1173
|
}
|
|
1121
1174
|
|
|
1175
|
+
// src/db/aggregate.ts
|
|
1176
|
+
var IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
1177
|
+
var AGGREGATE_FNS = [
|
|
1178
|
+
"count",
|
|
1179
|
+
"sum",
|
|
1180
|
+
"avg",
|
|
1181
|
+
"min",
|
|
1182
|
+
"max",
|
|
1183
|
+
"countDistinct"
|
|
1184
|
+
];
|
|
1185
|
+
function normalizeAggregateSpec(spec) {
|
|
1186
|
+
const by = [...spec.by ?? []];
|
|
1187
|
+
for (const col of by) {
|
|
1188
|
+
if (typeof col !== "string" || !IDENTIFIER_RE.test(col)) {
|
|
1189
|
+
throw new MindStudioError(
|
|
1190
|
+
`aggregate(): invalid group-by column "${col}" \u2014 column names are plain identifiers.`,
|
|
1191
|
+
"invalid_column",
|
|
1192
|
+
400
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const aliases = Object.keys(spec.select ?? {});
|
|
1197
|
+
if (aliases.length === 0) {
|
|
1198
|
+
throw new MindStudioError(
|
|
1199
|
+
"aggregate(): select must contain at least one aggregate, e.g. { n: { count: true } }.",
|
|
1200
|
+
"invalid_aggregate_spec",
|
|
1201
|
+
400
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const select = aliases.map((alias) => {
|
|
1205
|
+
if (!IDENTIFIER_RE.test(alias)) {
|
|
1206
|
+
throw new MindStudioError(
|
|
1207
|
+
`aggregate(): invalid alias "${alias}" \u2014 result property names are plain identifiers.`,
|
|
1208
|
+
"invalid_aggregate_alias",
|
|
1209
|
+
400
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
if (by.includes(alias)) {
|
|
1213
|
+
throw new MindStudioError(
|
|
1214
|
+
`aggregate(): alias "${alias}" collides with a group-by column of the same name.`,
|
|
1215
|
+
"invalid_aggregate_alias",
|
|
1216
|
+
400
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
const term = spec.select[alias];
|
|
1220
|
+
const fns = AGGREGATE_FNS.filter((fn2) => term && fn2 in term);
|
|
1221
|
+
if (fns.length !== 1) {
|
|
1222
|
+
throw new MindStudioError(
|
|
1223
|
+
`aggregate(): term "${alias}" must have exactly one aggregate key (count, sum, avg, min, max, countDistinct).`,
|
|
1224
|
+
"invalid_aggregate_spec",
|
|
1225
|
+
400
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
const fn = fns[0];
|
|
1229
|
+
if (fn === "count") {
|
|
1230
|
+
if (term[fn] !== true) {
|
|
1231
|
+
throw new MindStudioError(
|
|
1232
|
+
`aggregate(): term "${alias}" \u2014 count takes \`true\`, e.g. { count: true }.`,
|
|
1233
|
+
"invalid_aggregate_spec",
|
|
1234
|
+
400
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return { alias, fn };
|
|
1238
|
+
}
|
|
1239
|
+
const column = term[fn];
|
|
1240
|
+
if (typeof column !== "string" || !IDENTIFIER_RE.test(column)) {
|
|
1241
|
+
throw new MindStudioError(
|
|
1242
|
+
`aggregate(): term "${alias}" \u2014 invalid column "${String(column)}".`,
|
|
1243
|
+
"invalid_column",
|
|
1244
|
+
400
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
return { alias, fn, column };
|
|
1248
|
+
});
|
|
1249
|
+
if (spec.orderBy != null) {
|
|
1250
|
+
if (!aliases.includes(spec.orderBy) && !by.includes(spec.orderBy)) {
|
|
1251
|
+
throw new MindStudioError(
|
|
1252
|
+
`aggregate(): orderBy "${spec.orderBy}" is neither a select alias nor a group-by column.`,
|
|
1253
|
+
"invalid_aggregate_order",
|
|
1254
|
+
400
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
by,
|
|
1260
|
+
select,
|
|
1261
|
+
orderBy: spec.orderBy,
|
|
1262
|
+
desc: spec.desc,
|
|
1263
|
+
limit: spec.limit
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
function aggregateValues(fn, values) {
|
|
1267
|
+
const present = values.filter((v) => v != null);
|
|
1268
|
+
switch (fn) {
|
|
1269
|
+
case "count":
|
|
1270
|
+
return values.length;
|
|
1271
|
+
case "sum":
|
|
1272
|
+
return present.reduce((s, v) => s + v, 0);
|
|
1273
|
+
case "avg":
|
|
1274
|
+
return present.length === 0 ? null : present.reduce((s, v) => s + v, 0) / present.length;
|
|
1275
|
+
case "countDistinct":
|
|
1276
|
+
return new Set(present).size;
|
|
1277
|
+
case "min":
|
|
1278
|
+
return present.length === 0 ? null : present.reduce((m, v) => v < m ? v : m);
|
|
1279
|
+
case "max":
|
|
1280
|
+
return present.length === 0 ? null : present.reduce((m, v) => v > m ? v : m);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function compareForOrder(a, b) {
|
|
1284
|
+
if (a == null && b == null) return 0;
|
|
1285
|
+
if (a == null) return -1;
|
|
1286
|
+
if (b == null) return 1;
|
|
1287
|
+
if (a < b) return -1;
|
|
1288
|
+
if (a > b) return 1;
|
|
1289
|
+
return 0;
|
|
1290
|
+
}
|
|
1291
|
+
function computeAggregateInJs(rows, spec) {
|
|
1292
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1293
|
+
for (const row of rows) {
|
|
1294
|
+
const key = JSON.stringify(spec.by.map((col) => row[col]));
|
|
1295
|
+
const group = groups.get(key);
|
|
1296
|
+
if (group) group.push(row);
|
|
1297
|
+
else groups.set(key, [row]);
|
|
1298
|
+
}
|
|
1299
|
+
if (spec.by.length === 0 && groups.size === 0) {
|
|
1300
|
+
groups.set("[]", []);
|
|
1301
|
+
}
|
|
1302
|
+
let out = Array.from(groups.values()).map((groupRows) => {
|
|
1303
|
+
const result = {};
|
|
1304
|
+
for (const col of spec.by) {
|
|
1305
|
+
result[col] = groupRows[0]?.[col];
|
|
1306
|
+
}
|
|
1307
|
+
for (const term of spec.select) {
|
|
1308
|
+
result[term.alias] = aggregateValues(
|
|
1309
|
+
term.fn,
|
|
1310
|
+
term.column != null ? groupRows.map((r) => r[term.column]) : groupRows
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
return result;
|
|
1314
|
+
});
|
|
1315
|
+
if (spec.orderBy) {
|
|
1316
|
+
const key = spec.orderBy;
|
|
1317
|
+
out.sort((a, b) => compareForOrder(a[key], b[key]));
|
|
1318
|
+
if (spec.desc) out.reverse();
|
|
1319
|
+
}
|
|
1320
|
+
if (spec.limit != null) {
|
|
1321
|
+
out = out.slice(0, spec.limit);
|
|
1322
|
+
}
|
|
1323
|
+
return spec.by.length === 0 ? out[0] : out;
|
|
1324
|
+
}
|
|
1325
|
+
function deserializeAggregateRows(rows, spec, columns) {
|
|
1326
|
+
const schemaFor = /* @__PURE__ */ new Map();
|
|
1327
|
+
for (const col of spec.by) {
|
|
1328
|
+
schemaFor.set(
|
|
1329
|
+
col,
|
|
1330
|
+
columns.filter((c) => c.name === col)
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
for (const term of spec.select) {
|
|
1334
|
+
if ((term.fn === "min" || term.fn === "max") && term.column != null) {
|
|
1335
|
+
schemaFor.set(
|
|
1336
|
+
term.alias,
|
|
1337
|
+
columns.filter((c) => c.name === term.column).map((c) => ({ ...c, name: term.alias }))
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return rows.map((raw) => {
|
|
1342
|
+
const row = raw;
|
|
1343
|
+
const result = {};
|
|
1344
|
+
for (const [key, value] of Object.entries(row)) {
|
|
1345
|
+
const schema = schemaFor.get(key);
|
|
1346
|
+
if (schema) {
|
|
1347
|
+
result[key] = deserializeRow({ [key]: value }, schema)[key];
|
|
1348
|
+
} else {
|
|
1349
|
+
result[key] = value;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return result;
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1122
1356
|
// src/db/query.ts
|
|
1123
1357
|
var Query = class _Query {
|
|
1124
1358
|
_predicates;
|
|
@@ -1130,6 +1364,8 @@ var Query = class _Query {
|
|
|
1130
1364
|
/** @internal Pre-compiled WHERE clause (bypasses predicate compiler). Used by Table.get(). */
|
|
1131
1365
|
_rawWhere;
|
|
1132
1366
|
_rawWhereParams;
|
|
1367
|
+
/** @internal See AggregateState — set by aggregate terminals. */
|
|
1368
|
+
_aggregate;
|
|
1133
1369
|
/** @internal Post-process transform applied after row deserialization. */
|
|
1134
1370
|
_postProcess;
|
|
1135
1371
|
constructor(config, options) {
|
|
@@ -1142,6 +1378,7 @@ var Query = class _Query {
|
|
|
1142
1378
|
this._postProcess = options?.postProcess;
|
|
1143
1379
|
this._rawWhere = options?.rawWhere;
|
|
1144
1380
|
this._rawWhereParams = options?.rawWhereParams;
|
|
1381
|
+
this._aggregate = options?.aggregate;
|
|
1145
1382
|
}
|
|
1146
1383
|
_clone(overrides) {
|
|
1147
1384
|
return new _Query(this._config, {
|
|
@@ -1152,7 +1389,8 @@ var Query = class _Query {
|
|
|
1152
1389
|
offset: overrides.offset ?? this._offset,
|
|
1153
1390
|
postProcess: overrides.postProcess,
|
|
1154
1391
|
rawWhere: this._rawWhere,
|
|
1155
|
-
rawWhereParams: this._rawWhereParams
|
|
1392
|
+
rawWhereParams: this._rawWhereParams,
|
|
1393
|
+
aggregate: overrides.aggregate ?? this._aggregate
|
|
1156
1394
|
});
|
|
1157
1395
|
}
|
|
1158
1396
|
filter(predicate, bindings) {
|
|
@@ -1190,9 +1428,60 @@ var Query = class _Query {
|
|
|
1190
1428
|
}
|
|
1191
1429
|
count() {
|
|
1192
1430
|
return this._clone({
|
|
1431
|
+
aggregate: { kind: "scalar", fn: "count" },
|
|
1193
1432
|
postProcess: (rows) => rows.length
|
|
1194
1433
|
});
|
|
1195
1434
|
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Sum of a numeric field across matching rows. Compiles to a SQL
|
|
1437
|
+
* aggregate (TOTAL) when possible — no rows are fetched. Returns 0 for
|
|
1438
|
+
* an empty set; NULL values are skipped (SQL semantics).
|
|
1439
|
+
*/
|
|
1440
|
+
sum(accessor) {
|
|
1441
|
+
return this._clone({
|
|
1442
|
+
aggregate: { kind: "scalar", fn: "sum", accessor },
|
|
1443
|
+
postProcess: (rows) => rows.reduce((total, row) => {
|
|
1444
|
+
const value = accessor(row);
|
|
1445
|
+
return value == null ? total : total + value;
|
|
1446
|
+
}, 0)
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Average of a numeric field across matching rows. Compiles to SQL AVG
|
|
1451
|
+
* when possible. Returns null for an empty set (or when every value is
|
|
1452
|
+
* null) — SQL AVG semantics.
|
|
1453
|
+
*/
|
|
1454
|
+
avg(accessor) {
|
|
1455
|
+
return this._clone({
|
|
1456
|
+
aggregate: { kind: "scalar", fn: "avg", accessor },
|
|
1457
|
+
postProcess: (rows) => {
|
|
1458
|
+
const values = rows.map((row) => accessor(row)).filter((v) => v != null);
|
|
1459
|
+
return values.length === 0 ? null : values.reduce((s, v) => s + v, 0) / values.length;
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Number of distinct non-null values of a field across matching rows.
|
|
1465
|
+
* Compiles to SQL COUNT(DISTINCT col) when possible.
|
|
1466
|
+
*/
|
|
1467
|
+
countDistinct(accessor) {
|
|
1468
|
+
return this._clone({
|
|
1469
|
+
aggregate: { kind: "scalar", fn: "countDistinct", accessor },
|
|
1470
|
+
postProcess: (rows) => new Set(rows.map((row) => accessor(row)).filter((v) => v != null)).size
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
aggregate(spec) {
|
|
1474
|
+
const normalized = normalizeAggregateSpec(
|
|
1475
|
+
spec
|
|
1476
|
+
);
|
|
1477
|
+
return this._clone({
|
|
1478
|
+
aggregate: { kind: "grouped", spec: normalized },
|
|
1479
|
+
postProcess: (rows) => computeAggregateInJs(
|
|
1480
|
+
rows,
|
|
1481
|
+
normalized
|
|
1482
|
+
)
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1196
1485
|
some() {
|
|
1197
1486
|
return this._clone({
|
|
1198
1487
|
limit: 1,
|
|
@@ -1263,6 +1552,10 @@ var Query = class _Query {
|
|
|
1263
1552
|
const compiled = this._compilePredicates();
|
|
1264
1553
|
const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
|
|
1265
1554
|
if (compiled.allSql) {
|
|
1555
|
+
const agg = this._compileAggregateSql(compiled.sqlWhere);
|
|
1556
|
+
if (agg) {
|
|
1557
|
+
return { type: "query", query: agg.query, fallbackQuery: null, config: this._config, processRaw: agg.processRaw };
|
|
1558
|
+
}
|
|
1266
1559
|
const query = buildSelect(this._config.tableName, {
|
|
1267
1560
|
where: compiled.sqlWhere || void 0,
|
|
1268
1561
|
orderBy: sortField ?? void 0,
|
|
@@ -1294,6 +1587,9 @@ var Query = class _Query {
|
|
|
1294
1587
|
* For JS-fallback queries: filter, sort, and slice in JS.
|
|
1295
1588
|
*/
|
|
1296
1589
|
static _processResults(result, compiled) {
|
|
1590
|
+
if (compiled.processRaw) {
|
|
1591
|
+
return compiled.processRaw(result);
|
|
1592
|
+
}
|
|
1297
1593
|
const rows = result.rows.map(
|
|
1298
1594
|
(row) => deserializeRow(
|
|
1299
1595
|
row,
|
|
@@ -1326,10 +1622,81 @@ var Query = class _Query {
|
|
|
1326
1622
|
// PromiseLike
|
|
1327
1623
|
// -------------------------------------------------------------------------
|
|
1328
1624
|
then(onfulfilled, onrejected) {
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1625
|
+
return this._run().then(onfulfilled, onrejected);
|
|
1626
|
+
}
|
|
1627
|
+
async _run() {
|
|
1628
|
+
if (this._aggregate && !this._rawWhere) {
|
|
1629
|
+
const compiled = this._compilePredicates();
|
|
1630
|
+
if (compiled.allSql) {
|
|
1631
|
+
const agg = this._compileAggregateSql(compiled.sqlWhere);
|
|
1632
|
+
if (agg) {
|
|
1633
|
+
const results = await this._config.executeBatch([agg.query]);
|
|
1634
|
+
return agg.processRaw(results[0]);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const rows = await this._execute();
|
|
1639
|
+
return this._postProcess ? this._postProcess(rows) : rows;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* @internal Compile the pending aggregate into a single SQL statement +
|
|
1643
|
+
* result processor. Returns null (→ fetch path, postProcess computes the
|
|
1644
|
+
* aggregate) when there is no aggregate, when a pre-compiled WHERE is in
|
|
1645
|
+
* play (Table.get()), when limit/offset are set (aggregate-the-page
|
|
1646
|
+
* semantics — COUNT(*)/TOTAL can't express "over the limited page"), or
|
|
1647
|
+
* when a scalar accessor doesn't extract to a column (computed accessors
|
|
1648
|
+
* stay correct via JS, with a warning to prompt optimization).
|
|
1649
|
+
*/
|
|
1650
|
+
_compileAggregateSql(sqlWhere) {
|
|
1651
|
+
const agg = this._aggregate;
|
|
1652
|
+
if (!agg || this._rawWhere || this._limit != null || this._offset != null) {
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
const where = sqlWhere || void 0;
|
|
1656
|
+
if (agg.kind === "scalar") {
|
|
1657
|
+
let column = null;
|
|
1658
|
+
if (agg.accessor) {
|
|
1659
|
+
column = extractFieldName(agg.accessor);
|
|
1660
|
+
if (column == null) {
|
|
1661
|
+
console.warn(
|
|
1662
|
+
`[mindstudio] ${agg.fn}() accessor on '${this._config.tableName}' could not be compiled to SQL \u2014 fetching matching rows and computing in JS`
|
|
1663
|
+
);
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const query2 = buildScalarAggregate(
|
|
1668
|
+
this._config.tableName,
|
|
1669
|
+
agg.fn,
|
|
1670
|
+
column,
|
|
1671
|
+
where
|
|
1672
|
+
);
|
|
1673
|
+
const processRaw2 = (result) => {
|
|
1674
|
+
const value = result.rows[0]?.value;
|
|
1675
|
+
if (agg.fn === "avg") {
|
|
1676
|
+
return value == null ? null : Number(value);
|
|
1677
|
+
}
|
|
1678
|
+
return Number(value ?? 0);
|
|
1679
|
+
};
|
|
1680
|
+
return { query: query2, processRaw: processRaw2 };
|
|
1681
|
+
}
|
|
1682
|
+
const spec = agg.spec;
|
|
1683
|
+
const query = buildGroupedAggregate(this._config.tableName, {
|
|
1684
|
+
by: spec.by,
|
|
1685
|
+
select: spec.select,
|
|
1686
|
+
where,
|
|
1687
|
+
orderBy: spec.orderBy,
|
|
1688
|
+
desc: spec.desc,
|
|
1689
|
+
limit: spec.limit
|
|
1690
|
+
});
|
|
1691
|
+
const processRaw = (result) => {
|
|
1692
|
+
const rows = deserializeAggregateRows(
|
|
1693
|
+
result.rows,
|
|
1694
|
+
spec,
|
|
1695
|
+
this._config.columns
|
|
1696
|
+
);
|
|
1697
|
+
return spec.by.length === 0 ? rows[0] : rows;
|
|
1698
|
+
};
|
|
1699
|
+
return { query, processRaw };
|
|
1333
1700
|
}
|
|
1334
1701
|
catch(onrejected) {
|
|
1335
1702
|
return this.then(void 0, onrejected);
|
|
@@ -1560,7 +1927,34 @@ var Table = class {
|
|
|
1560
1927
|
max(accessor) {
|
|
1561
1928
|
return this.sortBy(accessor).reverse().first();
|
|
1562
1929
|
}
|
|
1563
|
-
/**
|
|
1930
|
+
/**
|
|
1931
|
+
* Sum of a numeric field across all rows (0 for an empty table).
|
|
1932
|
+
* Runs as a SQL aggregate — no rows are fetched. Chain from filter()
|
|
1933
|
+
* to sum a subset: `Orders.filter(...).sum(o => o.amount)`.
|
|
1934
|
+
*/
|
|
1935
|
+
sum(accessor) {
|
|
1936
|
+
return this.toArray().sum(accessor);
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Average of a numeric field across all rows (null for an empty table).
|
|
1940
|
+
* Runs as a SQL aggregate — no rows are fetched.
|
|
1941
|
+
*/
|
|
1942
|
+
avg(accessor) {
|
|
1943
|
+
return this.toArray().avg(accessor);
|
|
1944
|
+
}
|
|
1945
|
+
/**
|
|
1946
|
+
* Number of distinct non-null values of a field across all rows.
|
|
1947
|
+
* Runs as a SQL aggregate — no rows are fetched.
|
|
1948
|
+
*/
|
|
1949
|
+
countDistinct(accessor) {
|
|
1950
|
+
return this.toArray().countDistinct(accessor);
|
|
1951
|
+
}
|
|
1952
|
+
aggregate(spec) {
|
|
1953
|
+
return this.toArray().aggregate(spec);
|
|
1954
|
+
}
|
|
1955
|
+
/** Group rows by a field. Returns a Map of FULL rows per group — it
|
|
1956
|
+
* fetches everything. For grouped counts/sums/averages over tables that
|
|
1957
|
+
* can grow large, use `aggregate()` instead (SQL GROUP BY, no row fetch). */
|
|
1564
1958
|
groupBy(accessor) {
|
|
1565
1959
|
return new Query(this._config).groupBy(accessor);
|
|
1566
1960
|
}
|
|
@@ -1796,6 +2190,57 @@ var Table = class {
|
|
|
1796
2190
|
}
|
|
1797
2191
|
};
|
|
1798
2192
|
|
|
2193
|
+
// src/db/raw.ts
|
|
2194
|
+
var RawQuery = class _RawQuery {
|
|
2195
|
+
_databaseId;
|
|
2196
|
+
_executeBatch;
|
|
2197
|
+
_query;
|
|
2198
|
+
constructor(databaseId, executeBatch, query) {
|
|
2199
|
+
this._databaseId = databaseId;
|
|
2200
|
+
this._executeBatch = executeBatch;
|
|
2201
|
+
this._query = query;
|
|
2202
|
+
}
|
|
2203
|
+
// -------------------------------------------------------------------------
|
|
2204
|
+
// PromiseLike — executes on await
|
|
2205
|
+
// -------------------------------------------------------------------------
|
|
2206
|
+
then(onfulfilled, onrejected) {
|
|
2207
|
+
return this._execute().then(onfulfilled, onrejected);
|
|
2208
|
+
}
|
|
2209
|
+
catch(onrejected) {
|
|
2210
|
+
return this._execute().catch(onrejected);
|
|
2211
|
+
}
|
|
2212
|
+
// -------------------------------------------------------------------------
|
|
2213
|
+
// Batch support
|
|
2214
|
+
// -------------------------------------------------------------------------
|
|
2215
|
+
/** @internal Compile for db.batch(). */
|
|
2216
|
+
_compile() {
|
|
2217
|
+
return {
|
|
2218
|
+
type: "raw",
|
|
2219
|
+
query: this._query,
|
|
2220
|
+
config: { databaseId: this._databaseId },
|
|
2221
|
+
processResult: (result) => _RawQuery._process(result)
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
/** @internal Process a raw SQL result. Used by db.batch(). */
|
|
2225
|
+
static _processResults(result, compiled) {
|
|
2226
|
+
return compiled.processResult(result);
|
|
2227
|
+
}
|
|
2228
|
+
// -------------------------------------------------------------------------
|
|
2229
|
+
// Execution
|
|
2230
|
+
// -------------------------------------------------------------------------
|
|
2231
|
+
/** Schema-less deserialization: strips @@user@@ prefixes and best-effort
|
|
2232
|
+
* parses JSON-looking strings; makes no other representation promises. */
|
|
2233
|
+
static _process(result) {
|
|
2234
|
+
return result.rows.map(
|
|
2235
|
+
(row) => deserializeRow(row, [])
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
async _execute() {
|
|
2239
|
+
const results = await this._executeBatch([this._query]);
|
|
2240
|
+
return _RawQuery._process(results[0]);
|
|
2241
|
+
}
|
|
2242
|
+
};
|
|
2243
|
+
|
|
1799
2244
|
// src/db/index.ts
|
|
1800
2245
|
function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
1801
2246
|
return {
|
|
@@ -1823,6 +2268,16 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
1823
2268
|
fromNow: (ms) => Date.now() + ms,
|
|
1824
2269
|
// --- User references ---
|
|
1825
2270
|
userRef: (id) => id.startsWith(USER_PREFIX) ? id.slice(USER_PREFIX.length) : id,
|
|
2271
|
+
// --- Raw SQL (read-only) ---
|
|
2272
|
+
sql(query, params, options) {
|
|
2273
|
+
validateRawSql(query);
|
|
2274
|
+
const database = resolveDatabase(databases, options?.database);
|
|
2275
|
+
return new RawQuery(
|
|
2276
|
+
database.id,
|
|
2277
|
+
(queries) => executeBatch(database.id, queries),
|
|
2278
|
+
{ sql: query, params: params?.map(serializeParam) }
|
|
2279
|
+
);
|
|
2280
|
+
},
|
|
1826
2281
|
// --- Batch execution ---
|
|
1827
2282
|
batch: ((...operations) => {
|
|
1828
2283
|
return (async () => {
|
|
@@ -1833,8 +2288,11 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
1833
2288
|
if (op instanceof Mutation) {
|
|
1834
2289
|
return op._compile();
|
|
1835
2290
|
}
|
|
2291
|
+
if (op instanceof RawQuery) {
|
|
2292
|
+
return op._compile();
|
|
2293
|
+
}
|
|
1836
2294
|
throw new MindStudioError(
|
|
1837
|
-
"db.batch() only accepts Query and
|
|
2295
|
+
"db.batch() only accepts Query, Mutation, and RawQuery objects (from .filter(), .update(), .push(), db.sql(), etc.)",
|
|
1838
2296
|
"invalid_batch_operation",
|
|
1839
2297
|
400
|
|
1840
2298
|
);
|
|
@@ -1847,6 +2305,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
1847
2305
|
if (c.type === "query") {
|
|
1848
2306
|
const sqlQuery = c.query ?? c.fallbackQuery;
|
|
1849
2307
|
groups.get(dbId).push({ opIndex: i, sqlQueries: [sqlQuery] });
|
|
2308
|
+
} else if (c.type === "raw") {
|
|
2309
|
+
groups.get(dbId).push({ opIndex: i, sqlQueries: [c.query] });
|
|
1850
2310
|
} else {
|
|
1851
2311
|
groups.get(dbId).push({ opIndex: i, sqlQueries: c.queries });
|
|
1852
2312
|
}
|
|
@@ -1879,6 +2339,8 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
1879
2339
|
);
|
|
1880
2340
|
}
|
|
1881
2341
|
return Query._processResults(results[0], c);
|
|
2342
|
+
} else if (c.type === "raw") {
|
|
2343
|
+
return RawQuery._processResults(results[0], c);
|
|
1882
2344
|
} else {
|
|
1883
2345
|
return Mutation._processResults(results, c);
|
|
1884
2346
|
}
|
|
@@ -1887,7 +2349,23 @@ function createDb(databases, executeBatch, authConfig, syncRoles) {
|
|
|
1887
2349
|
})
|
|
1888
2350
|
};
|
|
1889
2351
|
}
|
|
1890
|
-
function
|
|
2352
|
+
function validateRawSql(query) {
|
|
2353
|
+
if (typeof query !== "string" || !query.trim()) {
|
|
2354
|
+
throw new MindStudioError(
|
|
2355
|
+
"db.sql() requires a non-empty SQL string.",
|
|
2356
|
+
"invalid_sql",
|
|
2357
|
+
400
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2360
|
+
if (!/^\s*(select|with)\b/i.test(query)) {
|
|
2361
|
+
throw new MindStudioError(
|
|
2362
|
+
"db.sql() is read-only \u2014 the statement must start with SELECT or WITH. Use Table methods (push/update/upsert/remove) for writes.",
|
|
2363
|
+
"sql_read_only",
|
|
2364
|
+
400
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function resolveDatabase(databases, hint) {
|
|
1891
2369
|
if (databases.length === 0) {
|
|
1892
2370
|
throw new MindStudioError(
|
|
1893
2371
|
`No databases found in app context. Make sure the app has at least one database configured.`,
|
|
@@ -1895,18 +2373,40 @@ function resolveTable(databases, tableName, databaseHint) {
|
|
|
1895
2373
|
400
|
|
1896
2374
|
);
|
|
1897
2375
|
}
|
|
1898
|
-
if (
|
|
2376
|
+
if (hint) {
|
|
1899
2377
|
const targetDb = databases.find(
|
|
1900
|
-
(db2) => db2.id ===
|
|
2378
|
+
(db2) => db2.id === hint || db2.name === hint
|
|
1901
2379
|
);
|
|
1902
2380
|
if (!targetDb) {
|
|
1903
2381
|
const available = databases.map((db2) => db2.name || db2.id).join(", ");
|
|
1904
2382
|
throw new MindStudioError(
|
|
1905
|
-
`Database "${
|
|
2383
|
+
`Database "${hint}" not found. Available databases: ${available}`,
|
|
1906
2384
|
"database_not_found",
|
|
1907
2385
|
400
|
|
1908
2386
|
);
|
|
1909
2387
|
}
|
|
2388
|
+
return targetDb;
|
|
2389
|
+
}
|
|
2390
|
+
if (databases.length > 1) {
|
|
2391
|
+
const available = databases.map((db2) => db2.name || db2.id).join(", ");
|
|
2392
|
+
throw new MindStudioError(
|
|
2393
|
+
`This app has multiple databases \u2014 pass { database } to pick one. Available databases: ${available}`,
|
|
2394
|
+
"ambiguous_database",
|
|
2395
|
+
400
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
return databases[0];
|
|
2399
|
+
}
|
|
2400
|
+
function resolveTable(databases, tableName, databaseHint) {
|
|
2401
|
+
if (databases.length === 0) {
|
|
2402
|
+
throw new MindStudioError(
|
|
2403
|
+
`No databases found in app context. Make sure the app has at least one database configured.`,
|
|
2404
|
+
"no_databases",
|
|
2405
|
+
400
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
if (databaseHint) {
|
|
2409
|
+
const targetDb = resolveDatabase(databases, databaseHint);
|
|
1910
2410
|
const table = targetDb.tables.find((t) => t.name === tableName);
|
|
1911
2411
|
if (!table) {
|
|
1912
2412
|
const available = targetDb.tables.map((t) => t.name).join(", ");
|
|
@@ -3056,13 +3556,6 @@ var stepMetadata = {
|
|
|
3056
3556
|
inputSchema: { "type": "object", "properties": { "webhookUrl": { "type": "string", "description": "Zapier webhook URL to send data to" }, "input": { "type": "object", "properties": {}, "required": [], "description": "Key-value pairs to send as the JSON POST body" } }, "required": ["webhookUrl", "input"] },
|
|
3057
3557
|
outputSchema: { "type": "object", "properties": { "data": { "description": "Parsed webhook response from Zapier (JSON object, array, or string)" } }, "required": ["data"] }
|
|
3058
3558
|
},
|
|
3059
|
-
"queryAppDatabase": {
|
|
3060
|
-
stepType: "queryAppDatabase",
|
|
3061
|
-
description: "Execute a SQL query against the app managed database.",
|
|
3062
|
-
usageNotes: '- Executes raw SQL against a SQLite database managed by the app.\n- For SELECT queries, returns rows as JSON.\n- For INSERT/UPDATE/DELETE, returns the number of affected rows.\n- Use {{variables}} directly in your SQL. By default they are automatically extracted\n and passed as safe parameterized values (preventing SQL injection).\n Example: INSERT INTO contacts (name, comment) VALUES ({{name}}, {{comment}})\n- Full MindStudio handlebars syntax is supported, including helpers like {{json myVar}},\n {{get myVar "$.path"}}, {{global.orgName}}, etc.\n- Set parameterize to false for raw/dynamic SQL where variables are interpolated directly\n into the query string. Use this when another step generates full or partial SQL, e.g.\n a bulk INSERT with a precomputed VALUES list. The user is responsible for sanitization\n when parameterize is false.',
|
|
3063
|
-
inputSchema: { "type": "object", "properties": { "databaseId": { "type": "string", "description": "Name or ID of the app data database to query" }, "sql": { "type": "string", "description": "SQL query to execute. Use {{variables}} directly in the SQL \u2014 they are handled according to the `parameterize` setting.\n\nWhen parameterize is true (default): {{variables}} are extracted from the SQL, replaced with ? placeholders, resolved via the full MindStudio handlebars pipeline, and passed as safe parameterized values to SQLite. This prevents SQL injection. Example: INSERT INTO contacts (name, email) VALUES ({{name}}, {{email}})\n\nWhen parameterize is false: The entire SQL string is resolved via compileString (standard handlebars interpolation) and executed as-is. Use this for dynamic/generated SQL where another step builds the query. The user is responsible for safety. Example: {{generatedInsertQuery}}\n\nAsk the user for the database schema if they have not already provided it." }, "parameterize": { "type": "boolean", "description": "Whether to treat {{variables}} as parameterized query values (default: true).\n\n- true: {{vars}} are extracted, replaced with ?, and passed as bind params. Safe from SQL injection. Use for standard CRUD operations.\n- false: {{vars}} are interpolated directly into the SQL string via handlebars. Use when another step generates full or partial SQL (e.g. bulk inserts with precomputed VALUES). The user is responsible for sanitization." } }, "required": ["databaseId", "sql"] },
|
|
3064
|
-
outputSchema: { "type": "object", "properties": { "rows": { "type": "array", "items": {}, "description": "Result rows for SELECT queries (empty array for write queries)" }, "changes": { "type": "number", "description": "Number of rows affected by INSERT, UPDATE, or DELETE queries (0 for SELECT)" } }, "required": ["rows", "changes"] }
|
|
3065
|
-
},
|
|
3066
3559
|
"queryDataSource": {
|
|
3067
3560
|
stepType: "queryDataSource",
|
|
3068
3561
|
description: "Legacy v1 data sources. v2 apps should use `dataSources.defineDataSource()` from @mindstudio-ai/agent instead, which owns parsing, chunking, embedding and citations. Search a vector data source (RAG) and return relevant document chunks.",
|
|
@@ -3175,9 +3668,13 @@ var stepMetadata = {
|
|
|
3175
3668
|
"searchGoogle": {
|
|
3176
3669
|
stepType: "searchGoogle",
|
|
3177
3670
|
description: "Search the web using Google and return structured results.",
|
|
3178
|
-
usageNotes:
|
|
3179
|
-
|
|
3180
|
-
|
|
3671
|
+
usageNotes: `- Defaults to us/english, but can optionally specify country and/or language.
|
|
3672
|
+
- Defaults to any time, but can optionally specify last hour, last day, week, month, or year.
|
|
3673
|
+
- Defaults to top 30 results, but can specify 1 to 100 results to return.
|
|
3674
|
+
- Set fetchTopN to also read the page content of the first N results in the same step, instead of following up with a separate Scrape URL step per result. Costs meaningfully more (each page is a full scrape), so ask for the fewest you need.
|
|
3675
|
+
- Also returns Google's "people also ask" and related searches when available, which are useful for choosing a follow-up query.`,
|
|
3676
|
+
inputSchema: { "type": "object", "properties": { "query": { "type": "string", "description": "The search query to send to Google" }, "exportType": { "enum": ["text", "json"], "type": "string", "description": 'Format for the variable value: "text" or "json"' }, "countryCode": { "type": "string", "description": "Google gl country code (defaults to US)" }, "languageCode": { "type": "string", "description": 'Google hl language code (defaults to "en")' }, "dateRange": { "enum": ["hour", "day", "week", "month", "year", "any"], "type": "string", "description": 'Time range filter: "hour", "day", "week", "month", "year", or "any"' }, "numResults": { "type": "number", "description": "Number of results to return (1-100, default: 30)" }, "fetchTopN": { "type": "number", "description": "Read the page content of the first N results in this same step (0-10, default 0 = off), rather than following up with a Scrape URL step per result. Each page is a full scrape, so this costs substantially more than the search alone." } }, "required": ["query", "exportType"] },
|
|
3677
|
+
outputSchema: { "type": "object", "properties": { "results": { "type": "array", "items": { "type": "object", "properties": { "title": { "type": "string", "description": "Title of the search result" }, "description": { "type": "string", "description": "Snippet/description of the search result" }, "url": { "type": "string", "description": "URL of the search result page" }, "content": { "type": "string", "description": "Page content, only present when fetchTopN covered this result" } }, "required": ["title", "description", "url"] }, "description": "List of search result entries" }, "peopleAlsoAsk": { "type": "array", "items": { "type": "object", "properties": { "question": { "type": "string" }, "snippet": { "type": "string" }, "link": { "type": "string" } } }, "description": `Google's "People also ask" entries, when Google returned any` }, "relatedSearches": { "type": "array", "items": { "type": "string" }, "description": "Google's related-search suggestions, when Google returned any" } }, "required": ["results"] }
|
|
3181
3678
|
},
|
|
3182
3679
|
"searchGoogleCalendarEvents": {
|
|
3183
3680
|
stepType: "searchGoogleCalendarEvents",
|
|
@@ -4729,9 +5226,6 @@ function applyStepMethods(AgentClass) {
|
|
|
4729
5226
|
proto.postToZapier = function(step, options) {
|
|
4730
5227
|
return this.executeStep("postToZapier", step, options);
|
|
4731
5228
|
};
|
|
4732
|
-
proto.queryAppDatabase = function(step, options) {
|
|
4733
|
-
return this.executeStep("queryAppDatabase", step, options);
|
|
4734
|
-
};
|
|
4735
5229
|
proto.queryDataSource = function(step, options) {
|
|
4736
5230
|
return this.executeStep("queryDataSource", step, options);
|
|
4737
5231
|
};
|
|
@@ -6295,6 +6789,24 @@ var MindStudioAgent = class {
|
|
|
6295
6789
|
ago: (ms) => Date.now() - ms,
|
|
6296
6790
|
fromNow: (ms) => Date.now() + ms,
|
|
6297
6791
|
userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
|
|
6792
|
+
// Raw SQL — validate synchronously; database resolution needs context,
|
|
6793
|
+
// so it happens inside the executeBatch closure at execution time
|
|
6794
|
+
// (the same deferral lazy defineTable uses).
|
|
6795
|
+
sql: ((query, params, options) => {
|
|
6796
|
+
validateRawSql(query);
|
|
6797
|
+
return new RawQuery(
|
|
6798
|
+
"",
|
|
6799
|
+
async (queries) => {
|
|
6800
|
+
await agent.ensureContext();
|
|
6801
|
+
const database = resolveDatabase(
|
|
6802
|
+
agent._getContext().databases,
|
|
6803
|
+
options?.database
|
|
6804
|
+
);
|
|
6805
|
+
return agent._executeDbBatch(database.id, queries);
|
|
6806
|
+
},
|
|
6807
|
+
{ sql: query, params: params?.map(serializeParam) }
|
|
6808
|
+
);
|
|
6809
|
+
}),
|
|
6298
6810
|
// Batch needs context — hydrate first, then delegate to real db
|
|
6299
6811
|
batch: ((...queries) => {
|
|
6300
6812
|
return (async () => {
|
|
@@ -6850,7 +7362,6 @@ var monacoSnippets = {
|
|
|
6850
7362
|
"postToSlackChannel": { fields: [["channelId", "string"], ["messageType", ["string", "blocks"]], ["message", "string"]], outputKeys: [] },
|
|
6851
7363
|
"postToX": { fields: [["text", "string"]], outputKeys: [] },
|
|
6852
7364
|
"postToZapier": { fields: [["webhookUrl", "string"], ["input", "object"]], outputKeys: ["data"] },
|
|
6853
|
-
"queryAppDatabase": { fields: [["databaseId", "string"], ["sql", "string"]], outputKeys: ["rows", "changes"] },
|
|
6854
7365
|
"queryDataSource": { fields: [["dataSourceId", "string"], ["query", "string"], ["maxResults", "number"]], outputKeys: ["text", "chunks", "query", "citations", "latencyMs"] },
|
|
6855
7366
|
"queryExternalDatabase": { fields: [["query", "string"], ["outputFormat", ["json", "csv"]]], outputKeys: ["data"] },
|
|
6856
7367
|
"redactPII": { fields: [["input", "string"], ["language", "string"], ["entities", "array"]], outputKeys: ["text"] },
|