@mindstudio-ai/agent 0.1.103 → 0.1.105
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 +649 -32
- package/dist/index.d.ts +532 -66
- package/dist/index.js +607 -26
- package/dist/index.js.map +1 -1
- package/dist/postinstall.js +649 -32
- package/llms.txt +74 -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(", ");
|
|
@@ -2278,6 +2778,38 @@ function createVoice(call) {
|
|
|
2278
2778
|
};
|
|
2279
2779
|
}
|
|
2280
2780
|
|
|
2781
|
+
// src/analytics/index.ts
|
|
2782
|
+
function createAnalytics(call) {
|
|
2783
|
+
return {
|
|
2784
|
+
query(spec) {
|
|
2785
|
+
return call("query", spec);
|
|
2786
|
+
},
|
|
2787
|
+
live() {
|
|
2788
|
+
return call("live", {});
|
|
2789
|
+
},
|
|
2790
|
+
sources(options) {
|
|
2791
|
+
return call("sources", options ?? {});
|
|
2792
|
+
},
|
|
2793
|
+
map(options) {
|
|
2794
|
+
return call("map", options ?? {});
|
|
2795
|
+
},
|
|
2796
|
+
aiSources(options) {
|
|
2797
|
+
return call("ai-sources", options ?? {});
|
|
2798
|
+
},
|
|
2799
|
+
crawlers: {
|
|
2800
|
+
overview(options) {
|
|
2801
|
+
return call("crawlers-overview", options ?? {});
|
|
2802
|
+
},
|
|
2803
|
+
timeseries(options) {
|
|
2804
|
+
return call("crawlers-timeseries", options ?? {});
|
|
2805
|
+
},
|
|
2806
|
+
recent(options) {
|
|
2807
|
+
return call("crawlers-recent", options ?? {});
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2281
2813
|
// src/task/index.ts
|
|
2282
2814
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2283
2815
|
|
|
@@ -3056,13 +3588,6 @@ var stepMetadata = {
|
|
|
3056
3588
|
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
3589
|
outputSchema: { "type": "object", "properties": { "data": { "description": "Parsed webhook response from Zapier (JSON object, array, or string)" } }, "required": ["data"] }
|
|
3058
3590
|
},
|
|
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
3591
|
"queryDataSource": {
|
|
3067
3592
|
stepType: "queryDataSource",
|
|
3068
3593
|
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.",
|
|
@@ -3266,9 +3791,11 @@ var stepMetadata = {
|
|
|
3266
3791
|
- Set replyTo to control the Reply-To address for replies.
|
|
3267
3792
|
- For threaded replies in a shared inbox, set inReplyTo (the Message-ID being replied to) and references (prior Message-IDs in the thread).
|
|
3268
3793
|
- connectionId can be a comma-separated list to send to multiple recipients.
|
|
3269
|
-
- The special connectionId "trigger_email" uses the email address that triggered the workflow
|
|
3270
|
-
|
|
3271
|
-
|
|
3794
|
+
- The special connectionId "trigger_email" uses the email address that triggered the workflow.
|
|
3795
|
+
- Sends default to category "marketing": every message carries a one-click unsubscribe header pointing at the app's own domain, and recipients who previously unsubscribed from this app are skipped and returned in "suppressed". Because the unsubscribe link is per-recipient, a marketing send delivers one separate message per recipient and cc/bcc are folded into that list.
|
|
3796
|
+
- Set category to "transactional" for receipts, alerts, password resets and anything a recipient must not be able to opt out of. Transactional sends carry no unsubscribe header, ignore the unsubscribe list, and keep cc/bcc on a single message \u2014 use it whenever cc/bcc semantics matter.`,
|
|
3797
|
+
inputSchema: { "type": "object", "properties": { "subject": { "type": "string", "description": "Email subject line" }, "body": { "type": "string", "description": "Email body content (plain text, markdown, HTML, or a CDN URL to an HTML file)" }, "to": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "cc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "bcc": { "anyOf": [{ "type": "string" }, { "type": "array", "items": { "type": "string" } }] }, "connectionId": { "type": "string", "description": "OAuth connection ID(s) for the recipient(s), comma-separated for multiple" }, "generateHtml": { "type": "boolean", "description": "When true, auto-convert the body text into a styled HTML email using AI" }, "generateHtmlInstructions": { "type": "string", "description": "Natural language instructions for the HTML generation style" }, "generateHtmlModelOverride": { "type": "object", "properties": { "model": { "type": "string", "description": 'Model identifier (e.g. "gpt-4", "claude-3-opus")' }, "temperature": { "type": "number", "description": "Sampling temperature for the model (0-2)" }, "maxResponseTokens": { "type": "number", "description": "Maximum number of tokens in the model's response" }, "ignorePreamble": { "type": "boolean", "description": "Whether to skip the system preamble/instructions" }, "userMessagePreprocessor": { "type": "object", "properties": { "dataSource": { "type": "string", "description": "Data source identifier for the preprocessor" }, "messageTemplate": { "type": "string", "description": "Template string applied to user messages before sending to the model" }, "maxResults": { "type": "number", "description": "Maximum number of results to include from the data source" }, "enabled": { "type": "boolean", "description": "Whether the preprocessor is active" }, "shouldInherit": { "type": "boolean", "description": "Whether child steps should inherit this preprocessor configuration" } }, "description": "Preprocessor applied to user messages before sending to the model" }, "preamble": { "type": "string", "description": "System preamble/instructions for the model" }, "multiModelEnabled": { "type": "boolean", "description": "Whether multi-model candidate generation is enabled" }, "editResponseEnabled": { "type": "boolean", "description": "Whether the user can edit the model's response" }, "config": { "type": "object", "properties": {}, "required": [], "description": "Additional model-specific configuration" } }, "required": ["model", "temperature", "maxResponseTokens"], "description": "Model settings override for HTML generation" }, "attachments": { "type": "array", "items": { "anyOf": [{ "type": "string" }, { "type": "object", "properties": { "url": { "type": "string" }, "filename": { "type": "string" }, "contentType": { "type": "string" } }, "required": ["url"] }] }, "description": "Files to attach: each entry is a URL string, or an object `{ url, filename?, contentType? }` to control the displayed filename and MIME type." }, "from": { "type": "string", "description": "Custom sender handle \u2014 only for apps with a custom domain or subdomain. Bare handle (`support`), full `support@your-domain.com`, or `Name <support@your-domain.com>`. Must resolve to a domain the app owns." }, "bodyType": { "enum": ["auto", "html", "markdown", "text"], "type": "string", "description": "How to interpret `body`: `auto` (default \u2014 detect HTML, else render markdown), `html` (send as-is), `markdown` (render to HTML), or `text` (send as plain text only, no HTML part). Every send includes a text/plain alternative." }, "text": { "type": "string", "description": "Explicit text/plain alternative body. Auto-derived from `body` if omitted." }, "replyTo": { "type": "string", "description": "Reply-To address for the email." }, "inReplyTo": { "type": "string", "description": "Message-ID this email replies to, for inbox threading (In-Reply-To header)." }, "references": { "type": "array", "items": { "type": "string" }, "description": "Prior Message-IDs in the thread, for inbox threading (References header)." }, "category": { "enum": ["transactional", "marketing"], "type": "string", "description": "`marketing` (default) adds one-click unsubscribe headers and skips recipients who unsubscribed from this app. Use `transactional` for receipts, alerts and password resets: no unsubscribe header, never suppressed, and cc/bcc preserved on a single message." } }, "required": ["subject", "body"] },
|
|
3798
|
+
outputSchema: { "type": "object", "properties": { "recipients": { "type": "array", "items": { "type": "string" }, "description": "To addresses the message was sent to." }, "cc": { "type": "array", "items": { "type": "string" }, "description": "Cc addresses on the message (empty if none)." }, "bcc": { "type": "array", "items": { "type": "string" }, "description": "Bcc addresses on the message (empty if none)." }, "from": { "type": "string", "description": "The resolved sender address the message went out as (auto-selected when no `from` is given)." }, "suppressed": { "type": "array", "items": { "type": "string" }, "description": "Recipients skipped because they unsubscribed from this app. Always empty for a `transactional` send." } }, "required": ["recipients", "cc", "bcc", "from", "suppressed"] }
|
|
3272
3799
|
},
|
|
3273
3800
|
"sendGmailDraft": {
|
|
3274
3801
|
stepType: "sendGmailDraft",
|
|
@@ -4733,9 +5260,6 @@ function applyStepMethods(AgentClass) {
|
|
|
4733
5260
|
proto.postToZapier = function(step, options) {
|
|
4734
5261
|
return this.executeStep("postToZapier", step, options);
|
|
4735
5262
|
};
|
|
4736
|
-
proto.queryAppDatabase = function(step, options) {
|
|
4737
|
-
return this.executeStep("queryAppDatabase", step, options);
|
|
4738
|
-
};
|
|
4739
5263
|
proto.queryDataSource = function(step, options) {
|
|
4740
5264
|
return this.executeStep("queryDataSource", step, options);
|
|
4741
5265
|
};
|
|
@@ -4962,6 +5486,7 @@ var MindStudioAgent = class {
|
|
|
4962
5486
|
_files;
|
|
4963
5487
|
_dataSources;
|
|
4964
5488
|
_voice;
|
|
5489
|
+
_analytics;
|
|
4965
5490
|
/** @internal Auth type — 'internal' for CALLBACK_TOKEN (managed mode), 'apiKey' otherwise. */
|
|
4966
5491
|
_authType;
|
|
4967
5492
|
/** @internal Usage source sent on step executions (from MINDSTUDIO_REQUEST_SOURCE).
|
|
@@ -5978,6 +6503,27 @@ var MindStudioAgent = class {
|
|
|
5978
6503
|
get files() {
|
|
5979
6504
|
return this._files ??= createFiles(this._filesRequest.bind(this));
|
|
5980
6505
|
}
|
|
6506
|
+
/**
|
|
6507
|
+
* The `analytics` namespace — read the app's own traffic + event analytics
|
|
6508
|
+
* (auto-tracked pageviews, `analytics.track()` events). One general
|
|
6509
|
+
* `query()` (metrics × dimensions × filters × time) plus `live()`,
|
|
6510
|
+
* `sources()`, `map()`, `aiSources()`, `crawlers.*`. See {@link Analytics}
|
|
6511
|
+
* for the lifetime-rollup vs 90-day-events split.
|
|
6512
|
+
*
|
|
6513
|
+
* @example
|
|
6514
|
+
* ```ts
|
|
6515
|
+
* const top = await agent.analytics.query({
|
|
6516
|
+
* metrics: ['pageviews', 'visitors'],
|
|
6517
|
+
* dimensions: ['path'],
|
|
6518
|
+
* dateRange: 'all',
|
|
6519
|
+
* });
|
|
6520
|
+
* ```
|
|
6521
|
+
*/
|
|
6522
|
+
get analytics() {
|
|
6523
|
+
return this._analytics ??= createAnalytics(
|
|
6524
|
+
this._analyticsRequest.bind(this)
|
|
6525
|
+
);
|
|
6526
|
+
}
|
|
5981
6527
|
/**
|
|
5982
6528
|
* Jewel surfaces: arrival-shaped triggers (`propose`) and the app-native
|
|
5983
6529
|
* approval queue (`queue.list` / `queue.resolve`). See {@link JewelsApi}.
|
|
@@ -6106,6 +6652,16 @@ var MindStudioAgent = class {
|
|
|
6106
6652
|
fallbackCode: "voice_error"
|
|
6107
6653
|
});
|
|
6108
6654
|
}
|
|
6655
|
+
/**
|
|
6656
|
+
* @internal Transport for the `analytics` namespace —
|
|
6657
|
+
* POST /_internal/v2/analytics/<op> with the raw hook token.
|
|
6658
|
+
*/
|
|
6659
|
+
async _analyticsRequest(op, body) {
|
|
6660
|
+
return this._brokeredRequest("analytics", op, body, {
|
|
6661
|
+
fallbackMessage: "Analytics read failed",
|
|
6662
|
+
fallbackCode: "analytics_error"
|
|
6663
|
+
});
|
|
6664
|
+
}
|
|
6109
6665
|
/**
|
|
6110
6666
|
* @internal Shared shape for the brokered `/_internal/v2/<ns>/<op>` data
|
|
6111
6667
|
* planes. Factored out rather than copied per namespace so error handling
|
|
@@ -6299,6 +6855,24 @@ var MindStudioAgent = class {
|
|
|
6299
6855
|
ago: (ms) => Date.now() - ms,
|
|
6300
6856
|
fromNow: (ms) => Date.now() + ms,
|
|
6301
6857
|
userRef: (id) => id.startsWith("@@user@@") ? id.slice("@@user@@".length) : id,
|
|
6858
|
+
// Raw SQL — validate synchronously; database resolution needs context,
|
|
6859
|
+
// so it happens inside the executeBatch closure at execution time
|
|
6860
|
+
// (the same deferral lazy defineTable uses).
|
|
6861
|
+
sql: ((query, params, options) => {
|
|
6862
|
+
validateRawSql(query);
|
|
6863
|
+
return new RawQuery(
|
|
6864
|
+
"",
|
|
6865
|
+
async (queries) => {
|
|
6866
|
+
await agent.ensureContext();
|
|
6867
|
+
const database = resolveDatabase(
|
|
6868
|
+
agent._getContext().databases,
|
|
6869
|
+
options?.database
|
|
6870
|
+
);
|
|
6871
|
+
return agent._executeDbBatch(database.id, queries);
|
|
6872
|
+
},
|
|
6873
|
+
{ sql: query, params: params?.map(serializeParam) }
|
|
6874
|
+
);
|
|
6875
|
+
}),
|
|
6302
6876
|
// Batch needs context — hydrate first, then delegate to real db
|
|
6303
6877
|
batch: ((...queries) => {
|
|
6304
6878
|
return (async () => {
|
|
@@ -6854,7 +7428,6 @@ var monacoSnippets = {
|
|
|
6854
7428
|
"postToSlackChannel": { fields: [["channelId", "string"], ["messageType", ["string", "blocks"]], ["message", "string"]], outputKeys: [] },
|
|
6855
7429
|
"postToX": { fields: [["text", "string"]], outputKeys: [] },
|
|
6856
7430
|
"postToZapier": { fields: [["webhookUrl", "string"], ["input", "object"]], outputKeys: ["data"] },
|
|
6857
|
-
"queryAppDatabase": { fields: [["databaseId", "string"], ["sql", "string"]], outputKeys: ["rows", "changes"] },
|
|
6858
7431
|
"queryDataSource": { fields: [["dataSourceId", "string"], ["query", "string"], ["maxResults", "number"]], outputKeys: ["text", "chunks", "query", "citations", "latencyMs"] },
|
|
6859
7432
|
"queryExternalDatabase": { fields: [["query", "string"], ["outputFormat", ["json", "csv"]]], outputKeys: ["data"] },
|
|
6860
7433
|
"redactPII": { fields: [["input", "string"], ["language", "string"], ["entities", "array"]], outputKeys: ["text"] },
|
|
@@ -6880,7 +7453,7 @@ var monacoSnippets = {
|
|
|
6880
7453
|
"searchXPosts": { fields: [["query", "string"], ["scope", ["recent", "all"]], ["options", "object"]], outputKeys: ["posts"] },
|
|
6881
7454
|
"searchYoutube": { fields: [["query", "string"], ["limitPages", "string"], ["filter", "string"], ["filterType", "string"]], outputKeys: ["results"] },
|
|
6882
7455
|
"searchYoutubeTrends": { fields: [["bp", ["now", "music", "gaming", "films"]], ["hl", "string"], ["gl", "string"]], outputKeys: [] },
|
|
6883
|
-
"sendEmail": { fields: [["subject", "string"], ["body", "string"]], outputKeys: ["recipients", "cc", "bcc", "from"] },
|
|
7456
|
+
"sendEmail": { fields: [["subject", "string"], ["body", "string"]], outputKeys: ["recipients", "cc", "bcc", "from", "suppressed"] },
|
|
6884
7457
|
"sendGmailDraft": { fields: [["draftId", "string"]], outputKeys: [] },
|
|
6885
7458
|
"sendGmailMessage": { fields: [["to", "string"], ["subject", "string"], ["message", "string"], ["messageType", ["plain", "html", "markdown"]]], outputKeys: ["messageId"] },
|
|
6886
7459
|
"sendSlackDirectMessage": { fields: [["slackUserId", "string"], ["messageType", ["string", "blocks"]], ["message", "string"]], outputKeys: [] },
|
|
@@ -6969,6 +7542,13 @@ var voice = new Proxy({}, {
|
|
|
6969
7542
|
return typeof value === "function" ? value.bind(target) : value;
|
|
6970
7543
|
}
|
|
6971
7544
|
});
|
|
7545
|
+
var analytics = new Proxy({}, {
|
|
7546
|
+
get(_, prop) {
|
|
7547
|
+
const target = mindstudio.analytics;
|
|
7548
|
+
const value = Reflect.get(target, prop, target);
|
|
7549
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
7550
|
+
}
|
|
7551
|
+
});
|
|
6972
7552
|
var session = new Proxy(
|
|
6973
7553
|
{},
|
|
6974
7554
|
{
|
|
@@ -6990,6 +7570,7 @@ export {
|
|
|
6990
7570
|
MindStudioAgent2 as MindStudioAgent,
|
|
6991
7571
|
MindStudioError,
|
|
6992
7572
|
Roles,
|
|
7573
|
+
analytics,
|
|
6993
7574
|
auth,
|
|
6994
7575
|
blockTypeAliases,
|
|
6995
7576
|
dataSources,
|