nestjs-profiler 1.0.25 → 1.0.26

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 (36) hide show
  1. package/README.md +120 -6
  2. package/collectors/event-collector.d.ts +18 -0
  3. package/collectors/event-collector.js +254 -0
  4. package/collectors/event-collector.js.map +1 -0
  5. package/common/profiler-options.interface.d.ts +8 -0
  6. package/common/profiler.model.d.ts +25 -0
  7. package/controllers/profiler.controller.d.ts +13 -1
  8. package/controllers/profiler.controller.js +103 -17
  9. package/controllers/profiler.controller.js.map +1 -1
  10. package/middleware/profiler-auth.middleware.d.ts +10 -0
  11. package/middleware/profiler-auth.middleware.js +110 -0
  12. package/middleware/profiler-auth.middleware.js.map +1 -0
  13. package/package.json +2 -2
  14. package/profiler.module.js +78 -0
  15. package/profiler.module.js.map +1 -1
  16. package/services/entity-explorer.service.d.ts +1 -1
  17. package/services/entity-explorer.service.js +7 -7
  18. package/services/entity-explorer.service.js.map +1 -1
  19. package/services/health.service.js +23 -8
  20. package/services/health.service.js.map +1 -1
  21. package/services/profiler.service.d.ts +10 -1
  22. package/services/profiler.service.js +103 -35
  23. package/services/profiler.service.js.map +1 -1
  24. package/services/route-explorer.service.d.ts +23 -1
  25. package/services/route-explorer.service.js +136 -10
  26. package/services/route-explorer.service.js.map +1 -1
  27. package/services/template-builder.service.d.ts +1 -0
  28. package/services/template-builder.service.js +184 -83
  29. package/services/template-builder.service.js.map +1 -1
  30. package/services/view.service.d.ts +1 -1
  31. package/services/view.service.js +30 -23
  32. package/services/view.service.js.map +1 -1
  33. package/views/events.html +763 -0
  34. package/views/layout.html +31 -9
  35. package/views/login.html +277 -0
  36. package/views/routes.html +405 -37
package/views/routes.html CHANGED
@@ -1,41 +1,409 @@
1
- <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
2
- <div class="p-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
3
- <div class="flex items-center space-x-4">
4
- <h2 class="font-semibold text-gray-700">Routes Explorer</h2>
5
- <div class="relative">
6
- <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
7
- <svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
9
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
10
- </svg>
1
+ <script>
2
+ window.__routes = {{{ routesJson }}};
3
+ </script>
4
+
5
+ <div id="routes-root">
6
+ <!-- Header bar -->
7
+ <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
8
+ <div class="p-4 border-b border-gray-200 bg-gray-50 flex flex-wrap gap-3 items-center justify-between">
9
+ <div class="flex items-center gap-4">
10
+ <h2 class="font-semibold text-gray-700">Routes Explorer</h2>
11
+ <div class="relative">
12
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
13
+ <svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
15
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
16
+ </svg>
17
+ </div>
18
+ <input id="rt-search"
19
+ class="block w-64 pl-9 pr-3 py-1.5 border border-gray-300 rounded-md text-sm bg-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
20
+ placeholder="Search routes…" type="search" autocomplete="off">
11
21
  </div>
12
- <input id="search" name="search"
13
- class="block w-64 pl-9 pr-3 py-1.5 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
14
- placeholder="Search routes..." type="search">
22
+ </div>
23
+ <div class="flex items-center gap-2 text-xs text-gray-500">
24
+ <span id="rt-count" class="bg-white border border-gray-200 px-2 py-1 rounded-md">{{ totalRoutes }} routes</span>
25
+ <span id="rt-ctrl-count" class="bg-white border border-gray-200 px-2 py-1 rounded-md"></span>
15
26
  </div>
16
27
  </div>
17
- <span class="text-xs text-gray-500 bg-white border border-gray-200 px-2 py-1 rounded-md">
18
- {{ totalRoutes }} Routes Found
19
- </span>
20
- </div>
21
- <div class="overflow-x-auto">
22
- <table class="min-w-full divide-y divide-gray-200">
23
- <thead class="bg-gray-50">
24
- <tr>
25
- <th scope="col"
26
- class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Method
27
- </th>
28
- <th scope="col"
29
- class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Path</th>
30
- <th scope="col"
31
- class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
32
- Controller</th>
33
- </tr>
34
- </thead>
35
- <tbody class="bg-white divide-y divide-gray-200">
36
- {{{ rows }}}
37
- </tbody>
38
- </table>
28
+
29
+ <!-- Method filter pills -->
30
+ <div class="px-4 py-2 border-b border-gray-100 bg-gray-50 flex flex-wrap gap-2" id="rt-method-filters"></div>
31
+
32
+ <!-- Accordion list -->
33
+ <div id="rt-list" class="divide-y divide-gray-100"></div>
34
+
35
+ <!-- Empty state -->
36
+ <div id="rt-empty" class="hidden p-10 text-center text-gray-400 text-sm italic">No routes match your search.</div>
39
37
  </div>
