nestlens 0.4.0 → 0.4.1

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.
Files changed (62) hide show
  1. package/dist/__tests__/watchers/query/typeorm-integration.spec.d.ts +2 -0
  2. package/dist/__tests__/watchers/query/typeorm-integration.spec.d.ts.map +1 -0
  3. package/dist/__tests__/watchers/query/typeorm-integration.spec.js +100 -0
  4. package/dist/__tests__/watchers/query/typeorm-integration.spec.js.map +1 -0
  5. package/dist/__tests__/watchers/query/types.spec.js +48 -263
  6. package/dist/__tests__/watchers/query/types.spec.js.map +1 -1
  7. package/dist/__tests__/watchers/query.watcher.spec.js +173 -522
  8. package/dist/__tests__/watchers/query.watcher.spec.js.map +1 -1
  9. package/dist/dashboard/public/assets/{BatchesPage-DmqbLFCs.js → BatchesPage-DrFl64wj.js} +1 -1
  10. package/dist/dashboard/public/assets/{CachePage-CfA-UrdM.js → CachePage-C57Pkxm0.js} +1 -1
  11. package/dist/dashboard/public/assets/{ClickableBadge-CW4jdcpl.js → ClickableBadge-BNUq-FXI.js} +1 -1
  12. package/dist/dashboard/public/assets/{CommandsPage-DJ1ZxHj8.js → CommandsPage-BdD-ojDk.js} +1 -1
  13. package/dist/dashboard/public/assets/{DashboardPage-BnnZ3TMF.js → DashboardPage-DCophLiw.js} +1 -1
  14. package/dist/dashboard/public/assets/{DumpsPage-fshG-ETf.js → DumpsPage-DtXeYPpQ.js} +1 -1
  15. package/dist/dashboard/public/assets/{EntryDetailPage-Cf7K34er.js → EntryDetailPage-qlWzdnXs.js} +1 -1
  16. package/dist/dashboard/public/assets/{EventsPage-DiPB0Sfy.js → EventsPage-BHpLjHZj.js} +1 -1
  17. package/dist/dashboard/public/assets/{ExceptionsPage-BoauuDFu.js → ExceptionsPage-Ww3jIO29.js} +1 -1
  18. package/dist/dashboard/public/assets/{GatesPage-CB0kqyp_.js → GatesPage-beRehq6b.js} +1 -1
  19. package/dist/dashboard/public/assets/{GraphQLPage-_inMfKh4.js → GraphQLPage-BZt0nDmS.js} +1 -1
  20. package/dist/dashboard/public/assets/{HttpClientPage-5vVzz7TU.js → HttpClientPage-Dac8U41f.js} +1 -1
  21. package/dist/dashboard/public/assets/{JobsPage-CHYjmhlh.js → JobsPage-zLQH5Skv.js} +1 -1
  22. package/dist/dashboard/public/assets/{LogsPage-z4yYJ1qQ.js → LogsPage-Dj7YEZlz.js} +1 -1
  23. package/dist/dashboard/public/assets/{MailPage-DQ2PuRS9.js → MailPage-C45lv85q.js} +1 -1
  24. package/dist/dashboard/public/assets/{ModelsPage-PBBI9K5M.js → ModelsPage-CveFVTw3.js} +1 -1
  25. package/dist/dashboard/public/assets/{NotificationsPage-CjleWNhr.js → NotificationsPage-CVAGh1SG.js} +1 -1
  26. package/dist/dashboard/public/assets/{PageHeader-QBzFo308.js → PageHeader-BhUyZtbe.js} +1 -1
  27. package/dist/dashboard/public/assets/{QueriesPage-Cpi1b2Mz.js → QueriesPage-BMpvUeTX.js} +1 -1
  28. package/dist/dashboard/public/assets/{RedisPage-C7vFV3r4.js → RedisPage-FPrKgNrc.js} +1 -1
  29. package/dist/dashboard/public/assets/{RequestsPage-B9-Qq2vH.js → RequestsPage-6JVqr7xE.js} +1 -1
  30. package/dist/dashboard/public/assets/{SchedulePage-BXPldddV.js → SchedulePage-DOt6jqTT.js} +1 -1
  31. package/dist/dashboard/public/assets/{ViewsPage-COo7BNBc.js → ViewsPage-RG6Ze2rn.js} +1 -1
  32. package/dist/dashboard/public/assets/{calendar-WMtbNas7.js → calendar-CEjtgImi.js} +1 -1
  33. package/dist/dashboard/public/assets/{circle-check-big-U-4nHjTQ.js → circle-check-big-BnRBDUDo.js} +1 -1
  34. package/dist/dashboard/public/assets/{eye-Bc29awES.js → eye-tfj-u8EL.js} +1 -1
  35. package/dist/dashboard/public/assets/{index-C8he4ZUV.js → index-OMQZjJAo.js} +2 -2
  36. package/dist/dashboard/public/assets/{types-BYamMo0R.js → types-Cubd9hBF.js} +1 -1
  37. package/dist/dashboard/public/assets/{zap-BxD9CCmf.js → zap-Bh7DgMLC.js} +1 -1
  38. package/dist/dashboard/public/index.html +1 -1
  39. package/dist/nestlens.module.d.ts.map +1 -1
  40. package/dist/nestlens.module.js +1 -0
  41. package/dist/nestlens.module.js.map +1 -1
  42. package/dist/watchers/query/index.d.ts +2 -0
  43. package/dist/watchers/query/index.d.ts.map +1 -1
  44. package/dist/watchers/query/index.js +2 -0
  45. package/dist/watchers/query/index.js.map +1 -1
  46. package/dist/watchers/query/query.watcher.d.ts +12 -8
  47. package/dist/watchers/query/query.watcher.d.ts.map +1 -1
  48. package/dist/watchers/query/query.watcher.js +80 -78
  49. package/dist/watchers/query/query.watcher.js.map +1 -1
  50. package/dist/watchers/query/typeorm-logger.d.ts +21 -0
  51. package/dist/watchers/query/typeorm-logger.d.ts.map +1 -0
  52. package/dist/watchers/query/typeorm-logger.js +53 -0
  53. package/dist/watchers/query/typeorm-logger.js.map +1 -0
  54. package/dist/watchers/query/typeorm-subscriber.d.ts +20 -0
  55. package/dist/watchers/query/typeorm-subscriber.d.ts.map +1 -0
  56. package/dist/watchers/query/typeorm-subscriber.js +34 -0
  57. package/dist/watchers/query/typeorm-subscriber.js.map +1 -0
  58. package/dist/watchers/query/types.d.ts +50 -29
  59. package/dist/watchers/query/types.d.ts.map +1 -1
  60. package/dist/watchers/query/types.js +21 -16
  61. package/dist/watchers/query/types.js.map +1 -1
  62. package/package.json +4 -1
