@dismissible/nestjs-api 0.0.2-canary.5daf195.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/config/.env.yaml +41 -0
  2. package/jest.config.ts +35 -0
  3. package/jest.e2e-config.ts +13 -0
  4. package/nest-cli.json +16 -0
  5. package/package.json +58 -0
  6. package/project.json +94 -0
  7. package/scripts/performance-test.config.json +29 -0
  8. package/scripts/performance-test.ts +845 -0
  9. package/src/app-test.factory.ts +39 -0
  10. package/src/app.module.ts +60 -0
  11. package/src/app.setup.ts +52 -0
  12. package/src/bootstrap.ts +29 -0
  13. package/src/config/app.config.spec.ts +117 -0
  14. package/src/config/app.config.ts +20 -0
  15. package/src/config/config.module.spec.ts +94 -0
  16. package/src/config/config.module.ts +50 -0
  17. package/src/config/default-app.config.spec.ts +74 -0
  18. package/src/config/default-app.config.ts +24 -0
  19. package/src/config/index.ts +2 -0
  20. package/src/cors/cors.config.spec.ts +162 -0
  21. package/src/cors/cors.config.ts +37 -0
  22. package/src/cors/index.ts +1 -0
  23. package/src/health/health.controller.spec.ts +24 -0
  24. package/src/health/health.controller.ts +16 -0
  25. package/src/health/health.module.ts +9 -0
  26. package/src/health/index.ts +2 -0
  27. package/src/helmet/helmet.config.spec.ts +197 -0
  28. package/src/helmet/helmet.config.ts +60 -0
  29. package/src/helmet/index.ts +1 -0
  30. package/src/index.ts +5 -0
  31. package/src/main.ts +3 -0
  32. package/src/server/index.ts +1 -0
  33. package/src/server/server.config.spec.ts +65 -0
  34. package/src/server/server.config.ts +8 -0
  35. package/src/swagger/index.ts +2 -0
  36. package/src/swagger/swagger.config.spec.ts +113 -0
  37. package/src/swagger/swagger.config.ts +12 -0
  38. package/src/swagger/swagger.factory.spec.ts +125 -0
  39. package/src/swagger/swagger.factory.ts +24 -0
  40. package/src/validation/index.ts +1 -0
  41. package/src/validation/validation.config.ts +47 -0
  42. package/test/config/.env.yaml +23 -0
  43. package/test/config-jwt-auth/.env.yaml +26 -0
  44. package/test/dismiss.e2e-spec.ts +61 -0
  45. package/test/full-cycle.e2e-spec.ts +55 -0
  46. package/test/get-or-create.e2e-spec.ts +51 -0
  47. package/test/jwt-auth.e2e-spec.ts +335 -0
  48. package/test/restore.e2e-spec.ts +61 -0
  49. package/tsconfig.app.json +13 -0
  50. package/tsconfig.e2e.json +12 -0
  51. package/tsconfig.json +13 -0
  52. package/tsconfig.spec.json +12 -0
