outlet-orm 2.5.0
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/LICENSE +21 -0
- package/README.md +705 -0
- package/bin/convert.js +679 -0
- package/bin/init.js +190 -0
- package/bin/migrate.js +442 -0
- package/lib/Database/DatabaseConnection.js +4 -0
- package/lib/Migrations/Migration.js +48 -0
- package/lib/Migrations/MigrationManager.js +326 -0
- package/lib/Schema/Schema.js +790 -0
- package/package.json +75 -0
- package/src/DatabaseConnection.js +697 -0
- package/src/Model.js +659 -0
- package/src/QueryBuilder.js +710 -0
- package/src/Relations/BelongsToManyRelation.js +466 -0
- package/src/Relations/BelongsToRelation.js +127 -0
- package/src/Relations/HasManyRelation.js +125 -0
- package/src/Relations/HasManyThroughRelation.js +112 -0
- package/src/Relations/HasOneRelation.js +114 -0
- package/src/Relations/HasOneThroughRelation.js +105 -0
- package/src/Relations/MorphManyRelation.js +69 -0
- package/src/Relations/MorphOneRelation.js +68 -0
- package/src/Relations/MorphToRelation.js +110 -0
- package/src/Relations/Relation.js +31 -0
- package/src/index.js +23 -0
- package/types/index.d.ts +272 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
// Load environment variables from .env if present
|
|
2
|
+
require('dotenv').config();
|
|
3
|
+
|
|
4
|
+
// Lazy driver holders
|
|
5
|
+
let mysql;
|
|
6
|
+
let PgClient;
|
|
7
|
+
let sqlite3;
|
|
8
|
+
|
|
9
|
+
function ensureDriver(driverName) {
|
|
10
|
+
let pkg;
|
|
11
|
+
try {
|
|
12
|
+
switch (driverName) {
|
|
13
|
+
case 'mysql':
|
|
14
|
+
pkg = 'mysql2';
|
|
15
|
+
if (!mysql) mysql = require('mysql2/promise');
|
|
16
|
+
return true;
|
|
17
|
+
case 'postgres':
|
|
18
|
+
case 'postgresql':
|
|
19
|
+
pkg = 'pg';
|
|
20
|
+
if (!PgClient) ({ Client: PgClient } = require('pg'));
|
|
21
|
+
return true;
|
|
22
|
+
case 'sqlite':
|
|
23
|
+
pkg = 'sqlite3';
|
|
24
|
+
if (!sqlite3) sqlite3 = require('sqlite3').verbose();
|
|
25
|
+
return true;
|
|
26
|
+
default:
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
} catch (e) {
|
|
30
|
+
const msg = `Database driver not installed: ${pkg}.\nInstall it with: npm i ${pkg} --save\nOr select a different driver via config/.env.`;
|
|
31
|
+
throw new Error(msg);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function coerceNumber(val) {
|
|
36
|
+
const n = Number(val);
|
|
37
|
+
return Number.isFinite(n) ? n : undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Database Connection Manager
|
|
42
|
+
* Supports MySQL, PostgreSQL, and SQLite
|
|
43
|
+
*/
|
|
44
|
+
class DatabaseConnection {
|
|
45
|
+
constructor(config) {
|
|
46
|
+
const cfg = config || {};
|
|
47
|
+
const env = process.env || {};
|
|
48
|
+
let driver = (cfg.driver || env.DB_DRIVER || env.DATABASE_DRIVER || 'mysql').toLowerCase();
|
|
49
|
+
if (driver === 'postgresql') driver = 'postgres';
|
|
50
|
+
if (driver === 'sqlite3') driver = 'sqlite';
|
|
51
|
+
|
|
52
|
+
const resolved = {
|
|
53
|
+
driver,
|
|
54
|
+
host: cfg.host || env.DB_HOST || 'localhost',
|
|
55
|
+
port: cfg.port || coerceNumber(env.DB_PORT),
|
|
56
|
+
user: cfg.user || env.DB_USER || env.DB_USERNAME,
|
|
57
|
+
password: cfg.password || env.DB_PASSWORD,
|
|
58
|
+
database: cfg.database || env.DB_DATABASE || env.DB_NAME,
|
|
59
|
+
connectionLimit: cfg.connectionLimit || coerceNumber(env.DB_POOL_MAX)
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (driver === 'sqlite' && !resolved.database) {
|
|
63
|
+
resolved.database = env.DB_FILE || env.SQLITE_DB || env.SQLITE_FILENAME || ':memory:';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.config = resolved;
|
|
67
|
+
this.driver = driver || 'mysql';
|
|
68
|
+
this.connection = null;
|
|
69
|
+
this.pool = null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Connect to the database
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async connect() {
|
|
77
|
+
if (this.connection) return;
|
|
78
|
+
|
|
79
|
+
switch (this.driver) {
|
|
80
|
+
case 'mysql':
|
|
81
|
+
ensureDriver('mysql');
|
|
82
|
+
await this.connectMySQL();
|
|
83
|
+
break;
|
|
84
|
+
case 'postgres':
|
|
85
|
+
case 'postgresql':
|
|
86
|
+
ensureDriver('postgres');
|
|
87
|
+
await this.connectPostgreSQL();
|
|
88
|
+
break;
|
|
89
|
+
case 'sqlite':
|
|
90
|
+
ensureDriver('sqlite');
|
|
91
|
+
await this.connectSQLite();
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
throw new Error(`Unsupported database driver: ${this.driver}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Connect to MySQL database
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
async connectMySQL() {
|
|
103
|
+
this.pool = mysql.createPool({
|
|
104
|
+
host: this.config.host || 'localhost',
|
|
105
|
+
port: this.config.port || 3306,
|
|
106
|
+
user: this.config.user,
|
|
107
|
+
password: this.config.password,
|
|
108
|
+
database: this.config.database,
|
|
109
|
+
waitForConnections: true,
|
|
110
|
+
connectionLimit: this.config.connectionLimit || 10,
|
|
111
|
+
queueLimit: 0
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Connect to PostgreSQL database
|
|
117
|
+
* @private
|
|
118
|
+
*/
|
|
119
|
+
async connectPostgreSQL() {
|
|
120
|
+
this.connection = new PgClient({
|
|
121
|
+
host: this.config.host || 'localhost',
|
|
122
|
+
port: this.config.port || 5432,
|
|
123
|
+
user: this.config.user,
|
|
124
|
+
password: this.config.password,
|
|
125
|
+
database: this.config.database
|
|
126
|
+
});
|
|
127
|
+
await this.connection.connect();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Connect to SQLite database
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
async connectSQLite() {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
this.connection = new sqlite3.Database(
|
|
137
|
+
this.config.database || ':memory:',
|
|
138
|
+
(err) => {
|
|
139
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
140
|
+
else resolve();
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Execute a SELECT query
|
|
148
|
+
* @param {string} table
|
|
149
|
+
* @param {Object} query
|
|
150
|
+
* @returns {Promise<Array>}
|
|
151
|
+
*/
|
|
152
|
+
async select(table, query) {
|
|
153
|
+
await this.connect();
|
|
154
|
+
|
|
155
|
+
const { sql, params } = this.buildSelectQuery(table, query);
|
|
156
|
+
|
|
157
|
+
switch (this.driver) {
|
|
158
|
+
case 'mysql':
|
|
159
|
+
return this.executeMySQLQuery(sql, params);
|
|
160
|
+
case 'postgres':
|
|
161
|
+
case 'postgresql':
|
|
162
|
+
return this.executePostgreSQLQuery(sql, params);
|
|
163
|
+
case 'sqlite':
|
|
164
|
+
return this.executeSQLiteQuery(sql, params);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Insert a record
|
|
170
|
+
* @param {string} table
|
|
171
|
+
* @param {Object} data
|
|
172
|
+
* @returns {Promise<Object>}
|
|
173
|
+
*/
|
|
174
|
+
async insert(table, data) {
|
|
175
|
+
await this.connect();
|
|
176
|
+
|
|
177
|
+
const columns = Object.keys(data);
|
|
178
|
+
const values = Object.values(data);
|
|
179
|
+
const placeholders = this.getPlaceholders(values.length);
|
|
180
|
+
|
|
181
|
+
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
182
|
+
|
|
183
|
+
switch (this.driver) {
|
|
184
|
+
case 'mysql': {
|
|
185
|
+
const [result] = await this.pool.execute(sql, values);
|
|
186
|
+
return { insertId: result.insertId, affectedRows: result.affectedRows };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'postgres':
|
|
190
|
+
case 'postgresql': {
|
|
191
|
+
const pgResult = await this.connection.query(
|
|
192
|
+
`${sql} RETURNING *`,
|
|
193
|
+
values
|
|
194
|
+
);
|
|
195
|
+
return { insertId: pgResult.rows[0].id, affectedRows: pgResult.rowCount };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'sqlite':
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
this.connection.run(sql, values, function(err) {
|
|
201
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
202
|
+
else resolve({ insertId: this.lastID, affectedRows: this.changes });
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Insert multiple records
|
|
210
|
+
* @param {string} table
|
|
211
|
+
* @param {Array<Object>} data
|
|
212
|
+
* @returns {Promise<Object>}
|
|
213
|
+
*/
|
|
214
|
+
async insertMany(table, data) {
|
|
215
|
+
if (data.length === 0) return { affectedRows: 0 };
|
|
216
|
+
|
|
217
|
+
await this.connect();
|
|
218
|
+
|
|
219
|
+
const columns = Object.keys(data[0]);
|
|
220
|
+
const valuesSets = data.map(row => Object.values(row));
|
|
221
|
+
|
|
222
|
+
const placeholderSet = `(${this.getPlaceholders(columns.length)})`;
|
|
223
|
+
const allPlaceholders = valuesSets.map(() => placeholderSet).join(', ');
|
|
224
|
+
const allValues = valuesSets.flat();
|
|
225
|
+
|
|
226
|
+
const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${allPlaceholders}`;
|
|
227
|
+
|
|
228
|
+
switch (this.driver) {
|
|
229
|
+
case 'mysql': {
|
|
230
|
+
const [result] = await this.pool.execute(sql, allValues);
|
|
231
|
+
return { affectedRows: result.affectedRows };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'postgres':
|
|
235
|
+
case 'postgresql': {
|
|
236
|
+
const pgResult = await this.connection.query(sql, allValues);
|
|
237
|
+
return { affectedRows: pgResult.rowCount };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case 'sqlite':
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
this.connection.run(sql, allValues, function(err) {
|
|
243
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
244
|
+
else resolve({ affectedRows: this.changes });
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Update records
|
|
252
|
+
* @param {string} table
|
|
253
|
+
* @param {Object} data
|
|
254
|
+
* @param {Object} query
|
|
255
|
+
* @returns {Promise<Object>}
|
|
256
|
+
*/
|
|
257
|
+
async update(table, data, query) {
|
|
258
|
+
await this.connect();
|
|
259
|
+
|
|
260
|
+
const setClauses = Object.keys(data).map(key => `${key} = ?`);
|
|
261
|
+
const { whereClause, params: whereParams } = this.buildWhereClause(query.wheres || []);
|
|
262
|
+
|
|
263
|
+
const sql = `UPDATE ${table} SET ${setClauses.join(', ')}${whereClause}`;
|
|
264
|
+
const params = [...Object.values(data), ...whereParams];
|
|
265
|
+
|
|
266
|
+
switch (this.driver) {
|
|
267
|
+
case 'mysql': {
|
|
268
|
+
const [result] = await this.pool.execute(
|
|
269
|
+
this.convertToDriverPlaceholder(sql),
|
|
270
|
+
params
|
|
271
|
+
);
|
|
272
|
+
return { affectedRows: result.affectedRows };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case 'postgres':
|
|
276
|
+
case 'postgresql': {
|
|
277
|
+
const pgResult = await this.connection.query(
|
|
278
|
+
this.convertToDriverPlaceholder(sql, 'postgres'),
|
|
279
|
+
params
|
|
280
|
+
);
|
|
281
|
+
return { affectedRows: pgResult.rowCount };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
case 'sqlite':
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
this.connection.run(sql, params, function(err) {
|
|
287
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
288
|
+
else resolve({ affectedRows: this.changes });
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Delete records
|
|
296
|
+
* @param {string} table
|
|
297
|
+
* @param {Object} query
|
|
298
|
+
* @returns {Promise<Object>}
|
|
299
|
+
*/
|
|
300
|
+
async delete(table, query) {
|
|
301
|
+
await this.connect();
|
|
302
|
+
|
|
303
|
+
const { whereClause, params } = this.buildWhereClause(query.wheres || []);
|
|
304
|
+
const sql = `DELETE FROM ${table}${whereClause}`;
|
|
305
|
+
|
|
306
|
+
switch (this.driver) {
|
|
307
|
+
case 'mysql': {
|
|
308
|
+
const [result] = await this.pool.execute(
|
|
309
|
+
this.convertToDriverPlaceholder(sql),
|
|
310
|
+
params
|
|
311
|
+
);
|
|
312
|
+
return { affectedRows: result.affectedRows };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
case 'postgres':
|
|
316
|
+
case 'postgresql': {
|
|
317
|
+
const pgResult = await this.connection.query(
|
|
318
|
+
this.convertToDriverPlaceholder(sql, 'postgres'),
|
|
319
|
+
params
|
|
320
|
+
);
|
|
321
|
+
return { affectedRows: pgResult.rowCount };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case 'sqlite':
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
this.connection.run(sql, params, function(err) {
|
|
327
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
328
|
+
else resolve({ affectedRows: this.changes });
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Atomically increment a column
|
|
336
|
+
* @param {string} table
|
|
337
|
+
* @param {string} column
|
|
338
|
+
* @param {number} amount
|
|
339
|
+
* @param {Object} query
|
|
340
|
+
* @returns {Promise<{affectedRows: number}>}
|
|
341
|
+
*/
|
|
342
|
+
async increment(table, column, query, amount = 1) {
|
|
343
|
+
await this.connect();
|
|
344
|
+
|
|
345
|
+
const { whereClause, params: whereParams } = this.buildWhereClause(query?.wheres || []);
|
|
346
|
+
const sql = `UPDATE ${table} SET ${column} = ${column} + ?${whereClause}`;
|
|
347
|
+
const params = [amount, ...whereParams];
|
|
348
|
+
|
|
349
|
+
switch (this.driver) {
|
|
350
|
+
case 'mysql': {
|
|
351
|
+
const [result] = await this.pool.execute(this.convertToDriverPlaceholder(sql), params);
|
|
352
|
+
return { affectedRows: result.affectedRows };
|
|
353
|
+
}
|
|
354
|
+
case 'postgres':
|
|
355
|
+
case 'postgresql': {
|
|
356
|
+
const res = await this.connection.query(this.convertToDriverPlaceholder(sql, 'postgres'), params);
|
|
357
|
+
return { affectedRows: res.rowCount };
|
|
358
|
+
}
|
|
359
|
+
case 'sqlite':
|
|
360
|
+
return new Promise((resolve, reject) => {
|
|
361
|
+
this.connection.run(sql, params, function(err) {
|
|
362
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
363
|
+
else resolve({ affectedRows: this.changes });
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Atomically decrement a column
|
|
371
|
+
* @param {string} table
|
|
372
|
+
* @param {string} column
|
|
373
|
+
* @param {number} amount
|
|
374
|
+
* @param {Object} query
|
|
375
|
+
* @returns {Promise<{affectedRows: number}>}
|
|
376
|
+
*/
|
|
377
|
+
async decrement(table, column, query, amount = 1) {
|
|
378
|
+
await this.connect();
|
|
379
|
+
|
|
380
|
+
const { whereClause, params: whereParams } = this.buildWhereClause(query?.wheres || []);
|
|
381
|
+
const sql = `UPDATE ${table} SET ${column} = ${column} - ?${whereClause}`;
|
|
382
|
+
const params = [amount, ...whereParams];
|
|
383
|
+
|
|
384
|
+
switch (this.driver) {
|
|
385
|
+
case 'mysql': {
|
|
386
|
+
const [result] = await this.pool.execute(this.convertToDriverPlaceholder(sql), params);
|
|
387
|
+
return { affectedRows: result.affectedRows };
|
|
388
|
+
}
|
|
389
|
+
case 'postgres':
|
|
390
|
+
case 'postgresql': {
|
|
391
|
+
const res = await this.connection.query(this.convertToDriverPlaceholder(sql, 'postgres'), params);
|
|
392
|
+
return { affectedRows: res.rowCount };
|
|
393
|
+
}
|
|
394
|
+
case 'sqlite':
|
|
395
|
+
return new Promise((resolve, reject) => {
|
|
396
|
+
this.connection.run(sql, params, function(err) {
|
|
397
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
398
|
+
else resolve({ affectedRows: this.changes });
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Count records
|
|
406
|
+
* @param {string} table
|
|
407
|
+
* @param {Object} query
|
|
408
|
+
* @returns {Promise<number>}
|
|
409
|
+
*/
|
|
410
|
+
async count(table, query) {
|
|
411
|
+
await this.connect();
|
|
412
|
+
|
|
413
|
+
const { whereClause, params } = this.buildWhereClause(query.wheres || []);
|
|
414
|
+
const sql = `SELECT COUNT(*) as count FROM ${table}${whereClause}`;
|
|
415
|
+
|
|
416
|
+
const rows = await this.executeRawQuery(sql, params);
|
|
417
|
+
return rows[0].count || rows[0].COUNT || 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Execute a raw query
|
|
422
|
+
* @param {string} sql
|
|
423
|
+
* @param {Array} params
|
|
424
|
+
* @returns {Promise<Array>}
|
|
425
|
+
*/
|
|
426
|
+
async executeRawQuery(sql, params = []) {
|
|
427
|
+
await this.connect();
|
|
428
|
+
|
|
429
|
+
switch (this.driver) {
|
|
430
|
+
case 'mysql':
|
|
431
|
+
return this.executeMySQLQuery(sql, params);
|
|
432
|
+
case 'postgres':
|
|
433
|
+
case 'postgresql':
|
|
434
|
+
return this.executePostgreSQLQuery(sql, params);
|
|
435
|
+
case 'sqlite':
|
|
436
|
+
return this.executeSQLiteQuery(sql, params);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Execute raw SQL and return driver-native results (used by migrations)
|
|
442
|
+
* @param {string} sql
|
|
443
|
+
* @param {Array} params
|
|
444
|
+
* @returns {Promise<any>}
|
|
445
|
+
*/
|
|
446
|
+
async execute(sql, params = []) {
|
|
447
|
+
await this.connect();
|
|
448
|
+
switch (this.driver) {
|
|
449
|
+
case 'mysql': {
|
|
450
|
+
const [result] = await this.pool.execute(sql, params);
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
case 'postgres':
|
|
454
|
+
case 'postgresql': {
|
|
455
|
+
const res = await this.connection.query(this.convertToDriverPlaceholder(sql, 'postgres'), params);
|
|
456
|
+
return res.rows ?? res;
|
|
457
|
+
}
|
|
458
|
+
case 'sqlite':
|
|
459
|
+
return new Promise((resolve, reject) => {
|
|
460
|
+
// Choose all/run based on query type
|
|
461
|
+
const isSelect = /^\s*select/i.test(sql);
|
|
462
|
+
if (isSelect) {
|
|
463
|
+
this.connection.all(sql, params, (err, rows) => {
|
|
464
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
465
|
+
else resolve(rows);
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
this.connection.run(sql, params, function(err) {
|
|
469
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
470
|
+
else resolve({ changes: this.changes, lastID: this.lastID });
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Execute MySQL query
|
|
479
|
+
* @private
|
|
480
|
+
*/
|
|
481
|
+
async executeMySQLQuery(sql, params) {
|
|
482
|
+
const [rows] = await this.pool.execute(sql, params);
|
|
483
|
+
return rows;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Execute PostgreSQL query
|
|
488
|
+
* @private
|
|
489
|
+
*/
|
|
490
|
+
async executePostgreSQLQuery(sql, params) {
|
|
491
|
+
const result = await this.connection.query(
|
|
492
|
+
this.convertToDriverPlaceholder(sql, 'postgres'),
|
|
493
|
+
params
|
|
494
|
+
);
|
|
495
|
+
return result.rows;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Execute SQLite query
|
|
500
|
+
* @private
|
|
501
|
+
*/
|
|
502
|
+
async executeSQLiteQuery(sql, params) {
|
|
503
|
+
return new Promise((resolve, reject) => {
|
|
504
|
+
this.connection.all(sql, params, (err, rows) => {
|
|
505
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
506
|
+
else resolve(rows);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Build SELECT query
|
|
513
|
+
* @private
|
|
514
|
+
*/
|
|
515
|
+
buildSelectQuery(table, query) {
|
|
516
|
+
const distinct = query.distinct ? 'DISTINCT ' : '';
|
|
517
|
+
const columns = query.columns && query.columns.length > 0
|
|
518
|
+
? query.columns.join(', ')
|
|
519
|
+
: '*';
|
|
520
|
+
|
|
521
|
+
let sql = `SELECT ${distinct}${columns} FROM ${table}`;
|
|
522
|
+
// JOINS
|
|
523
|
+
if (query.joins && query.joins.length > 0) {
|
|
524
|
+
const joinClauses = query.joins.map(j => {
|
|
525
|
+
const type = (j.type === 'left' ? 'LEFT JOIN' : 'INNER JOIN');
|
|
526
|
+
const op = j.operator || '=';
|
|
527
|
+
return ` ${type} ${j.table} ON ${j.first} ${op} ${j.second}`;
|
|
528
|
+
}).join('');
|
|
529
|
+
sql += joinClauses;
|
|
530
|
+
}
|
|
531
|
+
let params = [];
|
|
532
|
+
|
|
533
|
+
// WHERE clauses
|
|
534
|
+
if (query.wheres && query.wheres.length > 0) {
|
|
535
|
+
const { whereClause, params: whereParams } = this.buildWhereClause(query.wheres);
|
|
536
|
+
sql += whereClause;
|
|
537
|
+
params = [...params, ...whereParams];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// GROUP BY
|
|
541
|
+
if (query.groupBys && query.groupBys.length > 0) {
|
|
542
|
+
sql += ` GROUP BY ${query.groupBys.join(', ')}`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// HAVING
|
|
546
|
+
if (query.havings && query.havings.length > 0) {
|
|
547
|
+
const havingClauses = [];
|
|
548
|
+
for (const h of query.havings) {
|
|
549
|
+
if (h.type === 'basic') {
|
|
550
|
+
havingClauses.push(`${h.column} ${h.operator} ?`);
|
|
551
|
+
params.push(h.value);
|
|
552
|
+
} else if (h.type === 'count') {
|
|
553
|
+
const col = h.column && h.column !== '*' ? h.column : '*';
|
|
554
|
+
havingClauses.push(`COUNT(${col}) ${h.operator} ?`);
|
|
555
|
+
params.push(h.value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (havingClauses.length) {
|
|
559
|
+
sql += ` HAVING ${havingClauses.join(' AND ')}`;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ORDER BY
|
|
564
|
+
if (query.orders && query.orders.length > 0) {
|
|
565
|
+
const orderClauses = query.orders.map(
|
|
566
|
+
order => `${order.column} ${order.direction.toUpperCase()}`
|
|
567
|
+
);
|
|
568
|
+
sql += ` ORDER BY ${orderClauses.join(', ')}`;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// LIMIT
|
|
572
|
+
if (query.limit !== null && query.limit !== undefined) {
|
|
573
|
+
sql += ` LIMIT ${query.limit}`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// OFFSET
|
|
577
|
+
if (query.offset !== null && query.offset !== undefined) {
|
|
578
|
+
sql += ` OFFSET ${query.offset}`;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { sql, params };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Build WHERE clause
|
|
586
|
+
* @private
|
|
587
|
+
*/
|
|
588
|
+
buildWhereClause(wheres) {
|
|
589
|
+
if (!wheres || wheres.length === 0) {
|
|
590
|
+
return { whereClause: '', params: [] };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const clauses = [];
|
|
594
|
+
const params = [];
|
|
595
|
+
|
|
596
|
+
wheres.forEach((where, index) => {
|
|
597
|
+
const boolean = index === 0 ? 'WHERE' : (where.boolean || 'AND').toUpperCase();
|
|
598
|
+
|
|
599
|
+
switch (where.type) {
|
|
600
|
+
case 'basic':
|
|
601
|
+
clauses.push(`${boolean} ${where.column} ${where.operator} ?`);
|
|
602
|
+
params.push(where.value);
|
|
603
|
+
break;
|
|
604
|
+
|
|
605
|
+
case 'in': {
|
|
606
|
+
const inPlaceholders = where.values.map(() => '?').join(', ');
|
|
607
|
+
clauses.push(`${boolean} ${where.column} IN (${inPlaceholders})`);
|
|
608
|
+
params.push(...where.values);
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
case 'notIn': {
|
|
613
|
+
const notInPlaceholders = where.values.map(() => '?').join(', ');
|
|
614
|
+
clauses.push(`${boolean} ${where.column} NOT IN (${notInPlaceholders})`);
|
|
615
|
+
params.push(...where.values);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
case 'null':
|
|
620
|
+
clauses.push(`${boolean} ${where.column} IS NULL`);
|
|
621
|
+
break;
|
|
622
|
+
|
|
623
|
+
case 'notNull':
|
|
624
|
+
clauses.push(`${boolean} ${where.column} IS NOT NULL`);
|
|
625
|
+
break;
|
|
626
|
+
|
|
627
|
+
case 'between':
|
|
628
|
+
clauses.push(`${boolean} ${where.column} BETWEEN ? AND ?`);
|
|
629
|
+
params.push(...where.values);
|
|
630
|
+
break;
|
|
631
|
+
|
|
632
|
+
case 'like':
|
|
633
|
+
clauses.push(`${boolean} ${where.column} LIKE ?`);
|
|
634
|
+
params.push(where.value);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
whereClause: ' ' + clauses.join(' '),
|
|
641
|
+
params
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Get placeholders for SQL
|
|
647
|
+
* @private
|
|
648
|
+
*/
|
|
649
|
+
getPlaceholders(count) {
|
|
650
|
+
return Array(count).fill('?').join(', ');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Convert placeholders for specific driver
|
|
655
|
+
* @private
|
|
656
|
+
*/
|
|
657
|
+
convertToDriverPlaceholder(sql, driver = this.driver) {
|
|
658
|
+
if (driver === 'postgres' || driver === 'postgresql') {
|
|
659
|
+
let index = 1;
|
|
660
|
+
return sql.replace(/\?/g, () => `$${index++}`);
|
|
661
|
+
}
|
|
662
|
+
return sql;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Close the database connection
|
|
667
|
+
* @returns {Promise<void>}
|
|
668
|
+
*/
|
|
669
|
+
async close() {
|
|
670
|
+
if (this.pool) {
|
|
671
|
+
await this.pool.end();
|
|
672
|
+
this.pool = null;
|
|
673
|
+
}
|
|
674
|
+
if (this.connection) {
|
|
675
|
+
if (this.driver === 'postgres' || this.driver === 'postgresql') {
|
|
676
|
+
await this.connection.end();
|
|
677
|
+
} else if (this.driver === 'sqlite') {
|
|
678
|
+
await new Promise((resolve, reject) => {
|
|
679
|
+
this.connection.close((err) => {
|
|
680
|
+
if (err) reject(new Error(err.message || String(err)));
|
|
681
|
+
else resolve();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
this.connection = null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Backwards-compatible alias used by CLI
|
|
691
|
+
*/
|
|
692
|
+
async disconnect() {
|
|
693
|
+
return this.close();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
module.exports = DatabaseConnection;
|