@@ -1,195 +1,80 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  /**
37
- * QueryWatcher Tests
4
+ * QueryWatcher unit tests.
5
+ *
6
+ * Covers configuration handling, query payload formatting, slow detection,
7
+ * ignore-pattern filtering, and the auxiliary classes (subscriber + logger)
8
+ * that bridge TypeORM's public API into the watcher.
38
9
  *
39
- * Tests for the query watcher that monitors TypeORM and Prisma queries.
40
- * Follows AAA (Arrange-Act-Assert) pattern.
10
+ * Real TypeORM end-to-end coverage lives in
11
+ * `src/__tests__/watchers/query/typeorm-integration.spec.ts`.
41
12
  */
42
13
  const testing_1 = require("@nestjs/testing");
14
+ const core_1 = require("@nestjs/core");
43
15
  const collector_service_1 = require("../../core/collector.service");
44
16
  const nestlens_config_1 = require("../../nestlens.config");
45
17
  const query_watcher_1 = require("../../watchers/query/query.watcher");
46
- // Mock the types module
47
- jest.mock('../../watchers/query/types', () => ({
48
- isModuleAvailable: jest.fn(),
49
- tryRequire: jest.fn(),
50
- isTypeORMDataSource: jest.fn(),
51
- isPrismaClient: jest.fn(),
52
- }));
53
- const queryTypes = __importStar(require("../../watchers/query/types"));
18
+ const typeorm_subscriber_1 = require("../../watchers/query/typeorm-subscriber");
19
+ const typeorm_logger_1 = require("../../watchers/query/typeorm-logger");
54
20
  describe('QueryWatcher', () => {
55
- let watcher;
56
- let mockCollector;
57
- let mockConfig;
58
- const mockedTypes = queryTypes;
59
- const createWatcher = async (config) => {
60
- const module = await testing_1.Test.createTestingModule({
21
+ let collector;
22
+ const buildWatcher = async (config) => {
23
+ const moduleRef = await testing_1.Test.createTestingModule({
24
+ imports: [core_1.DiscoveryModule],
61
25
  providers: [
62
26
  query_watcher_1.QueryWatcher,
63
- { provide: collector_service_1.CollectorService, useValue: mockCollector },
27
+ { provide: collector_service_1.CollectorService, useValue: collector },
64
28
  { provide: nestlens_config_1.NESTLENS_CONFIG, useValue: config },
65
29
  ],
66
30
  }).compile();
67
- return module.get(query_watcher_1.QueryWatcher);
31
+ return moduleRef.get(query_watcher_1.QueryWatcher);
68
32
  };
69
33
  beforeEach(() => {
70
- jest.clearAllMocks();
71
- mockCollector = {
34
+ collector = {
72
35
  collect: jest.fn(),
73
36
  collectImmediate: jest.fn(),
74
37
  };
75
- mockConfig = {
76
- enabled: true,
77
- watchers: {
78
- query: { enabled: true, slowThreshold: 100 },
79
- },
80
- };
81
- // Default mock implementations
82
- mockedTypes.isModuleAvailable.mockReturnValue(false);
83
- mockedTypes.tryRequire.mockReturnValue(null);
84
- mockedTypes.isTypeORMDataSource.mockReturnValue(false);
85
- mockedTypes.isPrismaClient.mockReturnValue(false);
86
38
  });
87
- // ============================================================================
88
- // Config Handling
89
- // ============================================================================
90
- describe('Config Handling', () => {
91
- it('should be enabled when query watcher config is true', async () => {
92
- // Arrange
93
- mockConfig.watchers = { query: true };
94
- watcher = await createWatcher(mockConfig);
95
- // Act & Assert
96
- // Access private config through prototype for testing
39
+ describe('Config handling', () => {
40
+ it('treats `query: true` as enabled with default slowThreshold', async () => {
41
+ const watcher = await buildWatcher({ watchers: { query: true } });
97
42
  expect(watcher.config.enabled).toBe(true);
98
- });
99
- it('should be disabled when query watcher config is false', async () => {
100
- // Arrange
101
- mockConfig.watchers = { query: false };
102
- watcher = await createWatcher(mockConfig);
103
- // Act
104
- await watcher.onModuleInit();
105
- // Assert - should not try to initialize adapters
106
- expect(mockedTypes.isModuleAvailable).not.toHaveBeenCalled();
107
- });
108
- it('should use default slowThreshold of 100 when not specified', async () => {
109
- // Arrange
110
- mockConfig.watchers = { query: true };
111
- watcher = await createWatcher(mockConfig);
112
- // Assert
113
43
  expect(watcher.config.slowThreshold).toBe(100);
114
44
  });
115
- it('should use custom slowThreshold from config', async () => {
116
- // Arrange
117
- mockConfig.watchers = { query: { enabled: true, slowThreshold: 500 } };
118
- watcher = await createWatcher(mockConfig);
119
- // Assert
45
+ it('treats `query: false` as disabled', async () => {
46
+ const watcher = await buildWatcher({ watchers: { query: false } });
47
+ watcher.onApplicationBootstrap();
48
+ expect(watcher.config.enabled).toBe(false);
49
+ });
50
+ it('honours custom slowThreshold from object form', async () => {
51
+ const watcher = await buildWatcher({
52
+ watchers: { query: { enabled: true, slowThreshold: 500 } },
53
+ });
120
54
  expect(watcher.config.slowThreshold).toBe(500);
121
55
  });
122
- it('should be enabled by default when watchers config is undefined', async () => {
123
- // Arrange
124
- mockConfig.watchers = undefined;
125
- watcher = await createWatcher(mockConfig);
126
- // Assert
56
+ it('defaults to enabled when watchers config is undefined', async () => {
57
+ const watcher = await buildWatcher({});
127
58
  expect(watcher.config.enabled).toBe(true);
128
59
  });
129
60
  });
130
- // ============================================================================
131
- // Module Initialization
132
- // ============================================================================
133
- describe('Module Initialization', () => {
134
- beforeEach(async () => {
135
- watcher = await createWatcher(mockConfig);
136
- });
137
- it('should check for TypeORM module availability', async () => {
138
- // Arrange
139
- mockedTypes.isModuleAvailable.mockReturnValue(false);
140
- // Act
141
- await watcher.onModuleInit();
142
- // Assert
143
- expect(mockedTypes.isModuleAvailable).toHaveBeenCalledWith('typeorm');
144
- });
145
- it('should check for Prisma module availability', async () => {
146
- // Arrange
147
- mockedTypes.isModuleAvailable.mockReturnValue(false);
148
- // Act
149
- await watcher.onModuleInit();
150
- // Assert
151
- expect(mockedTypes.isModuleAvailable).toHaveBeenCalledWith('@prisma/client');
152
- });
153
- it('should not initialize when disabled', async () => {
154
- // Arrange
155
- mockConfig.watchers = { query: false };
156
- watcher = await createWatcher(mockConfig);
157
- // Act
158
- await watcher.onModuleInit();
159
- // Assert
160
- expect(mockedTypes.isModuleAvailable).not.toHaveBeenCalled();
161
- });
162
- it('should try to require TypeORM when available', async () => {
163
- // Arrange
164
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === 'typeorm');
165
- mockedTypes.tryRequire.mockReturnValue(null);
166
- // Act
167
- await watcher.onModuleInit();
168
- // Assert
169
- expect(mockedTypes.tryRequire).toHaveBeenCalledWith('typeorm');
170
- });
171
- });
172
- // ============================================================================
173
- // Query Handling (via handleQuery)
174
- // ============================================================================
175
- describe('Query Handling', () => {
61
+ describe('handleQuery', () => {
62
+ let watcher;
176
63
  beforeEach(async () => {
177
- watcher = await createWatcher(mockConfig);
64
+ watcher = await buildWatcher({
65
+ watchers: { query: { enabled: true, slowThreshold: 100 } },
66
+ });
178
67
  });
179
- it('should collect query with all fields', async () => {
180
- // Arrange
181
- const queryData = {
68
+ it('forwards a fully-populated payload to the collector', () => {
69
+ watcher.handleQuery({
182
70
  query: 'SELECT * FROM users WHERE id = ?',
183
71
  parameters: [1],
184
72
  duration: 50,
185
73
  source: 'typeorm',
186
74
  connection: 'default',
187
75
  requestId: 'req-123',
188
- };
189
- // Act
190
- watcher.handleQuery(queryData);
191
- // Assert
192
- expect(mockCollector.collect).toHaveBeenCalledWith('query', {
76
+ });
77
+ expect(collector.collect).toHaveBeenCalledWith('query', {
193
78
  query: 'SELECT * FROM users WHERE id = ?',
194
79
  parameters: [1],
195
80
  duration: 50,
@@ -198,403 +83,169 @@ describe('QueryWatcher', () => {
198
83
  connection: 'default',
199
84
  }, 'req-123');
200
85
  });
201
- it('should mark queries as slow when exceeding threshold', async () => {
202
- // Arrange
203
- const queryData = {
204
- query: 'SELECT * FROM large_table',
205
- duration: 150, // Above 100ms threshold
206
- source: 'typeorm',
207
- };
208
- // Act
209
- watcher.handleQuery(queryData);
210
- // Assert
211
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
212
- slow: true,
213
- }), undefined);
214
- });
215
- it('should not mark queries as slow when below threshold', async () => {
216
- // Arrange
217
- const queryData = {
218
- query: 'SELECT * FROM users',
219
- duration: 50, // Below 100ms threshold
86
+ it('marks queries above slowThreshold as slow', () => {
87
+ watcher.handleQuery({
88
+ query: 'SELECT * FROM big',
89
+ duration: 250,
220
90
  source: 'typeorm',
221
- };
222
- // Act
223
- watcher.handleQuery(queryData);
224
- // Assert
225
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
226
- slow: false,
227
- }), undefined);
228
- });
229
- it('should use custom slowThreshold', async () => {
230
- // Arrange
231
- mockConfig.watchers = { query: { enabled: true, slowThreshold: 200 } };
232
- watcher = await createWatcher(mockConfig);
233
- const queryData = {
234
- query: 'SELECT * FROM users',
235
- duration: 150, // Below 200ms custom threshold
236
- source: 'prisma',
237
- };
238
- // Act
239
- watcher.handleQuery(queryData);
240
- // Assert
241
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
242
- slow: false,
243
- }), undefined);
91
+ });
92
+ expect(collector.collect).toHaveBeenCalledWith('query', expect.objectContaining({ slow: true }), undefined);
244
93
  });
245
- it('should handle queries without parameters', async () => {
246
- // Arrange
247
- const queryData = {
248
- query: 'SELECT COUNT(*) FROM users',
249
- duration: 10,
94
+ it('marks queries at or below slowThreshold as not slow', () => {
95
+ watcher.handleQuery({
96
+ query: 'SELECT 1',
97
+ duration: 100,
250
98
  source: 'typeorm',
251
- };
252
- // Act
253
- watcher.handleQuery(queryData);
254
- // Assert
255
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
256
- parameters: undefined,
257
- }), undefined);
99
+ });
100
+ expect(collector.collect).toHaveBeenCalledWith('query', expect.objectContaining({ slow: false }), undefined);
258
101
  });
259
- it('should handle queries without connection name', async () => {
260
- // Arrange
261
- const queryData = {
262
- query: 'SELECT * FROM posts',
263
- duration: 20,
102
+ it('respects custom slowThreshold', async () => {
103
+ const w = await buildWatcher({
104
+ watchers: { query: { enabled: true, slowThreshold: 200 } },
105
+ });
106
+ w.handleQuery({
107
+ query: 'SELECT * FROM x',
108
+ duration: 150,
264
109
  source: 'prisma',
265
- };
266
- // Act
267
- watcher.handleQuery(queryData);
268
- // Assert
269
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
270
- connection: undefined,
271
- }), undefined);
110
+ });
111
+ expect(collector.collect).toHaveBeenCalledWith('query', expect.objectContaining({ slow: false }), undefined);
272
112
  });
273
- });
274
- // ============================================================================
275
- // Ignore Patterns
276
- // ============================================================================
277
- describe('Ignore Patterns', () => {
278
- it('should skip queries matching ignore patterns', async () => {
279
- // Arrange
280
- mockConfig.watchers = {
281
- query: {
282
- enabled: true,
283
- ignorePatterns: [/^PRAGMA/, /^SELECT 1/],
113
+ it('skips queries matching any ignore pattern', async () => {
114
+ const w = await buildWatcher({
115
+ watchers: {
116
+ query: { enabled: true, ignorePatterns: [/^PRAGMA/, /information_schema/] },
284
117
  },
285
- };
286
- watcher = await createWatcher(mockConfig);
287
- // Act
288
- watcher.handleQuery({
118
+ });
119
+ w.handleQuery({
289
120
  query: 'PRAGMA table_info(users)',
290
- duration: 5,
121
+ duration: 1,
291
122
  source: 'typeorm',
292
123
  });
293
- // Assert
294
- expect(mockCollector.collect).not.toHaveBeenCalled();
295
- });
296
- it('should skip queries matching any ignore pattern', async () => {
297
- // Arrange
298
- mockConfig.watchers = {
299
- query: {
300
- enabled: true,
301
- ignorePatterns: [/^PRAGMA/, /^SELECT 1/, /health_check/],
302
- },
303
- };
304
- watcher = await createWatcher(mockConfig);
305
- // Act
306
- watcher.handleQuery({
307
- query: 'SELECT 1 AS result',
308
- duration: 1,
124
+ w.handleQuery({
125
+ query: 'SELECT * FROM information_schema.tables',
126
+ duration: 5,
309
127
  source: 'typeorm',
310
128
  });
311
- // Assert
312
- expect(mockCollector.collect).not.toHaveBeenCalled();
129
+ expect(collector.collect).not.toHaveBeenCalled();
313
130
  });
314
- it('should collect queries not matching ignore patterns', async () => {
315
- // Arrange
316
- mockConfig.watchers = {
317
- query: {
318
- enabled: true,
319
- ignorePatterns: [/^PRAGMA/],
320
- },
321
- };
322
- watcher = await createWatcher(mockConfig);
323
- // Act
131
+ it('normalises whitespace and trims the query', () => {
324
132
  watcher.handleQuery({
325
- query: 'SELECT * FROM users',
133
+ query: ' SELECT *\n FROM users WHERE id = 1 ',
326
134
  duration: 10,
327
135
  source: 'typeorm',
328
136
  });
329
- // Assert
330
- expect(mockCollector.collect).toHaveBeenCalled();
331
- });
332
- });
333
- // ============================================================================
334
- // Query Formatting
335
- // ============================================================================
336
- describe('Query Formatting', () => {
337
- beforeEach(async () => {
338
- watcher = await createWatcher(mockConfig);
339
- });
340
- it('should normalize whitespace in queries', async () => {
341
- // Arrange
342
- const queryData = {
343
- query: 'SELECT * FROM users WHERE id = 1',
344
- duration: 10,
345
- source: 'typeorm',
346
- };
347
- // Act
348
- const formatted = watcher.formatQuery(queryData.query);
349
- // Assert
350
- expect(formatted).toBe('SELECT * FROM users WHERE id = 1');
351
- });
352
- it('should trim leading and trailing whitespace', async () => {
353
- // Arrange
354
- const query = ' SELECT * FROM users ';
355
- // Act
356
- const formatted = watcher.formatQuery(query);
357
- // Assert
358
- expect(formatted).toBe('SELECT * FROM users');
359
- });
360
- it('should handle newlines in queries', async () => {
361
- // Arrange
362
- const query = `SELECT *
363
- FROM users
364
- WHERE id = 1`;
365
- // Act
366
- const formatted = watcher.formatQuery(query);
367
- // Assert
368
- expect(formatted).toBe('SELECT * FROM users WHERE id = 1');
137
+ expect(collector.collect).toHaveBeenCalledWith('query', expect.objectContaining({ query: 'SELECT * FROM users WHERE id = 1' }), undefined);
369
138
  });
370
139
  });
371
- // ============================================================================
372
- // TypeORM Integration
373
- // ============================================================================
374
- describe('TypeORM Integration', () => {
375
- it('should attach logger to initialized DataSource', async () => {
376
- // Arrange
377
- const mockDataSource = {
378
- isInitialized: true,
379
- options: { name: 'test-connection' },
380
- driver: {
381
- afterQuery: jest.fn(),
382
- },
383
- };
384
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === 'typeorm');
385
- mockedTypes.tryRequire.mockReturnValue({
386
- getDataSources: () => [mockDataSource],
387
- });
388
- mockedTypes.isTypeORMDataSource.mockReturnValue(true);
389
- watcher = await createWatcher(mockConfig);
390
- // Act
391
- await watcher.onModuleInit();
392
- // Assert
393
- expect(mockDataSource.driver.afterQuery).not.toBe(undefined);
394
- });
395
- it('should intercept afterQuery and collect queries', async () => {
396
- // Arrange
397
- let capturedAfterQuery;
398
- const originalAfterQuery = jest.fn();
399
- const mockDataSource = {
400
- isInitialized: true,
401
- options: { name: 'default' },
402
- driver: {
403
- get afterQuery() {
404
- return originalAfterQuery;
405
- },
406
- set afterQuery(fn) {
407
- capturedAfterQuery = fn;
408
- },
409
- },
410
- };
411
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === 'typeorm');
412
- mockedTypes.tryRequire.mockReturnValue({
413
- getDataSources: () => [mockDataSource],
140
+ describe('TypeORM DataSource discovery', () => {
141
+ it('discovers DataSource instances exposed via DiscoveryService and dedupes by reference', async () => {
142
+ const fakeDataSource = {
143
+ constructor: { name: 'DataSource' },
144
+ options: { name: 'primary', type: 'better-sqlite3' },
145
+ subscribers: [],
146
+ logger: undefined,
147
+ };
148
+ // Build a wrapper that resembles NestJS's InstanceWrapper minimally.
149
+ const wrappers = [{ instance: fakeDataSource }, { instance: fakeDataSource }];
150
+ const watcher = await buildWatcher({
151
+ watchers: { query: { enabled: true, slowThreshold: 100 } },
414
152
  });
415
- mockedTypes.isTypeORMDataSource.mockReturnValue(true);
416
- watcher = await createWatcher(mockConfig);
417
- await watcher.onModuleInit();
418
- // Act - simulate TypeORM calling afterQuery
419
- capturedAfterQuery?.call(mockDataSource.driver, 'SELECT * FROM users', [1, 2], { rows: [] }, 25);
420
- // Assert
421
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
422
- query: 'SELECT * FROM users',
423
- parameters: [1, 2],
424
- duration: 25,
425
- source: 'typeorm',
426
- connection: 'default',
427
- }), undefined);
428
- });
429
- it('should handle uninitialized DataSource', async () => {
430
- // Arrange
431
- let initializeHook;
432
- const mockDataSource = {
433
- isInitialized: false,
434
- options: { name: 'default' },
435
- driver: { afterQuery: null },
436
- initialize: jest.fn().mockImplementation(async () => {
437
- mockDataSource.isInitialized = true;
438
- return mockDataSource;
439
- }),
440
- };
441
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === 'typeorm');
442
- mockedTypes.tryRequire.mockReturnValue({
443
- getDataSources: () => [mockDataSource],
153
+ watcher.discoveryService = {
154
+ getProviders: () => wrappers,
155
+ };
156
+ const found = watcher.discoverTypeORMDataSources();
157
+ expect(found).toHaveLength(1);
158
+ expect(found[0]).toBe(fakeDataSource);
159
+ });
160
+ it('attaches subscriber + wraps logger and refuses to attach twice', async () => {
161
+ const fakeDataSource = {
162
+ constructor: { name: 'DataSource' },
163
+ options: { name: 'default', type: 'better-sqlite3' },
164
+ subscribers: [],
165
+ logger: undefined,
166
+ };
167
+ const watcher = await buildWatcher({
168
+ watchers: { query: { enabled: true, slowThreshold: 100 } },
444
169
  });
445
- mockedTypes.isTypeORMDataSource.mockReturnValue(true);
446
- watcher = await createWatcher(mockConfig);
447
- await watcher.onModuleInit();
448
- // Capture the wrapped initialize function
449
- initializeHook = mockDataSource.initialize;
450
- // Act - call the wrapped initialize
451
- await initializeHook?.();
452
- // Assert - original initialize was called
453
- expect(mockDataSource.isInitialized).toBe(true);
170
+ const first = watcher.attachToDataSource(fakeDataSource);
171
+ const second = watcher.attachToDataSource(fakeDataSource);
172
+ expect(first).toBe(true);
173
+ expect(second).toBe(false);
174
+ const subs = fakeDataSource.subscribers;
175
+ expect(subs).toHaveLength(1);
176
+ expect(subs[0]).toBeInstanceOf(typeorm_subscriber_1.NestLensQuerySubscriber);
177
+ expect(fakeDataSource.logger).toBeInstanceOf(typeorm_logger_1.NestLensTypeOrmLogger);
454
178
  });
455
179
  });
456
- // ============================================================================
457
- // Prisma Integration
458
- // ============================================================================
459
- describe('Prisma Integration', () => {
460
- it('should attach middleware to global Prisma client', async () => {
461
- // Arrange
462
- const mockPrismaClient = {
463
- $use: jest.fn(),
464
- };
465
- global.prisma = mockPrismaClient;
466
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === '@prisma/client');
467
- mockedTypes.isPrismaClient.mockReturnValue(true);
468
- watcher = await createWatcher(mockConfig);
469
- // Act
470
- await watcher.onModuleInit();
471
- // Assert
472
- expect(mockPrismaClient.$use).toHaveBeenCalled();
473
- // Cleanup
474
- delete global.prisma;
475
- });
476
- it('should collect Prisma queries through middleware', async () => {
477
- // Arrange
478
- let capturedMiddleware;
479
- const mockPrismaClient = {
480
- $use: jest.fn((middleware) => {
481
- capturedMiddleware = middleware;
482
- }),
483
- };
484
- global.prisma = mockPrismaClient;
485
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === '@prisma/client');
486
- mockedTypes.isPrismaClient.mockReturnValue(true);
487
- watcher = await createWatcher(mockConfig);
488
- await watcher.onModuleInit();
489
- const mockNext = jest.fn().mockResolvedValue({ id: 1, name: 'Test' });
490
- // Act - simulate Prisma middleware call
491
- const startTime = Date.now();
492
- await capturedMiddleware?.({ model: 'User', action: 'findMany', args: { where: { active: true } } }, mockNext);
493
- // Assert
494
- expect(mockNext).toHaveBeenCalled();
495
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
496
- query: 'User.findMany',
497
- parameters: [{ where: { active: true } }],
498
- source: 'prisma',
499
- }), undefined);
500
- // Cleanup
501
- delete global.prisma;
502
- });
503
- it('should skip Prisma if client has no $use method', async () => {
504
- // Arrange
505
- const mockPrismaClient = {};
506
- global.prisma = mockPrismaClient;
507
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === '@prisma/client');
508
- mockedTypes.isPrismaClient.mockReturnValue(true);
509
- watcher = await createWatcher(mockConfig);
510
- // Act
511
- await watcher.onModuleInit();
512
- // Assert - should not throw
513
- expect(mockCollector.collect).not.toHaveBeenCalled();
514
- // Cleanup
515
- delete global.prisma;
516
- });
517
- it('should handle missing model in Prisma params', async () => {
518
- // Arrange
519
- let capturedMiddleware;
520
- const mockPrismaClient = {
521
- $use: jest.fn((middleware) => {
522
- capturedMiddleware = middleware;
523
- }),
524
- };
525
- global.prisma = mockPrismaClient;
526
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === '@prisma/client');
527
- mockedTypes.isPrismaClient.mockReturnValue(true);
528
- watcher = await createWatcher(mockConfig);
529
- await watcher.onModuleInit();
530
- const mockNext = jest.fn().mockResolvedValue([]);
531
- // Act
532
- await capturedMiddleware?.({ model: undefined, action: '$queryRaw', args: undefined }, mockNext);
533
- // Assert
534
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
535
- query: 'unknown.$queryRaw',
536
- parameters: undefined,
537
- }), undefined);
538
- // Cleanup
539
- delete global.prisma;
540
- });
541
- });
542
- // ============================================================================
543
- // Error Handling
544
- // ============================================================================
545
- describe('Error Handling', () => {
546
- it('should gracefully handle TypeORM initialization errors', async () => {
547
- // Arrange
548
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === 'typeorm');
549
- mockedTypes.tryRequire.mockImplementation(() => {
550
- throw new Error('TypeORM not found');
180
+ describe('NestLensQuerySubscriber', () => {
181
+ it('translates an afterQuery event into a QueryData payload', () => {
182
+ const handler = jest.fn();
183
+ const sub = new typeorm_subscriber_1.NestLensQuerySubscriber(handler, 'main');
184
+ sub.afterQuery({
185
+ query: 'SELECT 1',
186
+ parameters: [],
187
+ executionTime: 12,
188
+ success: true,
551
189
  });
552
- watcher = await createWatcher(mockConfig);
553
- // Act & Assert - should not throw
554
- await expect(watcher.onModuleInit()).resolves.not.toThrow();
555
- });
556
- it('should gracefully handle Prisma initialization errors', async () => {
557
- // Arrange
558
- mockedTypes.isModuleAvailable.mockImplementation((mod) => mod === '@prisma/client');
559
- mockedTypes.isPrismaClient.mockImplementation(() => {
560
- throw new Error('Prisma error');
190
+ expect(handler).toHaveBeenCalledWith({
191
+ query: 'SELECT 1',
192
+ parameters: [],
193
+ duration: 12,
194
+ source: 'typeorm',
195
+ connection: 'main',
196
+ success: true,
197
+ error: undefined,
561
198
  });
562
- watcher = await createWatcher(mockConfig);
563
- // Act & Assert - should not throw
564
- await expect(watcher.onModuleInit()).resolves.not.toThrow();
565
199
  });
566
- });
567
- // ============================================================================
568
- // Source Types
569
- // ============================================================================
570
- describe('Source Types', () => {
571
- beforeEach(async () => {
572
- watcher = await createWatcher(mockConfig);
200
+ it('ignores events without a query string', () => {
201
+ const handler = jest.fn();
202
+ const sub = new typeorm_subscriber_1.NestLensQuerySubscriber(handler, 'main');
203
+ sub.afterQuery({});
204
+ expect(handler).not.toHaveBeenCalled();
573
205
  });
574
- it('should identify TypeORM queries', async () => {
575
- // Arrange & Act
576
- watcher.handleQuery({
577
- query: 'SELECT * FROM users',
578
- duration: 10,
206
+ });
207
+ describe('NestLensTypeOrmLogger', () => {
208
+ it('records logQueryError as a failed query and delegates to the wrapped logger', () => {
209
+ const handler = jest.fn();
210
+ const delegate = { logQueryError: jest.fn() };
211
+ const logger = new typeorm_logger_1.NestLensTypeOrmLogger(handler, 'default', delegate);
212
+ const error = new Error('boom');
213
+ logger.logQueryError(error, 'SELECT * FROM missing', [1]);
214
+ expect(handler).toHaveBeenCalledWith({
215
+ query: 'SELECT * FROM missing',
216
+ parameters: [1],
217
+ duration: 0,
579
218
  source: 'typeorm',
580
219
  connection: 'default',
220
+ success: false,
221
+ error,
581
222
  });
582
- // Assert
583
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
584
- source: 'typeorm',
585
- }), undefined);
586
- });
587
- it('should identify Prisma queries', async () => {
588
- // Arrange & Act
589
- watcher.handleQuery({
590
- query: 'User.findMany',
591
- duration: 15,
592
- source: 'prisma',
593
- });
594
- // Assert
595
- expect(mockCollector.collect).toHaveBeenCalledWith('query', expect.objectContaining({
596
- source: 'prisma',
597
- }), undefined);
223
+ expect(delegate.logQueryError).toHaveBeenCalledWith(error, 'SELECT * FROM missing', [1], undefined);
224
+ });
225
+ it('records logQuerySlow with the reported execution time', () => {
226
+ const handler = jest.fn();
227
+ const logger = new typeorm_logger_1.NestLensTypeOrmLogger(handler, 'default');
228
+ logger.logQuerySlow(450, 'SELECT * FROM big', []);
229
+ expect(handler).toHaveBeenCalledWith(expect.objectContaining({ duration: 450, success: true }));
230
+ });
231
+ it('forwards delegate-only methods without invoking the handler', () => {
232
+ const handler = jest.fn();
233
+ const delegate = {
234
+ logQuery: jest.fn(),
235
+ logSchemaBuild: jest.fn(),
236
+ logMigration: jest.fn(),
237
+ log: jest.fn(),
238
+ };
239
+ const logger = new typeorm_logger_1.NestLensTypeOrmLogger(handler, 'default', delegate);
240
+ logger.logQuery('SELECT 1', []);
241
+ logger.logSchemaBuild('build');
242
+ logger.logMigration('migrate');
243
+ logger.log('warn', 'hi');
244
+ expect(handler).not.toHaveBeenCalled();
245
+ expect(delegate.logQuery).toHaveBeenCalled();
246
+ expect(delegate.logSchemaBuild).toHaveBeenCalled();
247
+ expect(delegate.logMigration).toHaveBeenCalled();
248
+ expect(delegate.log).toHaveBeenCalled();
598
249
  });
599
250
  });
600
251
  });