trickle-observe 0.2.106 → 0.2.108

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.
@@ -26,6 +26,34 @@ export declare function patchBetterSqlite3(dbConstructor: any, debug: boolean):
26
26
  * Called from observe-register when ioredis is required.
27
27
  */
28
28
  export declare function patchIoredis(ioredisModule: any, debug: boolean): void;
29
+ /**
30
+ * Patch @prisma/client to capture queries.
31
+ * Prisma has its own query engine (Rust binary) so patching pg/mysql2 won't work.
32
+ * Instead, we hook into Prisma's $on('query') event and $use() middleware.
33
+ */
34
+ export declare function patchPrisma(prismaModule: any, debug: boolean): void;
35
+ /**
36
+ * Patch Drizzle ORM to capture queries.
37
+ * Drizzle exposes a logger option in its constructor.
38
+ * We patch the db object's execute method to capture queries.
39
+ */
40
+ export declare function patchDrizzle(drizzleModule: any, debug: boolean): void;
41
+ /**
42
+ * Patch Knex to capture queries.
43
+ * Knex emits 'query' and 'query-response' events on the knex instance.
44
+ */
45
+ export declare function patchKnex(knexModule: any, debug: boolean): void;
46
+ /**
47
+ * Patch TypeORM to capture queries via its Logger interface.
48
+ * TypeORM DataSource accepts a `logger` option. We wrap createConnection/DataSource
49
+ * to inject a custom logger that captures all queries.
50
+ */
51
+ export declare function patchTypeORM(typeormModule: any, debug: boolean): void;
52
+ /**
53
+ * Patch Sequelize to capture queries via its logging option.
54
+ * Sequelize accepts a `logging` function in its constructor options.
55
+ */
56
+ export declare function patchSequelize(sequelizeModule: any, debug: boolean): void;
29
57
  /**
30
58
  * Patch mongoose to capture MongoDB operations.
31
59
  * Called from observe-register when mongoose is required.
@@ -47,6 +47,11 @@ exports.patchPg = patchPg;
47
47
  exports.patchMysql2 = patchMysql2;
48
48
  exports.patchBetterSqlite3 = patchBetterSqlite3;
49
49
  exports.patchIoredis = patchIoredis;
50
+ exports.patchPrisma = patchPrisma;
51
+ exports.patchDrizzle = patchDrizzle;
52
+ exports.patchKnex = patchKnex;
53
+ exports.patchTypeORM = patchTypeORM;
54
+ exports.patchSequelize = patchSequelize;
50
55
  exports.patchMongoose = patchMongoose;
51
56
  const fs = __importStar(require("fs"));
52
57
  const path = __importStar(require("path"));
@@ -333,6 +338,341 @@ function patchIoredis(ioredisModule, debug) {
333
338
  if (debug)
334
339
  console.log('[trickle/db] Redis (ioredis) query tracing enabled');
335
340
  }
341
+ /**
342
+ * Patch @prisma/client to capture queries.
343
+ * Prisma has its own query engine (Rust binary) so patching pg/mysql2 won't work.
344
+ * Instead, we hook into Prisma's $on('query') event and $use() middleware.
345
+ */
346
+ function patchPrisma(prismaModule, debug) {
347
+ debugMode = debug;
348
+ const PrismaClient = prismaModule.PrismaClient;
349
+ if (!PrismaClient || PrismaClient.__trickle_patched)
350
+ return;
351
+ const originalConstructor = PrismaClient;
352
+ const OriginalPrototype = PrismaClient.prototype;
353
+ // Wrap the constructor to inject query logging
354
+ const patchedConstructor = function (opts = {}) {
355
+ // Enable query logging in Prisma
356
+ if (!opts.log)
357
+ opts.log = [];
358
+ const logEntries = Array.isArray(opts.log) ? opts.log : [];
359
+ // Add query event if not already present
360
+ const hasQueryLog = logEntries.some((entry) => (typeof entry === 'string' && entry === 'query') ||
361
+ (typeof entry === 'object' && entry.emit === 'event' && entry.level === 'query'));
362
+ if (!hasQueryLog) {
363
+ logEntries.push({ emit: 'event', level: 'query' });
364
+ }
365
+ opts.log = logEntries;
366
+ // Call original constructor
367
+ const instance = new originalConstructor(opts);
368
+ // Subscribe to query events
369
+ try {
370
+ instance.$on('query', (e) => {
371
+ const queryText = (e.query || '').substring(0, MAX_QUERY_LENGTH);
372
+ const params = e.params ? (() => { try {
373
+ return JSON.parse(e.params).slice(0, 5);
374
+ }
375
+ catch {
376
+ return undefined;
377
+ } })() : undefined;
378
+ writeQuery({
379
+ kind: 'query',
380
+ query: queryText,
381
+ params,
382
+ durationMs: e.duration || 0,
383
+ rowCount: 0, // Prisma query events don't include row count
384
+ timestamp: Date.now(),
385
+ });
386
+ if (debugMode) {
387
+ console.log(`[trickle/db] Prisma: ${queryText.substring(0, 60)}... (${e.duration}ms)`);
388
+ }
389
+ });
390
+ }
391
+ catch { /* $on may not be available on all Prisma versions */ }
392
+ return instance;
393
+ };
394
+ // Copy prototype chain
395
+ patchedConstructor.prototype = OriginalPrototype;
396
+ Object.setPrototypeOf(patchedConstructor, originalConstructor);
397
+ // Copy static properties
398
+ for (const key of Object.getOwnPropertyNames(originalConstructor)) {
399
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
400
+ try {
401
+ Object.defineProperty(patchedConstructor, key, Object.getOwnPropertyDescriptor(originalConstructor, key));
402
+ }
403
+ catch { }
404
+ }
405
+ }
406
+ prismaModule.PrismaClient = patchedConstructor;
407
+ prismaModule.PrismaClient.__trickle_patched = true;
408
+ if (debug)
409
+ console.log('[trickle/db] Prisma query tracing enabled');
410
+ }
411
+ /**
412
+ * Patch Drizzle ORM to capture queries.
413
+ * Drizzle exposes a logger option in its constructor.
414
+ * We patch the db object's execute method to capture queries.
415
+ */
416
+ function patchDrizzle(drizzleModule, debug) {
417
+ debugMode = debug;
418
+ // drizzle-orm exports various dialect-specific functions
419
+ // The common pattern is drizzle(client, { logger: true })
420
+ // We patch by wrapping the module's default export or named exports
421
+ for (const key of Object.keys(drizzleModule)) {
422
+ const fn = drizzleModule[key];
423
+ if (typeof fn !== 'function')
424
+ continue;
425
+ if (fn.__trickle_patched)
426
+ continue;
427
+ // Only patch functions that look like drizzle constructors
428
+ // (they take a client + config object)
429
+ drizzleModule[key] = function patchedDrizzle(...args) {
430
+ // Inject a custom logger into the config
431
+ const config = args[1] && typeof args[1] === 'object' ? { ...args[1] } : (args.length > 1 ? args[1] : {});
432
+ if (typeof config === 'object' && config !== null) {
433
+ const origLogger = config.logger;
434
+ config.logger = {
435
+ logQuery(query, params) {
436
+ const startTime = performance.now();
437
+ writeQuery({
438
+ kind: 'query',
439
+ query: query.substring(0, MAX_QUERY_LENGTH),
440
+ params: Array.isArray(params) ? params.slice(0, 5) : undefined,
441
+ durationMs: 0, // Drizzle logger doesn't provide timing
442
+ rowCount: 0,
443
+ timestamp: Date.now(),
444
+ });
445
+ // Call original logger if present
446
+ if (origLogger && typeof origLogger === 'object' && 'logQuery' in origLogger) {
447
+ origLogger.logQuery(query, params);
448
+ }
449
+ },
450
+ };
451
+ args[1] = config;
452
+ }
453
+ return fn.apply(this, args);
454
+ };
455
+ drizzleModule[key].__trickle_patched = true;
456
+ }
457
+ if (debug)
458
+ console.log('[trickle/db] Drizzle ORM query tracing enabled');
459
+ }
460
+ /**
461
+ * Patch Knex to capture queries.
462
+ * Knex emits 'query' and 'query-response' events on the knex instance.
463
+ */
464
+ function patchKnex(knexModule, debug) {
465
+ debugMode = debug;
466
+ const origKnex = knexModule.default || knexModule;
467
+ if (typeof origKnex !== 'function' || origKnex.__trickle_patched)
468
+ return;
469
+ const wrapped = function patchedKnex(...args) {
470
+ const instance = origKnex.apply(this, args);
471
+ // Track query start times
472
+ const queryTimers = new Map();
473
+ try {
474
+ instance.on('query', (data) => {
475
+ queryTimers.set(data.__knexQueryUid || '', performance.now());
476
+ });
477
+ instance.on('query-response', (_response, data) => {
478
+ const startTime = queryTimers.get(data.__knexQueryUid || '');
479
+ const durationMs = startTime ? Math.round((performance.now() - startTime) * 100) / 100 : 0;
480
+ queryTimers.delete(data.__knexQueryUid || '');
481
+ writeQuery({
482
+ kind: 'query',
483
+ query: (data.sql || '').substring(0, MAX_QUERY_LENGTH),
484
+ params: Array.isArray(data.bindings) ? data.bindings.slice(0, 5) : undefined,
485
+ durationMs,
486
+ rowCount: 0,
487
+ timestamp: Date.now(),
488
+ });
489
+ });
490
+ instance.on('query-error', (error, data) => {
491
+ const startTime = queryTimers.get(data.__knexQueryUid || '');
492
+ const durationMs = startTime ? Math.round((performance.now() - startTime) * 100) / 100 : 0;
493
+ queryTimers.delete(data.__knexQueryUid || '');
494
+ writeQuery({
495
+ kind: 'query',
496
+ query: (data.sql || '').substring(0, MAX_QUERY_LENGTH),
497
+ durationMs,
498
+ rowCount: 0,
499
+ error: error?.message?.substring(0, 200),
500
+ timestamp: Date.now(),
501
+ });
502
+ });
503
+ }
504
+ catch { /* query events not available */ }
505
+ return instance;
506
+ };
507
+ // Copy properties
508
+ Object.setPrototypeOf(wrapped, origKnex);
509
+ for (const key of Object.getOwnPropertyNames(origKnex)) {
510
+ if (key !== 'length' && key !== 'name') {
511
+ try {
512
+ Object.defineProperty(wrapped, key, Object.getOwnPropertyDescriptor(origKnex, key));
513
+ }
514
+ catch { }
515
+ }
516
+ }
517
+ if (knexModule.default) {
518
+ knexModule.default = wrapped;
519
+ }
520
+ else {
521
+ // knex exports the function directly via module.exports
522
+ // We can't replace module.exports from here, but observe-register handles that
523
+ }
524
+ wrapped.__trickle_patched = true;
525
+ if (debug)
526
+ console.log('[trickle/db] Knex query tracing enabled');
527
+ }
528
+ /**
529
+ * Patch TypeORM to capture queries via its Logger interface.
530
+ * TypeORM DataSource accepts a `logger` option. We wrap createConnection/DataSource
531
+ * to inject a custom logger that captures all queries.
532
+ */
533
+ function patchTypeORM(typeormModule, debug) {
534
+ debugMode = debug;
535
+ // Patch DataSource constructor (TypeORM 0.3+)
536
+ const DataSource = typeormModule.DataSource;
537
+ if (DataSource && !DataSource.__trickle_patched) {
538
+ const OrigDataSource = DataSource;
539
+ typeormModule.DataSource = function PatchedDataSource(options) {
540
+ // Inject custom logger
541
+ if (!options._trickle_injected) {
542
+ options._trickle_injected = true;
543
+ const origLogger = options.logger;
544
+ options.logger = {
545
+ logQuery(query, parameters) {
546
+ writeQuery({
547
+ kind: 'query',
548
+ query: query.substring(0, MAX_QUERY_LENGTH),
549
+ params: parameters?.slice(0, 5),
550
+ durationMs: 0,
551
+ rowCount: 0,
552
+ timestamp: Date.now(),
553
+ });
554
+ if (origLogger && typeof origLogger === 'object' && 'logQuery' in origLogger) {
555
+ origLogger.logQuery(query, parameters);
556
+ }
557
+ },
558
+ logQueryError(error, query, parameters) {
559
+ writeQuery({
560
+ kind: 'query',
561
+ query: query.substring(0, MAX_QUERY_LENGTH),
562
+ params: parameters?.slice(0, 5),
563
+ durationMs: 0,
564
+ rowCount: 0,
565
+ error: error.substring(0, 200),
566
+ timestamp: Date.now(),
567
+ });
568
+ },
569
+ logQuerySlow(time, query, parameters) {
570
+ writeQuery({
571
+ kind: 'query',
572
+ query: query.substring(0, MAX_QUERY_LENGTH),
573
+ params: parameters?.slice(0, 5),
574
+ durationMs: time,
575
+ rowCount: 0,
576
+ timestamp: Date.now(),
577
+ });
578
+ },
579
+ logSchemaBuild() { },
580
+ logMigration() { },
581
+ log() { },
582
+ };
583
+ }
584
+ return new OrigDataSource(options);
585
+ };
586
+ // Copy statics
587
+ Object.setPrototypeOf(typeormModule.DataSource, OrigDataSource);
588
+ typeormModule.DataSource.prototype = OrigDataSource.prototype;
589
+ typeormModule.DataSource.__trickle_patched = true;
590
+ }
591
+ // Also patch createConnection (TypeORM 0.2)
592
+ if (typeormModule.createConnection && !typeormModule.createConnection.__trickle_patched) {
593
+ const origCreate = typeormModule.createConnection;
594
+ typeormModule.createConnection = function patchedCreateConnection(options) {
595
+ if (options && typeof options === 'object' && !options.logging) {
596
+ options.logging = true;
597
+ }
598
+ return origCreate(options);
599
+ };
600
+ typeormModule.createConnection.__trickle_patched = true;
601
+ }
602
+ if (debug)
603
+ console.log('[trickle/db] TypeORM query tracing enabled');
604
+ }
605
+ /**
606
+ * Patch Sequelize to capture queries via its logging option.
607
+ * Sequelize accepts a `logging` function in its constructor options.
608
+ */
609
+ function patchSequelize(sequelizeModule, debug) {
610
+ debugMode = debug;
611
+ const Sequelize = sequelizeModule.Sequelize || sequelizeModule.default || sequelizeModule;
612
+ if (!Sequelize || typeof Sequelize !== 'function' || Sequelize.__trickle_patched)
613
+ return;
614
+ const origConstructor = Sequelize;
615
+ const patchedSequelize = function (...args) {
616
+ // Sequelize(uri, options) or Sequelize(database, user, pass, options)
617
+ let options;
618
+ if (args.length >= 4) {
619
+ options = args[3] = args[3] || {};
620
+ }
621
+ else if (args.length >= 2 && typeof args[1] === 'object') {
622
+ options = args[1];
623
+ }
624
+ else if (args.length === 1 && typeof args[0] === 'object') {
625
+ options = args[0];
626
+ }
627
+ else {
628
+ options = {};
629
+ args.push(options);
630
+ }
631
+ // Wrap the logging function
632
+ const origLogging = options.logging;
633
+ options.logging = (sql, timing) => {
634
+ const queryText = typeof sql === 'string' ? sql : String(sql);
635
+ // Sequelize prepends "Executed (default): " or "Executing (default): " to queries
636
+ const cleanQuery = queryText.replace(/^Execut(?:ed|ing) \([^)]*\):\s*/, '');
637
+ const durationMs = typeof timing === 'number' ? timing : 0;
638
+ writeQuery({
639
+ kind: 'query',
640
+ query: cleanQuery.substring(0, MAX_QUERY_LENGTH),
641
+ durationMs,
642
+ rowCount: 0,
643
+ timestamp: Date.now(),
644
+ });
645
+ // Call original logger
646
+ if (typeof origLogging === 'function') {
647
+ origLogging(sql, timing);
648
+ }
649
+ };
650
+ options.benchmark = true; // Enable timing in log
651
+ // Call original constructor
652
+ if (new.target) {
653
+ return new origConstructor(...args);
654
+ }
655
+ return origConstructor.apply(this, args);
656
+ };
657
+ // Copy prototype and statics
658
+ patchedSequelize.prototype = origConstructor.prototype;
659
+ Object.setPrototypeOf(patchedSequelize, origConstructor);
660
+ for (const key of Object.getOwnPropertyNames(origConstructor)) {
661
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
662
+ try {
663
+ Object.defineProperty(patchedSequelize, key, Object.getOwnPropertyDescriptor(origConstructor, key));
664
+ }
665
+ catch { }
666
+ }
667
+ }
668
+ // Replace in module exports
669
+ if (sequelizeModule.Sequelize) {
670
+ sequelizeModule.Sequelize = patchedSequelize;
671
+ }
672
+ patchedSequelize.__trickle_patched = true;
673
+ if (debug)
674
+ console.log('[trickle/db] Sequelize query tracing enabled');
675
+ }
336
676
  /**
337
677
  * Patch mongoose to capture MongoDB operations.
338
678
  * Called from observe-register when mongoose is required.
@@ -1203,6 +1203,51 @@ if (enabled) {
1203
1203
  }
1204
1204
  catch { /* not critical */ }
