stabilize-orm 1.1.7 → 1.1.8

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/bun.lock CHANGED
@@ -7,6 +7,7 @@
7
7
  "@types/pg": "^8.15.5",
8
8
  "@types/uuid": "^11.0.0",
9
9
  "commander": "^12.1.0",
10
+ "dotenv": "^17.2.3",
10
11
  "figlet": "^1.9.3",
11
12
  "glob": "^11.0.0",
12
13
  "ioredis": "^5.4.1",
@@ -306,6 +307,8 @@
306
307
 
307
308
  "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
308
309
 
310
+ "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="],
311
+
309
312
  "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
310
313
 
311
314
  "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
package/client.ts CHANGED
@@ -37,7 +37,7 @@ function isMySQLPool(client: any): client is mysql.Pool {
37
37
  export class DBClient {
38
38
  private client!: Database | Pool | mysql.Pool | PoolClient | mysql.PoolConnection;
39
39
  private logger: Logger;
40
- public readonly config: DBConfig;
40
+ public readonly config: DBConfig;
41
41
  private retryAttempts: number;
42
42
  private retryDelay: number;
43
43
  private maxJitter: number;
@@ -82,12 +82,12 @@ export class DBClient {
82
82
  } else if (isMySQLConfig(config)) {
83
83
  this.client = mysql.createPool(config.connectionString);
84
84
  this.logger.logDebug(`Initialized MySQL Pool client.`);
85
- } else {
86
- this.client = new Pool({ connectionString: config.connectionString });
85
+ } else if (config.type = DBType.Postgres) {
86
+ this.client = new Pool({ connectionString: config.connectionString! });
87
87
  this.logger.logDebug(`Initialized Postgres Pool client.`);
88
88
  }
89
89
  }
90
-
90
+
91
91
  /** @internal Gets a random jitter value to add to retry delays. */
92
92
  private getJitter = () => Math.random() * this.maxJitter;
93
93
 
@@ -103,50 +103,53 @@ export class DBClient {
103
103
  * const users = await dbClient.query('SELECT * FROM users WHERE status = ?', ['active']);
104
104
  * ```
105
105
  */
106
-
107
- async query<T>(query: string, params: any[] = []): Promise<T[]> {
108
- const start = Date.now();
109
-
110
- for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
111
- try {
112
- let result: any;
113
-
114
- // Log the query before execution
115
- this.logger.logQuery(query, params);
116
-
117
- if (this.client instanceof Database) { // SQLite
118
- let stmt = this.preparedStatements.get(query);
119
- if (!stmt) {
120
- stmt = this.client.prepare(query);
121
- this.preparedStatements.set(query, stmt);
122
- }
123
- result = stmt.all(...params);
124
- } else if (isMySQLPool(this.client) || ('query' in this.client && 'release' in this.client && !(this.client instanceof Pool))) { // mysql2 Pool or Connection
125
- const [rows] = await (this.client as mysql.Pool).query(query, params);
126
- result = rows;
127
- } else { // Postgres Pool or Client
128
-
129
- // Correctly replace '?' with '$1', '$2', etc.
130
- let paramIndex = 0;
131
- const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
132
- const pgResult = await (this.client as Pool).query(pgQuery, params);
133
- result = pgResult.rows;
134
- }
135
-
136
- const executionTime = Date.now() - start;
137
- this.logger.logQuery(query, params, executionTime);
138
- return result as T[];
139
- } catch (error) {
140
- this.logger.logError(error as Error);
141
- if (attempt === this.retryAttempts) {
142
- throw new StabilizeError(`Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`, "QUERY_ERROR");
143
- }
144
- await new Promise(res => setTimeout(res, this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter()));
145
- }
106
+
107
+ async query<T>(query: string, params: any[] = []): Promise<T[]> {
108
+ const start = Date.now();
109
+
110
+ for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
111
+ try {
112
+ let result: any;
113
+
114
+ // Log the query before execution
115
+ this.logger.logQuery(query, params);
116
+
117
+ if (this.client instanceof Database) { // SQLite
118
+ let stmt = this.preparedStatements.get(query);
119
+ if (!stmt) {
120
+ stmt = this.client.prepare(query);
121
+ this.preparedStatements.set(query, stmt);
122
+ }
123
+ result = stmt.all(...params);
124
+ } else if (this.config.type === DBType.MySQL && isMySQLPool(this.client)) { // MySQL
125
+ const [rows] = await (this.client as mysql.Pool).query(query, params);
126
+ result = rows;
127
+ } else if (this.config.type === DBType.Postgres ) { // Postgres
128
+
129
+ let paramIndex = 0;
130
+ const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
131
+ const pgResult = await (this.client as Pool).query(pgQuery, params);
132
+ result = Array.isArray(pgResult.rows) ? pgResult.rows : [];
133
+ } else {
134
+ throw new StabilizeError("Unknown database client type", "QUERY_ERROR");
135
+ }
136
+
137
+ const executionTime = Date.now() - start;
138
+ this.logger.logQuery(query, params, executionTime);
139
+ return Array.isArray(result) ? result as T[] : [];
140
+ } catch (error) {
141
+ console.log("error: ", error);
142
+
143
+ this.logger.logError(error as Error);
144
+ if (attempt === this.retryAttempts) {
145
+ throw new StabilizeError(`Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`, "QUERY_ERROR");
146
146
  }
147
- // This line should theoretically be unreachable if retryAttempts >= 1
148
- throw new StabilizeError("Query failed: maximum retries reached without success", "QUERY_ERROR");
147
+ await new Promise(res => setTimeout(res, this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter()));
148
+ }
149
149
  }
150
+ // This line should theoretically be unreachable if retryAttempts >= 1
151
+ throw new StabilizeError("Query failed: maximum retries reached without success", "QUERY_ERROR");
152
+ }
150
153
 
151
154
  /**
152
155
  * Executes a series of database operations within a single atomic transaction.
@@ -169,23 +172,23 @@ export class DBClient {
169
172
  const tx = this.client.transaction(() => callback(this));
170
173
  return tx();
171
174
  }
172
-
175
+
173
176
  if (isMySQLPool(this.client)) {
174
- const connection = await this.client.getConnection();
175
- const txClient = new DBClient(this.config, this.logger, connection);
176
- this.logger.logDebug("Starting MySQL transaction.");
177
- try {
178
- await txClient.query("START TRANSACTION");
179
- const result = await callback(txClient);
180
- await txClient.query("COMMIT");
181
- return result;
182
- } catch (error) {
183
- await txClient.query("ROLLBACK");
184
- throw error;
185
- } finally {
186
- connection.release();
187
- this.logger.logDebug("MySQL transaction connection released.");
188
- }
177
+ const connection = await this.client.getConnection();
178
+ const txClient = new DBClient(this.config, this.logger, connection);
179
+ this.logger.logDebug("Starting MySQL transaction.");
180
+ try {
181
+ await txClient.query("START TRANSACTION");
182
+ const result = await callback(txClient);
183
+ await txClient.query("COMMIT");
184
+ return result;
185
+ } catch (error) {
186
+ await txClient.query("ROLLBACK");
187
+ throw error;
188
+ } finally {
189
+ connection.release();
190
+ this.logger.logDebug("MySQL transaction connection released.");
191
+ }
189
192
  }
190
193
 
191
194
  if (this.client instanceof Pool) {
@@ -193,12 +196,12 @@ export class DBClient {
193
196
  const txClient = new DBClient(this.config, this.logger, connection);
194
197
  this.logger.logDebug("Starting Postgres transaction.");
195
198
  try {
196
- await txClient.query("BEGIN");
199
+ await txClient.migrationQuery("BEGIN");
197
200
  const result = await callback(txClient);
198
- await txClient.query("COMMIT");
201
+ await txClient.migrationQuery("COMMIT");
199
202
  return result;
200
203
  } catch (error) {
201
- await txClient.query("ROLLBACK");
204
+ await txClient.migrationQuery("ROLLBACK");
202
205
  throw error;
203
206
  } finally {
204
207
  connection.release();
@@ -222,4 +225,46 @@ export class DBClient {
222
225
  this.client = null!;
223
226
  this.logger.logInfo("Database connection closed");
224
227
  }
228
+
229
+ /**
230
+ * Executes a SQL query for migrations/transactions that
231
+ * does NOT expect any result rows and returns void.
232
+ * This is used for DDL and transaction statements (e.g. CREATE TABLE, BEGIN, COMMIT)
233
+ * that should never be iterated over.
234
+ *
235
+ * Logs the execution time for each query.
236
+ *
237
+ * @param query The SQL query string with `?` as placeholders.
238
+ * @param params An array of parameters to bind to the query.
239
+ * @returns A promise that resolves when the query has executed.
240
+ * @example
241
+ * ```
242
+ * await dbClient.migrationQuery('CREATE TABLE ...');
243
+ * await dbClient.migrationQuery('BEGIN');
244
+ * await dbClient.migrationQuery('COMMIT');
245
+ * ```
246
+ */
247
+ async migrationQuery(query: string, params: any[] = []): Promise<void> {
248
+ const start = Date.now();
249
+ this.logger.logQuery(query, params);
250
+
251
+ if (this.client instanceof Database) { // SQLite
252
+ let stmt = this.preparedStatements.get(query);
253
+ if (!stmt) {
254
+ stmt = this.client.prepare(query);
255
+ this.preparedStatements.set(query, stmt);
256
+ }
257
+ stmt.run(...params);
258
+ } else if (isMySQLPool(this.client) || ('query' in this.client && 'release' in this.client && !(this.client instanceof Pool))) { // mysql2 Pool or Connection
259
+ await (this.client as mysql.Pool).query(query, params);
260
+ } else if (this.config.type = DBType.Postgres) { // Postgres Pool or Client
261
+ let paramIndex = 0;
262
+ const pgQuery = query.replace(/\?/g, () => `$${++paramIndex}`);
263
+ await (this.client as Pool).query(pgQuery, params);
264
+ }
265
+
266
+ const executionTime = Date.now() - start;
267
+ this.logger.logQuery(query, params, executionTime);
268
+ }
269
+
225
270
  }
package/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  * @file stabilize.ts
3
3
  * @description The main entry point for the Stabilize ORM, tying together the client, cache, and repositories.
4
4
  * @author ElectronSz
5
- * @date 2025-10-15 20:35:34
6
5
  */
7
6
  import { Cache } from "./cache";
8
7
  import { DBClient } from "./client";
package/migrations.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { DBClient } from "./client";
9
9
  import { ModelKey, ColumnKey, ValidatorKey, SoftDeleteKey } from "./decorators";
10
- import { type DBConfig, type Migration, StabilizeError, DBType } from "./types";
10
+ import { type DBConfig, type Migration, StabilizeError, DBType, DataTypes } from "./types";
11
11
 
12
12
  type ColumnData = { name: string; type: string };
13
13
  type ColumnMetadata = Record<string, ColumnData>;
@@ -30,6 +30,87 @@ function formatQuery(query: string, dbType: DBType): string {
30
30
  return query;
31
31
  }
32
32
 
33
+
34
+ /**
35
+ * Maps an abstract data type (from DataTypes enum or a string) to the correct SQL type string
36
+ * for the specified database dialect (Postgres, MySQL, or SQLite).
37
+ *
38
+ * This function enables model definitions to be portable across different databases by
39
+ * converting each logical type to its proper SQL type in CREATE TABLE migrations.
40
+ *
41
+ * @param dt - The data type to map. Accepts either a value from the DataTypes enum, or a string
42
+ * (e.g., "string", "integer", "boolean", etc.).
43
+ * @param dbType - The target database dialect (DBType.Postgres, DBType.MySQL, or DBType.SQLite).
44
+ * @returns The SQL column type string appropriate for the database and logical type.
45
+ *
46
+ */
47
+ function mapDataTypeToSql(dt: DataTypes | string, dbType: DBType): string {
48
+ let type: string;
49
+ if (typeof dt === "string") {
50
+ type = dt.toLowerCase();
51
+ } else {
52
+ type = DataTypes[dt].toLowerCase();
53
+ }
54
+
55
+ if (dbType === DBType.Postgres) {
56
+ switch (type) {
57
+ case "string": return "TEXT";
58
+ case "text": return "TEXT";
59
+ case "integer": return "INTEGER";
60
+ case "bigint": return "BIGINT";
61
+ case "float": return "REAL";
62
+ case "double": return "DOUBLE PRECISION";
63
+ case "decimal": return "DECIMAL";
64
+ case "boolean": return "BOOLEAN";
65
+ case "date": return "DATE";
66
+ case "datetime": return "TIMESTAMP";
67
+ case "json": return "JSONB";
68
+ case "uuid": return "UUID";
69
+ case "blob": return "BYTEA";
70
+ default: return "TEXT";
71
+ }
72
+ }
73
+ if (dbType === DBType.MySQL) {
74
+ switch (type) {
75
+ case "string": return "VARCHAR(255)";
76
+ case "text": return "TEXT";
77
+ case "integer": return "INT";
78
+ case "bigint": return "BIGINT";
79
+ case "float": return "FLOAT";
80
+ case "double": return "DOUBLE";
81
+ case "decimal": return "DECIMAL(10,2)";
82
+ case "boolean": return "TINYINT(1)";
83
+ case "date": return "DATE";
84
+ case "datetime": return "DATETIME";
85
+ case "json": return "JSON";
86
+ case "uuid": return "CHAR(36)";
87
+ case "blob": return "BLOB";
88
+ default: return "TEXT";
89
+ }
90
+ }
91
+ // SQLite
92
+ if (dbType === DBType.SQLite) {
93
+ switch (type) {
94
+ case "string": return "TEXT";
95
+ case "text": return "TEXT";
96
+ case "integer": return "INTEGER";
97
+ case "bigint": return "INTEGER";
98
+ case "float": return "REAL";
99
+ case "double": return "REAL";
100
+ case "decimal": return "NUMERIC";
101
+ case "boolean": return "INTEGER";
102
+ case "date": return "TEXT";
103
+ case "datetime": return "TEXT";
104
+ case "json": return "TEXT";
105
+ case "uuid": return "TEXT";
106
+ case "blob": return "BLOB";
107
+ default: return "TEXT";
108
+ }
109
+ }
110
+ return "TEXT";
111
+ }
112
+
113
+
33
114
  /**
34
115
  * @internal
35
116
  * Gets the database-specific SQL for an auto-incrementing primary key.
@@ -62,7 +143,7 @@ function getTimestampType(dbType: DBType): string {
62
143
  return "DATETIME";
63
144
  case DBType.SQLite:
64
145
  default:
65
- return "TEXT";
146
+ return "TEXT";
66
147
  }
67
148
  }
68
149
 
@@ -95,8 +176,8 @@ function getTimestampDefault(dbType: DBType): string {
95
176
  */
96
177
  export async function generateMigration(
97
178
  model: new (...args: any[]) => any,
98
- name: string,
99
- dbType: DBType = DBType.Postgres,
179
+ name: string,
180
+ dbType: DBType,
100
181
  ): Promise<Migration> {
101
182
  const tableName = Reflect.getMetadata(ModelKey, model);
102
183
  if (!tableName) {
@@ -105,19 +186,18 @@ export async function generateMigration(
105
186
 
106
187
  const columns: ColumnMetadata = Reflect.getMetadata(ColumnKey, model.prototype) || {};
107
188
  const validators: ValidatorMetadata = Reflect.getMetadata(ValidatorKey, model.prototype) || {};
108
- const softDeleteField = Reflect.getMetadata(SoftDeleteKey, model.prototype);
109
189
 
110
- const columnDefs = Object.entries(columns).map(([key, col]) => {
111
- if (col.name === "id") {
112
- return `id ${getAutoIncrementPK(dbType)}`;
113
- }
190
+ const columnDefs: string[] = [];
191
+
192
+ for (const [key, col] of Object.entries(columns)) {
193
+ const defParts: string[] = [];
114
194
 
115
- const defParts: string[] = [col.name];
116
-
117
- if (["createdAt", "updatedAt"].includes(key) || (softDeleteField && key === softDeleteField)) {
118
- defParts.push(getTimestampType(dbType));
195
+ if (col.name === "id") {
196
+ defParts.push("id");
197
+ defParts.push(getAutoIncrementPK(dbType));
119
198
  } else {
120
- defParts.push(col.type);
199
+ defParts.push(col.name);
200
+ defParts.push(mapDataTypeToSql(col.type, dbType));
121
201
  }
122
202
 
123
203
  if (validators[key]?.includes("required")) {
@@ -127,21 +207,13 @@ export async function generateMigration(
127
207
  defParts.push("UNIQUE");
128
208
  }
129
209
 
130
- if (key === "createdAt") {
131
- defParts.push(getTimestampDefault(dbType));
132
- }
133
-
134
- return defParts.join(" ");
135
- });
136
-
137
- if (softDeleteField && !columns[softDeleteField]) {
138
- columnDefs.push(`${softDeleteField} ${getTimestampType(dbType)}`);
210
+ columnDefs.push(defParts.join(" "));
139
211
  }
140
212
 
141
213
  const up = [`CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs.join(", ")})`];
142
214
  const down = [`DROP TABLE IF EXISTS ${tableName}`];
143
215
 
144
- return { up, down, name: tableName };
216
+ return { up, down, name };
145
217
  }
146
218
 
147
219
  /**
@@ -153,20 +225,20 @@ export async function generateMigration(
153
225
  function getMigrationsTableSQL(dbType: DBType): string {
154
226
  switch (dbType) {
155
227
  case DBType.Postgres:
156
- return `CREATE TABLE IF NOT EXISTS migrations (
228
+ return `CREATE TABLE IF NOT EXISTS stabilize_migrations (
157
229
  id SERIAL PRIMARY KEY,
158
230
  name VARCHAR(255) UNIQUE NOT NULL,
159
231
  applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
160
232
  )`;
161
233
  case DBType.MySQL:
162
- return `CREATE TABLE IF NOT EXISTS migrations (
234
+ return `CREATE TABLE IF NOT EXISTS stabilize_migrations (
163
235
  id INT AUTO_INCREMENT PRIMARY KEY,
164
236
  name VARCHAR(255) UNIQUE NOT NULL,
165
237
  applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
166
238
  )`;
167
239
  case DBType.SQLite:
168
240
  default:
169
- return `CREATE TABLE IF NOT EXISTS migrations (
241
+ return `CREATE TABLE IF NOT EXISTS stabilize_migrations (
170
242
  id INTEGER PRIMARY KEY AUTOINCREMENT,
171
243
  name TEXT UNIQUE NOT NULL,
172
244
  applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
@@ -190,8 +262,8 @@ export async function runMigrations(config: DBConfig, migrations: Migration[]) {
190
262
 
191
263
  for (const [index, migration] of migrations.entries()) {
192
264
  const name = migration.name || `migration_${index}_${new Date().getTime()}`;
193
-
194
- const selectQuery = formatQuery(`SELECT id FROM migrations WHERE name = ?`, dbType);
265
+
266
+ const selectQuery = formatQuery(`SELECT id FROM stabilize_migrations WHERE name = ?`, dbType);
195
267
  const applied = await client.query<{ id: number }>(selectQuery, [name]);
196
268
 
197
269
  if (applied.length === 0) {
@@ -200,17 +272,17 @@ export async function runMigrations(config: DBConfig, migrations: Migration[]) {
200
272
  for (const query of migration.up) {
201
273
  await txClient.query(query);
202
274
  }
203
-
204
- const insertQuery = formatQuery(`INSERT INTO migrations (name, applied_at) VALUES (?, ?)`, dbType);
275
+
276
+ const insertQuery = formatQuery(`INSERT INTO stabilize_migrations (name, applied_at) VALUES (?, ?)`, dbType);
205
277
  await txClient.query(insertQuery, [name, new Date().toISOString()]);
206
-
278
+
207
279
  console.log(`Migration ${name} applied successfully.`);
208
280
  });
209
281
  }
210
282
  }
211
283
  } catch (error) {
212
284
  console.error("Migration failed:", error);
213
- throw error;
285
+ throw error;
214
286
  } finally {
215
287
  await client.close();
216
288
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stabilize-orm",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/repository.ts CHANGED
@@ -227,7 +227,7 @@ export class Repository<T> {
227
227
  );
228
228
  return result;
229
229
  }
230
-
230
+
231
231
  /**
232
232
  * Creates multiple records in the database in batches.
233
233
  * @param entities An array of entities to create.
@@ -275,47 +275,72 @@ export class Repository<T> {
275
275
  const batch = entities.slice(i, i + batchSize);
276
276
  const keys = Object.keys(batch[0]!).filter((k) => this.columns[k]);
277
277
  const columnNames = keys.map((k) => this.columns[k]?.name).join(", ");
278
- const placeholders = `(${keys.map(() => "?").join(", ")})`;
279
- let query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
280
- const params = batch.flatMap((entity) =>
278
+
279
+ let query: string;
280
+ let params: any[] = batch.flatMap((entity) =>
281
281
  keys.map((k) => (entity as any)[k]),
282
282
  );
283
283
 
284
- let batchResults: T[] = [];
285
- let ids: (number | string)[] = [];
286
-
287
284
  if (dbType === DBType.Postgres) {
288
- query += " RETURNING *";
289
- batchResults = await client.query<T>(query, params);
290
- ids = batchResults.map((r) => (r as any).id);
285
+ // PostgreSQL: numbered placeholders ($1, $2, ...)
286
+ let paramIdx = 1;
287
+ const valuePlaceholders = batch
288
+ .map(
289
+ () =>
290
+ `(${keys.map(() => `$${paramIdx++}`).join(", ")})`
291
+ )
292
+ .join(", ");
293
+ query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${valuePlaceholders} RETURNING *`;
294
+ const batchResults = await client.query<T>(query, params);
295
+ const ids = batchResults.map((r) => (r as any).id);
296
+
297
+ // Handle relation loading if needed
298
+ let finalResults = batchResults;
299
+ if (ids.length > 0 && batchResults.length === 0) {
300
+ const queryBuilder = this.find().where(
301
+ `id IN (${ids.map(() => "?").join(", ")})`,
302
+ ...ids,
303
+ );
304
+ if (options.relations) {
305
+ for (const rel of options.relations) {
306
+ await this.loadRelation(queryBuilder, rel);
307
+ }
308
+ }
309
+ finalResults = await queryBuilder.execute(client);
310
+ }
311
+ results.push(...finalResults);
312
+
291
313
  } else {
314
+ // SQLite/MySQL: ? placeholders
315
+ const placeholders = `(${keys.map(() => "?").join(", ")})`;
316
+ query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
292
317
  await client.query(query, params);
293
- ids = (
318
+ const ids = (
294
319
  await client.query<{ id: number }>(
295
320
  `SELECT id FROM ${this.table} ORDER BY id DESC LIMIT ?`,
296
321
  [batch.length],
297
322
  )
298
323
  ).map((row) => row.id);
299
- }
300
324
 
301
- if (ids.length > 0 && batchResults.length === 0) {
302
- const queryBuilder = this.find().where(
303
- `id IN (${ids.map(() => "?").join(", ")})`,
304
- ...ids,
305
- );
306
- if (options.relations) {
307
- for (const rel of options.relations) {
308
- await this.loadRelation(queryBuilder, rel);
325
+ let batchResults: T[] = [];
326
+ if (ids.length > 0) {
327
+ const queryBuilder = this.find().where(
328
+ `id IN (${ids.map(() => "?").join(", ")})`,
329
+ ...ids,
330
+ );
331
+ if (options.relations) {
332
+ for (const rel of options.relations) {
333
+ await this.loadRelation(queryBuilder, rel);
334
+ }
309
335
  }
336
+ batchResults = await queryBuilder.execute(client);
310
337
  }
311
- batchResults = await queryBuilder.execute(client);
338
+ results.push(...batchResults);
312
339
  }
313
-
314
- results.push(...batchResults);
315
340
  }
316
-
341
+
317
342
  if (this.cache) await this.cache.invalidatePattern(`find:${this.table}:*`);
318
-
343
+
319
344
  this.logger.logDebug(
320
345
  `Bulk created ${results.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
321
346
  );
@@ -495,7 +520,7 @@ export class Repository<T> {
495
520
  let id: number | string | undefined = (results[0] as any)?.id || (entity as any).id;
496
521
 
497
522
  if (!id && dbType !== DBType.Postgres) {
498
- if (dbType === DBType.SQLite) {
523
+ if (dbType === DBType.SQLite) {
499
524
  id = (await client.query<{ id: number }>("SELECT last_insert_rowid() as id"))[0]?.id;
500
525
  } else if (dbType === DBType.MySQL) {
501
526
  const result = await client.query<{ "LAST_INSERT_ID()": number }>("SELECT LAST_INSERT_ID()");
@@ -513,7 +538,7 @@ export class Repository<T> {
513
538
  await this.cache.set(`findOne:${this.table}:${id}`, [result], 60);
514
539
  }
515
540
  }
516
-
541
+
517
542
  this.logger.logDebug(
518
543
  `Upserted ${this.table} with ID ${id} in ${(performance.now() - start).toFixed(2)}ms`,
519
544
  );
@@ -545,7 +570,7 @@ export class Repository<T> {
545
570
  ? `UPDATE ${this.table} SET ${this.softDeleteField} = ? WHERE id = ?`
546
571
  : `DELETE FROM ${this.table} WHERE id = ?`;
547
572
  const params = this.softDeleteField ? [new Date().toISOString(), id] : [id];
548
-
573
+
549
574
  await client.query(query, params);
550
575
 
551
576
  if (this.cache) {
@@ -638,7 +663,7 @@ export class Repository<T> {
638
663
  "RECOVER_ERROR",
639
664
  );
640
665
  }
641
-
666
+
642
667
  await client.query(
643
668
  `UPDATE ${this.table} SET ${this.softDeleteField} = NULL WHERE id = ?`,
644
669
  [id],
@@ -653,7 +678,7 @@ export class Repository<T> {
653
678
  `findOne:${this.table}:${id}`,
654
679
  ]);
655
680
  }
656
-
681
+
657
682
  this.logger.logDebug(
658
683
  `Recovered ${this.table} with ID ${id} in ${(performance.now() - start).toFixed(2)}ms`,
659
684
  );