@dbcube/query-builder 5.1.5 → 5.2.1

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.cjs CHANGED
@@ -38,6 +38,8 @@ module.exports = __toCommonJS(index_exports);
38
38
 
39
39
  // src/lib/Database.ts
40
40
  var import_fs = __toESM(require("fs"));
41
+ var import_path2 = __toESM(require("path"));
42
+ var import_crypto = require("crypto");
41
43
  var import_core2 = require("@dbcube/core");
42
44
 
43
45
  // src/lib/Trigger.ts
@@ -74,7 +76,10 @@ var Trigger = class {
74
76
  const pathFile = import_path.default.resolve(process.cwd(), "dbcube", "triggers", `${trigger.database_ref}_${trigger.table_ref}_${trigger.type}.js`);
75
77
  const requireUrl = typeof __filename !== "undefined" ? __filename : process.cwd();
76
78
  const require2 = (0, import_module.createRequire)(requireUrl);
77
- delete require2.cache[require2.resolve(pathFile)];
79
+ try {
80
+ delete require2.cache[require2.resolve(pathFile)];
81
+ } catch {
82
+ }
78
83
  const triggerModule = require2(pathFile);
79
84
  const dataProcess = triggerModule.default || triggerModule;
80
85
  await dataProcess({ db: this.instance, oldData: row, newData: row });
@@ -91,6 +96,7 @@ var Database = class _Database {
91
96
  engine;
92
97
  computedFields;
93
98
  triggers;
99
+ txId = null;
94
100
  constructor(name) {
95
101
  this.name = name;
96
102
  const engine = new import_core2.QueryEngine(name);
@@ -98,6 +104,57 @@ var Database = class _Database {
98
104
  this.computedFields = [];
99
105
  this.triggers = [];
100
106
  }
107
+ /**
108
+ * Executes raw SQL (MySQL/PostgreSQL/SQLite) or a raw command document (MongoDB)
109
+ * with bound parameters. The escape hatch for anything the builder doesn't cover.
110
+ *
111
+ * @example
112
+ * const rows = await db.raw('SELECT * FROM users WHERE age > ?', [25]);
113
+ * await db.raw('CREATE INDEX idx_users_email ON users(email)');
114
+ */
115
+ async raw(query, params = []) {
116
+ const response = await this.engine.rawQuery(query, params, this.txId ?? void 0);
117
+ if (response.status != 200) {
118
+ returnFormattedError(response.status, response.message);
119
+ throw new Error(response.message);
120
+ }
121
+ return response.data;
122
+ }
123
+ /**
124
+ * Runs a callback inside a database transaction. Everything executed through
125
+ * the received connection is committed atomically; any thrown error rolls
126
+ * the whole transaction back.
127
+ *
128
+ * Requires daemon mode (enabled by default with an up-to-date query-engine).
129
+ *
130
+ * @example
131
+ * await db.transaction(async (trx) => {
132
+ * await trx.table('accounts').where('id', '=', 1).update({ balance: 50 });
133
+ * await trx.table('accounts').where('id', '=', 2).update({ balance: 150 });
134
+ * });
135
+ */
136
+ async transaction(callback) {
137
+ const txId = await this.engine.beginTransaction();
138
+ const trx = new _Database(this.name);
139
+ trx.engine = this.engine;
140
+ trx.computedFields = this.computedFields;
141
+ trx.triggers = this.triggers;
142
+ trx.txId = txId;
143
+ try {
144
+ const result = await callback(trx);
145
+ await this.engine.commitTransaction(txId);
146
+ return result;
147
+ } catch (error) {
148
+ try {
149
+ await this.engine.rollbackTransaction(txId);
150
+ } catch (rollbackError) {
151
+ console.error(
152
+ `[dbcube] Rollback of transaction ${txId} failed: ${rollbackError?.message ?? rollbackError}`
153
+ );
154
+ }
155
+ throw error;
156
+ }
157
+ }
101
158
  async useComputes() {
102
159
  const newDatabase = new _Database(this.name);
103
160
  const arrayComputedFields = await import_core2.ComputedFieldProcessor.getComputedFields(this.name);
@@ -118,10 +175,9 @@ var Database = class _Database {
118
175
  if (response.status != 200) {
119
176
  returnFormattedError(response.status, response.message);
120
177
  }
121
- return response.data;
122
178
  }
123
179
  async disconnect() {
124
- return this.engine.run("query_engine", [
180
+ await this.engine.run("query_engine", [
125
181
  "--action",
126
182
  "disconnect"
127
183
  ]);
@@ -164,7 +220,7 @@ var Database = class _Database {
164
220
  * const columns = await db.table('users').columns().get();
165
221
  */
166
222
  table(tableName) {
167
- return new Table(this, this.name, tableName, this.engine, this.computedFields, this.triggers);
223
+ return new Table(this, this.name, tableName, this.engine, this.computedFields, this.triggers, this.txId);
168
224
  }
169
225
  setComputedFields(computedFields) {
170
226
  this.computedFields = computedFields;
@@ -180,12 +236,19 @@ var Table = class _Table {
180
236
  computedFields = [];
181
237
  trigger;
182
238
  triggers;
183
- constructor(instance, databaseName, tableName, engine = null, computedFields = [], triggers = []) {
239
+ txId = null;
240
+ relations = [];
241
+ /** Builders de grupo mutan en sitio (ver clone()) */
242
+ _mutable = false;
243
+ instance;
244
+ constructor(instance, databaseName, tableName, engine, computedFields = [], triggers = [], txId = null) {
184
245
  this.engine = engine;
246
+ this.instance = instance;
185
247
  this.computedFields = computedFields;
186
248
  this.triggers = triggers;
187
249
  this.trigger = new Trigger(instance, databaseName, triggers);
188
250
  this.nextType = "AND";
251
+ this.txId = txId;
189
252
  this.dml = {
190
253
  type: "select",
191
254
  database: databaseName,
@@ -199,7 +262,8 @@ var Table = class _Table {
199
262
  limit: null,
200
263
  offset: null,
201
264
  data: null,
202
- aggregation: null
265
+ aggregation: null,
266
+ having: []
203
267
  };
204
268
  }
205
269
  /**
@@ -256,7 +320,8 @@ var Table = class _Table {
256
320
  */
257
321
  whereGroup(callback) {
258
322
  const clone = this.clone();
259
- const groupQuery = new _Table(this, clone.dml.database, clone.dml.table, clone.engine);
323
+ const groupQuery = new _Table(this.instance, clone.dml.database, clone.dml.table, clone.engine);
324
+ groupQuery._mutable = true;
260
325
  callback(groupQuery);
261
326
  clone.dml.where.push({
262
327
  type: clone.nextType,
@@ -761,6 +826,9 @@ var Table = class _Table {
761
826
  clone.dml.data = null;
762
827
  clone.dml.aggregation = null;
763
828
  const result = await clone.getResponse();
829
+ if (this.relations.length > 0 && Array.isArray(result)) {
830
+ return await this.attachRelations(result);
831
+ }
764
832
  return result;
765
833
  } catch (error) {
766
834
  throw error;
@@ -800,11 +868,10 @@ var Table = class _Table {
800
868
  * console.log(user); // { id: 1, name: 'John' }
801
869
  */
802
870
  async find(value, column = "id") {
803
- const clone = this.clone();
871
+ const clone = this.clone().where(column, "=", value);
804
872
  clone.dml.type = "select";
805
873
  clone.dml.data = null;
806
874
  clone.dml.aggregation = null;
807
- clone.where(column, "=", value);
808
875
  clone.dml.limit = 1;
809
876
  try {
810
877
  const result = await clone.getResponse();
@@ -861,8 +928,7 @@ var Table = class _Table {
861
928
  }
862
929
  clone.dml.type = "update";
863
930
  clone.dml.data = data;
864
- await clone.getResponse(clone.dml, "Update");
865
- return data;
931
+ return clone.getResponse(clone.dml, "Update");
866
932
  }
867
933
  /**
868
934
  * Deletes rows from the table based on the defined conditions.
@@ -882,6 +948,338 @@ var Table = class _Table {
882
948
  const deleteData = await clone.getResponse(clone.dml, "Delete");
883
949
  return deleteData;
884
950
  }
951
+ /**
952
+ * Adds a WHERE NOT IN condition to the query.
953
+ *
954
+ * @example
955
+ * const users = await db.table('users').whereNotIn('status', ['banned', 'deleted']).get();
956
+ */
957
+ whereNotIn(column, values) {
958
+ const clone = this.clone();
959
+ if (Array.isArray(values) && values.length > 0) {
960
+ clone.dml.where.push({
961
+ column,
962
+ operator: "NOT IN",
963
+ value: values,
964
+ type: clone.nextType,
965
+ isGroup: false
966
+ });
967
+ clone.nextType = "AND";
968
+ }
969
+ return clone;
970
+ }
971
+ /**
972
+ * Sets an explicit OFFSET for the query (alternative to page()).
973
+ *
974
+ * @example
975
+ * const rows = await db.table('logs').orderBy('id', 'ASC').limit(50).offset(100).get();
976
+ */
977
+ offset(number) {
978
+ const clone = this.clone();
979
+ clone.dml.offset = Number(number);
980
+ return clone;
981
+ }
982
+ /**
983
+ * Appends raw expressions to the SELECT list (aggregates, functions, aliases).
984
+ * Combine with groupBy() and having() for per-group metrics.
985
+ *
986
+ * @example
987
+ * const stats = await db.table('orders')
988
+ * .select(['user_id'])
989
+ * .selectRaw(['COUNT(*) AS order_count', 'SUM(amount) AS total'])
990
+ * .groupBy('user_id')
991
+ * .having('order_count', '>', 5)
992
+ * .get();
993
+ */
994
+ selectRaw(expressions) {
995
+ const clone = this.clone();
996
+ if (clone.dml.columns.length === 1 && clone.dml.columns[0] === "*") {
997
+ clone.dml.columns = [...expressions];
998
+ } else {
999
+ clone.dml.columns = [...clone.dml.columns, ...expressions];
1000
+ }
1001
+ return clone;
1002
+ }
1003
+ /**
1004
+ * Adds a HAVING condition (filters grouped results).
1005
+ *
1006
+ * @example
1007
+ * .groupBy('user_id').having('COUNT(*)', '>', 5)
1008
+ */
1009
+ having(column, operator, value) {
1010
+ const clone = this.clone();
1011
+ clone.dml.having = clone.dml.having ?? [];
1012
+ clone.dml.having.push({
1013
+ column,
1014
+ operator,
1015
+ value,
1016
+ type: "AND",
1017
+ isGroup: false
1018
+ });
1019
+ return clone;
1020
+ }
1021
+ /**
1022
+ * Checks if at least one row matches the current conditions.
1023
+ * Cheaper than first(): selects a constant with LIMIT 1.
1024
+ *
1025
+ * @example
1026
+ * const taken = await db.table('users').where('email', '=', email).exists();
1027
+ */
1028
+ async exists() {
1029
+ const clone = this.clone();
1030
+ clone.dml.type = "select";
1031
+ clone.dml.columns = ["1 AS dbcube_exists"];
1032
+ clone.dml.data = null;
1033
+ clone.dml.aggregation = null;
1034
+ clone.dml.limit = 1;
1035
+ clone.dml.offset = null;
1036
+ const result = await clone.getResponse();
1037
+ return Array.isArray(result) && result.length > 0;
1038
+ }
1039
+ /**
1040
+ * Fetches one page of results plus pagination metadata in a single call.
1041
+ * Runs the page query and the total count with the same conditions.
1042
+ *
1043
+ * @example
1044
+ * const { items, total, totalPages, hasNext } = await db.table('products')
1045
+ * .where('published', '=', true)
1046
+ * .orderBy('id', 'ASC')
1047
+ * .paginate(2, 25);
1048
+ */
1049
+ async paginate(page = 1, perPage = 20) {
1050
+ const pageNum = Math.max(1, Number(page));
1051
+ const size = Math.max(1, Number(perPage));
1052
+ const itemsQuery = this.limit(size).page(pageNum);
1053
+ const [items, total] = await Promise.all([
1054
+ itemsQuery.get(),
1055
+ this.count()
1056
+ ]);
1057
+ const totalNum = Number(total);
1058
+ const totalPages = Math.ceil(totalNum / size);
1059
+ return {
1060
+ items,
1061
+ page: pageNum,
1062
+ perPage: size,
1063
+ total: totalNum,
1064
+ totalPages,
1065
+ hasNext: pageNum < totalPages,
1066
+ hasPrev: pageNum > 1
1067
+ };
1068
+ }
1069
+ /**
1070
+ * Processes all matching rows in batches of `size`, keeping memory flat.
1071
+ * Return `false` from the callback to stop early.
1072
+ *
1073
+ * @example
1074
+ * await db.table('logs').orderBy('id', 'ASC').chunk(500, async (rows) => {
1075
+ * await processBatch(rows);
1076
+ * });
1077
+ */
1078
+ async chunk(size, callback) {
1079
+ const batchSize = Math.max(1, Number(size));
1080
+ let page = 1;
1081
+ while (true) {
1082
+ const rows = await this.limit(batchSize).page(page).get();
1083
+ if (rows.length === 0) break;
1084
+ const result = await callback(rows, page);
1085
+ if (result === false) break;
1086
+ if (rows.length < batchSize) break;
1087
+ page++;
1088
+ }
1089
+ }
1090
+ /**
1091
+ * Atomically increments a numeric column: `SET col = col + amount`.
1092
+ * Requires at least one WHERE condition. Extra fields can be updated in the same statement.
1093
+ *
1094
+ * @example
1095
+ * await db.table('products').where('id', '=', 5).increment('stock', 3);
1096
+ * await db.table('posts').where('id', '=', 1).increment('views', 1, { last_viewed_at: new Date().toISOString() });
1097
+ */
1098
+ async increment(column, amount = 1, extra = {}) {
1099
+ const clone = this.clone();
1100
+ if (clone.dml.where.length === 0) {
1101
+ throw new Error("You must specify at least one WHERE condition to perform an increment.");
1102
+ }
1103
+ clone.dml.type = "update";
1104
+ clone.dml.data = { ...extra, [column]: { $inc: Number(amount) } };
1105
+ return clone.getResponse();
1106
+ }
1107
+ /**
1108
+ * Atomically decrements a numeric column: `SET col = col - amount`.
1109
+ *
1110
+ * @example
1111
+ * await db.table('products').where('id', '=', 5).decrement('stock', 1);
1112
+ */
1113
+ async decrement(column, amount = 1, extra = {}) {
1114
+ return this.increment(column, -Math.abs(Number(amount)), extra);
1115
+ }
1116
+ /**
1117
+ * Deletes ALL rows from the table. The only write operation allowed
1118
+ * without a WHERE — the destructive intent is explicit in the name.
1119
+ *
1120
+ * @example
1121
+ * await db.table('temp_imports').truncate();
1122
+ */
1123
+ async truncate() {
1124
+ const clone = this.clone();
1125
+ clone.dml.type = "delete";
1126
+ clone.dml.where = [];
1127
+ return clone.getResponse();
1128
+ }
1129
+ /**
1130
+ * Inserts rows, updating them instead when a conflict occurs on the given keys.
1131
+ * MySQL → ON DUPLICATE KEY UPDATE · PostgreSQL/SQLite → ON CONFLICT ... DO UPDATE.
1132
+ *
1133
+ * @param data Rows to insert.
1134
+ * @param conflictKeys Column(s) with the UNIQUE/PK constraint that triggers the update.
1135
+ * @param updateColumns Columns to overwrite on conflict (defaults to every non-conflict column).
1136
+ *
1137
+ * @example
1138
+ * await db.table('settings').upsert(
1139
+ * [{ key: 'theme', value: 'dark' }],
1140
+ * ['key']
1141
+ * );
1142
+ */
1143
+ async upsert(data, conflictKeys, updateColumns) {
1144
+ if (!Array.isArray(data) || data.length === 0) {
1145
+ throw new Error("The upsert method requires a non-empty array of objects.");
1146
+ }
1147
+ if (!Array.isArray(conflictKeys) || conflictKeys.length === 0) {
1148
+ throw new Error("The upsert method requires at least one conflict key column.");
1149
+ }
1150
+ const motor = this.engine?.getConfig?.()?.type ?? "mysql";
1151
+ const table = this.dml.table;
1152
+ const columns = Object.keys(data[0]);
1153
+ const targets = updateColumns ?? columns.filter((c) => !conflictKeys.includes(c));
1154
+ assertValidIdentifier(table, "table");
1155
+ for (const c of columns) assertValidIdentifier(c, "column");
1156
+ for (const c of conflictKeys) assertValidIdentifier(c, "conflict key");
1157
+ for (const c of targets) assertValidIdentifier(c, "update column");
1158
+ if (motor === "mongodb") {
1159
+ const updates = data.map((row) => {
1160
+ const q = {};
1161
+ for (const k of conflictKeys) q[k] = row[k];
1162
+ const set = {};
1163
+ for (const c of targets) if (c in row) set[c] = row[c];
1164
+ const setOnInsert = {};
1165
+ for (const c of columns) {
1166
+ if (!targets.includes(c) && !conflictKeys.includes(c)) setOnInsert[c] = row[c];
1167
+ }
1168
+ if (!("uuid" in row)) setOnInsert["uuid"] = (0, import_crypto.randomUUID)();
1169
+ const u = {};
1170
+ if (Object.keys(set).length) u["$set"] = set;
1171
+ if (Object.keys(setOnInsert).length) u["$setOnInsert"] = setOnInsert;
1172
+ return { q, u, upsert: true, multi: false };
1173
+ });
1174
+ const command = JSON.stringify({ update: table, updates });
1175
+ const response2 = await this.engine.rawQuery(command, [], this.txId ?? void 0);
1176
+ if (response2.status != 200) {
1177
+ returnFormattedError(response2.status, response2.message);
1178
+ throw new Error(response2.message);
1179
+ }
1180
+ return response2.data;
1181
+ }
1182
+ const quote = (id) => motor === "mysql" ? `\`${id}\`` : `"${id}"`;
1183
+ const params = [];
1184
+ const placeholder = () => motor === "postgres" || motor === "postgresql" ? `$${params.length}` : "?";
1185
+ const rowsSql = data.map((row) => {
1186
+ const cells = columns.map((col) => {
1187
+ params.push(row[col] === void 0 ? null : row[col]);
1188
+ return placeholder();
1189
+ });
1190
+ return `(${cells.join(", ")})`;
1191
+ }).join(", ");
1192
+ const colsSql = columns.map(quote).join(", ");
1193
+ let sql;
1194
+ if (motor === "mysql") {
1195
+ const updates = targets.map((c) => `${quote(c)} = VALUES(${quote(c)})`).join(", ");
1196
+ sql = `INSERT INTO ${quote(table)} (${colsSql}) VALUES ${rowsSql} ON DUPLICATE KEY UPDATE ${updates}`;
1197
+ } else {
1198
+ const conflict = conflictKeys.map(quote).join(", ");
1199
+ const updates = targets.map((c) => `${quote(c)} = excluded.${quote(c)}`).join(", ");
1200
+ sql = `INSERT INTO ${quote(table)} (${colsSql}) VALUES ${rowsSql} ON CONFLICT (${conflict}) DO UPDATE SET ${updates}`;
1201
+ }
1202
+ if (motor === "sqlite") {
1203
+ await this.engine.rawQuery("SELECT name FROM sqlite_master LIMIT 1", [], this.txId ?? void 0);
1204
+ }
1205
+ const response = await this.engine.rawQuery(sql, params, this.txId ?? void 0);
1206
+ if (response.status != 200) {
1207
+ returnFormattedError(response.status, response.message);
1208
+ throw new Error(response.message);
1209
+ }
1210
+ return response.data;
1211
+ }
1212
+ /**
1213
+ * Declares a relation to eager-load with the results of get().
1214
+ * Relations are resolved from `foreign` definitions in your .cube files,
1215
+ * or explicitly via options. Loads each relation with ONE batched query
1216
+ * (whereIn) — no N+1.
1217
+ *
1218
+ * @example
1219
+ * // hasMany: orders.user_id → users.id (auto-detected from orders.table.cube)
1220
+ * const users = await db.table('users').with('orders').get();
1221
+ * // users[0].orders === [{...}, {...}]
1222
+ *
1223
+ * // belongsTo: posts.author_id → users.id, attached as a single object
1224
+ * const posts = await db.table('posts')
1225
+ * .with('author', { table: 'users', foreignKey: 'author_id', type: 'one' })
1226
+ * .get();
1227
+ */
1228
+ with(relation, options = {}) {
1229
+ const clone = this.clone();
1230
+ clone.relations.push({ name: relation, options });
1231
+ return clone;
1232
+ }
1233
+ async attachRelations(rows) {
1234
+ if (rows.length === 0 || this.relations.length === 0) return rows;
1235
+ for (const { name, options } of this.relations) {
1236
+ const relatedTable = options.table ?? name;
1237
+ const cubeMeta = loadCubeRelations();
1238
+ let type = options.type;
1239
+ let foreignKey = options.foreignKey;
1240
+ let localKey = options.localKey ?? "id";
1241
+ if (!foreignKey || !type) {
1242
+ const childFks = cubeMeta[relatedTable]?.foreigns ?? [];
1243
+ const ownFks = cubeMeta[this.dml.table]?.foreigns ?? [];
1244
+ const childToParent = childFks.find((fk) => fk.table === this.dml.table);
1245
+ const parentToChild = ownFks.find((fk) => fk.table === relatedTable);
1246
+ if (childToParent) {
1247
+ type = type ?? "many";
1248
+ foreignKey = foreignKey ?? childToParent.column_name;
1249
+ localKey = options.localKey ?? childToParent.column;
1250
+ } else if (parentToChild) {
1251
+ type = type ?? "one";
1252
+ foreignKey = foreignKey ?? parentToChild.column_name;
1253
+ localKey = options.localKey ?? parentToChild.column;
1254
+ } else {
1255
+ type = type ?? "many";
1256
+ foreignKey = foreignKey ?? `${singularize(this.dml.table)}_id`;
1257
+ }
1258
+ }
1259
+ const relatedQuery = new _Table(this.instance, this.dml.database, relatedTable, this.engine, this.computedFields, this.triggers, this.txId);
1260
+ if (type === "one") {
1261
+ const fkValues = [...new Set(rows.map((r) => r[foreignKey]).filter((v) => v !== null && v !== void 0))];
1262
+ const related = fkValues.length > 0 ? await relatedQuery.whereIn(localKey, fkValues).get() : [];
1263
+ const index = new Map(related.map((r) => [r[localKey], r]));
1264
+ for (const row of rows) {
1265
+ row[name] = index.get(row[foreignKey]) ?? null;
1266
+ }
1267
+ } else {
1268
+ const ids = [...new Set(rows.map((r) => r[localKey]).filter((v) => v !== null && v !== void 0))];
1269
+ const related = ids.length > 0 ? await relatedQuery.whereIn(foreignKey, ids).get() : [];
1270
+ const groups = /* @__PURE__ */ new Map();
1271
+ for (const rel of related) {
1272
+ const key = rel[foreignKey];
1273
+ if (!groups.has(key)) groups.set(key, []);
1274
+ groups.get(key).push(rel);
1275
+ }
1276
+ for (const row of rows) {
1277
+ row[name] = groups.get(row[localKey]) ?? [];
1278
+ }
1279
+ }
1280
+ }
1281
+ return rows;
1282
+ }
885
1283
  async getResponse(dml = null, type = null) {
886
1284
  const localDML = dml ? dml : this.dml;
887
1285
  const computedFieldsNeeded = [];
@@ -911,52 +1309,32 @@ var Table = class _Table {
911
1309
  const newDML = { ...localDML, data: [data] };
912
1310
  if (beffore) {
913
1311
  const interceptor = await this.trigger.execute("before" + type, data);
914
- const response = await this.engine.run("query_engine", [
915
- "--action",
916
- "execute",
917
- "--dml",
918
- JSON.stringify(newDML)
919
- ]);
1312
+ const response = await this.engine.executeDml(newDML, this.txId ?? void 0);
920
1313
  if (response.status != 200) {
921
- interceptor.discard();
1314
+ interceptor?.discard();
922
1315
  returnFormattedError(response.status, response.message);
923
1316
  }
924
- await interceptor.commit();
1317
+ await interceptor?.commit();
925
1318
  arrayResult = response.data;
926
1319
  }
927
1320
  if (after) {
928
- const response = await this.engine.run("query_engine", [
929
- "--action",
930
- "execute",
931
- "--dml",
932
- JSON.stringify(newDML)
933
- ]);
1321
+ const response = await this.engine.executeDml(newDML, this.txId ?? void 0);
934
1322
  if (response.status != 200) {
935
1323
  returnFormattedError(response.status, response.message);
936
1324
  }
937
1325
  const interceptor = await this.trigger.execute("after" + type, data);
938
- await interceptor.commit();
1326
+ await interceptor?.commit();
939
1327
  }
940
1328
  }
941
1329
  } else {
942
- const response = await this.engine.run("query_engine", [
943
- "--action",
944
- "execute",
945
- "--dml",
946
- JSON.stringify(localDML)
947
- ]);
1330
+ const response = await this.engine.executeDml(localDML, this.txId ?? void 0);
948
1331
  if (response.status != 200) {
949
1332
  returnFormattedError(response.status, response.message);
950
1333
  }
951
1334
  arrayResult = response.data;
952
1335
  }
953
1336
  } else {
954
- const response = await this.engine.run("query_engine", [
955
- "--action",
956
- "execute",
957
- "--dml",
958
- JSON.stringify(localDML)
959
- ]);
1337
+ const response = await this.engine.executeDml(localDML, this.txId ?? void 0);
960
1338
  if (response.status != 200) {
961
1339
  returnFormattedError(response.status, response.message);
962
1340
  }
@@ -974,12 +1352,16 @@ var Table = class _Table {
974
1352
  return arrayResult;
975
1353
  }
976
1354
  clone() {
1355
+ if (this._mutable) return this;
977
1356
  const cloned = Object.create(Object.getPrototypeOf(this));
978
1357
  cloned.engine = this.engine;
1358
+ cloned.instance = this.instance;
979
1359
  cloned.nextType = this.nextType;
980
1360
  cloned.computedFields = this.computedFields;
981
1361
  cloned.trigger = this.trigger;
982
1362
  cloned.triggers = this.triggers;
1363
+ cloned.txId = this.txId;
1364
+ cloned.relations = [...this.relations];
983
1365
  cloned.dml = {
984
1366
  ...this.dml,
985
1367
  columns: [...this.dml.columns],
@@ -987,6 +1369,7 @@ var Table = class _Table {
987
1369
  where: [...this.dml.where],
988
1370
  orderBy: [...this.dml.orderBy],
989
1371
  groupBy: [...this.dml.groupBy],
1372
+ having: [...this.dml.having ?? []],
990
1373
  // Clonar propiedades que faltaban para evitar mutación compartida
991
1374
  data: this.dml.data ? Array.isArray(this.dml.data) ? [...this.dml.data] : { ...this.dml.data } : null,
992
1375
  aggregation: this.dml.aggregation ? { ...this.dml.aggregation } : null
@@ -994,6 +1377,63 @@ var Table = class _Table {
994
1377
  return cloned;
995
1378
  }
996
1379
  };
1380
+ var cubeRelationsCache = null;
1381
+ function loadCubeRelations() {
1382
+ if (cubeRelationsCache) return cubeRelationsCache;
1383
+ const result = {};
1384
+ const scanDir = (dir) => {
1385
+ let entries;
1386
+ try {
1387
+ entries = import_fs.default.readdirSync(dir, { withFileTypes: true });
1388
+ } catch {
1389
+ return;
1390
+ }
1391
+ for (const entry of entries) {
1392
+ const full = import_path2.default.join(dir, entry.name);
1393
+ if (entry.isDirectory()) {
1394
+ if (entry.name !== "node_modules" && entry.name !== "triggers" && entry.name !== "logs") {
1395
+ scanDir(full);
1396
+ }
1397
+ } else if (entry.name.endsWith(".table.cube")) {
1398
+ try {
1399
+ const content = import_fs.default.readFileSync(full, "utf8");
1400
+ const nameMatch = content.match(/@meta\s*\(\s*\{[\s\S]*?name\s*:\s*"([^"]+)"/);
1401
+ const tableName = nameMatch ? nameMatch[1] : import_path2.default.basename(entry.name, ".table.cube");
1402
+ const foreigns = [];
1403
+ const colRegex = /(\w+)\s*:\s*\{(?:[^{}]|\{[^{}]*\})*?foreign\s*:\s*\{([^}]*)\}/g;
1404
+ let m;
1405
+ while ((m = colRegex.exec(content)) !== null) {
1406
+ const fkTable = m[2].match(/table\s*:\s*"([^"]+)"/);
1407
+ const fkColumn = m[2].match(/column\s*:\s*"([^"]+)"/);
1408
+ if (fkTable) {
1409
+ foreigns.push({
1410
+ column_name: m[1],
1411
+ table: fkTable[1],
1412
+ column: fkColumn ? fkColumn[1] : "id"
1413
+ });
1414
+ }
1415
+ }
1416
+ result[tableName] = { foreigns };
1417
+ } catch {
1418
+ }
1419
+ }
1420
+ }
1421
+ };
1422
+ scanDir(import_path2.default.join(process.cwd(), "dbcube"));
1423
+ cubeRelationsCache = result;
1424
+ return result;
1425
+ }
1426
+ function assertValidIdentifier(name, kind) {
1427
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1428
+ throw new Error(`Invalid ${kind} name: '${name}'. Only letters, numbers and underscore are allowed.`);
1429
+ }
1430
+ }
1431
+ function singularize(word) {
1432
+ if (word.endsWith("ies")) return word.slice(0, -3) + "y";
1433
+ if (word.endsWith("ses")) return word.slice(0, -2);
1434
+ if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1);
1435
+ return word;
1436
+ }
997
1437
  function returnFormattedError(status, message) {
998
1438
  const RESET = "\x1B[0m";
999
1439
  const RED = "\x1B[31m";