stabilize-orm 1.1.6 → 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,14 +103,17 @@ export class DBClient {
103
103
  * const users = await dbClient.query('SELECT * FROM users WHERE status = ?', ['active']);
104
104
  * ```
105
105
  */
106
+
106
107
  async query<T>(query: string, params: any[] = []): Promise<T[]> {
107
108
  const start = Date.now();
108
- this.logger.logQuery(query, params);
109
109
 
110
110
  for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
111
111
  try {
112
112
  let result: any;
113
113
 
114
+ // Log the query before execution
115
+ this.logger.logQuery(query, params);
116
+
114
117
  if (this.client instanceof Database) { // SQLite
115
118
  let stmt = this.preparedStatements.get(query);
116
119
  if (!stmt) {
@@ -118,25 +121,34 @@ export class DBClient {
118
121
  this.preparedStatements.set(query, stmt);
119
122
  }
120
123
  result = stmt.all(...params);
121
- } else if (isMySQLPool(this.client) || ('query' in this.client && 'release' in this.client)) { // mysql2 Pool or Connection
122
- const [rows] = await (this.client as mysql.Pool).query(query, params);
123
- result = rows;
124
- } else { // Postgres Pool or Client
125
- const pgQuery = query.replace(/\?/g, (_, i) => `$${i + 1}`);
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}`);
126
131
  const pgResult = await (this.client as Pool).query(pgQuery, params);
127
- result = pgResult.rows;
132
+ result = Array.isArray(pgResult.rows) ? pgResult.rows : [];
133
+ } else {
134
+ throw new StabilizeError("Unknown database client type", "QUERY_ERROR");
128
135
  }
129
136
 
130
137
  const executionTime = Date.now() - start;
131
138
  this.logger.logQuery(query, params, executionTime);
132
- return result as T[];
139
+ return Array.isArray(result) ? result as T[] : [];
133
140
  } catch (error) {
141
+ console.log("error: ", error);
142
+
134
143
  this.logger.logError(error as Error);
135
- if (attempt === this.retryAttempts) throw new StabilizeError(`Query failed: ${(error as Error).message}`, "QUERY_ERROR");
144
+ if (attempt === this.retryAttempts) {
145
+ throw new StabilizeError(`Query failed after ${this.retryAttempts} attempts: ${(error as Error).message}`, "QUERY_ERROR");
146
+ }
136
147
  await new Promise(res => setTimeout(res, this.retryDelay * Math.pow(2, attempt - 1) + this.getJitter()));
137
148
  }
138
149
  }
139
- throw new StabilizeError("Query failed: no attempts made", "QUERY_ERROR");
150
+ // This line should theoretically be unreachable if retryAttempts >= 1
151
+ throw new StabilizeError("Query failed: maximum retries reached without success", "QUERY_ERROR");
140
152
  }
141
153
 
142
154
  /**
@@ -160,23 +172,23 @@ export class DBClient {
160
172
  const tx = this.client.transaction(() => callback(this));
161
173
  return tx();
162
174
  }
163
-
175
+
164
176
  if (isMySQLPool(this.client)) {
165
- const connection = await this.client.getConnection();
166
- const txClient = new DBClient(this.config, this.logger, connection);
167
- this.logger.logDebug("Starting MySQL transaction.");
168
- try {
169
- await txClient.query("START TRANSACTION");
170
- const result = await callback(txClient);
171
- await txClient.query("COMMIT");
172
- return result;
173
- } catch (error) {
174
- await txClient.query("ROLLBACK");
175
- throw error;
176
- } finally {
177
- connection.release();
178
- this.logger.logDebug("MySQL transaction connection released.");
179
- }
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
+ }
180
192
  }
181
193
 
182
194
  if (this.client instanceof Pool) {
@@ -184,12 +196,12 @@ export class DBClient {
184
196
  const txClient = new DBClient(this.config, this.logger, connection);
185
197
  this.logger.logDebug("Starting Postgres transaction.");
186
198
  try {
187
- await txClient.query("BEGIN");
199
+ await txClient.migrationQuery("BEGIN");
188
200
  const result = await callback(txClient);
189
- await txClient.query("COMMIT");
201
+ await txClient.migrationQuery("COMMIT");
190
202
  return result;
191
203
  } catch (error) {
192
- await txClient.query("ROLLBACK");
204
+ await txClient.migrationQuery("ROLLBACK");
193
205
  throw error;
194
206
  } finally {
195
207
  connection.release();
@@ -213,4 +225,46 @@ export class DBClient {
213
225
  this.client = null!;
214
226
  this.logger.logInfo("Database connection closed");
215
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
+
216
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.6",
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
  );