1205
1205
  }
1206
+ // Prisma ORM
1207
+ if (request === '@prisma/client' && !expressPatched.has('@prisma/client')) {
1208
+ expressPatched.add('@prisma/client');
1209
+ try {
1210
+ const { patchPrisma } = require(path_1.default.join(__dirname, 'db-observer.js'));
1211
+ patchPrisma(exports, debug);
1212
+ }
1213
+ catch { /* not critical */ }
1214
+ }
1215
+ // Drizzle ORM
1216
+ if (request.startsWith('drizzle-orm') && !expressPatched.has('drizzle-orm')) {
1217
+ expressPatched.add('drizzle-orm');
1218
+ try {
1219
+ const { patchDrizzle } = require(path_1.default.join(__dirname, 'db-observer.js'));
1220
+ patchDrizzle(exports, debug);
1221
+ }
1222
+ catch { /* not critical */ }
1223
+ }
1224
+ // Knex query builder
1225
+ if (request === 'knex' && !expressPatched.has('knex')) {
1226
+ expressPatched.add('knex');
1227
+ try {
1228
+ const { patchKnex } = require(path_1.default.join(__dirname, 'db-observer.js'));
1229
+ patchKnex(exports, debug);
1230
+ }
1231
+ catch { /* not critical */ }
1232
+ }
1233
+ // TypeORM
1234
+ if (request === 'typeorm' && !expressPatched.has('typeorm')) {
1235
+ expressPatched.add('typeorm');
1236
+ try {
1237
+ const { patchTypeORM } = require(path_1.default.join(__dirname, 'db-observer.js'));
1238
+ patchTypeORM(exports, debug);
1239
+ }
1240
+ catch { /* not critical */ }
1241
+ }
1242
+ // Sequelize
1243
+ if (request === 'sequelize' && !expressPatched.has('sequelize')) {
1244
+ expressPatched.add('sequelize');
1245
+ try {
1246
+ const { patchSequelize } = require(path_1.default.join(__dirname, 'db-observer.js'));
1247
+ patchSequelize(exports, debug);
1248
+ }
1249
+ catch { /* not critical */ }
1250
+ }
1206
1251
  // Redis (ioredis)
