@mindstudio-ai/agent 0.1.9 → 0.1.11

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/index.js CHANGED
@@ -211,6 +211,19 @@ var Roles = new Proxy(
211
211
  );
212
212
 
213
213
  // src/db/sql.ts
214
+ function serializeParam(val) {
215
+ if (val === null || val === void 0) return null;
216
+ if (typeof val === "boolean") return val ? 1 : 0;
217
+ if (typeof val === "number" || typeof val === "string") return val;
218
+ return JSON.stringify(val);
219
+ }
220
+ function serializeColumnParam(val, columnName, columns) {
221
+ const col = columns.find((c) => c.name === columnName);
222
+ if (col?.type === "user" && typeof val === "string") {
223
+ return `@@user@@${val}`;
224
+ }
225
+ return serializeParam(val);
226
+ }
214
227
  function escapeValue(val) {
215
228
  if (val === null || val === void 0) return "NULL";
216
229
  if (typeof val === "boolean") return val ? "1" : "0";
@@ -219,13 +232,6 @@ function escapeValue(val) {
219
232
  const json = JSON.stringify(val);
220
233
  return `'${json.replace(/'/g, "''")}'`;
221
234
  }
222
- function serializeValue(val, columnName, columns) {
223
- const col = columns.find((c) => c.name === columnName);
224
- if (col?.type === "user" && typeof val === "string") {
225
- return escapeValue(`@@user@@${val}`);
226
- }
227
- return escapeValue(val);
228
- }
229
235
  var USER_PREFIX = "@@user@@";
230
236
  function deserializeRow(row, columns) {
231
237
  const result = {};
@@ -247,37 +253,54 @@ function deserializeRow(row, columns) {
247
253
  }
248
254
  function buildSelect(table, options = {}) {
249
255
  let sql = `SELECT * FROM ${table}`;
250
- if (options.where) sql += ` WHERE ${options.where}`;
256
+ const params = [];
257
+ if (options.where) {
258
+ sql += ` WHERE ${options.where}`;
259
+ if (options.whereParams) params.push(...options.whereParams);
260
+ }
251
261
  if (options.orderBy) sql += ` ORDER BY ${options.orderBy}${options.desc ? " DESC" : " ASC"}`;
252
262
  if (options.limit != null) sql += ` LIMIT ${options.limit}`;
253
263
  if (options.offset != null) sql += ` OFFSET ${options.offset}`;
254
- return sql;
264
+ return { sql, params: params.length > 0 ? params : void 0 };
255
265
  }
256
- function buildCount(table, where) {
266
+ function buildCount(table, where, whereParams) {
257
267
  let sql = `SELECT COUNT(*) as count FROM ${table}`;
258
268
  if (where) sql += ` WHERE ${where}`;
259
- return sql;
269
+ return { sql, params: whereParams?.length ? whereParams : void 0 };
260
270
  }
261
- function buildExists(table, where, negate) {
271
+ function buildExists(table, where, whereParams, negate) {
262
272
  const inner = where ? `SELECT 1 FROM ${table} WHERE ${where}` : `SELECT 1 FROM ${table}`;
263
273
  const fn = negate ? "NOT EXISTS" : "EXISTS";
264
- return `SELECT ${fn}(${inner}) as result`;
274
+ return { sql: `SELECT ${fn}(${inner}) as result`, params: whereParams?.length ? whereParams : void 0 };
265
275
  }
266
276
  function buildInsert(table, data, columns) {
267
277
  const filtered = stripSystemColumns(data);
268
278
  const keys = Object.keys(filtered);
269
- const vals = keys.map((k) => serializeValue(filtered[k], k, columns));
270
- return `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${vals.join(", ")})`;
279
+ const placeholders = keys.map(() => "?").join(", ");
280
+ const params = keys.map((k) => serializeColumnParam(filtered[k], k, columns));
281
+ return {
282
+ sql: `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) RETURNING *`,
283
+ params
284
+ };
271
285
  }
272
286
  function buildUpdate(table, id, data, columns) {
273
287
  const filtered = stripSystemColumns(data);
274
- const assignments = Object.entries(filtered).map(([k, v]) => `${k} = ${serializeValue(v, k, columns)}`).join(", ");
275
- return `UPDATE ${table} SET ${assignments} WHERE id = ${escapeValue(id)}`;
288
+ const keys = Object.keys(filtered);
289
+ const assignments = keys.map((k) => `${k} = ?`).join(", ");
290
+ const params = [
291
+ ...keys.map((k) => serializeColumnParam(filtered[k], k, columns)),
292
+ id
293
+ // for WHERE id = ?
294
+ ];
295
+ return {
296
+ sql: `UPDATE ${table} SET ${assignments} WHERE id = ? RETURNING *`,
297
+ params
298
+ };
276
299
  }
277
- function buildDelete(table, where) {
300
+ function buildDelete(table, where, whereParams) {
278
301
  let sql = `DELETE FROM ${table}`;
279
302
  if (where) sql += ` WHERE ${where}`;
280
- return sql;
303
+ return { sql, params: whereParams?.length ? whereParams : void 0 };
281
304
  }
282
305
  var SYSTEM_COLUMNS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "lastUpdatedBy"]);
283
306
  function stripSystemColumns(data) {
@@ -693,8 +716,7 @@ var Parser = class {
693
716
  return PARSE_FAILED;
694
717
  }
695
718
  /**
696
- * Attempt to resolve a closure variable by invoking the original function
697
- * with a recording Proxy and inspecting what values it compares against.
719
+ * Attempt to resolve a closure variable's value.
698
720
  *
699
721
  * This handles the common pattern:
700
722
  * ```ts
@@ -702,40 +724,28 @@ var Parser = class {
702
724
  * orders.filter(o => o.requestedBy === userId)
703
725
  * ```
704
726
  *
705
- * The Proxy captures property accesses on the parameter and we can then
706
- * extract the comparison value from the function's behavior. However,
707
- * this approach has limitations if the function throws, has side effects,
708
- * or uses the variable in a non-comparison context, we fall back to JS.
727
+ * Closure variable resolution is fundamentally limited in JavaScript
728
+ * we can't access another function's closure scope from outside without
729
+ * `eval`. The `===` operator can't be overridden via Proxy or
730
+ * Symbol.toPrimitive, so we can't intercept comparisons.
731
+ *
732
+ * For now, this falls back to JS execution. The predicate still works
733
+ * correctly — it just scans all rows instead of generating SQL.
734
+ * This is the most common reason for JS fallback in practice, since
735
+ * almost every real-world filter references a variable like `userId`.
736
+ *
737
+ * A future improvement could accept an explicit `vars` argument:
738
+ * ```ts
739
+ * orders.filter(o => o.requestedBy === $userId, { $userId: auth.userId })
740
+ * ```
709
741
  */
710
742
  resolveClosureVariable() {
711
- const identToken = this.advance();
712
- let closureExpr = identToken.value;
743
+ this.advance();
713
744
  while (this.match("dot") && this.tokens[this.pos + 1]?.type === "identifier") {
714
745
  this.advance();
715
- closureExpr += "." + this.advance().value;
716
- }
717
- try {
718
- const MARKER = /* @__PURE__ */ Symbol("field_access_marker");
719
- const accessed = [];
720
- const proxy = new Proxy(
721
- {},
722
- {
723
- get(_, prop) {
724
- accessed.push(prop);
725
- return new Proxy(() => MARKER, {
726
- get(_2, nestedProp) {
727
- accessed.push(nestedProp);
728
- return MARKER;
729
- }
730
- });
731
- }
732
- }
733
- );
734
- void proxy;
735
- return PARSE_FAILED;
736
- } catch {
737
- return PARSE_FAILED;
746
+ this.advance();
738
747
  }
748
+ return PARSE_FAILED;
739
749
  }
740
750
  /**
741
751
  * Look ahead to check if the next tokens form `.includes(`.
@@ -792,17 +802,11 @@ function isComparisonOp(value) {
792
802
 
793
803
  // src/db/query.ts
794
804
  var Query = class _Query {
795
- /** @internal Accumulated predicate functions to filter by. */
796
805
  _predicates;
797
- /** @internal The field accessor for sorting, if set. */
798
806
  _sortAccessor;
799
- /** @internal Whether the sort order is reversed (DESC). */
800
807
  _reversed;
801
- /** @internal Maximum number of results (SQL LIMIT). */
802
808
  _limit;
803
- /** @internal Number of results to skip (SQL OFFSET). */
804
809
  _offset;
805
- /** @internal Binding to the database execution layer. */
806
810
  _config;
807
811
  constructor(config, options) {
808
812
  this._config = config;
@@ -812,10 +816,6 @@ var Query = class _Query {
812
816
  this._limit = options?.limit;
813
817
  this._offset = options?.offset;
814
818
  }
815
- /**
816
- * Create a clone of this query with some options overridden.
817
- * Used internally by chain methods to maintain immutability.
818
- */
819
819
  _clone(overrides) {
820
820
  return new _Query(this._config, {
821
821
  predicates: overrides.predicates ?? this._predicates,
@@ -826,126 +826,73 @@ var Query = class _Query {
826
826
  });
827
827
  }
828
828
  // -------------------------------------------------------------------------
829
- // Chain methods — return new Query instances
829
+ // Chain methods
830
830
  // -------------------------------------------------------------------------
831
- /**
832
- * Add a filter predicate. Multiple filters are ANDed together.
833
- *
834
- * @example
835
- * ```ts
836
- * const active = Orders.filter(o => o.status === 'active');
837
- * const expensive = active.filter(o => o.amount > 5000);
838
- * // WHERE status = 'active' AND amount > 5000
839
- * ```
840
- */
841
831
  filter(predicate) {
842
- return this._clone({
843
- predicates: [...this._predicates, predicate]
844
- });
832
+ return this._clone({ predicates: [...this._predicates, predicate] });
845
833
  }
846
- /**
847
- * Sort results by a field (ascending by default).
848
- * Use `.reverse()` after `.sortBy()` for descending order.
849
- *
850
- * @example
851
- * ```ts
852
- * const newest = Orders.sortBy(o => o.createdAt).reverse();
853
- * ```
854
- */
855
834
  sortBy(accessor) {
856
835
  return this._clone({ sortAccessor: accessor });
857
836
  }
858
- /**
859
- * Reverse the current sort order. If no sort is set, this has no effect.
860
- */
861
837
  reverse() {
862
838
  return this._clone({ reversed: !this._reversed });
863
839
  }
864
- /**
865
- * Limit the number of results returned.
866
- *
867
- * @example
868
- * ```ts
869
- * const top10 = Orders.sortBy(o => o.amount).reverse().take(10);
870
- * ```
871
- */
872
840
  take(n) {
873
841
  return this._clone({ limit: n });
874
842
  }
875
- /**
876
- * Skip the first n results. Use with `.take()` for pagination.
877
- *
878
- * @example
879
- * ```ts
880
- * const page2 = Orders.sortBy(o => o.createdAt).skip(50).take(50);
881
- * ```
882
- */
883
843
  skip(n) {
884
844
  return this._clone({ offset: n });
885
845
  }
886
846
  // -------------------------------------------------------------------------
887
- // Terminal methods — execute the query and return results
847
+ // Terminal methods
888
848
  // -------------------------------------------------------------------------
889
- /**
890
- * Return the first matching row, or null if no rows match.
891
- * Applies the current sort order before taking the first result.
892
- */
893
849
  async first() {
894
850
  const rows = await this._clone({ limit: 1 })._execute();
895
851
  return rows[0] ?? null;
896
852
  }
897
- /**
898
- * Return the last matching row (per current sort), or null.
899
- * Flips the sort direction and takes 1 row.
900
- */
901
853
  async last() {
902
854
  const rows = await this._clone({ limit: 1, reversed: !this._reversed })._execute();
903
855
  return rows[0] ?? null;
904
856
  }
905
- /**
906
- * Count matching rows. Returns a number, not the rows themselves.
907
- * Executes as `SELECT COUNT(*)` when predicates compile to SQL.
908
- */
909
857
  async count() {
910
858
  const compiled = this._compilePredicates();
911
859
  if (compiled.allSql) {
912
- const where = compiled.sqlWhere || void 0;
913
- const sql = buildCount(this._config.tableName, where);
914
- const result = await this._config.executeQuery(sql);
915
- const row = result.rows[0];
860
+ const query = buildCount(
861
+ this._config.tableName,
862
+ compiled.sqlWhere || void 0
863
+ );
864
+ const results = await this._config.executeBatch([query]);
865
+ const row = results[0]?.rows[0];
916
866
  return row?.count ?? 0;
917
867
  }
918
868
  const rows = await this._fetchAndFilterInJs(compiled);
919
869
  return rows.length;
920
870
  }
921
- /**
922
- * Check if any row matches the current filters. Short-circuits —
923
- * doesn't load all rows when using SQL.
924
- */
925
871
  async some() {
926
872
  const compiled = this._compilePredicates();
927
873
  if (compiled.allSql) {
928
- const where = compiled.sqlWhere || void 0;
929
- const sql = buildExists(this._config.tableName, where);
930
- const result = await this._config.executeQuery(sql);
931
- const row = result.rows[0];
874
+ const query = buildExists(
875
+ this._config.tableName,
876
+ compiled.sqlWhere || void 0
877
+ );
878
+ const results = await this._config.executeBatch([query]);
879
+ const row = results[0]?.rows[0];
932
880
  return row?.result === 1;
933
881
  }
934
882
  const rows = await this._fetchAndFilterInJs(compiled);
935
883
  return rows.length > 0;
936
884
  }
937
- /**
938
- * Check if all rows match the current filters. Short-circuits on false.
939
- *
940
- * Implemented as NOT EXISTS(... WHERE NOT predicate) — returns true
941
- * if no rows fail the predicate.
942
- */
943
885
  async every() {
944
886
  const compiled = this._compilePredicates();
945
887
  if (compiled.allSql && compiled.sqlWhere) {
946
- const sql = buildExists(this._config.tableName, `NOT (${compiled.sqlWhere})`, true);
947
- const result = await this._config.executeQuery(sql);
948
- const row = result.rows[0];
888
+ const query = buildExists(
889
+ this._config.tableName,
890
+ `NOT (${compiled.sqlWhere})`,
891
+ void 0,
892
+ true
893
+ );
894
+ const results = await this._config.executeBatch([query]);
895
+ const row = results[0]?.rows[0];
949
896
  return row?.result === 1;
950
897
  }
951
898
  if (this._predicates.length === 0) return true;
@@ -954,24 +901,12 @@ var Query = class _Query {
954
901
  (row) => this._predicates.every((pred) => pred(row))
955
902
  );
956
903
  }
957
- /**
958
- * Return the row with the minimum value for the given field.
959
- * Executes as `ORDER BY field ASC LIMIT 1` in SQL.
960
- */
961
904
  async min(accessor) {
962
905
  return this.sortBy(accessor).first();
963
906
  }
964
- /**
965
- * Return the row with the maximum value for the given field.
966
- * Executes as `ORDER BY field DESC LIMIT 1` in SQL.
967
- */
968
907
  async max(accessor) {
969
908
  return this.sortBy(accessor).reverse().first();
970
909
  }
971
- /**
972
- * Group rows by a field value. Returns a Map.
973
- * Always executes in JS (no SQL equivalent for grouping into a Map).
974
- */
975
910
  async groupBy(accessor) {
976
911
  const rows = await this._execute();
977
912
  const map = /* @__PURE__ */ new Map();
@@ -987,40 +922,97 @@ var Query = class _Query {
987
922
  return map;
988
923
  }
989
924
  // -------------------------------------------------------------------------
990
- // PromiseLike implementationmakes `await query` work
925
+ // Batch compilationused by db.batch() to extract SQL without executing
991
926
  // -------------------------------------------------------------------------
992
927
  /**
993
- * PromiseLike.then() executes the query and pipes the result.
994
- * This is what makes `const rows = await query` work.
928
+ * @internal Compile this query into a SqlQuery for batch execution.
929
+ *
930
+ * Returns the compiled SQL query (if all predicates compile to SQL),
931
+ * or null (if JS fallback is needed). In the fallback case, a bare
932
+ * `SELECT *` is returned as `fallbackQuery` so the batch can fetch
933
+ * all rows and this query can filter them in JS post-fetch.
934
+ */
935
+ _compile() {
936
+ const compiled = this._compilePredicates();
937
+ const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
938
+ if (compiled.allSql) {
939
+ const query = buildSelect(this._config.tableName, {
940
+ where: compiled.sqlWhere || void 0,
941
+ orderBy: sortField ?? void 0,
942
+ desc: this._reversed,
943
+ limit: this._limit,
944
+ offset: this._offset
945
+ });
946
+ return { query, fallbackQuery: null, config: this._config };
947
+ }
948
+ const fallbackQuery = buildSelect(this._config.tableName);
949
+ return {
950
+ query: null,
951
+ fallbackQuery,
952
+ config: this._config,
953
+ predicates: this._predicates,
954
+ sortAccessor: this._sortAccessor,
955
+ reversed: this._reversed,
956
+ limit: this._limit,
957
+ offset: this._offset
958
+ };
959
+ }
960
+ /**
961
+ * @internal Process raw SQL results into typed rows. Used by db.batch()
962
+ * after executing the compiled query.
963
+ *
964
+ * For SQL-compiled queries: just deserialize the rows.
965
+ * For JS-fallback queries: filter, sort, and slice in JS.
995
966
  */
967
+ static _processResults(result, compiled) {
968
+ const rows = result.rows.map(
969
+ (row) => deserializeRow(
970
+ row,
971
+ compiled.config.columns
972
+ )
973
+ );
974
+ if (compiled.query) return rows;
975
+ let filtered = compiled.predicates ? rows.filter((row) => compiled.predicates.every((pred) => pred(row))) : rows;
976
+ if (compiled.sortAccessor) {
977
+ const accessor = compiled.sortAccessor;
978
+ const reversed = compiled.reversed ?? false;
979
+ filtered.sort((a, b) => {
980
+ const aVal = accessor(a);
981
+ const bVal = accessor(b);
982
+ if (aVal < bVal) return reversed ? 1 : -1;
983
+ if (aVal > bVal) return reversed ? -1 : 1;
984
+ return 0;
985
+ });
986
+ }
987
+ if (compiled.offset != null || compiled.limit != null) {
988
+ const start = compiled.offset ?? 0;
989
+ const end = compiled.limit != null ? start + compiled.limit : void 0;
990
+ filtered = filtered.slice(start, end);
991
+ }
992
+ return filtered;
993
+ }
994
+ // -------------------------------------------------------------------------
995
+ // PromiseLike
996
+ // -------------------------------------------------------------------------
996
997
  then(onfulfilled, onrejected) {
997
998
  return this._execute().then(onfulfilled, onrejected);
998
999
  }
999
1000
  // -------------------------------------------------------------------------
1000
1001
  // Execution internals
1001
1002
  // -------------------------------------------------------------------------
1002
- /**
1003
- * Execute the query and return typed result rows.
1004
- *
1005
- * This is the core execution method. It:
1006
- * 1. Tries to compile all predicates to SQL
1007
- * 2. If all compile → builds and executes a single SQL query
1008
- * 3. If any fail → fetches all rows and processes in JS
1009
- * 4. Deserializes rows (user prefix stripping, JSON parsing)
1010
- */
1011
1003
  async _execute() {
1012
1004
  const compiled = this._compilePredicates();
1013
1005
  if (compiled.allSql) {
1014
1006
  const sortField = this._sortAccessor ? extractFieldName(this._sortAccessor) : void 0;
1015
- const sql = buildSelect(this._config.tableName, {
1007
+ const query = buildSelect(this._config.tableName, {
1016
1008
  where: compiled.sqlWhere || void 0,
1017
1009
  orderBy: sortField ?? void 0,
1018
1010
  desc: this._reversed,
1019
1011
  limit: this._limit,
1020
1012
  offset: this._offset
1021
1013
  });
1022
- const result = await this._config.executeQuery(sql);
1023
- return result.rows.map(
1014
+ const results = await this._config.executeBatch([query]);
1015
+ return results[0].rows.map(
1024
1016
  (row) => deserializeRow(
1025
1017
  row,
1026
1018
  this._config.columns
@@ -1045,14 +1037,6 @@ var Query = class _Query {
1045
1037
  }
1046
1038
  return rows;
1047
1039
  }
1048
- /**
1049
- * Compile all accumulated predicates and determine the execution strategy.
1050
- *
1051
- * Returns an object with:
1052
- * - `allSql`: whether all predicates compiled to SQL
1053
- * - `sqlWhere`: combined WHERE clause (ANDed) if all compiled
1054
- * - `compiled`: individual compilation results
1055
- */
1056
1040
  _compilePredicates() {
1057
1041
  if (this._predicates.length === 0) {
1058
1042
  return { allSql: true, sqlWhere: "", compiled: [] };
@@ -1065,12 +1049,6 @@ var Query = class _Query {
1065
1049
  }
1066
1050
  return { allSql, sqlWhere, compiled };
1067
1051
  }
1068
- /**
1069
- * Fetch all rows from the table and apply JS predicates.
1070
- * This is the fallback path when SQL compilation fails.
1071
- *
1072
- * Logs a warning to stderr so developers know they're on the slow path.
1073
- */
1074
1052
  async _fetchAndFilterInJs(compiled) {
1075
1053
  const allRows = await this._fetchAllRows();
1076
1054
  if (compiled.compiled.some((c) => c.type === "js")) {
@@ -1082,14 +1060,10 @@ var Query = class _Query {
1082
1060
  (row) => this._predicates.every((pred) => pred(row))
1083
1061
  );
1084
1062
  }
1085
- /**
1086
- * Fetch all rows from the table (SELECT * with no WHERE).
1087
- * Used by the JS fallback path.
1088
- */
1089
1063
  async _fetchAllRows() {
1090
- const sql = buildSelect(this._config.tableName);
1091
- const result = await this._config.executeQuery(sql);
1092
- return result.rows.map(
1064
+ const query = buildSelect(this._config.tableName);
1065
+ const results = await this._config.executeBatch([query]);
1066
+ return results[0].rows.map(
1093
1067
  (row) => deserializeRow(row, this._config.columns)
1094
1068
  );
1095
1069
  }
@@ -1104,298 +1078,147 @@ function extractFieldName(accessor) {
1104
1078
 
1105
1079
  // src/db/table.ts
1106
1080
  var Table = class {
1107
- /** @internal Runtime config binding this table to the execution layer. */
1081
+ /** @internal */
1108
1082
  _config;
1109
1083
  constructor(config) {
1110
1084
  this._config = config;
1111
1085
  }
1112
1086
  // -------------------------------------------------------------------------
1113
- // Reads — direct (return Promises)
1087
+ // Reads — direct
1114
1088
  // -------------------------------------------------------------------------
1115
- /**
1116
- * Get a single row by ID. Returns null if not found.
1117
- *
1118
- * @example
1119
- * ```ts
1120
- * const order = await Orders.get('abc-123');
1121
- * if (order) console.log(order.status);
1122
- * ```
1123
- */
1124
1089
  async get(id) {
1125
- const sql = buildSelect(this._config.tableName, {
1126
- where: `id = ${escapeValue(id)}`,
1090
+ const query = buildSelect(this._config.tableName, {
1091
+ where: `id = ?`,
1092
+ whereParams: [id],
1127
1093
  limit: 1
1128
1094
  });
1129
- const result = await this._config.executeQuery(sql);
1130
- if (result.rows.length === 0) return null;
1095
+ const results = await this._config.executeBatch([query]);
1096
+ if (results[0].rows.length === 0) return null;
1131
1097
  return deserializeRow(
1132
- result.rows[0],
1098
+ results[0].rows[0],
1133
1099
  this._config.columns
1134
1100
  );
1135
1101
  }
1136
- /**
1137
- * Find the first row matching a predicate. Returns null if none match.
1138
- *
1139
- * @example
1140
- * ```ts
1141
- * const activeOrder = await Orders.findOne(o => o.status === 'active');
1142
- * ```
1143
- */
1144
1102
  async findOne(predicate) {
1145
1103
  return this.filter(predicate).first();
1146
1104
  }
1147
- /**
1148
- * Count rows, optionally filtered by a predicate.
1149
- *
1150
- * @example
1151
- * ```ts
1152
- * const total = await Orders.count();
1153
- * const pending = await Orders.count(o => o.status === 'pending');
1154
- * ```
1155
- */
1156
1105
  async count(predicate) {
1157
- if (predicate) {
1158
- return this.filter(predicate).count();
1159
- }
1160
- const sql = buildCount(this._config.tableName);
1161
- const result = await this._config.executeQuery(sql);
1162
- const row = result.rows[0];
1106
+ if (predicate) return this.filter(predicate).count();
1107
+ const query = buildCount(this._config.tableName);
1108
+ const results = await this._config.executeBatch([query]);
1109
+ const row = results[0]?.rows[0];
1163
1110
  return row?.count ?? 0;
1164
1111
  }
1165
- /**
1166
- * Check if any row matches a predicate. Short-circuits.
1167
- *
1168
- * @example
1169
- * ```ts
1170
- * const hasActive = await Orders.some(o => o.status === 'active');
1171
- * ```
1172
- */
1173
1112
  async some(predicate) {
1174
1113
  return this.filter(predicate).some();
1175
1114
  }
1176
- /**
1177
- * Check if all rows match a predicate.
1178
- *
1179
- * @example
1180
- * ```ts
1181
- * const allComplete = await Orders.every(o => o.status === 'completed');
1182
- * ```
1183
- */
1184
1115
  async every(predicate) {
1185
1116
  return this.filter(predicate).every();
1186
1117
  }
1187
- /**
1188
- * Check if the table has zero rows.
1189
- *
1190
- * @example
1191
- * ```ts
1192
- * if (await Orders.isEmpty()) console.log('No orders yet');
1193
- * ```
1194
- */
1195
1118
  async isEmpty() {
1196
- const sql = buildExists(this._config.tableName, void 0, true);
1197
- const result = await this._config.executeQuery(sql);
1198
- const row = result.rows[0];
1119
+ const query = buildExists(this._config.tableName, void 0, void 0, true);
1120
+ const results = await this._config.executeBatch([query]);
1121
+ const row = results[0]?.rows[0];
1199
1122
  return row?.result === 1;
1200
1123
  }
1201
- /**
1202
- * Return the row with the minimum value for a field.
1203
- * Executes as `ORDER BY field ASC LIMIT 1`.
1204
- *
1205
- * @example
1206
- * ```ts
1207
- * const cheapest = await Orders.min(o => o.amount);
1208
- * ```
1209
- */
1210
1124
  async min(accessor) {
1211
1125
  return this.sortBy(accessor).first();
1212
1126
  }
1213
- /**
1214
- * Return the row with the maximum value for a field.
1215
- * Executes as `ORDER BY field DESC LIMIT 1`.
1216
- *
1217
- * @example
1218
- * ```ts
1219
- * const mostExpensive = await Orders.max(o => o.amount);
1220
- * ```
1221
- */
1222
1127
  async max(accessor) {
1223
1128
  return this.sortBy(accessor).reverse().first();
1224
1129
  }
1225
- /**
1226
- * Group all rows by a field value. Returns a Map.
1227
- *
1228
- * @example
1229
- * ```ts
1230
- * const byStatus = await Orders.groupBy(o => o.status);
1231
- * // Map { 'pending' => [...], 'approved' => [...] }
1232
- * ```
1233
- */
1234
1130
  async groupBy(accessor) {
1235
1131
  return new Query(this._config).groupBy(accessor);
1236
1132
  }
1237
1133
  // -------------------------------------------------------------------------
1238
- // Reads — chainable (return Query<T>)
1134
+ // Reads — chainable
1239
1135
  // -------------------------------------------------------------------------
1240
- /**
1241
- * Filter rows by a predicate. Returns a chainable Query.
1242
- *
1243
- * The predicate is compiled to SQL when possible. If compilation fails,
1244
- * the query falls back to fetching all rows and filtering in JS.
1245
- *
1246
- * @example
1247
- * ```ts
1248
- * const active = await Orders.filter(o => o.status === 'active');
1249
- * const recentActive = await Orders
1250
- * .filter(o => o.status === 'active')
1251
- * .sortBy(o => o.createdAt)
1252
- * .reverse()
1253
- * .take(10);
1254
- * ```
1255
- */
1256
1136
  filter(predicate) {
1257
1137
  return new Query(this._config).filter(predicate);
1258
1138
  }
1259
- /**
1260
- * Sort all rows by a field. Returns a chainable Query.
1261
- *
1262
- * @example
1263
- * ```ts
1264
- * const newest = await Orders.sortBy(o => o.createdAt).reverse().take(5);
1265
- * ```
1266
- */
1267
1139
  sortBy(accessor) {
1268
1140
  return new Query(this._config).sortBy(accessor);
1269
1141
  }
1270
1142
  async push(data) {
1271
1143
  const isArray = Array.isArray(data);
1272
1144
  const items = isArray ? data : [data];
1273
- const results = [];
1274
- for (const item of items) {
1275
- const insertSql = buildInsert(
1145
+ const queries = items.map(
1146
+ (item) => buildInsert(
1276
1147
  this._config.tableName,
1277
1148
  item,
1278
1149
  this._config.columns
1279
- );
1280
- await this._config.executeQuery(insertSql);
1281
- const fetchSql = `SELECT * FROM ${this._config.tableName} WHERE rowid = last_insert_rowid()`;
1282
- const fetchResult = await this._config.executeQuery(fetchSql);
1283
- if (fetchResult.rows.length > 0) {
1284
- results.push(
1285
- deserializeRow(
1286
- fetchResult.rows[0],
1287
- this._config.columns
1288
- )
1150
+ )
1151
+ );
1152
+ const results = await this._config.executeBatch(queries);
1153
+ const rows = results.map((r) => {
1154
+ if (r.rows.length > 0) {
1155
+ return deserializeRow(
1156
+ r.rows[0],
1157
+ this._config.columns
1289
1158
  );
1290
1159
  }
1291
- }
1292
- return isArray ? results : results[0];
1160
+ return void 0;
1161
+ });
1162
+ return isArray ? rows : rows[0];
1293
1163
  }
1294
1164
  /**
1295
1165
  * Update a row by ID. Only the provided fields are changed.
1296
- * Returns the updated row.
1297
- *
1298
- * System columns cannot be updated — they're stripped automatically.
1299
- * `updatedAt` and `lastUpdatedBy` are set by the platform.
1300
- *
1301
- * @example
1302
- * ```ts
1303
- * const updated = await Orders.update(order.id, { status: 'approved' });
1304
- * console.log(updated.updatedAt); // freshly updated
1305
- * ```
1166
+ * Returns the updated row via `UPDATE ... RETURNING *`.
1306
1167
  */
1307
1168
  async update(id, data) {
1308
- const updateSql = buildUpdate(
1169
+ const query = buildUpdate(
1309
1170
  this._config.tableName,
1310
1171
  id,
1311
1172
  data,
1312
1173
  this._config.columns
1313
1174
  );
1314
- await this._config.executeQuery(updateSql);
1315
- const fetchSql = buildSelect(this._config.tableName, {
1316
- where: `id = ${escapeValue(id)}`,
1317
- limit: 1
1318
- });
1319
- const result = await this._config.executeQuery(fetchSql);
1175
+ const results = await this._config.executeBatch([query]);
1320
1176
  return deserializeRow(
1321
- result.rows[0],
1177
+ results[0].rows[0],
1322
1178
  this._config.columns
1323
1179
  );
1324
1180
  }
1325
- /**
1326
- * Remove a row by ID.
1327
- *
1328
- * @example
1329
- * ```ts
1330
- * await Orders.remove('abc-123');
1331
- * ```
1332
- */
1333
1181
  async remove(id) {
1334
- const sql = buildDelete(
1335
- this._config.tableName,
1336
- `id = ${escapeValue(id)}`
1337
- );
1338
- await this._config.executeQuery(sql);
1182
+ const query = buildDelete(this._config.tableName, `id = ?`, [id]);
1183
+ await this._config.executeBatch([query]);
1339
1184
  }
1340
1185
  /**
1341
1186
  * Remove all rows matching a predicate. Returns the count removed.
1342
- *
1343
- * The predicate is compiled to SQL when possible. If compilation fails,
1344
- * the function fetches all matching rows, collects their IDs, and
1345
- * deletes them individually.
1346
- *
1347
- * @example
1348
- * ```ts
1349
- * const removed = await Orders.removeAll(o => o.status === 'rejected');
1350
- * console.log(`Removed ${removed} orders`);
1351
- * ```
1352
1187
  */
1353
1188
  async removeAll(predicate) {
1354
1189
  const compiled = compilePredicate(predicate);
1355
1190
  if (compiled.type === "sql") {
1356
- const sql = buildDelete(this._config.tableName, compiled.where);
1357
- const result = await this._config.executeQuery(sql);
1358
- return result.changes;
1191
+ const query = buildDelete(this._config.tableName, compiled.where);
1192
+ const results = await this._config.executeBatch([query]);
1193
+ return results[0].changes;
1359
1194
  }
1360
1195
  console.warn(
1361
1196
  `[mindstudio] removeAll predicate on ${this._config.tableName} could not be compiled to SQL \u2014 fetching all rows first`
1362
1197
  );
1363
- const allSql = buildSelect(this._config.tableName);
1364
- const allResult = await this._config.executeQuery(allSql);
1365
- const allRows = allResult.rows.map(
1198
+ const allQuery = buildSelect(this._config.tableName);
1199
+ const allResults = await this._config.executeBatch([allQuery]);
1200
+ const allRows = allResults[0].rows.map(
1366
1201
  (r) => deserializeRow(
1367
1202
  r,
1368
1203
  this._config.columns
1369
1204
  )
1370
1205
  );
1371
1206
  const matching = allRows.filter((row) => predicate(row));
1372
- let count = 0;
1373
- for (const row of matching) {
1374
- const id = row.id;
1375
- if (id) {
1376
- const sql = buildDelete(this._config.tableName, `id = ${escapeValue(id)}`);
1377
- await this._config.executeQuery(sql);
1378
- count++;
1379
- }
1207
+ if (matching.length === 0) return 0;
1208
+ const deleteQueries = matching.filter((row) => row.id).map((row) => buildDelete(this._config.tableName, `id = ?`, [row.id]));
1209
+ if (deleteQueries.length > 0) {
1210
+ await this._config.executeBatch(deleteQueries);
1380
1211
  }
1381
- return count;
1212
+ return matching.length;
1382
1213
  }
1383
- /**
1384
- * Remove all rows from the table.
1385
- *
1386
- * @example
1387
- * ```ts
1388
- * await Orders.clear();
1389
- * ```
1390
- */
1391
1214
  async clear() {
1392
- const sql = buildDelete(this._config.tableName);
1393
- await this._config.executeQuery(sql);
1215
+ const query = buildDelete(this._config.tableName);
1216
+ await this._config.executeBatch([query]);
1394
1217
  }
1395
1218
  };
1396
1219
 
1397
1220
  // src/db/index.ts
1398
- function createDb(databases, executeQuery) {
1221
+ function createDb(databases, executeBatch) {
1399
1222
  return {
1400
1223
  defineTable(name, options) {
1401
1224
  const resolved = resolveTable(databases, name, options?.database);
@@ -1403,7 +1226,7 @@ function createDb(databases, executeQuery) {
1403
1226
  databaseId: resolved.databaseId,
1404
1227
  tableName: name,
1405
1228
  columns: resolved.columns,
1406
- executeQuery: (sql) => executeQuery(resolved.databaseId, sql)
1229
+ executeBatch: (queries) => executeBatch(resolved.databaseId, queries)
1407
1230
  };
1408
1231
  return new Table(config);
1409
1232
  },
@@ -1414,7 +1237,49 @@ function createDb(databases, executeQuery) {
1414
1237
  hours: (n) => n * 36e5,
1415
1238
  minutes: (n) => n * 6e4,
1416
1239
  ago: (ms) => Date.now() - ms,
1417
- fromNow: (ms) => Date.now() + ms
1240
+ fromNow: (ms) => Date.now() + ms,
1241
+ // --- Batch execution ---
1242
+ batch: ((...queries) => {
1243
+ return (async () => {
1244
+ const compiled = queries.map((q) => {
1245
+ if (!(q instanceof Query)) {
1246
+ throw new MindStudioError(
1247
+ "db.batch() only accepts Query objects (from .filter(), .sortBy(), etc.)",
1248
+ "invalid_batch_query",
1249
+ 400
1250
+ );
1251
+ }
1252
+ return q._compile();
1253
+ });
1254
+ const groups = /* @__PURE__ */ new Map();
1255
+ for (let i = 0; i < compiled.length; i++) {
1256
+ const c = compiled[i];
1257
+ const dbId = c.config.databaseId;
1258
+ const sqlQuery = c.query ?? c.fallbackQuery;
1259
+ if (!groups.has(dbId)) groups.set(dbId, []);
1260
+ groups.get(dbId).push({ index: i, sqlQuery });
1261
+ }
1262
+ const allResults = new Array(compiled.length);
1263
+ await Promise.all(
1264
+ Array.from(groups.entries()).map(async ([dbId, entries]) => {
1265
+ const sqlQueries = entries.map((e) => e.sqlQuery);
1266
+ const results = await executeBatch(dbId, sqlQueries);
1267
+ for (let i = 0; i < entries.length; i++) {
1268
+ allResults[entries[i].index] = results[i];
1269
+ }
1270
+ })
1271
+ );
1272
+ return compiled.map((c, i) => {
1273
+ const result = allResults[i];
1274
+ if (!c.query && c.predicates?.length) {
1275
+ console.warn(
1276
+ `[mindstudio] db.batch(): filter on ${c.config.tableName} could not be compiled to SQL \u2014 processing in JS`
1277
+ );
1278
+ }
1279
+ return Query._processResults(result, c);
1280
+ });
1281
+ })();
1282
+ })
1418
1283
  };
1419
1284
  }
1420
1285
  function resolveTable(databases, tableName, databaseHint) {
@@ -2325,6 +2190,9 @@ var MindStudioAgent = class {
2325
2190
  * ```
2326
2191
  */
2327
2192
  get auth() {
2193
+ if (!this._auth) {
2194
+ this._trySandboxHydration();
2195
+ }
2328
2196
  if (!this._auth) {
2329
2197
  throw new MindStudioError(
2330
2198
  "Auth context not yet loaded. Call `await agent.ensureContext()` or perform any db operation first (which auto-hydrates context). Inside the MindStudio sandbox, context is loaded automatically.",
@@ -2350,6 +2218,9 @@ var MindStudioAgent = class {
2350
2218
  * ```
2351
2219
  */
2352
2220
  get db() {
2221
+ if (!this._db) {
2222
+ this._trySandboxHydration();
2223
+ }
2353
2224
  if (this._db) return this._db;
2354
2225
  return this._createLazyDb();
2355
2226
  }
@@ -2407,7 +2278,7 @@ var MindStudioAgent = class {
2407
2278
  this._auth = new AuthContext(context.auth);
2408
2279
  this._db = createDb(
2409
2280
  context.databases,
2410
- this._executeDbQuery.bind(this)
2281
+ this._executeDbBatch.bind(this)
2411
2282
  );
2412
2283
  }
2413
2284
  /**
@@ -2428,25 +2299,40 @@ var MindStudioAgent = class {
2428
2299
  }
2429
2300
  }
2430
2301
  /**
2431
- * @internal Execute a SQL query against a managed database.
2432
- * Used as the `executeQuery` callback for Table instances.
2302
+ * @internal Execute a batch of SQL queries against a managed database.
2303
+ * Used as the `executeBatch` callback for Table/Query instances.
2433
2304
  *
2434
- * Calls the `queryAppDatabase` step with `parameterize: false`
2435
- * (the SDK builds fully-formed SQL with escaped inline values).
2305
+ * Calls `POST /_internal/v2/db/query` directly with the hook token
2306
+ * (raw, no Bearer prefix). All queries run on a single SQLite connection,
2307
+ * enabling RETURNING clauses and multi-statement batches.
2436
2308
  */
2437
- async _executeDbQuery(databaseId, sql) {
2438
- const result = await this.executeStep("queryAppDatabase", {
2439
- databaseId,
2440
- sql,
2441
- parameterize: false
2309
+ async _executeDbBatch(databaseId, queries) {
2310
+ const url = `${this._httpConfig.baseUrl}/_internal/v2/db/query`;
2311
+ const res = await fetch(url, {
2312
+ method: "POST",
2313
+ headers: {
2314
+ "Content-Type": "application/json",
2315
+ Authorization: this._httpConfig.token
2316
+ },
2317
+ body: JSON.stringify({ databaseId, queries })
2442
2318
  });
2443
- return { rows: result.rows ?? [], changes: result.changes ?? 0 };
2319
+ if (!res.ok) {
2320
+ let message = `Database query failed: ${res.status} ${res.statusText}`;
2321
+ try {
2322
+ const body = await res.json();
2323
+ if (body.error) message = body.error;
2324
+ } catch {
2325
+ }
2326
+ throw new MindStudioError(message, "db_query_error", res.status);
2327
+ }
2328
+ const data = await res.json();
2329
+ return data.results;
2444
2330
  }
2445
2331
  /**
2446
2332
  * @internal Create a lazy Db proxy that auto-hydrates context.
2447
2333
  *
2448
2334
  * defineTable() returns Table instances immediately (no async needed).
2449
- * But the Table's executeQuery callback is wrapped to call ensureContext()
2335
+ * But the Table's executeBatch callback is wrapped to call ensureContext()
2450
2336
  * before the first query, so context is fetched lazily.
2451
2337
  */
2452
2338
  _createLazyDb() {
@@ -2458,7 +2344,7 @@ var MindStudioAgent = class {
2458
2344
  databaseId: "",
2459
2345
  tableName: name,
2460
2346
  columns: [],
2461
- executeQuery: async (sql) => {
2347
+ executeBatch: async (queries) => {
2462
2348
  await agent.ensureContext();
2463
2349
  const databases = agent._context.databases;
2464
2350
  let targetDb;
@@ -2472,7 +2358,7 @@ var MindStudioAgent = class {
2472
2358
  );
2473
2359
  }
2474
2360
  const databaseId = targetDb?.id ?? databases[0]?.id ?? "";
2475
- return agent._executeDbQuery(databaseId, sql);
2361
+ return agent._executeDbBatch(databaseId, queries);
2476
2362
  }
2477
2363
  });
2478
2364
  },
@@ -2482,7 +2368,14 @@ var MindStudioAgent = class {
2482
2368
  hours: (n) => n * 36e5,
2483
2369
  minutes: (n) => n * 6e4,
2484
2370
  ago: (ms) => Date.now() - ms,
2485
- fromNow: (ms) => Date.now() + ms
2371
+ fromNow: (ms) => Date.now() + ms,
2372
+ // Batch needs context — hydrate first, then delegate to real db
2373
+ batch: ((...queries) => {
2374
+ return (async () => {
2375
+ await agent.ensureContext();
2376
+ return agent._db.batch(...queries);
2377
+ })();
2378
+ })
2486
2379
  };
2487
2380
  }
2488
2381
  // -------------------------------------------------------------------------