mysql-orm-lite 1.0.1 → 2.1.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/README.md +394 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.js +106 -0
- package/dist/lib/TransactionCRUD.d.ts +42 -0
- package/dist/lib/TransactionCRUD.js +202 -0
- package/dist/lib/connectionManager.d.ts +47 -0
- package/dist/lib/connectionManager.js +172 -0
- package/dist/lib/crud.d.ts +73 -0
- package/dist/lib/crud.js +435 -0
- package/dist/lib/performanceMonitor.d.ts +50 -0
- package/dist/lib/performanceMonitor.js +134 -0
- package/dist/lib/schema.d.ts +39 -0
- package/dist/lib/schema.js +181 -0
- package/dist/lib/transactionManager.d.ts +9 -0
- package/{lib → dist/lib}/transactionManager.js +7 -7
- package/dist/types.d.ts +124 -0
- package/dist/types.js +2 -0
- package/package.json +10 -4
- package/src/index.ts +76 -0
- package/{lib/TransactionCRUD.js → src/lib/TransactionCRUD.ts} +31 -26
- package/{lib/connectionManager.js → src/lib/connectionManager.ts} +59 -24
- package/{lib/crud.js → src/lib/crud.ts} +217 -45
- package/src/lib/performanceMonitor.ts +147 -0
- package/src/lib/schema.ts +207 -0
- package/src/lib/transactionManager.ts +25 -0
- package/src/types.ts +143 -0
- package/tsconfig.json +25 -0
- package/index.js +0 -42
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import * as mysql from 'mysql2/promise';
|
|
2
|
+
import { DbConfig, Logger } from '../types';
|
|
3
3
|
|
|
4
4
|
// Default logger
|
|
5
|
-
const defaultLogger = {
|
|
6
|
-
info: (...args) => console.log('[INFO]', ...args),
|
|
7
|
-
error: (...args) => console.error('[ERROR]', ...args),
|
|
8
|
-
warn: (...args) => console.warn('[WARN]', ...args)
|
|
5
|
+
const defaultLogger: Logger = {
|
|
6
|
+
info: (...args: any[]) => console.log('[INFO]', ...args),
|
|
7
|
+
error: (...args: any[]) => console.error('[ERROR]', ...args),
|
|
8
|
+
warn: (...args: any[]) => console.warn('[WARN]', ...args)
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
const pools = new Map();
|
|
12
|
-
let logger = defaultLogger;
|
|
13
|
-
let defaultConfig = null;
|
|
11
|
+
const pools = new Map<string, mysql.Pool>();
|
|
12
|
+
let logger: Logger = defaultLogger;
|
|
13
|
+
let defaultConfig: DbConfig | null = null;
|
|
14
14
|
|
|
15
|
-
const getPoolKey = (config) => {
|
|
15
|
+
const getPoolKey = (config: DbConfig): string => {
|
|
16
16
|
return `${config.host}:${config.port || 3306}:${config.user}:${config.database}`;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
const connectionManager = {
|
|
20
20
|
/**
|
|
21
21
|
* Initialize with default config and optional logger
|
|
22
|
-
* @param
|
|
23
|
-
* @param
|
|
22
|
+
* @param config - Database configuration
|
|
23
|
+
* @param customLogger - Custom logger (optional)
|
|
24
24
|
*/
|
|
25
|
-
init: (config, customLogger) => {
|
|
25
|
+
init: (config: DbConfig, customLogger?: Logger) => {
|
|
26
26
|
if (!config || !config.database) {
|
|
27
27
|
throw new Error('Valid database configuration required');
|
|
28
28
|
}
|
|
@@ -35,18 +35,18 @@ const connectionManager = {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Set custom logger
|
|
38
|
-
* @param
|
|
38
|
+
* @param customLogger - Logger with info, error, warn methods
|
|
39
39
|
*/
|
|
40
|
-
setLogger: (customLogger) => {
|
|
40
|
+
setLogger: (customLogger: Logger) => {
|
|
41
41
|
logger = customLogger;
|
|
42
42
|
},
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Get or create a connection pool
|
|
46
|
-
* @param
|
|
47
|
-
* @returns
|
|
46
|
+
* @param dbConfig - Database configuration (optional)
|
|
47
|
+
* @returns MySQL connection pool
|
|
48
48
|
*/
|
|
49
|
-
getPool: async (dbConfig) => {
|
|
49
|
+
getPool: async (dbConfig?: DbConfig): Promise<mysql.Pool> => {
|
|
50
50
|
const conf = dbConfig || defaultConfig;
|
|
51
51
|
if (!conf || !conf.database) {
|
|
52
52
|
throw new Error('Database configuration missing. Call connectionManager.init() first or provide dbConfig');
|
|
@@ -54,7 +54,7 @@ const connectionManager = {
|
|
|
54
54
|
|
|
55
55
|
const key = getPoolKey(conf);
|
|
56
56
|
if (pools.has(key)) {
|
|
57
|
-
return pools.get(key)
|
|
57
|
+
return pools.get(key)!;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
try {
|
|
@@ -81,12 +81,12 @@ const connectionManager = {
|
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Close a specific pool
|
|
84
|
-
* @param
|
|
84
|
+
* @param config - Database configuration
|
|
85
85
|
*/
|
|
86
|
-
closePool: async (config) => {
|
|
86
|
+
closePool: async (config: DbConfig) => {
|
|
87
87
|
const key = getPoolKey(config);
|
|
88
88
|
if (pools.has(key)) {
|
|
89
|
-
const pool = pools.get(key)
|
|
89
|
+
const pool = pools.get(key)!;
|
|
90
90
|
await pool.end();
|
|
91
91
|
pools.delete(key);
|
|
92
92
|
logger.info(`Closed connection pool for ${key}`);
|
|
@@ -111,7 +111,42 @@ const connectionManager = {
|
|
|
111
111
|
/**
|
|
112
112
|
* Get logger instance
|
|
113
113
|
*/
|
|
114
|
-
getLogger: () => logger
|
|
114
|
+
getLogger: (): Logger => logger,
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get connection pool statistics
|
|
118
|
+
* @param dbConfig - Optional database configuration
|
|
119
|
+
* @returns Connection pool stats
|
|
120
|
+
*/
|
|
121
|
+
getPoolStats: async (dbConfig?: DbConfig) => {
|
|
122
|
+
const conf = dbConfig || defaultConfig;
|
|
123
|
+
if (!conf || !conf.database) {
|
|
124
|
+
throw new Error('Database configuration missing');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const key = getPoolKey(conf);
|
|
128
|
+
const pool = pools.get(key);
|
|
129
|
+
|
|
130
|
+
if (!pool) {
|
|
131
|
+
return {
|
|
132
|
+
activeConnections: 0,
|
|
133
|
+
idleConnections: 0,
|
|
134
|
+
totalConnections: 0,
|
|
135
|
+
waitingRequests: 0
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Access pool internals (mysql2 specific)
|
|
140
|
+
const poolInfo = (pool as any).pool;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
activeConnections: poolInfo?._acquiringConnections?.length || 0,
|
|
144
|
+
idleConnections: poolInfo?._freeConnections?.length || 0,
|
|
145
|
+
totalConnections: poolInfo?._allConnections?.length || 0,
|
|
146
|
+
waitingRequests: poolInfo?._connectionQueue?.length || 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
115
149
|
};
|
|
116
150
|
|
|
117
|
-
|
|
151
|
+
export default connectionManager;
|
|
152
|
+
export { connectionManager };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import connectionManager from './connectionManager';
|
|
2
|
+
import performanceMonitor from './performanceMonitor';
|
|
3
|
+
import { DbConfig, SelectOptions, UpdateOptions, DeleteOptions, WhereConditions } from '../types';
|
|
3
4
|
|
|
4
|
-
const utils = {
|
|
5
|
-
handleConnectionError(err) {
|
|
6
|
-
const errorMessages = {
|
|
5
|
+
export const utils = {
|
|
6
|
+
handleConnectionError(err: any): Error {
|
|
7
|
+
const errorMessages: Record<string, string> = {
|
|
7
8
|
PROTOCOL_CONNECTION_LOST: 'Database connection was closed.',
|
|
8
9
|
ER_CON_COUNT_ERROR: 'Database has too many connections.',
|
|
9
10
|
ECONNREFUSED: 'Database connection was refused.',
|
|
@@ -12,7 +13,7 @@ const utils = {
|
|
|
12
13
|
return new Error(errorMessages[err.code] || err.message);
|
|
13
14
|
},
|
|
14
15
|
|
|
15
|
-
logQueryPerformance(query, startTime, params = []) {
|
|
16
|
+
logQueryPerformance(query: string, startTime: number, params: any[] = []): number {
|
|
16
17
|
const logger = connectionManager.getLogger();
|
|
17
18
|
const duration = Date.now() - startTime;
|
|
18
19
|
if (duration > 1000) {
|
|
@@ -25,16 +26,20 @@ const utils = {
|
|
|
25
26
|
return duration;
|
|
26
27
|
},
|
|
27
28
|
|
|
28
|
-
async executeQuery({ query, params = [], dbConfig, operation = 'query' }) {
|
|
29
|
+
async executeQuery({ query, params = [], dbConfig, operation = 'query' }: { query: string; params?: any[]; dbConfig?: DbConfig; operation?: string }): Promise<any> {
|
|
29
30
|
const startTime = Date.now();
|
|
30
31
|
const logger = connectionManager.getLogger();
|
|
31
32
|
|
|
32
33
|
try {
|
|
33
34
|
const pool = await connectionManager.getPool(dbConfig);
|
|
34
35
|
const [results] = await pool.query(query, params);
|
|
35
|
-
this.logQueryPerformance(query, startTime, params);
|
|
36
|
+
const duration = this.logQueryPerformance(query, startTime, params);
|
|
37
|
+
|
|
38
|
+
// Record in performance monitor if enabled
|
|
39
|
+
performanceMonitor.recordQuery(query, duration, params);
|
|
40
|
+
|
|
36
41
|
return results;
|
|
37
|
-
} catch (error) {
|
|
42
|
+
} catch (error: any) {
|
|
38
43
|
logger.error(`${operation} Error: ${error.message}`);
|
|
39
44
|
logger.error(`Query: ${query}`);
|
|
40
45
|
if (params.length) logger.error('Parameters:', JSON.stringify(params));
|
|
@@ -42,8 +47,8 @@ const utils = {
|
|
|
42
47
|
}
|
|
43
48
|
},
|
|
44
49
|
|
|
45
|
-
validateUpdateParams(table, data, query) {
|
|
46
|
-
const errors = [];
|
|
50
|
+
validateUpdateParams(table: string, data: Record<string, any>, query: string): string[] {
|
|
51
|
+
const errors: string[] = [];
|
|
47
52
|
if (!table || typeof table !== 'string') errors.push('Invalid table name');
|
|
48
53
|
else if (!/^[a-zA-Z0-9_]+$/.test(table)) errors.push('Table name contains invalid characters');
|
|
49
54
|
|
|
@@ -56,9 +61,9 @@ const utils = {
|
|
|
56
61
|
return errors;
|
|
57
62
|
},
|
|
58
63
|
|
|
59
|
-
prepareUpdateParams(data) {
|
|
60
|
-
const setFields = [];
|
|
61
|
-
const params = [];
|
|
64
|
+
prepareUpdateParams(data: Record<string, any>): { setFields: string[]; params: any[] } {
|
|
65
|
+
const setFields: string[] = [];
|
|
66
|
+
const params: any[] = [];
|
|
62
67
|
|
|
63
68
|
for (const [key, value] of Object.entries(data)) {
|
|
64
69
|
if (value === undefined) continue;
|
|
@@ -79,13 +84,13 @@ const utils = {
|
|
|
79
84
|
return { setFields, params };
|
|
80
85
|
},
|
|
81
86
|
|
|
82
|
-
_buildWhereClause(conditions) {
|
|
83
|
-
const params = [];
|
|
84
|
-
const buildCondition = (key, value) => {
|
|
87
|
+
_buildWhereClause(conditions: WhereConditions): { clause: string; params: any[] } {
|
|
88
|
+
const params: any[] = [];
|
|
89
|
+
const buildCondition = (key: string, value: any): string => {
|
|
85
90
|
const column = key;
|
|
86
91
|
if (value === null) return `${column} IS NULL`;
|
|
87
92
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
88
|
-
return Object.entries(value).map(([op, val]) => {
|
|
93
|
+
return Object.entries(value).map(([op, val]: [string, any]) => {
|
|
89
94
|
switch (op) {
|
|
90
95
|
case '$eq': params.push(val); return `${column} = ?`;
|
|
91
96
|
case '$ne': params.push(val); return `${column} != ?`;
|
|
@@ -117,7 +122,7 @@ const utils = {
|
|
|
117
122
|
return `${column} = ?`;
|
|
118
123
|
};
|
|
119
124
|
|
|
120
|
-
const walk = (cond) => {
|
|
125
|
+
const walk = (cond: any): string => {
|
|
121
126
|
if (!cond || typeof cond !== 'object') return '';
|
|
122
127
|
if (Array.isArray(cond)) return cond.map(walk).join(' AND ');
|
|
123
128
|
|
|
@@ -132,7 +137,7 @@ const utils = {
|
|
|
132
137
|
return { clause, params };
|
|
133
138
|
},
|
|
134
139
|
|
|
135
|
-
_buildSelectQuery(options) {
|
|
140
|
+
_buildSelectQuery(options: SelectOptions): { query: string; params: any[] } {
|
|
136
141
|
let { table, fields = '*', joins = [], where, orderBy, limit, offset, groupBy, having, alias, forUpdate = false } = options;
|
|
137
142
|
|
|
138
143
|
if (fields && Array.isArray(fields) && fields.length > 0) {
|
|
@@ -142,7 +147,7 @@ const utils = {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
let query = `SELECT ${fields} FROM ${table}${alias ? ' ' + alias : ''}`;
|
|
145
|
-
const allParams = [];
|
|
150
|
+
const allParams: any[] = [];
|
|
146
151
|
|
|
147
152
|
for (const join of joins) {
|
|
148
153
|
let { type = 'INNER', table: joinTable, alias, on } = join;
|
|
@@ -174,7 +179,7 @@ const utils = {
|
|
|
174
179
|
return { query, params: allParams };
|
|
175
180
|
},
|
|
176
181
|
|
|
177
|
-
_buildUpdateQuery(options) {
|
|
182
|
+
_buildUpdateQuery(options: UpdateOptions): { query: string; params: any[] } {
|
|
178
183
|
const { table, data, where } = options;
|
|
179
184
|
const { setFields, params } = this.prepareUpdateParams(data);
|
|
180
185
|
|
|
@@ -194,12 +199,12 @@ const utils = {
|
|
|
194
199
|
return { query, params: allParams };
|
|
195
200
|
},
|
|
196
201
|
|
|
197
|
-
_buildDeleteQuery(options) {
|
|
202
|
+
_buildDeleteQuery(options: DeleteOptions): { query: string; params: any[] } {
|
|
198
203
|
const { table, where } = options;
|
|
199
204
|
if (!table) throw new Error('Table name is required for delete');
|
|
200
205
|
|
|
201
206
|
let query = `DELETE FROM ${table}`;
|
|
202
|
-
const allParams = [];
|
|
207
|
+
const allParams: any[] = [];
|
|
203
208
|
|
|
204
209
|
if (where) {
|
|
205
210
|
const { clause, params: whereParams } = this._buildWhereClause(where);
|
|
@@ -218,18 +223,18 @@ const utils = {
|
|
|
218
223
|
};
|
|
219
224
|
|
|
220
225
|
// Public API
|
|
221
|
-
|
|
226
|
+
export const find = async function (query: string, params: any[] = [], dbConfig?: DbConfig): Promise<any[]> {
|
|
222
227
|
if (!query) return [];
|
|
223
228
|
return await utils.executeQuery({ query, params, dbConfig, operation: 'find' });
|
|
224
229
|
};
|
|
225
230
|
|
|
226
|
-
|
|
231
|
+
export const findCount = async function (query: string, params: any[] = [], dbConfig?: DbConfig): Promise<number> {
|
|
227
232
|
if (!query) return 0;
|
|
228
233
|
const results = await utils.executeQuery({ query, params, dbConfig, operation: 'findCount' });
|
|
229
234
|
return results && results[0] ? results[0].count : 0;
|
|
230
235
|
};
|
|
231
236
|
|
|
232
|
-
|
|
237
|
+
export const insert = async function (table: string, data: Record<string, any>, dbConfig?: DbConfig, debug = false, isIgnore = false): Promise<any> {
|
|
233
238
|
if (!table || !data || typeof data !== 'object') throw new Error('Invalid table or data');
|
|
234
239
|
|
|
235
240
|
const logger = connectionManager.getLogger();
|
|
@@ -248,7 +253,7 @@ exports.insert = async function (table, data, dbConfig, debug = false, isIgnore
|
|
|
248
253
|
return result.insertId;
|
|
249
254
|
};
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
export const update = async function (table: string, data: Record<string, any>, query: string, dbConfig?: DbConfig, debug = false): Promise<number> {
|
|
252
257
|
const errors = utils.validateUpdateParams(table, data, query);
|
|
253
258
|
if (errors.length > 0) throw new Error(`Validation failed: ${errors.join(', ')}`);
|
|
254
259
|
|
|
@@ -265,7 +270,7 @@ exports.update = async function (table, data, query, dbConfig, debug = false) {
|
|
|
265
270
|
return result.affectedRows;
|
|
266
271
|
};
|
|
267
272
|
|
|
268
|
-
|
|
273
|
+
export const _delete = async function (query: string, table?: string, dbConfig?: DbConfig): Promise<number> {
|
|
269
274
|
if (!query || !query.toLowerCase().includes('where')) throw new Error('Invalid query or missing WHERE clause');
|
|
270
275
|
|
|
271
276
|
const logger = connectionManager.getLogger();
|
|
@@ -275,31 +280,198 @@ exports.delete = async function (query, table, dbConfig) {
|
|
|
275
280
|
return result.affectedRows;
|
|
276
281
|
};
|
|
277
282
|
|
|
278
|
-
|
|
279
|
-
|
|
283
|
+
export { _delete as delete };
|
|
284
|
+
|
|
285
|
+
// Bulk Operations
|
|
286
|
+
export const bulkInsert = async function (
|
|
287
|
+
table: string,
|
|
288
|
+
records: Record<string, any>[],
|
|
289
|
+
options?: { batchSize?: number; ignore?: boolean },
|
|
290
|
+
dbConfig?: DbConfig
|
|
291
|
+
): Promise<{ totalInserted: number; batches: number; firstInsertId?: number; lastInsertId?: number }> {
|
|
292
|
+
if (!table || !records || !Array.isArray(records) || records.length === 0) {
|
|
293
|
+
throw new Error('Table name and records are required for bulk insert');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const batchSize = options?.batchSize || 1000;
|
|
297
|
+
const isIgnore = options?.ignore || false;
|
|
298
|
+
const logger = connectionManager.getLogger();
|
|
299
|
+
|
|
300
|
+
// Validate all records have the same fields
|
|
301
|
+
const fields = Object.keys(records[0]);
|
|
302
|
+
for (const record of records) {
|
|
303
|
+
const recordFields = Object.keys(record);
|
|
304
|
+
if (recordFields.length !== fields.length || !recordFields.every(f => fields.includes(f))) {
|
|
305
|
+
throw new Error('All records must have the same fields for bulk insert');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let totalInserted = 0;
|
|
310
|
+
let firstInsertId: number | undefined;
|
|
311
|
+
let lastInsertId: number | undefined;
|
|
312
|
+
const batches = Math.ceil(records.length / batchSize);
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
315
|
+
const batch = records.slice(i, i + batchSize);
|
|
316
|
+
const values: any[] = [];
|
|
317
|
+
const placeholders: string[] = [];
|
|
318
|
+
|
|
319
|
+
for (const record of batch) {
|
|
320
|
+
const recordValues = fields.map(field => record[field]);
|
|
321
|
+
values.push(...recordValues);
|
|
322
|
+
placeholders.push(`(${fields.map(() => '?').join(', ')})`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const sql = isIgnore
|
|
326
|
+
? `INSERT IGNORE INTO ${table} (${fields.join(', ')}) VALUES ${placeholders.join(', ')}`
|
|
327
|
+
: `INSERT INTO ${table} (${fields.join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
328
|
+
|
|
329
|
+
const result = await utils.executeQuery({ query: sql, params: values, dbConfig, operation: 'bulkInsert' });
|
|
330
|
+
|
|
331
|
+
totalInserted += result.affectedRows || 0;
|
|
332
|
+
|
|
333
|
+
if (i === 0 && result.insertId) {
|
|
334
|
+
firstInsertId = result.insertId;
|
|
335
|
+
}
|
|
336
|
+
if (result.insertId) {
|
|
337
|
+
lastInsertId = result.insertId + (result.affectedRows || 1) - 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
logger.info(`Bulk inserted ${totalInserted} records into ${table} in ${batches} batches`);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
totalInserted,
|
|
345
|
+
batches,
|
|
346
|
+
firstInsertId,
|
|
347
|
+
lastInsertId
|
|
348
|
+
};
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export const upsert = async function (
|
|
352
|
+
table: string,
|
|
353
|
+
data: Record<string, any>,
|
|
354
|
+
options: { conflictKey: string | string[]; updateFields?: string[] },
|
|
355
|
+
dbConfig?: DbConfig
|
|
356
|
+
): Promise<{ action: 'inserted' | 'updated'; affectedRows: number; insertId?: number }> {
|
|
357
|
+
if (!table || !data || !options.conflictKey) {
|
|
358
|
+
throw new Error('Table, data, and conflictKey are required for upsert');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const fields = Object.keys(data);
|
|
362
|
+
const values = Object.values(data);
|
|
363
|
+
const placeholders = fields.map(() => '?').join(', ');
|
|
364
|
+
|
|
365
|
+
// Determine which fields to update on duplicate key
|
|
366
|
+
const conflictKeys = Array.isArray(options.conflictKey) ? options.conflictKey : [options.conflictKey];
|
|
367
|
+
const updateFields = options.updateFields || fields.filter(f => !conflictKeys.includes(f));
|
|
368
|
+
|
|
369
|
+
if (updateFields.length === 0) {
|
|
370
|
+
throw new Error('No fields to update on duplicate key');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Build UPDATE clause
|
|
374
|
+
const updateClause = updateFields.map(field => `${field} = VALUES(${field})`).join(', ');
|
|
375
|
+
|
|
376
|
+
const sql = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updateClause}`;
|
|
377
|
+
|
|
378
|
+
const result = await utils.executeQuery({ query: sql, params: values, dbConfig, operation: 'upsert' });
|
|
379
|
+
|
|
380
|
+
// affectedRows = 1 means inserted, 2 means updated, 0 means no change
|
|
381
|
+
const action: 'inserted' | 'updated' = result.affectedRows === 1 ? 'inserted' : 'updated';
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
action,
|
|
385
|
+
affectedRows: result.affectedRows,
|
|
386
|
+
insertId: result.insertId
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
export const bulkUpsert = async function (
|
|
391
|
+
table: string,
|
|
392
|
+
records: Record<string, any>[],
|
|
393
|
+
options: { conflictKey: string | string[]; updateFields?: string[]; batchSize?: number },
|
|
394
|
+
dbConfig?: DbConfig
|
|
395
|
+
): Promise<{ totalAffected: number; batches: number }> {
|
|
396
|
+
if (!table || !records || records.length === 0) {
|
|
397
|
+
throw new Error('Table name and records are required for bulk upsert');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const batchSize = options?.batchSize || 1000;
|
|
401
|
+
const logger = connectionManager.getLogger();
|
|
402
|
+
|
|
403
|
+
// Validate all records have the same fields
|
|
404
|
+
const fields = Object.keys(records[0]);
|
|
405
|
+
for (const record of records) {
|
|
406
|
+
const recordFields = Object.keys(record);
|
|
407
|
+
if (recordFields.length !== fields.length || !recordFields.every(f => fields.includes(f))) {
|
|
408
|
+
throw new Error('All records must have the same fields for bulk upsert');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Determine which fields to update on duplicate key
|
|
413
|
+
const conflictKeys = Array.isArray(options.conflictKey) ? options.conflictKey : [options.conflictKey];
|
|
414
|
+
const updateFields = options.updateFields || fields.filter(f => !conflictKeys.includes(f));
|
|
415
|
+
|
|
416
|
+
if (updateFields.length === 0) {
|
|
417
|
+
throw new Error('No fields to update on duplicate key');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const updateClause = updateFields.map(field => `${field} = VALUES(${field})`).join(', ');
|
|
421
|
+
|
|
422
|
+
let totalAffected = 0;
|
|
423
|
+
const batches = Math.ceil(records.length / batchSize);
|
|
424
|
+
|
|
425
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
426
|
+
const batch = records.slice(i, i + batchSize);
|
|
427
|
+
const values: any[] = [];
|
|
428
|
+
const placeholders: string[] = [];
|
|
429
|
+
|
|
430
|
+
for (const record of batch) {
|
|
431
|
+
const recordValues = fields.map(field => record[field]);
|
|
432
|
+
values.push(...recordValues);
|
|
433
|
+
placeholders.push(`(${fields.map(() => '?').join(', ')})`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const sql = `INSERT INTO ${table} (${fields.join(', ')}) VALUES ${placeholders.join(', ')} ON DUPLICATE KEY UPDATE ${updateClause}`;
|
|
437
|
+
|
|
438
|
+
const result = await utils.executeQuery({ query: sql, params: values, dbConfig, operation: 'bulkUpsert' });
|
|
439
|
+
totalAffected += result.affectedRows || 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
logger.info(`Bulk upsert affected ${totalAffected} records in ${table} in ${batches} batches`);
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
totalAffected,
|
|
446
|
+
batches
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Query Builder Methods
|
|
451
|
+
export const buildAndExecuteSelectQuery = async function (options: SelectOptions, dbConfig?: DbConfig): Promise<any[]> {
|
|
280
452
|
const { query, params } = utils._buildSelectQuery(options);
|
|
281
453
|
return await utils.executeQuery({ query, params, dbConfig, operation: 'buildAndExecuteSelectQuery' });
|
|
282
454
|
};
|
|
283
455
|
|
|
284
|
-
|
|
456
|
+
export const buildAndExecuteUpdateQuery = async function (options: UpdateOptions, dbConfig?: DbConfig): Promise<number> {
|
|
285
457
|
const { query, params } = utils._buildUpdateQuery(options);
|
|
286
|
-
|
|
458
|
+
const result = await utils.executeQuery({ query, params, dbConfig, operation: 'buildAndExecuteUpdateQuery' });
|
|
459
|
+
return result.affectedRows;
|
|
287
460
|
};
|
|
288
461
|
|
|
289
|
-
|
|
462
|
+
export const buildAndExecuteDeleteQuery = async function (options: DeleteOptions, dbConfig?: DbConfig): Promise<number> {
|
|
290
463
|
const { query, params } = utils._buildDeleteQuery(options);
|
|
291
|
-
|
|
464
|
+
const result = await utils.executeQuery({ query, params, dbConfig, operation: 'buildAndExecuteDeleteQuery' });
|
|
465
|
+
return result.affectedRows;
|
|
292
466
|
};
|
|
293
467
|
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
exports.updateWhere = exports.buildAndExecuteUpdateQuery;
|
|
300
|
-
exports.updateQuery = exports.buildAndExecuteUpdateQuery;
|
|
468
|
+
// Aliases
|
|
469
|
+
export const select = buildAndExecuteSelectQuery;
|
|
470
|
+
export const findWhere = buildAndExecuteSelectQuery;
|
|
471
|
+
export const query = buildAndExecuteSelectQuery;
|
|
301
472
|
|
|
302
|
-
|
|
303
|
-
|
|
473
|
+
export const updateWhere = buildAndExecuteUpdateQuery;
|
|
474
|
+
export const updateQuery = buildAndExecuteUpdateQuery;
|
|
304
475
|
|
|
305
|
-
|
|
476
|
+
export const deleteWhere = buildAndExecuteDeleteQuery;
|
|
477
|
+
export const remove = buildAndExecuteDeleteQuery;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { QueryMetric, PerformanceStats, ConnectionPoolStats } from '../types';
|
|
2
|
+
|
|
3
|
+
class PerformanceMonitor {
|
|
4
|
+
private enabled: boolean = false;
|
|
5
|
+
private queries: QueryMetric[] = [];
|
|
6
|
+
private startTime: number = 0;
|
|
7
|
+
private queryCountByType = {
|
|
8
|
+
SELECT: 0,
|
|
9
|
+
INSERT: 0,
|
|
10
|
+
UPDATE: 0,
|
|
11
|
+
DELETE: 0,
|
|
12
|
+
OTHER: 0
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enable performance monitoring
|
|
17
|
+
*/
|
|
18
|
+
enable(): void {
|
|
19
|
+
this.enabled = true;
|
|
20
|
+
this.startTime = Date.now();
|
|
21
|
+
console.log('[Performance Monitor] Enabled');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Disable performance monitoring
|
|
26
|
+
*/
|
|
27
|
+
disable(): void {
|
|
28
|
+
this.enabled = false;
|
|
29
|
+
console.log('[Performance Monitor] Disabled');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if monitoring is enabled
|
|
34
|
+
*/
|
|
35
|
+
isEnabled(): boolean {
|
|
36
|
+
return this.enabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Record a query execution
|
|
41
|
+
*/
|
|
42
|
+
recordQuery(query: string, duration: number, params?: any[]): void {
|
|
43
|
+
if (!this.enabled) return;
|
|
44
|
+
|
|
45
|
+
const type = this.detectQueryType(query);
|
|
46
|
+
|
|
47
|
+
const metric: QueryMetric = {
|
|
48
|
+
query,
|
|
49
|
+
duration,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
type,
|
|
52
|
+
params
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
this.queries.push(metric);
|
|
56
|
+
this.queryCountByType[type]++;
|
|
57
|
+
|
|
58
|
+
// Keep only last 1000 queries to prevent memory issues
|
|
59
|
+
if (this.queries.length > 1000) {
|
|
60
|
+
this.queries.shift();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Detect query type from SQL string
|
|
66
|
+
*/
|
|
67
|
+
private detectQueryType(query: string): QueryMetric['type'] {
|
|
68
|
+
const normalized = query.trim().toUpperCase();
|
|
69
|
+
|
|
70
|
+
if (normalized.startsWith('SELECT')) return 'SELECT';
|
|
71
|
+
if (normalized.startsWith('INSERT')) return 'INSERT';
|
|
72
|
+
if (normalized.startsWith('UPDATE')) return 'UPDATE';
|
|
73
|
+
if (normalized.startsWith('DELETE')) return 'DELETE';
|
|
74
|
+
|
|
75
|
+
return 'OTHER';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get current performance statistics
|
|
80
|
+
*/
|
|
81
|
+
getStats(): PerformanceStats {
|
|
82
|
+
const totalQueries = this.queries.length;
|
|
83
|
+
const totalDuration = this.queries.reduce((sum, q) => sum + q.duration, 0);
|
|
84
|
+
const averageQueryTime = totalQueries > 0 ? totalDuration / totalQueries : 0;
|
|
85
|
+
|
|
86
|
+
const slowestQueries = [...this.queries]
|
|
87
|
+
.sort((a, b) => b.duration - a.duration)
|
|
88
|
+
.slice(0, 10);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
enabled: this.enabled,
|
|
92
|
+
totalQueries,
|
|
93
|
+
averageQueryTime: Math.round(averageQueryTime * 100) / 100,
|
|
94
|
+
slowestQueries,
|
|
95
|
+
queryCountByType: { ...this.queryCountByType },
|
|
96
|
+
startTime: this.startTime,
|
|
97
|
+
uptime: this.startTime > 0 ? Date.now() - this.startTime : 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get slowest queries
|
|
103
|
+
*/
|
|
104
|
+
getSlowQueries(limit: number = 10): QueryMetric[] {
|
|
105
|
+
return [...this.queries]
|
|
106
|
+
.sort((a, b) => b.duration - a.duration)
|
|
107
|
+
.slice(0, limit);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reset all collected metrics
|
|
112
|
+
*/
|
|
113
|
+
reset(): void {
|
|
114
|
+
this.queries = [];
|
|
115
|
+
this.queryCountByType = {
|
|
116
|
+
SELECT: 0,
|
|
117
|
+
INSERT: 0,
|
|
118
|
+
UPDATE: 0,
|
|
119
|
+
DELETE: 0,
|
|
120
|
+
OTHER: 0
|
|
121
|
+
};
|
|
122
|
+
this.startTime = Date.now();
|
|
123
|
+
console.log('[Performance Monitor] Reset');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get queries executed in the last N milliseconds
|
|
128
|
+
*/
|
|
129
|
+
getRecentQueries(milliseconds: number = 60000): QueryMetric[] {
|
|
130
|
+
const cutoff = Date.now() - milliseconds;
|
|
131
|
+
return this.queries.filter(q => q.timestamp >= cutoff);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get queries per second
|
|
136
|
+
*/
|
|
137
|
+
getQueriesPerSecond(): number {
|
|
138
|
+
const uptime = this.startTime > 0 ? (Date.now() - this.startTime) / 1000 : 0;
|
|
139
|
+
return uptime > 0 ? this.queries.length / uptime : 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Singleton instance
|
|
144
|
+
const performanceMonitor = new PerformanceMonitor();
|
|
145
|
+
|
|
146
|
+
export default performanceMonitor;
|
|
147
|
+
export { performanceMonitor, PerformanceMonitor };
|