1207
1252
  if (request === 'ioredis' && !expressPatched.has('ioredis')) {
1208
1253
  expressPatched.add('ioredis');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.106",
3
+ "version": "0.2.108",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -332,6 +332,367 @@ export function patchIoredis(ioredisModule: any, debug: boolean): void {
332
332
  if (debug) console.log('[trickle/db] Redis (ioredis) query tracing enabled');
333
333
  }
334
334
 
335
+ /**
336
+ * Patch @prisma/client to capture queries.
337
+ * Prisma has its own query engine (Rust binary) so patching pg/mysql2 won't work.
338
+ * Instead, we hook into Prisma's $on('query') event and $use() middleware.
339
+ */
340
+ export function patchPrisma(prismaModule: any, debug: boolean): void {
341
+ debugMode = debug;
342
+
343
+ const PrismaClient = prismaModule.PrismaClient;
344
+ if (!PrismaClient || (PrismaClient as any).__trickle_patched) return;
345
+
346
+ const originalConstructor = PrismaClient;
347
+ const OriginalPrototype = PrismaClient.prototype;
348
+
349
+ // Wrap the constructor to inject query logging
350
+ const patchedConstructor = function (this: any, opts: any = {}) {
351
+ // Enable query logging in Prisma
352
+ if (!opts.log) opts.log = [];
353
+ const logEntries = Array.isArray(opts.log) ? opts.log : [];
354
+
355
+ // Add query event if not already present
356
+ const hasQueryLog = logEntries.some((entry: any) =>
357
+ (typeof entry === 'string' && entry === 'query') ||
358
+ (typeof entry === 'object' && entry.emit === 'event' && entry.level === 'query')
359
+ );
360
+ if (!hasQueryLog) {
361
+ logEntries.push({ emit: 'event', level: 'query' });
362
+ }
363
+ opts.log = logEntries;
364
+
365
+ // Call original constructor
366
+ const instance = new originalConstructor(opts);
367
+
368
+ // Subscribe to query events
369
+ try {
370
+ instance.$on('query', (e: any) => {
371
+ const queryText = (e.query || '').substring(0, MAX_QUERY_LENGTH);
372
+ const params = e.params ? (() => { try { return JSON.parse(e.params).slice(0, 5); } catch { return undefined; } })() : undefined;
373
+ writeQuery({
374
+ kind: 'query',
375
+ query: queryText,
376
+ params,
377
+ durationMs: e.duration || 0,
378
+ rowCount: 0, // Prisma query events don't include row count
379
+ timestamp: Date.now(),
380
+ });
381
+ if (debugMode) {
382
+ console.log(`[trickle/db] Prisma: ${queryText.substring(0, 60)}... (${e.duration}ms)`);
383
+ }
384
+ });
385
+ } catch { /* $on may not be available on all Prisma versions */ }
386
+
387
+ return instance;
388
+ };
389
+
390
+ // Copy prototype chain
391
+ patchedConstructor.prototype = OriginalPrototype;
392
+ Object.setPrototypeOf(patchedConstructor, originalConstructor);
393
+
394
+ // Copy static properties
395
+ for (const key of Object.getOwnPropertyNames(originalConstructor)) {
396
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
397
+ try {
398
+ Object.defineProperty(patchedConstructor, key, Object.getOwnPropertyDescriptor(originalConstructor, key)!);
399
+ } catch {}
400
+ }
401
+ }
402
+
403
+ prismaModule.PrismaClient = patchedConstructor;
404
+ (prismaModule.PrismaClient as any).__trickle_patched = true;
405
+
406
+ if (debug) console.log('[trickle/db] Prisma query tracing enabled');
407
+ }
408
+
409
+ /**
410
+ * Patch Drizzle ORM to capture queries.
411
+ * Drizzle exposes a logger option in its constructor.
412
+ * We patch the db object's execute method to capture queries.
413
+ */
414
+ export function patchDrizzle(drizzleModule: any, debug: boolean): void {
415
+ debugMode = debug;
416
+
417
+ // drizzle-orm exports various dialect-specific functions
418
+ // The common pattern is drizzle(client, { logger: true })
419
+ // We patch by wrapping the module's default export or named exports
420
+
421
+ for (const key of Object.keys(drizzleModule)) {
422
+ const fn = drizzleModule[key];
423
+ if (typeof fn !== 'function') continue;
424
+ if ((fn as any).__trickle_patched) continue;
425
+
426
+ // Only patch functions that look like drizzle constructors
427
+ // (they take a client + config object)
428
+ drizzleModule[key] = function patchedDrizzle(...args: any[]): any {
429
+ // Inject a custom logger into the config
430
+ const config = args[1] && typeof args[1] === 'object' ? { ...args[1] } : (args.length > 1 ? args[1] : {});
431
+
432
+ if (typeof config === 'object' && config !== null) {
433
+ const origLogger = config.logger;
434
+ config.logger = {
435
+ logQuery(query: string, params: unknown[]): void {
436
+ const startTime = performance.now();
437
+ writeQuery({
438
+ kind: 'query',
439
+ query: query.substring(0, MAX_QUERY_LENGTH),
440
+ params: Array.isArray(params) ? params.slice(0, 5) : undefined,
441
+ durationMs: 0, // Drizzle logger doesn't provide timing
442
+ rowCount: 0,
443
+ timestamp: Date.now(),
444
+ });
445
+ // Call original logger if present
446
+ if (origLogger && typeof origLogger === 'object' && 'logQuery' in origLogger) {
447
+ (origLogger as any).logQuery(query, params);
448
+ }
449
+ },
450
+ };
451
+ args[1] = config;
452
+ }
453
+
454
+ return fn.apply(this, args);
455
+ };
456
+ (drizzleModule[key] as any).__trickle_patched = true;
457
+ }
458
+
459
+ if (debug) console.log('[trickle/db] Drizzle ORM query tracing enabled');
460
+ }
461
+
462
+ /**
463
+ * Patch Knex to capture queries.
464
+ * Knex emits 'query' and 'query-response' events on the knex instance.
465
+ */
466
+ export function patchKnex(knexModule: any, debug: boolean): void {
467
+ debugMode = debug;
468
+
469
+ const origKnex = knexModule.default || knexModule;
470
+ if (typeof origKnex !== 'function' || (origKnex as any).__trickle_patched) return;
471
+
472
+ const wrapped = function patchedKnex(this: any, ...args: any[]): any {
473
+ const instance = origKnex.apply(this, args);
474
+
475
+ // Track query start times
476
+ const queryTimers = new Map<string, number>();
477
+
478
+ try {
479
+ instance.on('query', (data: any) => {
480
+ queryTimers.set(data.__knexQueryUid || '', performance.now());
481
+ });
482
+
483
+ instance.on('query-response', (_response: any, data: any) => {
484
+ const startTime = queryTimers.get(data.__knexQueryUid || '');
485
+ const durationMs = startTime ? Math.round((performance.now() - startTime) * 100) / 100 : 0;
486
+ queryTimers.delete(data.__knexQueryUid || '');
487
+
488
+ writeQuery({
489
+ kind: 'query',
490
+ query: (data.sql || '').substring(0, MAX_QUERY_LENGTH),
491
+ params: Array.isArray(data.bindings) ? data.bindings.slice(0, 5) : undefined,
492
+ durationMs,
493
+ rowCount: 0,
494
+ timestamp: Date.now(),
495
+ });
496
+ });
497
+
498
+ instance.on('query-error', (error: any, data: any) => {
499
+ const startTime = queryTimers.get(data.__knexQueryUid || '');
500
+ const durationMs = startTime ? Math.round((performance.now() - startTime) * 100) / 100 : 0;
501
+ queryTimers.delete(data.__knexQueryUid || '');
502
+
503
+ writeQuery({
504
+ kind: 'query',
505
+ query: (data.sql || '').substring(0, MAX_QUERY_LENGTH),
506
+ durationMs,
507
+ rowCount: 0,
508
+ error: error?.message?.substring(0, 200),
509
+ timestamp: Date.now(),
510
+ });
511
+ });
512
+ } catch { /* query events not available */ }
513
+
514
+ return instance;
515
+ };
516
+
517
+ // Copy properties
518
+ Object.setPrototypeOf(wrapped, origKnex);
519
+ for (const key of Object.getOwnPropertyNames(origKnex)) {
520
+ if (key !== 'length' && key !== 'name') {
521
+ try { Object.defineProperty(wrapped, key, Object.getOwnPropertyDescriptor(origKnex, key)!); } catch {}
522
+ }
523
+ }
524
+
525
+ if (knexModule.default) {
526
+ knexModule.default = wrapped;
527
+ } else {
528
+ // knex exports the function directly via module.exports
529
+ // We can't replace module.exports from here, but observe-register handles that
530
+ }
531
+
532
+ (wrapped as any).__trickle_patched = true;
533
+ if (debug) console.log('[trickle/db] Knex query tracing enabled');
534
+ }
535
+
536
+ /**
537
+ * Patch TypeORM to capture queries via its Logger interface.
538
+ * TypeORM DataSource accepts a `logger` option. We wrap createConnection/DataSource
539
+ * to inject a custom logger that captures all queries.
540
+ */
541
+ export function patchTypeORM(typeormModule: any, debug: boolean): void {
542
+ debugMode = debug;
543
+
544
+ // Patch DataSource constructor (TypeORM 0.3+)
545
+ const DataSource = typeormModule.DataSource;
546
+ if (DataSource && !DataSource.__trickle_patched) {
547
+ const OrigDataSource = DataSource;
548
+
549
+ typeormModule.DataSource = function PatchedDataSource(options: any) {
550
+ // Inject custom logger
551
+ if (!options._trickle_injected) {
552
+ options._trickle_injected = true;
553
+ const origLogger = options.logger;
554
+
555
+ options.logger = {
556
+ logQuery(query: string, parameters?: any[]) {
557
+ writeQuery({
558
+ kind: 'query',
559
+ query: query.substring(0, MAX_QUERY_LENGTH),
560
+ params: parameters?.slice(0, 5),
561
+ durationMs: 0,
562
+ rowCount: 0,
563
+ timestamp: Date.now(),
564
+ });
565
+ if (origLogger && typeof origLogger === 'object' && 'logQuery' in origLogger) {
566
+ origLogger.logQuery(query, parameters);
567
+ }
568
+ },
569
+ logQueryError(error: string, query: string, parameters?: any[]) {
570
+ writeQuery({
571
+ kind: 'query',
572
+ query: query.substring(0, MAX_QUERY_LENGTH),
573
+ params: parameters?.slice(0, 5),
574
+ durationMs: 0,
575
+ rowCount: 0,
576
+ error: error.substring(0, 200),
577
+ timestamp: Date.now(),
578
+ });
579
+ },
580
+ logQuerySlow(time: number, query: string, parameters?: any[]) {
581
+ writeQuery({
582
+ kind: 'query',
583
+ query: query.substring(0, MAX_QUERY_LENGTH),
584
+ params: parameters?.slice(0, 5),
585
+ durationMs: time,
586
+ rowCount: 0,
587
+ timestamp: Date.now(),
588
+ });
589
+ },
590
+ logSchemaBuild() {},
591
+ logMigration() {},
592
+ log() {},
593
+ };
594
+ }
595
+
596
+ return new OrigDataSource(options);
597
+ };
598
+
599
+ // Copy statics
600
+ Object.setPrototypeOf(typeormModule.DataSource, OrigDataSource);
601
+ typeormModule.DataSource.prototype = OrigDataSource.prototype;
602
+ typeormModule.DataSource.__trickle_patched = true;
603
+ }
604
+
605
+ // Also patch createConnection (TypeORM 0.2)
606
+ if (typeormModule.createConnection && !(typeormModule.createConnection as any).__trickle_patched) {
607
+ const origCreate = typeormModule.createConnection;
608
+ typeormModule.createConnection = function patchedCreateConnection(options: any) {
609
+ if (options && typeof options === 'object' && !options.logging) {
610
+ options.logging = true;
611
+ }
612
+ return origCreate(options);
613
+ };
614
+ (typeormModule.createConnection as any).__trickle_patched = true;
615
+ }
616
+
617
+ if (debug) console.log('[trickle/db] TypeORM query tracing enabled');
618
+ }
619
+
620
+ /**
621
+ * Patch Sequelize to capture queries via its logging option.
622
+ * Sequelize accepts a `logging` function in its constructor options.
623
+ */
624
+ export function patchSequelize(sequelizeModule: any, debug: boolean): void {
625
+ debugMode = debug;
626
+
627
+ const Sequelize = sequelizeModule.Sequelize || sequelizeModule.default || sequelizeModule;
628
+ if (!Sequelize || typeof Sequelize !== 'function' || (Sequelize as any).__trickle_patched) return;
629
+
630
+ const origConstructor = Sequelize;
631
+
632
+ const patchedSequelize = function (this: any, ...args: any[]) {
633
+ // Sequelize(uri, options) or Sequelize(database, user, pass, options)
634
+ let options: any;
635
+ if (args.length >= 4) {
636
+ options = args[3] = args[3] || {};
637
+ } else if (args.length >= 2 && typeof args[1] === 'object') {
638
+ options = args[1];
639
+ } else if (args.length === 1 && typeof args[0] === 'object') {
640
+ options = args[0];
641
+ } else {
642
+ options = {};
643
+ args.push(options);
644
+ }
645
+
646
+ // Wrap the logging function
647
+ const origLogging = options.logging;
648
+ options.logging = (sql: string, timing?: any) => {
649
+ const queryText = typeof sql === 'string' ? sql : String(sql);
650
+ // Sequelize prepends "Executed (default): " or "Executing (default): " to queries
651
+ const cleanQuery = queryText.replace(/^Execut(?:ed|ing) \([^)]*\):\s*/, '');
652
+ const durationMs = typeof timing === 'number' ? timing : 0;
653
+
654
+ writeQuery({
655
+ kind: 'query',
656
+ query: cleanQuery.substring(0, MAX_QUERY_LENGTH),
657
+ durationMs,
658
+ rowCount: 0,
659
+ timestamp: Date.now(),
660
+ });
661
+
662
+ // Call original logger
663
+ if (typeof origLogging === 'function') {
664
+ origLogging(sql, timing);
665
+ }
666
+ };
667
+ options.benchmark = true; // Enable timing in log
668
+
669
+ // Call original constructor
670
+ if (new.target) {
671
+ return new origConstructor(...args);
672
+ }
673
+ return origConstructor.apply(this, args);
674
+ };
675
+
676
+ // Copy prototype and statics
677
+ patchedSequelize.prototype = origConstructor.prototype;
678
+ Object.setPrototypeOf(patchedSequelize, origConstructor);
679
+ for (const key of Object.getOwnPropertyNames(origConstructor)) {
680
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
681
+ try {
682
+ Object.defineProperty(patchedSequelize, key, Object.getOwnPropertyDescriptor(origConstructor, key)!);
683
+ } catch {}
684
+ }
685
+ }
686
+
687
+ // Replace in module exports
688
+ if (sequelizeModule.Sequelize) {
689
+ sequelizeModule.Sequelize = patchedSequelize;
690
+ }
691
+ (patchedSequelize as any).__trickle_patched = true;
692
+
693
+ if (debug) console.log('[trickle/db] Sequelize query tracing enabled');
694
+ }
695
+
335
696
  /**
336
697
  * Patch mongoose to capture MongoDB operations.
337
698
  * Called from observe-register when mongoose is required.
@@ -1188,6 +1188,51 @@ if (enabled) {
1188
1188
  } catch { /* not critical */ }
