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 +3 -0
- package/client.ts +87 -33
- package/index.ts +0 -1
- package/migrations.ts +105 -33
- package/package.json +1 -1
- package/repository.ts +55 -30
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 (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
} else
|
|
125
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
199
|
+
await txClient.migrationQuery("BEGIN");
|
|
188
200
|
const result = await callback(txClient);
|
|
189
|
-
await txClient.
|
|
201
|
+
await txClient.migrationQuery("COMMIT");
|
|
190
202
|
return result;
|
|
191
203
|
} catch (error) {
|
|
192
|
-
await txClient.
|
|
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
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
|
|
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 =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
190
|
+
const columnDefs: string[] = [];
|
|
191
|
+
|
|
192
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
193
|
+
const defParts: string[] = [];
|
|
114
194
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
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
|
-
|
|
279
|
-
let query
|
|
280
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|