@@ -0,0 +1,845 @@
1
+ import autocannon from 'autocannon';
2
+ import { URL } from 'url';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+
6
+ interface PerformanceConfig {
7
+ createCount: number;
8
+ getCount: number;
9
+ apiUrl: string;
10
+ connections: number;
11
+ duration: number;
12
+ }
13
+
14
+ interface TestResult {
15
+ phase: string;
16
+ requests: {
17
+ total: number;
18
+ average: number;
19
+ min: number;
20
+ max: number;
21
+ };
22
+ latency: {
23
+ average: number;
24
+ min: number;
25
+ max: number;
26
+ p50: number;
27
+ p90: number;
28
+ p99: number;
29
+ p99_9: number;
30
+ };
31
+ throughput: {
32
+ average: number;
33
+ min: number;
34
+ max: number;
35
+ };
36
+ errors: number;
37
+ timeouts: number;
38
+ duration: number;
39
+ }
40
+
41
+ type RequestConfig = { method: string; path: string };
42
+ type SetRequestsFn = (requests: RequestConfig[]) => void;
43
+
44
+ interface AutocannonClient {
45
+ setRequests: SetRequestsFn;
46
+ }
47
+
48
+ function createPathCycler(paths: string[]): (client: AutocannonClient) => void {
49
+ let pathCounter = 0;
50
+
51
+ return (client: AutocannonClient) => {
52
+ let connectionIndex = pathCounter++;
53
+
54
+ client.setRequests([{ method: 'GET', path: paths[connectionIndex % paths.length] }]);
55
+
56
+ const originalSetRequests = client.setRequests.bind(client);
57
+ client.setRequests = function (_requests: RequestConfig[]) {
58
+ connectionIndex = (connectionIndex + 1) % paths.length;
59
+ return originalSetRequests([{ method: 'GET', path: paths[connectionIndex] }]);
60
+ };
61
+ };
62
+ }
63
+
64
+ function getConfig(): PerformanceConfig {
65
+ return {
66
+ createCount: parseInt(process.env.PERF_CREATE_COUNT || '100', 10),
67
+ getCount: parseInt(process.env.PERF_GET_COUNT || '100', 10),
68
+ apiUrl: process.env.PERF_API_URL || 'http://localhost:3001',
69
+ connections: parseInt(process.env.PERF_CONNECTIONS || '10', 10),
70
+ duration: parseInt(process.env.PERF_DURATION || '10', 10),
71
+ };
72
+ }
73
+
74
+ function getOutputPath(): string | null {
75
+ return process.env.PERF_OUTPUT_PATH || null;
76
+ }
77
+
78
+ function formatNumber(num: number): string {
79
+ return num.toLocaleString('en-US', {
80
+ minimumFractionDigits: 2,
81
+ maximumFractionDigits: 2,
82
+ });
83
+ }
84
+
85
+ function formatBytes(bytes: number): string {
86
+ const units = ['B', 'KB', 'MB', 'GB'];
87
+ let size = bytes;
88
+ let unitIndex = 0;
89
+
90
+ while (size >= 1024 && unitIndex < units.length - 1) {
91
+ size /= 1024;
92
+ unitIndex++;
93
+ }
94
+
95
+ return `${formatNumber(size)} ${units[unitIndex]}`;
96
+ }
97
+
98
+ function formatDuration(ms: number): string {
99
+ if (ms < 1000) {
100
+ return `${formatNumber(ms)} ms`;
101
+ }
102
+ return `${formatNumber(ms / 1000)} s`;
103
+ }
104
+
105
+ function printResults(result: TestResult): void {
106
+ console.log('\n' + '='.repeat(80));
107
+ console.log(`Phase: ${result.phase}`);
108
+ console.log('='.repeat(80));
109
+ console.log(`Duration: ${formatDuration(result.duration)}`);
110
+ console.log('\nšŸ“Š Requests:');
111
+ console.log(` Total: ${result.requests.total.toLocaleString()}`);
112
+ console.log(` Average: ${formatNumber(result.requests.average)} req/s`);
113
+ console.log(` Min: ${formatNumber(result.requests.min)} req/s`);
114
+ console.log(` Max: ${formatNumber(result.requests.max)} req/s`);
115
+
116
+ console.log('\nā±ļø Latency:');
117
+ console.log(` Average: ${formatNumber(result.latency.average)} ms`);
118
+ console.log(` Min: ${formatNumber(result.latency.min)} ms`);
119
+ console.log(` Max: ${formatNumber(result.latency.max)} ms`);
120
+ console.log(` p50: ${formatNumber(result.latency.p50)} ms`);
121
+ console.log(` p90: ${formatNumber(result.latency.p90)} ms`);
122
+ console.log(` p99: ${formatNumber(result.latency.p99)} ms`);
123
+ console.log(` p99.9: ${formatNumber(result.latency.p99_9)} ms`);
124
+
125
+ console.log('\nšŸ“ˆ Throughput:');
126
+ console.log(` Average: ${formatBytes(result.throughput.average)}/s`);
127
+ console.log(` Min: ${formatBytes(result.throughput.min)}/s`);
128
+ console.log(` Max: ${formatBytes(result.throughput.max)}/s`);
129
+
130
+ if (result.errors > 0 || result.timeouts > 0) {
131
+ console.log('\nāš ļø Errors:');
132
+ if (result.errors > 0) {
133
+ console.log(` Errors: ${result.errors.toLocaleString()}`);
134
+ }
135
+ if (result.timeouts > 0) {
136
+ console.log(` Timeouts: ${result.timeouts.toLocaleString()}`);
137
+ }
138
+ }
139
+
140
+ console.log('='.repeat(80) + '\n');
141
+ }
142
+
143
+ function generateHTMLReport(results: TestResult[], config: PerformanceConfig): string {
144
+ const timestamp = new Date().toISOString();
145
+ const createResult = results.find((r) => r.phase === 'Create Items');
146
+ const getResult = results.find((r) => r.phase === 'Get Items');
147
+
148
+ return `<!DOCTYPE html>
149
+ <html lang="en">
150
+ <head>
151
+ <meta charset="UTF-8">
152
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
153
+ <title>Performance Test Report</title>
154
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
155
+ <style>
156
+ * {
157
+ margin: 0;
158
+ padding: 0;
159
+ box-sizing: border-box;
160
+ }
161
+
162
+ body {
163
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
164
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
165
+ padding: 20px;
166
+ min-height: 100vh;
167
+ }
168
+
169
+ .container {
170
+ max-width: 1400px;
171
+ margin: 0 auto;
172
+ background: white;
173
+ border-radius: 12px;
174
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
175
+ overflow: hidden;
176
+ }
177
+
178
+ .header {
179
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
180
+ color: white;
181
+ padding: 40px;
182
+ text-align: center;
183
+ }
184
+
185
+ .header h1 {
186
+ font-size: 2.5em;
187
+ margin-bottom: 10px;
188
+ }
189
+
190
+ .header .timestamp {
191
+ opacity: 0.9;
192
+ font-size: 0.9em;
193
+ }
194
+
195
+ .config-section {
196
+ background: #f8f9fa;
197
+ padding: 30px 40px;
198
+ border-bottom: 1px solid #e9ecef;
199
+ }
200
+
201
+ .config-section h2 {
202
+ color: #495057;
203
+ margin-bottom: 20px;
204
+ font-size: 1.5em;
205
+ }
206
+
207
+ .config-grid {
208
+ display: grid;
209
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
210
+ gap: 15px;
211
+ }
212
+
213
+ .config-item {
214
+ background: white;
215
+ padding: 15px;
216
+ border-radius: 8px;
217
+ border: 1px solid #e9ecef;
218
+ }
219
+
220
+ .config-item label {
221
+ display: block;
222
+ font-size: 0.85em;
223
+ color: #6c757d;
224
+ margin-bottom: 5px;
225
+ text-transform: uppercase;
226
+ letter-spacing: 0.5px;
227
+ }
228
+
229
+ .config-item value {
230
+ display: block;
231
+ font-size: 1.2em;
232
+ font-weight: 600;
233
+ color: #212529;
234
+ }
235
+
236
+ .phases {
237
+ display: grid;
238
+ grid-template-columns: repeat(auto-fit, minmax(600px, 1fr));
239
+ gap: 30px;
240
+ padding: 40px;
241
+ }
242
+
243
+ .phase-card {
244
+ background: #f8f9fa;
245
+ border-radius: 12px;
246
+ padding: 30px;
247
+ border: 2px solid #e9ecef;
248
+ transition: transform 0.2s, box-shadow 0.2s;
249
+ }
250
+
251
+ .phase-card:hover {
252
+ transform: translateY(-2px);
253
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
254
+ }
255
+
256
+ .phase-card h2 {
257
+ color: #495057;
258
+ margin-bottom: 25px;
259
+ font-size: 1.8em;
260
+ border-bottom: 3px solid #667eea;
261
+ padding-bottom: 10px;
262
+ }
263
+
264
+ .metric-section {
265
+ margin-bottom: 30px;
266
+ }
267
+
268
+ .metric-section h3 {
269
+ color: #6c757d;
270
+ font-size: 1.1em;
271
+ margin-bottom: 15px;
272
+ text-transform: uppercase;
273
+ letter-spacing: 1px;
274
+ }
275
+
276
+ table {
277
+ width: 100%;
278
+ border-collapse: collapse;
279
+ background: white;
280
+ border-radius: 8px;
281
+ overflow: hidden;
282
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
283
+ }
284
+
285
+ th {
286
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
287
+ color: white;
288
+ padding: 12px 15px;
289
+ text-align: left;
290
+ font-weight: 600;
291
+ font-size: 0.9em;
292
+ text-transform: uppercase;
293
+ letter-spacing: 0.5px;
294
+ }
295
+
296
+ td {
297
+ padding: 12px 15px;
298
+ border-bottom: 1px solid #e9ecef;
299
+ color: #495057;
300
+ }
301
+
302
+ tr:last-child td {
303
+ border-bottom: none;
304
+ }
305
+
306
+ tr:hover {
307
+ background: #f8f9fa;
308
+ }
309
+
310
+ .value-highlight {
311
+ font-weight: 600;
312
+ color: #667eea;
313
+ }
314
+
315
+ .error-section {
316
+ background: #fff3cd;
317
+ border: 2px solid #ffc107;
318
+ border-radius: 8px;
319
+ padding: 20px;
320
+ margin-top: 20px;
321
+ }
322
+
323
+ .error-section h3 {
324
+ color: #856404;
325
+ margin-bottom: 10px;
326
+ }
327
+
328
+ .error-section p {
329
+ color: #856404;
330
+ margin: 5px 0;
331
+ }
332
+
333
+ .chart-container {
334
+ margin-top: 20px;
335
+ position: relative;
336
+ height: 300px;
337
+ background: white;
338
+ border-radius: 8px;
339
+ padding: 20px;
340
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
341
+ }
342
+
343
+ .summary {
344
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
345
+ color: white;
346
+ padding: 40px;
347
+ text-align: center;
348
+ }
349
+
350
+ .summary h2 {
351
+ margin-bottom: 20px;
352
+ font-size: 2em;
353
+ }
354
+
355
+ .summary-stats {
356
+ display: grid;
357
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
358
+ gap: 20px;
359
+ margin-top: 30px;
360
+ }
361
+
362
+ .summary-stat {
363
+ background: rgba(255, 255, 255, 0.1);
364
+ padding: 20px;
365
+ border-radius: 8px;
366
+ backdrop-filter: blur(10px);
367
+ }
368
+
369
+ .summary-stat label {
370
+ display: block;
371
+ font-size: 0.85em;
372
+ opacity: 0.9;
373
+ margin-bottom: 8px;
374
+ text-transform: uppercase;
375
+ letter-spacing: 1px;
376
+ }
377
+
378
+ .summary-stat value {
379
+ display: block;
380
+ font-size: 2em;
381
+ font-weight: 700;
382
+ }
383
+ </style>
384
+ </head>
385
+ <body>
386
+ <div class="container">
387
+ <div class="header">
388
+ <h1>šŸš€ Performance Test Report</h1>
389
+ <div class="timestamp">Generated: ${new Date(timestamp).toLocaleString()}</div>
390
+ </div>
391
+
392
+ <div class="config-section">
393
+ <h2>Test Configuration</h2>
394
+ <div class="config-grid">
395
+ <div class="config-item">
396
+ <label>API URL</label>
397
+ <value>${config.apiUrl}</value>
398
+ </div>
399
+ <div class="config-item">
400
+ <label>Create Count</label>
401
+ <value>${config.createCount.toLocaleString()}</value>
402
+ </div>
403
+ <div class="config-item">
404
+ <label>Get Count</label>
405
+ <value>${config.getCount.toLocaleString()}</value>
406
+ </div>
407
+ <div class="config-item">
408
+ <label>Connections</label>
409
+ <value>${config.connections}</value>
410
+ </div>
411
+ <div class="config-item">
412
+ <label>Duration</label>
413
+ <value>${config.duration}s</value>
414
+ </div>
415
+ </div>
416
+ </div>
417
+
418
+ <div class="phases">
419
+ ${results
420
+ .map(
421
+ (result) => `
422
+ <div class="phase-card">
423
+ <h2>${result.phase}</h2>
424
+
425
+ <div class="metric-section">
426
+ <h3>šŸ“Š Requests</h3>
427
+ <table>
428
+ <thead>
429
+ <tr>
430
+ <th>Metric</th>
431
+ <th>Value</th>
432
+ </tr>
433
+ </thead>
434
+ <tbody>
435
+ <tr>
436
+ <td>Total</td>
437
+ <td class="value-highlight">${result.requests.total.toLocaleString()}</td>
438
+ </tr>
439
+ <tr>
440
+ <td>Average</td>
441
+ <td class="value-highlight">${formatNumber(result.requests.average)} req/s</td>
442
+ </tr>
443
+ <tr>
444
+ <td>Min</td>
445
+ <td>${formatNumber(result.requests.min)} req/s</td>
446
+ </tr>
447
+ <tr>
448
+ <td>Max</td>
449
+ <td>${formatNumber(result.requests.max)} req/s</td>
450
+ </tr>
451
+ </tbody>
452
+ </table>
453
+ </div>
454
+
455
+ <div class="metric-section">
456
+ <h3>ā±ļø Latency</h3>
457
+ <table>
458
+ <thead>
459
+ <tr>
460
+ <th>Percentile</th>
461
+ <th>Value (ms)</th>
462
+ </tr>
463
+ </thead>
464
+ <tbody>
465
+ <tr>
466
+ <td>Average</td>
467
+ <td class="value-highlight">${formatNumber(result.latency.average)}</td>
468
+ </tr>
469
+ <tr>
470
+ <td>Min</td>
471
+ <td>${formatNumber(result.latency.min)}</td>
472
+ </tr>
473
+ <tr>
474
+ <td>Max</td>
475
+ <td>${formatNumber(result.latency.max)}</td>
476
+ </tr>
477
+ <tr>
478
+ <td>p50</td>
479
+ <td>${formatNumber(result.latency.p50)}</td>
480
+ </tr>
481
+ <tr>
482
+ <td>p90</td>
483
+ <td>${formatNumber(result.latency.p90)}</td>
484
+ </tr>
485
+ <tr>
486
+ <td>p99</td>
487
+ <td>${formatNumber(result.latency.p99)}</td>
488
+ </tr>
489
+ <tr>
490
+ <td>p99.9</td>
491
+ <td>${formatNumber(result.latency.p99_9)}</td>
492
+ </tr>
493
+ </tbody>
494
+ </table>
495
+ </div>
496
+
497
+ <div class="metric-section">
498
+ <h3>šŸ“ˆ Throughput</h3>
499
+ <table>
500
+ <thead>
501
+ <tr>
502
+ <th>Metric</th>
503
+ <th>Value</th>
504
+ </tr>
505
+ </thead>
506
+ <tbody>
507
+ <tr>
508
+ <td>Average</td>
509
+ <td class="value-highlight">${formatBytes(result.throughput.average)}/s</td>
510
+ </tr>
511
+ <tr>
512
+ <td>Min</td>
513
+ <td>${formatBytes(result.throughput.min)}/s</td>
514
+ </tr>
515
+ <tr>
516
+ <td>Max</td>
517
+ <td>${formatBytes(result.throughput.max)}/s</td>
518
+ </tr>
519
+ </tbody>
520
+ </table>
521
+ </div>
522
+
523
+ <div class="metric-section">
524
+ <h3>šŸ“‰ Latency Distribution</h3>
525
+ <div class="chart-container">
526
+ <canvas id="latency-chart-${result.phase.replace(/\s+/g, '-').toLowerCase()}"></canvas>
527
+ </div>
528
+ </div>
529
+
530
+ ${
531
+ result.errors > 0 || result.timeouts > 0
532
+ ? `
533
+ <div class="error-section">
534
+ <h3>āš ļø Errors</h3>
535
+ ${result.errors > 0 ? `<p><strong>Errors:</strong> ${result.errors.toLocaleString()}</p>` : ''}
536
+ ${result.timeouts > 0 ? `<p><strong>Timeouts:</strong> ${result.timeouts.toLocaleString()}</p>` : ''}
537
+ </div>
538
+ `
539
+ : ''
540
+ }
541
+ </div>
542
+ `,
543
+ )
544
+ .join('')}
545
+ </div>
546
+
547
+ <div class="summary">
548
+ <h2>Summary</h2>
549
+ <div class="summary-stats">
550
+ ${
551
+ createResult
552
+ ? `
553
+ <div class="summary-stat">
554
+ <label>Create - Avg Latency</label>
555
+ <value>${formatNumber(createResult.latency.average)}ms</value>
556
+ </div>
557
+ <div class="summary-stat">
558
+ <label>Create - Requests/s</label>
559
+ <value>${formatNumber(createResult.requests.average)}</value>
560
+ </div>
561
+ `
562
+ : ''
563
+ }
564
+ ${
565
+ getResult
566
+ ? `
567
+ <div class="summary-stat">
568
+ <label>Get - Avg Latency</label>
569
+ <value>${formatNumber(getResult.latency.average)}ms</value>
570
+ </div>
571
+ <div class="summary-stat">
572
+ <label>Get - Requests/s</label>
573
+ <value>${formatNumber(getResult.requests.average)}</value>
574
+ </div>
575
+ `
576
+ : ''
577
+ }
578
+ </div>
579
+ </div>
580
+ </div>
581
+
582
+ <script>
583
+ const results = ${JSON.stringify(results)};
584
+
585
+ results.forEach((result, index) => {
586
+ const canvasId = 'latency-chart-' + result.phase.replace(/\\s+/g, '-').toLowerCase();
587
+ const ctx = document.getElementById(canvasId);
588
+ if (!ctx) return;
589
+
590
+ new Chart(ctx, {
591
+ type: 'bar',
592
+ data: {
593
+ labels: ['p50', 'p90', 'p99', 'p99.9'],
594
+ datasets: [{
595
+ label: 'Latency (ms)',
596
+ data: [
597
+ result.latency.p50,
598
+ result.latency.p90,
599
+ result.latency.p99,
600
+ result.latency.p99_9
601
+ ],
602
+ backgroundColor: [
603
+ 'rgba(102, 126, 234, 0.8)',
604
+ 'rgba(118, 75, 162, 0.8)',
605
+ 'rgba(255, 99, 132, 0.8)',
606
+ 'rgba(255, 159, 64, 0.8)'
607
+ ],
608
+ borderColor: [
609
+ 'rgba(102, 126, 234, 1)',
610
+ 'rgba(118, 75, 162, 1)',
611
+ 'rgba(255, 99, 132, 1)',
612
+ 'rgba(255, 159, 64, 1)'
613
+ ],
614
+ borderWidth: 2
615
+ }]
616
+ },
617
+ options: {
618
+ responsive: true,
619
+ maintainAspectRatio: false,
620
+ plugins: {
621
+ legend: {
622
+ display: false
623
+ },
624
+ title: {
625
+ display: true,
626
+ text: 'Latency Percentiles',
627
+ font: {
628
+ size: 16,
629
+ weight: 'bold'
630
+ }
631
+ }
632
+ },
633
+ scales: {
634
+ y: {
635
+ beginAtZero: true,
636
+ title: {
637
+ display: true,
638
+ text: 'Latency (ms)'
639
+ }
640
+ }
641
+ }
642
+ }
643
+ });
644
+ });
645
+ </script>
646
+ </body>
647
+ </html>`;
648
+ }
649
+
650
+ function saveHTMLReport(html: string, outputPath: string | null): string {
651
+ const defaultPath = path.join(process.cwd(), 'perf', `performance-report-${Date.now()}.html`);
652
+ const finalPath = outputPath || defaultPath;
653
+
654
+ const dir = path.dirname(finalPath);
655
+ if (!fs.existsSync(dir)) {
656
+ fs.mkdirSync(dir, { recursive: true });
657
+ }
658
+
659
+ fs.writeFileSync(finalPath, html, 'utf-8');
660
+ return finalPath;
661
+ }
662
+
663
+ async function runCreatePhase(
664
+ config: PerformanceConfig,
665
+ itemIds: string[],
666
+ userIds: string[],
667
+ ): Promise<TestResult> {
668
+ const baseUrl = new URL(config.apiUrl);
669
+ const paths: string[] = [];
670
+
671
+ for (let i = 0; i < config.createCount; i++) {
672
+ const userId = userIds[i % userIds.length];
673
+ const itemId = itemIds[i];
674
+ paths.push(`/v1/users/${userId}/items/${itemId}`);
675
+ }
676
+
677
+ const instance = autocannon(
678
+ {
679
+ url: baseUrl.origin,
680
+ connections: config.connections,
681
+ duration: config.duration,
682
+ setupClient: createPathCycler(paths) as autocannon.SetupClientFunction,
683
+ },
684
+ (err, _result) => {
685
+ if (err) {
686
+ console.error('Error during create phase:', err);
687
+ throw err;
688
+ }
689
+ },
690
+ );
691
+
692
+ return new Promise((resolve, reject) => {
693
+ instance.on('done', (result) => {
694
+ const testResult: TestResult = {
695
+ phase: 'Create Items',
696
+ requests: {
697
+ total: result.requests.total,
698
+ average: result.requests.average,
699
+ min: result.requests.min,
700
+ max: result.requests.max,
701
+ },
702
+ latency: {
703
+ average: result.latency.average,
704
+ min: result.latency.min,
705
+ max: result.latency.max,
706
+ p50: result.latency.p50,
707
+ p90: result.latency.p90,
708
+ p99: result.latency.p99,
709
+ p99_9: result.latency.p99_9,
710
+ },
711
+ throughput: {
712
+ average: result.throughput.average,
713
+ min: result.throughput.min,
714
+ max: result.throughput.max,
715
+ },
716
+ errors: result.errors,
717
+ timeouts: result.timeouts,
718
+ duration: result.duration,
719
+ };
720
+ resolve(testResult);
721
+ });
722
+
723
+ instance.on('error', (err) => {
724
+ reject(err);
725
+ });
726
+ });
727
+ }
728
+
729
+ async function runGetPhase(
730
+ config: PerformanceConfig,
731
+ itemIds: string[],
732
+ userIds: string[],
733
+ ): Promise<TestResult> {
734
+ const baseUrl = new URL(config.apiUrl);
735
+ const paths: string[] = [];
736
+
737
+ const itemsToGet = Math.min(config.getCount, itemIds.length);
738
+ for (let i = 0; i < itemsToGet; i++) {
739
+ const userId = userIds[i % userIds.length];
740
+ const itemId = itemIds[i];
741
+ paths.push(`/v1/users/${userId}/items/${itemId}`);
742
+ }
743
+
744
+ const instance = autocannon(
745
+ {
746
+ url: baseUrl.origin,
747
+ connections: config.connections,
748
+ duration: config.duration,
749
+ setupClient: createPathCycler(paths) as autocannon.SetupClientFunction,
750
+ },
751
+ (err, _result) => {
752
+ if (err) {
753
+ console.error('Error during get phase:', err);
754
+ throw err;
755
+ }
756
+ },
757
+ );
758
+
759
+ return new Promise((resolve, reject) => {
760
+ instance.on('done', (result) => {
761
+ const testResult: TestResult = {
762
+ phase: 'Get Items',
763
+ requests: {
764
+ total: result.requests.total,
765
+ average: result.requests.average,
766
+ min: result.requests.min,
767
+ max: result.requests.max,
768
+ },
769
+ latency: {
770
+ average: result.latency.average,
771
+ min: result.latency.min,
772
+ max: result.latency.max,
773
+ p50: result.latency.p50,
774
+ p90: result.latency.p90,
775
+ p99: result.latency.p99,
776
+ p99_9: result.latency.p99_9,
777
+ },
778
+ throughput: {
779
+ average: result.throughput.average,
780
+ min: result.throughput.min,
781
+ max: result.throughput.max,
782
+ },
783
+ errors: result.errors,
784
+ timeouts: result.timeouts,
785
+ duration: result.duration,
786
+ };
787
+ resolve(testResult);
788
+ });
789
+
790
+ instance.on('error', (err) => {
791
+ reject(err);
792
+ });
793
+ });
794
+ }
795
+
796
+ async function main(): Promise<void> {
797
+ const config = getConfig();
798
+
799
+ console.log('šŸš€ Performance Test Configuration:');
800
+ console.log(` API URL: ${config.apiUrl}`);
801
+ console.log(` Create Count: ${config.createCount.toLocaleString()}`);
802
+ console.log(` Get Count: ${config.getCount.toLocaleString()}`);
803
+ console.log(` Connections: ${config.connections}`);
804
+ console.log(` Duration: ${config.duration}s`);
805
+ console.log('');
806
+
807
+ const itemIds: string[] = [];
808
+ const userIds: string[] = [];
809
+
810
+ for (let i = 0; i < Math.max(config.createCount, config.getCount); i++) {
811
+ itemIds.push(`perf-test-item-${i + 1}`);
812
+ }
813
+
814
+ const numUsers = Math.max(10, Math.floor(config.createCount / 10));
815
+ for (let i = 0; i < numUsers; i++) {
816
+ userIds.push(`perf-test-user-${i + 1}`);
817
+ }
818
+
819
+ try {
820
+ console.log('šŸ“ Phase 1: Creating items...');
821
+ const createResult = await runCreatePhase(config, itemIds, userIds);
822
+ printResults(createResult);
823
+
824
+ await new Promise((resolve) => setTimeout(resolve, 1000));
825
+
826
+ console.log('šŸ“– Phase 2: Getting existing items...');
827
+ const getResult = await runGetPhase(config, itemIds, userIds);
828
+ printResults(getResult);
829
+
830
+ const outputPath = getOutputPath();
831
+ const htmlReport = generateHTMLReport([createResult, getResult], config);
832
+ const savedPath = saveHTMLReport(htmlReport, outputPath);
833
+
834
+ console.log(`\nšŸ“„ HTML report saved to: ${savedPath}`);
835
+ console.log('āœ… Performance test completed successfully!');
836
+ } catch (error) {
837
+ console.error('āŒ Performance test failed:', error);
838
+ process.exit(1);
839
+ }
840
+ }
841
+
842
+ main().catch((error) => {
843
+ console.error('Fatal error:', error);
844
+ process.exit(1);
845
+ });