40
- {{{ emptyState }}}
41
- </div>
38
+ </div>
39
+
40
+ <style>
41
+ .rt-card-body { display: none; }
42
+ .rt-card-body.open { display: block; }
43
+ .rt-chevron { transition: transform 0.18s ease; }
44
+ .rt-chevron.open { transform: rotate(90deg); }
45
+ .rt-pill { display: inline-flex; align-items: center; font-size: 0.7rem; font-weight: 600;
46
+ padding: 0 6px; height: 20px; border-radius: 9999px; text-transform: uppercase; letter-spacing: .03em; }
47
+ .rt-type-pill { display: inline-block; font-size: 0.68rem; padding: 1px 7px; border-radius: 4px;
48
+ font-weight: 500; background: #f3f4f6; color: #374151; font-family: monospace; }
49
+ .rt-method-btn { font-size: 0.7rem; font-weight: 600; padding: 2px 10px; border-radius: 9999px;
50
+ border: 1px solid transparent; cursor: pointer; transition: opacity .15s; }
51
+ .rt-method-btn.active { opacity: 1; }
52
+ .rt-method-btn:not(.active) { opacity: 0.45; }
53
+ .rt-param-row { display: flex; align-items: baseline; gap: 8px; padding: 4px 0;
54
+ border-bottom: 1px solid #f3f4f6; }
55
+ .rt-param-row:last-child { border-bottom: none; }
56
+ .rt-param-name { font-family: monospace; font-size: 0.8rem; color: #1e293b; font-weight: 600; min-width: 120px; }
57
+ .rt-param-type { font-family: monospace; font-size: 0.75rem; color: #6366f1; }
58
+ .rt-section-label { font-size: 0.65rem; font-weight: 700; text-transform: uppercase;
59
+ letter-spacing: .06em; color: #9ca3af; margin-bottom: 6px; }
60
+ .rt-required-badge { font-size: 0.6rem; padding: 1px 5px; border-radius: 3px;
61
+ background: #fee2e2; color: #dc2626; font-weight: 600; }
62
+ .rt-optional-badge { font-size: 0.6rem; padding: 1px 5px; border-radius: 3px;
63
+ background: #f3f4f6; color: #6b7280; font-weight: 600; }
64
+ .rt-param-block { border-bottom: 1px solid #f3f4f6; }
65
+ .rt-param-block:last-child { border-bottom: none; }
66
+ .rt-param-block .rt-param-row { border-bottom: none; }
67
+ .rt-props-block { margin: 2px 0 6px 16px; padding: 6px 10px;
68
+ background: #f8fafc; border-left: 2px solid #e0e7ff;
69
+ border-radius: 0 4px 4px 0; }
70
+ .rt-props-label { font-size: 0.6rem; font-weight: 700; text-transform: uppercase;
71
+ letter-spacing: .06em; color: #a5b4fc; margin-bottom: 4px; }
72
+ .rt-prop-row { display: flex; align-items: baseline; gap: 8px; padding: 2px 0; }
73
+ .rt-prop-name { font-family: monospace; font-size: 0.75rem; color: #374151; font-weight: 500; min-width: 110px; }
74
+ .rt-copy-btn { display: inline-flex; align-items: center; justify-content: center;
75
+ width: 22px; height: 22px; border-radius: 4px; border: 1px solid #e5e7eb;
76
+ background: #fff; color: #9ca3af; cursor: pointer; flex-shrink: 0;
77
+ transition: color .15s, border-color .15s; padding: 0; }
78
+ .rt-copy-btn:hover { color: #6366f1; border-color: #a5b4fc; }
79
+ </style>
80
+
81
+ <script>
82
+ (function () {
83
+ const METHOD_COLORS = {
84
+ GET: { bg: '#dcfce7', text: '#166534' },
85
+ POST: { bg: '#dbeafe', text: '#1e40af' },
86
+ PUT: { bg: '#fef3c7', text: '#92400e' },
87
+ PATCH: { bg: '#ffedd5', text: '#9a3412' },
88
+ DELETE: { bg: '#fee2e2', text: '#991b1b' },
89
+ OPTIONS: { bg: '#f3e8ff', text: '#6b21a8' },
90
+ HEAD: { bg: '#e0f2fe', text: '#0c4a6e' },
91
+ ALL: { bg: '#f1f5f9', text: '#475569' },
92
+ };
93
+
94
+ function escHtml(s) {
95
+ return String(s)
96
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;')
97
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
98
+ }
99
+
100
+ function methodBadge(m) {
101
+ const c = METHOD_COLORS[m] || METHOD_COLORS['ALL'];
102
+ return `<span class="rt-pill" style="background:${c.bg};color:${c.text}">${escHtml(m)}</span>`;
103
+ }
104
+
105
+ function makeCopyBtn(text) {
106
+ if (!text) return '';
107
+ const btn = document.createElement('button');
108
+ btn.className = 'rt-copy-btn';
109
+ btn.title = text;
110
+ btn.innerHTML = `<svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
111
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
112
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
113
+ </svg>`;
114
+ btn.addEventListener('click', (e) => {
115
+ e.stopPropagation();
116
+ navigator.clipboard.writeText(text).then(() => {
117
+ btn.innerHTML = `<svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="#22c55e" stroke-width="2.5">
118
+ <polyline points="20 6 9 17 4 12"/>
119
+ </svg>`;
120
+ setTimeout(() => {
121
+ btn.innerHTML = `<svg width="13" height="13" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
122
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
123
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
124
+ </svg>`;
125
+ }, 1500);
126
+ });
127
+ });
128
+ return btn;
129
+ }
130
+
131
+ function renderParamRow(p) {
132
+ const wrap = document.createElement('div');
133
+ wrap.className = 'rt-param-block';
134
+
135
+ // Main row
136
+ const row = document.createElement('div');
137
+ row.className = 'rt-param-row';
138
+
139
+ const nameEl = document.createElement('span');
140
+ nameEl.className = 'rt-param-name';
141
+ nameEl.textContent = p.name;
142
+ row.appendChild(nameEl);
143
+
144
+ // Type + optional copy btn
145
+ const typeWrap = document.createElement('span');
146
+ typeWrap.style.display = 'inline-flex';
147
+ typeWrap.style.alignItems = 'center';
148
+ typeWrap.style.gap = '4px';
149
+ const typeEl = document.createElement('span');
150
+ typeEl.className = 'rt-param-type';
151
+ typeEl.textContent = p.dataType;
152
+ typeWrap.appendChild(typeEl);
153
+ if (p.file) typeWrap.appendChild(makeCopyBtn(p.file));
154
+ row.appendChild(typeWrap);
155
+
156
+ const inBadge = document.createElement('span');
157
+ inBadge.className = 'rt-type-pill';
158
+ inBadge.style.background = '#ede9fe';
159
+ inBadge.style.color = '#5b21b6';
160
+ inBadge.textContent = p.paramType;
161
+ row.appendChild(inBadge);
162
+
163
+ const reqBadge = document.createElement('span');
164
+ reqBadge.className = p.required ? 'rt-required-badge' : 'rt-optional-badge';
165
+ reqBadge.textContent = p.required ? 'required' : 'optional';
166
+ row.appendChild(reqBadge);
167
+
168
+ wrap.appendChild(row);
169
+
170
+ // Properties sub-block
171
+ if (p.properties && p.properties.length > 0) {
172
+ const block = document.createElement('div');
173
+ block.className = 'rt-props-block';
174
+ const lbl = document.createElement('div');
175
+ lbl.className = 'rt-props-label';
176
+ lbl.textContent = 'properties';
177
+ block.appendChild(lbl);
178
+ p.properties.forEach(prop => {
179
+ const pr = document.createElement('div');
180
+ pr.className = 'rt-prop-row';
181
+ const pn = document.createElement('span');
182
+ pn.className = 'rt-prop-name';
183
+ pn.textContent = prop.name;
184
+ const pt = document.createElement('span');
185
+ pt.className = 'rt-param-type';
186
+ pt.textContent = prop.type;
187
+ pr.appendChild(pn);
188
+ pr.appendChild(pt);
189
+ block.appendChild(pr);
190
+ });
191
+ wrap.appendChild(block);
192
+ } else if (p.properties) {
193
+ const block = document.createElement('div');
194
+ block.className = 'rt-props-block';
195
+ const hint = document.createElement('p');
196
+ hint.className = 'text-xs text-gray-400 italic';
197
+ hint.textContent = 'No property metadata found. Add class-validator or @ApiProperty() decorators.';
198
+ block.appendChild(hint);
199
+ wrap.appendChild(block);
200
+ }
201
+
202
+ return wrap;
203
+ }
204
+
205
+ function renderBody(body) {
206
+ if (!body) return null;
207
+
208
+ const wrap = document.createElement('div');
209
+ wrap.className = 'mb-4';
210
+
211
+ const lbl = document.createElement('div');
212
+ lbl.className = 'rt-section-label';
213
+ lbl.textContent = 'Request Body';
214
+ wrap.appendChild(lbl);
215
+
216
+ // Type name row + copy btn
217
+ const typeRow = document.createElement('div');
218
+ typeRow.style.display = 'inline-flex';
219
+ typeRow.style.alignItems = 'center';
220
+ typeRow.style.gap = '6px';
221
+ typeRow.style.marginBottom = '8px';
222
+
223
+ const nameEl = document.createElement('span');
224
+ nameEl.className = 'text-sm font-semibold text-gray-800';
225
+ nameEl.textContent = body.typeName;
226
+ typeRow.appendChild(nameEl);
227
+
228
+ const dtoBadge = document.createElement('span');
229
+ dtoBadge.className = 'rt-type-pill';
230
+ dtoBadge.textContent = 'DTO';
231
+ typeRow.appendChild(dtoBadge);
232
+
233
+ if (body.file) typeRow.appendChild(makeCopyBtn(body.file));
234
+
235
+ wrap.appendChild(typeRow);
236
+
237
+ if (body.properties && body.properties.length > 0) {
238
+ body.properties.forEach(p => {
239
+ const pr = document.createElement('div');
240
+ pr.className = 'rt-param-row';
241
+ const pn = document.createElement('span');
242
+ pn.className = 'rt-param-name';
243
+ pn.textContent = p.name;
244
+ const pt = document.createElement('span');
245
+ pt.className = 'rt-param-type';
246
+ pt.textContent = p.type;
247
+ pr.appendChild(pn);
248
+ pr.appendChild(pt);
249
+ wrap.appendChild(pr);
250
+ });
251
+ } else {
252
+ const hint = document.createElement('p');
253
+ hint.className = 'text-xs text-gray-400 italic mt-1';
254
+ hint.textContent = 'No property metadata found. Add class-validator or @ApiProperty() decorators to inspect properties.';
255
+ wrap.appendChild(hint);
256
+ }
257
+
258
+ return wrap;
259
+ }
260
+
261
+ function buildCard(route) {
262
+ const pathParams = route.params.filter(p => p.paramType === 'path');
263
+ const queryParams = route.params.filter(p => p.paramType === 'query');
264
+ const headerParams = route.params.filter(p => p.paramType === 'header');
265
+ const hasDetails = pathParams.length || queryParams.length || headerParams.length || route.body;
266
+
267
+ const div = document.createElement('div');
268
+ div.className = 'rt-card';
269
+ div.dataset.method = route.method;
270
+ div.dataset.path = route.path.toLowerCase();
271
+ div.dataset.ctrl = (route.controller + '.' + route.handler).toLowerCase();
272
+
273
+ // ── Header row ────────────────────────────────────────────────────────
274
+ const header = document.createElement('div');
275
+ header.className = 'rt-card-header flex items-center gap-3 px-4 py-3 select-none' +
276
+ (hasDetails ? ' cursor-pointer hover:bg-gray-50' : '');
277
+
278
+ const badgeWrap = document.createElement('div');
279
+ badgeWrap.className = 'flex-shrink-0 w-16 text-right';
280
+ badgeWrap.innerHTML = methodBadge(route.method);
281
+ header.appendChild(badgeWrap);
282
+
283
+ const pathEl = document.createElement('code');
284
+ pathEl.className = 'flex-1 text-sm text-gray-800 font-mono truncate';
285
+ pathEl.title = route.path;
286
+ pathEl.textContent = route.path;
287
+ header.appendChild(pathEl);
288
+
289
+ const ctrlEl = document.createElement('span');
290
+ ctrlEl.className = 'text-xs text-gray-400 hidden md:block flex-shrink-0';
291
+ ctrlEl.innerHTML = `${escHtml(route.controller)}.<span class="text-gray-600 font-medium">${escHtml(route.handler)}()</span>`;
292
+ header.appendChild(ctrlEl);
293
+
294
+ const chevSvg = document.createElement('span');
295
+ chevSvg.innerHTML = hasDetails
296
+ ? `<svg class="rt-chevron w-4 h-4 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>`
297
+ : `<svg class="w-4 h-4 flex-shrink-0 opacity-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>`;
298
+ header.appendChild(chevSvg);
299
+ div.appendChild(header);
300
+
301
+ // ── Detail body ───────────────────────────────────────────────────────
302
+ if (hasDetails) {
303
+ const body = document.createElement('div');
304
+ body.className = 'rt-card-body px-5 py-4 bg-gray-50 border-t border-gray-100';
305
+
306
+ function addSection(label, paramList) {
307
+ if (!paramList.length) return;
308
+ const sec = document.createElement('div');
309
+ sec.className = 'mb-4';
310
+ const lbl = document.createElement('div');
311
+ lbl.className = 'rt-section-label';
312
+ lbl.textContent = label;
313
+ sec.appendChild(lbl);
314
+ paramList.forEach(p => sec.appendChild(renderParamRow(p)));
315
+ body.appendChild(sec);
316
+ }
317
+
318
+ addSection('Path Parameters', pathParams);
319
+ addSection('Query Parameters', queryParams);
320
+ addSection('Header Parameters', headerParams);
321
+
322
+ if (route.body) {
323
+ const bodySection = renderBody(route.body);
324
+ if (bodySection) body.appendChild(bodySection);
325
+ }
326
+
327
+ div.appendChild(body);
328
+
329
+ header.addEventListener('click', () => {
330
+ const open = body.classList.toggle('open');
331
+ const chev = div.querySelector('.rt-chevron');
332
+ if (chev) chev.classList.toggle('open', open);
333
+ });
334
+ }
335
+
336
+ return div;
337
+ }
338
+
339
+ function render(routes) {
340
+ const list = document.getElementById('rt-list');
341
+ list.innerHTML = '';
342
+ routes.forEach(r => list.appendChild(buildCard(r)));
343
+ document.getElementById('rt-empty').classList.toggle('hidden', routes.length > 0);
344
+ }
345
+
346
+ function getUniqueControllers(routes) {
347
+ return new Set(routes.map(r => r.controller)).size;
348
+ }
349
+
350
+ function init() {
351
+ const allRoutes = window.__routes || [];
352
+
353
+ // Stats
354
+ document.getElementById('rt-ctrl-count').textContent =
355
+ getUniqueControllers(allRoutes) + ' controllers';
356
+
357
+ // Method filter pills
358
+ const methods = [...new Set(allRoutes.map(r => r.method))].sort();
359
+ const activeFilters = new Set(methods);
360
+ const filtersEl = document.getElementById('rt-method-filters');
361
+
362
+ methods.forEach(m => {
363
+ const c = METHOD_COLORS[m] || METHOD_COLORS['ALL'];
364
+ const btn = document.createElement('button');
365
+ btn.className = 'rt-method-btn active';
366
+ btn.style.background = c.bg;
367
+ btn.style.color = c.text;
368
+ btn.style.borderColor = c.text + '33';
369
+ btn.textContent = m;
370
+ btn.addEventListener('click', () => {
371
+ if (activeFilters.has(m)) {
372
+ activeFilters.delete(m);
373
+ btn.classList.remove('active');
374
+ } else {
375
+ activeFilters.add(m);
376
+ btn.classList.add('active');
377
+ }
378
+ applyFilters();
379
+ });
380
+ filtersEl.appendChild(btn);
381
+ });
382
+
383
+ let searchQuery = '';
384
+ document.getElementById('rt-search').addEventListener('input', e => {
385
+ searchQuery = e.target.value.trim().toLowerCase();
386
+ applyFilters();
387
+ });
388
+
389
+ function applyFilters() {
390
+ const visible = allRoutes.filter(r => {
391
+ if (!activeFilters.has(r.method)) return false;
392
+ if (!searchQuery) return true;
393
+ return r.path.toLowerCase().includes(searchQuery) ||
394
+ (r.controller + '.' + r.handler).toLowerCase().includes(searchQuery);
395
+ });
396
+ render(visible);
397
+ document.getElementById('rt-count').textContent = visible.length + ' routes';
398
+ }
399
+
400
+ render(allRoutes);
401
+ }
402
+
403
+ if (document.readyState === 'loading') {
404
+ document.addEventListener('DOMContentLoaded', init);
405
+ } else {
406
+ init();
407
+ }
408
+ })();
409
+ </script>