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.
- package/dist/__tests__/watchers/query/typeorm-integration.spec.d.ts +2 -0
- package/dist/__tests__/watchers/query/typeorm-integration.spec.d.ts.map +1 -0
- package/dist/__tests__/watchers/query/typeorm-integration.spec.js +100 -0
- package/dist/__tests__/watchers/query/typeorm-integration.spec.js.map +1 -0
- package/dist/__tests__/watchers/query/types.spec.js +48 -263
- package/dist/__tests__/watchers/query/types.spec.js.map +1 -1
- package/dist/__tests__/watchers/query.watcher.spec.js +173 -522
- package/dist/__tests__/watchers/query.watcher.spec.js.map +1 -1
- package/dist/dashboard/public/assets/{BatchesPage-DmqbLFCs.js → BatchesPage-DrFl64wj.js} +1 -1
- package/dist/dashboard/public/assets/{CachePage-CfA-UrdM.js → CachePage-C57Pkxm0.js} +1 -1
- package/dist/dashboard/public/assets/{ClickableBadge-CW4jdcpl.js → ClickableBadge-BNUq-FXI.js} +1 -1
- package/dist/dashboard/public/assets/{CommandsPage-DJ1ZxHj8.js → CommandsPage-BdD-ojDk.js} +1 -1
- package/dist/dashboard/public/assets/{DashboardPage-BnnZ3TMF.js → DashboardPage-DCophLiw.js} +1 -1
- package/dist/dashboard/public/assets/{DumpsPage-fshG-ETf.js → DumpsPage-DtXeYPpQ.js} +1 -1
- package/dist/dashboard/public/assets/{EntryDetailPage-Cf7K34er.js → EntryDetailPage-qlWzdnXs.js} +1 -1
- package/dist/dashboard/public/assets/{EventsPage-DiPB0Sfy.js → EventsPage-BHpLjHZj.js} +1 -1
- package/dist/dashboard/public/assets/{ExceptionsPage-BoauuDFu.js → ExceptionsPage-Ww3jIO29.js} +1 -1
- package/dist/dashboard/public/assets/{GatesPage-CB0kqyp_.js → GatesPage-beRehq6b.js} +1 -1
- package/dist/dashboard/public/assets/{GraphQLPage-_inMfKh4.js → GraphQLPage-BZt0nDmS.js} +1 -1
- package/dist/dashboard/public/assets/{HttpClientPage-5vVzz7TU.js → HttpClientPage-Dac8U41f.js} +1 -1
- package/dist/dashboard/public/assets/{JobsPage-CHYjmhlh.js → JobsPage-zLQH5Skv.js} +1 -1
- package/dist/dashboard/public/assets/{LogsPage-z4yYJ1qQ.js → LogsPage-Dj7YEZlz.js} +1 -1
- package/dist/dashboard/public/assets/{MailPage-DQ2PuRS9.js → MailPage-C45lv85q.js} +1 -1
- package/dist/dashboard/public/assets/{ModelsPage-PBBI9K5M.js → ModelsPage-CveFVTw3.js} +1 -1
- package/dist/dashboard/public/assets/{NotificationsPage-CjleWNhr.js → NotificationsPage-CVAGh1SG.js} +1 -1
- package/dist/dashboard/public/assets/{PageHeader-QBzFo308.js → PageHeader-BhUyZtbe.js} +1 -1
- package/dist/dashboard/public/assets/{QueriesPage-Cpi1b2Mz.js → QueriesPage-BMpvUeTX.js} +1 -1
- package/dist/dashboard/public/assets/{RedisPage-C7vFV3r4.js → RedisPage-FPrKgNrc.js} +1 -1
- package/dist/dashboard/public/assets/{RequestsPage-B9-Qq2vH.js → RequestsPage-6JVqr7xE.js} +1 -1
- package/dist/dashboard/public/assets/{SchedulePage-BXPldddV.js → SchedulePage-DOt6jqTT.js} +1 -1
- package/dist/dashboard/public/assets/{ViewsPage-COo7BNBc.js → ViewsPage-RG6Ze2rn.js} +1 -1
- package/dist/dashboard/public/assets/{calendar-WMtbNas7.js → calendar-CEjtgImi.js} +1 -1
- package/dist/dashboard/public/assets/{circle-check-big-U-4nHjTQ.js → circle-check-big-BnRBDUDo.js} +1 -1
- package/dist/dashboard/public/assets/{eye-Bc29awES.js → eye-tfj-u8EL.js} +1 -1
- package/dist/dashboard/public/assets/{index-C8he4ZUV.js → index-OMQZjJAo.js} +2 -2
- package/dist/dashboard/public/assets/{types-BYamMo0R.js → types-Cubd9hBF.js} +1 -1
- package/dist/dashboard/public/assets/{zap-BxD9CCmf.js → zap-Bh7DgMLC.js} +1 -1
- package/dist/dashboard/public/index.html +1 -1
- package/dist/nestlens.module.d.ts.map +1 -1
- package/dist/nestlens.module.js +1 -0
- package/dist/nestlens.module.js.map +1 -1
- package/dist/watchers/query/index.d.ts +2 -0
- package/dist/watchers/query/index.d.ts.map +1 -1
- package/dist/watchers/query/index.js +2 -0
- package/dist/watchers/query/index.js.map +1 -1
- package/dist/watchers/query/query.watcher.d.ts +12 -8
- package/dist/watchers/query/query.watcher.d.ts.map +1 -1
- package/dist/watchers/query/query.watcher.js +80 -78
- package/dist/watchers/query/query.watcher.js.map +1 -1
- package/dist/watchers/query/typeorm-logger.d.ts +21 -0
- package/dist/watchers/query/typeorm-logger.d.ts.map +1 -0
- package/dist/watchers/query/typeorm-logger.js +53 -0
- package/dist/watchers/query/typeorm-logger.js.map +1 -0
- package/dist/watchers/query/typeorm-subscriber.d.ts +20 -0
- package/dist/watchers/query/typeorm-subscriber.d.ts.map +1 -0
- package/dist/watchers/query/typeorm-subscriber.js +34 -0
- package/dist/watchers/query/typeorm-subscriber.js.map +1 -0
- package/dist/watchers/query/types.d.ts +50 -29
- package/dist/watchers/query/types.d.ts.map +1 -1
- package/dist/watchers/query/types.js +21 -16
- package/dist/watchers/query/types.js.map +1 -1
- 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
|
|
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
|
-
*
|
|
40
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
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
|
|
31
|
+
return moduleRef.get(query_watcher_1.QueryWatcher);
|
|
68
32
|
};
|
|
69
33
|
beforeEach(() => {
|
|
70
|
-
|
|
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
|
-
|
|
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('
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
watcher
|
|
119
|
-
|
|
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('
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
64
|
+
watcher = await buildWatcher({
|
|
65
|
+
watchers: { query: { enabled: true, slowThreshold: 100 } },
|
|
66
|
+
});
|
|
178
67
|
});
|
|
179
|
-
it('
|
|
180
|
-
|
|
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
|
-
|
|
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('
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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('
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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('
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
// Act
|
|
288
|
-
watcher.handleQuery({
|
|
118
|
+
});
|
|
119
|
+
w.handleQuery({
|
|
289
120
|
query: 'PRAGMA table_info(users)',
|
|
290
|
-
duration:
|
|
121
|
+
duration: 1,
|
|
291
122
|
source: 'typeorm',
|
|
292
123
|
});
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
312
|
-
expect(mockCollector.collect).not.toHaveBeenCalled();
|
|
129
|
+
expect(collector.collect).not.toHaveBeenCalled();
|
|
313
130
|
});
|
|
314
|
-
it('
|
|
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
|
|
133
|
+
query: ' SELECT *\n FROM users WHERE id = 1 ',
|
|
326
134
|
duration: 10,
|
|
327
135
|
source: 'typeorm',
|
|
328
136
|
});
|
|
329
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
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
|
});
|