1189
1189
  }
1190
1190
 
1191
+ // Prisma ORM
1192
+ if (request === '@prisma/client' && !expressPatched.has('@prisma/client')) {
1193
+ expressPatched.add('@prisma/client');
1194
+ try {
1195
+ const { patchPrisma } = require(path.join(__dirname, 'db-observer.js'));
1196
+ patchPrisma(exports, debug);
1197
+ } catch { /* not critical */ }
1198
+ }
1199
+
1200
+ // Drizzle ORM
1201
+ if (request.startsWith('drizzle-orm') && !expressPatched.has('drizzle-orm')) {
1202
+ expressPatched.add('drizzle-orm');
1203
+ try {
1204
+ const { patchDrizzle } = require(path.join(__dirname, 'db-observer.js'));
1205
+ patchDrizzle(exports, debug);
1206
+ } catch { /* not critical */ }
1207
+ }
1208
+
1209
+ // Knex query builder
1210
+ if (request === 'knex' && !expressPatched.has('knex')) {
1211
+ expressPatched.add('knex');
1212
+ try {
1213
+ const { patchKnex } = require(path.join(__dirname, 'db-observer.js'));
1214
+ patchKnex(exports, debug);
1215
+ } catch { /* not critical */ }
1216
+ }
1217
+
1218
+ // TypeORM
1219
+ if (request === 'typeorm' && !expressPatched.has('typeorm')) {
1220
+ expressPatched.add('typeorm');
1221
+ try {
1222
+ const { patchTypeORM } = require(path.join(__dirname, 'db-observer.js'));
1223
+ patchTypeORM(exports, debug);
1224
+ } catch { /* not critical */ }
1225
+ }
1226
+
1227
+ // Sequelize
1228
+ if (request === 'sequelize' && !expressPatched.has('sequelize')) {
1229
+ expressPatched.add('sequelize');
1230
+ try {
1231
+ const { patchSequelize } = require(path.join(__dirname, 'db-observer.js'));
1232
+ patchSequelize(exports, debug);
1233
+ } catch { /* not critical */ }
1234
+ }
1235
+
1191
1236
  // Redis (ioredis)
1192
1237
  if (request === 'ioredis' && !expressPatched.has('ioredis')) {
1193
1238
  expressPatched.add('ioredis');