@onebun/core 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/application/application.test.ts +237 -0
- package/src/application/application.ts +146 -1
- package/src/index.ts +2 -0
- package/src/types.ts +79 -0
package/package.json
CHANGED
|
@@ -1000,6 +1000,243 @@ describe('OneBunApplication', () => {
|
|
|
1000
1000
|
title: 'Post 123 by User 42',
|
|
1001
1001
|
});
|
|
1002
1002
|
});
|
|
1003
|
+
|
|
1004
|
+
test('should handle trailing slashes - route without trailing slash matches request with trailing slash', async () => {
|
|
1005
|
+
@Controller('/api')
|
|
1006
|
+
class ApiController extends BaseController {
|
|
1007
|
+
@Get('/users/:page')
|
|
1008
|
+
async getUsers(@Param('page') page: string) {
|
|
1009
|
+
return { users: ['Alice', 'Bob'], page: parseInt(page) };
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
@Module({
|
|
1014
|
+
controllers: [ApiController],
|
|
1015
|
+
})
|
|
1016
|
+
class TestModule {}
|
|
1017
|
+
|
|
1018
|
+
const app = createTestApp(TestModule);
|
|
1019
|
+
await app.start();
|
|
1020
|
+
|
|
1021
|
+
// Request WITH trailing slash should match route WITHOUT trailing slash
|
|
1022
|
+
const request = new Request('http://localhost:3000/api/users/1/', {
|
|
1023
|
+
method: 'GET',
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1027
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1028
|
+
|
|
1029
|
+
// Should return 200, not 404 (route should be found)
|
|
1030
|
+
expect(response).toBeInstanceOf(Response);
|
|
1031
|
+
expect(response.status).toBe(200);
|
|
1032
|
+
|
|
1033
|
+
const body = await response.json();
|
|
1034
|
+
expect(body.result).toEqual({ users: ['Alice', 'Bob'], page: 1 });
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
test('should handle trailing slashes - both with and without slash return same result', async () => {
|
|
1038
|
+
@Controller('/api')
|
|
1039
|
+
class ApiController extends BaseController {
|
|
1040
|
+
@Get('/items/:category')
|
|
1041
|
+
async getItems(@Param('category') category: string) {
|
|
1042
|
+
return { items: [1, 2, 3], category };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
@Module({
|
|
1047
|
+
controllers: [ApiController],
|
|
1048
|
+
})
|
|
1049
|
+
class TestModule {}
|
|
1050
|
+
|
|
1051
|
+
const app = createTestApp(TestModule);
|
|
1052
|
+
await app.start();
|
|
1053
|
+
|
|
1054
|
+
// Request WITHOUT trailing slash
|
|
1055
|
+
const requestWithout = new Request('http://localhost:3000/api/items/electronics', {
|
|
1056
|
+
method: 'GET',
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Request WITH trailing slash
|
|
1060
|
+
const requestWith = new Request('http://localhost:3000/api/items/electronics/', {
|
|
1061
|
+
method: 'GET',
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1065
|
+
const responseWithout = await (mockServer as any).fetchHandler(requestWithout);
|
|
1066
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1067
|
+
const responseWith = await (mockServer as any).fetchHandler(requestWith);
|
|
1068
|
+
|
|
1069
|
+
// Both should be valid Response objects
|
|
1070
|
+
expect(responseWithout).toBeInstanceOf(Response);
|
|
1071
|
+
expect(responseWith).toBeInstanceOf(Response);
|
|
1072
|
+
|
|
1073
|
+
// Both should return 200 (not 404)
|
|
1074
|
+
expect(responseWithout.status).toBe(200);
|
|
1075
|
+
expect(responseWith.status).toBe(200);
|
|
1076
|
+
|
|
1077
|
+
const bodyWithout = await responseWithout.json();
|
|
1078
|
+
const bodyWith = await responseWith.json();
|
|
1079
|
+
|
|
1080
|
+
expect(bodyWithout.result).toEqual(bodyWith.result);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
test('should handle trailing slashes with route parameters', async () => {
|
|
1084
|
+
@Controller('/api')
|
|
1085
|
+
class ApiController extends BaseController {
|
|
1086
|
+
@Get('/users/:id')
|
|
1087
|
+
async getUser(@Param('id') id: string) {
|
|
1088
|
+
return { id: parseInt(id) };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
@Module({
|
|
1093
|
+
controllers: [ApiController],
|
|
1094
|
+
})
|
|
1095
|
+
class TestModule {}
|
|
1096
|
+
|
|
1097
|
+
const app = createTestApp(TestModule);
|
|
1098
|
+
await app.start();
|
|
1099
|
+
|
|
1100
|
+
// Request WITH trailing slash on parameterized route
|
|
1101
|
+
const request = new Request('http://localhost:3000/api/users/123/', {
|
|
1102
|
+
method: 'GET',
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1106
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1107
|
+
const body = await response.json();
|
|
1108
|
+
|
|
1109
|
+
expect(response.status).toBe(200);
|
|
1110
|
+
expect(body.result).toEqual({ id: 123 });
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
test('should handle root path correctly (no trailing slash removal)', async () => {
|
|
1114
|
+
@Controller('/root')
|
|
1115
|
+
class RootController extends BaseController {
|
|
1116
|
+
@Get('/:id')
|
|
1117
|
+
async getById(@Param('id') id: string) {
|
|
1118
|
+
return { message: 'root', id };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
@Module({
|
|
1123
|
+
controllers: [RootController],
|
|
1124
|
+
})
|
|
1125
|
+
class TestModule {}
|
|
1126
|
+
|
|
1127
|
+
const app = createTestApp(TestModule);
|
|
1128
|
+
await app.start();
|
|
1129
|
+
|
|
1130
|
+
// Test that trailing slash is handled correctly even for short paths
|
|
1131
|
+
const request = new Request('http://localhost:3000/root/42/', {
|
|
1132
|
+
method: 'GET',
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1136
|
+
const response = await (mockServer as any).fetchHandler(request);
|
|
1137
|
+
|
|
1138
|
+
expect(response).toBeInstanceOf(Response);
|
|
1139
|
+
expect(response.status).toBe(200);
|
|
1140
|
+
|
|
1141
|
+
const body = await response.json();
|
|
1142
|
+
expect(body.result).toEqual({ message: 'root', id: '42' });
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
test('should handle exact root path with trailing slash', async () => {
|
|
1146
|
+
@Controller('/health')
|
|
1147
|
+
class HealthController extends BaseController {
|
|
1148
|
+
@Get('/:type')
|
|
1149
|
+
async check(@Param('type') type: string) {
|
|
1150
|
+
return { status: 'ok', type };
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
@Module({
|
|
1155
|
+
controllers: [HealthController],
|
|
1156
|
+
})
|
|
1157
|
+
class TestModule {}
|
|
1158
|
+
|
|
1159
|
+
const app = createTestApp(TestModule);
|
|
1160
|
+
await app.start();
|
|
1161
|
+
|
|
1162
|
+
// Root path "/" should remain "/" after normalization
|
|
1163
|
+
const requestWithSlash = new Request('http://localhost:3000/health/live/', {
|
|
1164
|
+
method: 'GET',
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
const requestWithoutSlash = new Request('http://localhost:3000/health/live', {
|
|
1168
|
+
method: 'GET',
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1172
|
+
const responseWith = await (mockServer as any).fetchHandler(requestWithSlash);
|
|
1173
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1174
|
+
const responseWithout = await (mockServer as any).fetchHandler(requestWithoutSlash);
|
|
1175
|
+
|
|
1176
|
+
// Both should work identically
|
|
1177
|
+
expect(responseWith.status).toBe(200);
|
|
1178
|
+
expect(responseWithout.status).toBe(200);
|
|
1179
|
+
|
|
1180
|
+
const bodyWith = await responseWith.json();
|
|
1181
|
+
const bodyWithout = await responseWithout.json();
|
|
1182
|
+
expect(bodyWith.result).toEqual(bodyWithout.result);
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
test('should normalize metrics route labels - trailing slash requests use same label as non-trailing', async () => {
|
|
1186
|
+
@Controller('/api')
|
|
1187
|
+
class ApiController extends BaseController {
|
|
1188
|
+
@Get('/data')
|
|
1189
|
+
async getData() {
|
|
1190
|
+
return { data: 'test' };
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
@Module({
|
|
1195
|
+
controllers: [ApiController],
|
|
1196
|
+
})
|
|
1197
|
+
class TestModule {}
|
|
1198
|
+
|
|
1199
|
+
const app = createTestApp(TestModule, {
|
|
1200
|
+
metrics: { path: '/metrics' },
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
// Track recorded metrics
|
|
1204
|
+
const recordedMetrics: Array<{ route: string }> = [];
|
|
1205
|
+
|
|
1206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1207
|
+
const mockMetricsService: any = {
|
|
1208
|
+
getMetrics: mock(() => Promise.resolve('# metrics data')),
|
|
1209
|
+
getContentType: mock(() => 'text/plain'),
|
|
1210
|
+
startSystemMetricsCollection: mock(),
|
|
1211
|
+
recordHttpRequest: mock((data: { route: string }) => {
|
|
1212
|
+
recordedMetrics.push({ route: data.route });
|
|
1213
|
+
}),
|
|
1214
|
+
};
|
|
1215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1216
|
+
(app as any).metricsService = mockMetricsService;
|
|
1217
|
+
|
|
1218
|
+
await app.start();
|
|
1219
|
+
|
|
1220
|
+
// Make requests with both trailing and non-trailing slash
|
|
1221
|
+
const requestWithout = new Request('http://localhost:3000/api/data', {
|
|
1222
|
+
method: 'GET',
|
|
1223
|
+
});
|
|
1224
|
+
const requestWith = new Request('http://localhost:3000/api/data/', {
|
|
1225
|
+
method: 'GET',
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1229
|
+
await (mockServer as any).fetchHandler(requestWithout);
|
|
1230
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1231
|
+
await (mockServer as any).fetchHandler(requestWith);
|
|
1232
|
+
|
|
1233
|
+
// Both requests should record metrics with the same route label (without trailing slash)
|
|
1234
|
+
expect(recordedMetrics.length).toBe(2);
|
|
1235
|
+
expect(recordedMetrics[0].route).toBe('/api/data');
|
|
1236
|
+
expect(recordedMetrics[1].route).toBe('/api/data');
|
|
1237
|
+
// Verify they are the same (no duplication due to trailing slash)
|
|
1238
|
+
expect(recordedMetrics[0].route).toBe(recordedMetrics[1].route);
|
|
1239
|
+
});
|
|
1003
1240
|
});
|
|
1004
1241
|
|
|
1005
1242
|
describe('Tracing integration', () => {
|
|
@@ -50,6 +50,21 @@ try {
|
|
|
50
50
|
// Metrics module not available - this is optional
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Conditionally import docs (optional dependency - not added to package.json to avoid circular deps)
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
let generateOpenApiSpec: any;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
let generateSwaggerUiHtml: any;
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
61
|
+
const docsModule = require('@onebun/docs');
|
|
62
|
+
generateOpenApiSpec = docsModule.generateOpenApiSpec;
|
|
63
|
+
generateSwaggerUiHtml = docsModule.generateSwaggerUiHtml;
|
|
64
|
+
} catch {
|
|
65
|
+
// Docs module not available - this is optional
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
// Import tracing modules directly
|
|
54
69
|
|
|
55
70
|
// Global trace context for current request
|
|
@@ -63,6 +78,26 @@ function clearGlobalTraceContext(): void {
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Normalize URL path by removing trailing slashes (except for root path).
|
|
83
|
+
* This ensures consistent route matching and metrics collection.
|
|
84
|
+
*
|
|
85
|
+
* @param path - The URL path to normalize
|
|
86
|
+
* @returns Normalized path without trailing slash
|
|
87
|
+
* @example
|
|
88
|
+
* normalizePath('/users/') // => '/users'
|
|
89
|
+
* normalizePath('/users') // => '/users'
|
|
90
|
+
* normalizePath('/') // => '/'
|
|
91
|
+
* normalizePath('/api/v1/') // => '/api/v1'
|
|
92
|
+
*/
|
|
93
|
+
function normalizePath(path: string): string {
|
|
94
|
+
if (path === '/' || path.length <= 1) {
|
|
95
|
+
return path;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return path.endsWith('/') ? path.slice(0, -1) : path;
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
/**
|
|
67
102
|
* OneBun Application
|
|
68
103
|
*/
|
|
@@ -85,6 +120,9 @@ export class OneBunApplication {
|
|
|
85
120
|
private wsHandler: WsHandler | null = null;
|
|
86
121
|
private queueService: QueueService | null = null;
|
|
87
122
|
private queueAdapter: QueueAdapter | null = null;
|
|
123
|
+
// Docs (OpenAPI/Swagger) - generated on start()
|
|
124
|
+
private openApiSpec: Record<string, unknown> | null = null;
|
|
125
|
+
private swaggerHtml: string | null = null;
|
|
88
126
|
|
|
89
127
|
/**
|
|
90
128
|
* Create application instance
|
|
@@ -287,6 +325,9 @@ export class OneBunApplication {
|
|
|
287
325
|
// Initialize Queue system if configured or handlers exist
|
|
288
326
|
await this.initializeQueue(controllers);
|
|
289
327
|
|
|
328
|
+
// Initialize Docs (OpenAPI/Swagger) if enabled and available
|
|
329
|
+
await this.initializeDocs(controllers);
|
|
330
|
+
|
|
290
331
|
// Create a map of routes with metadata
|
|
291
332
|
const routes = new Map<
|
|
292
333
|
string,
|
|
@@ -384,6 +425,10 @@ export class OneBunApplication {
|
|
|
384
425
|
// Get metrics path
|
|
385
426
|
const metricsPath = this.options.metrics?.path || '/metrics';
|
|
386
427
|
|
|
428
|
+
// Get docs paths
|
|
429
|
+
const docsPath = this.options.docs?.path || '/docs';
|
|
430
|
+
const openApiPath = this.options.docs?.jsonPath || '/openapi.json';
|
|
431
|
+
|
|
387
432
|
// Create server with proper context binding
|
|
388
433
|
const app = this;
|
|
389
434
|
const hasWebSocketGateways = this.wsHandler?.hasGateways() ?? false;
|
|
@@ -408,7 +453,10 @@ export class OneBunApplication {
|
|
|
408
453
|
websocket: wsHandlers,
|
|
409
454
|
async fetch(req, server) {
|
|
410
455
|
const url = new URL(req.url);
|
|
411
|
-
const
|
|
456
|
+
const rawPath = url.pathname;
|
|
457
|
+
// Normalize path to ensure consistent routing and metrics
|
|
458
|
+
// (removes trailing slash except for root path)
|
|
459
|
+
const path = normalizePath(rawPath);
|
|
412
460
|
const method = req.method;
|
|
413
461
|
const startTime = Date.now();
|
|
414
462
|
|
|
@@ -498,6 +546,29 @@ export class OneBunApplication {
|
|
|
498
546
|
}
|
|
499
547
|
}
|
|
500
548
|
|
|
549
|
+
// Handle docs endpoints (OpenAPI/Swagger)
|
|
550
|
+
if (app.options.docs?.enabled !== false && app.openApiSpec) {
|
|
551
|
+
// Serve Swagger UI HTML
|
|
552
|
+
if (path === docsPath && method === 'GET' && app.swaggerHtml) {
|
|
553
|
+
return new Response(app.swaggerHtml, {
|
|
554
|
+
headers: {
|
|
555
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
556
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Serve OpenAPI JSON spec
|
|
562
|
+
if (path === openApiPath && method === 'GET') {
|
|
563
|
+
return new Response(JSON.stringify(app.openApiSpec, null, 2), {
|
|
564
|
+
headers: {
|
|
565
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
566
|
+
'Content-Type': 'application/json',
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
501
572
|
// Handle metrics endpoint
|
|
502
573
|
if (path === metricsPath && method === 'GET' && app.metricsService) {
|
|
503
574
|
try {
|
|
@@ -1122,6 +1193,80 @@ export class OneBunApplication {
|
|
|
1122
1193
|
return this.queueService;
|
|
1123
1194
|
}
|
|
1124
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* Initialize the documentation system (OpenAPI/Swagger)
|
|
1198
|
+
*/
|
|
1199
|
+
private async initializeDocs(controllers: Function[]): Promise<void> {
|
|
1200
|
+
const docsOptions = this.options.docs;
|
|
1201
|
+
|
|
1202
|
+
// Skip if docs are explicitly disabled or @onebun/docs is not available
|
|
1203
|
+
if (docsOptions?.enabled === false) {
|
|
1204
|
+
this.logger.debug('Documentation explicitly disabled');
|
|
1205
|
+
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (!generateOpenApiSpec || !generateSwaggerUiHtml) {
|
|
1210
|
+
if (docsOptions?.enabled === true) {
|
|
1211
|
+
this.logger.warn(
|
|
1212
|
+
'Documentation enabled but @onebun/docs module not available. Install with: bun add @onebun/docs',
|
|
1213
|
+
);
|
|
1214
|
+
} else {
|
|
1215
|
+
this.logger.debug('@onebun/docs module not available, documentation disabled');
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
try {
|
|
1222
|
+
// Generate OpenAPI spec from controllers
|
|
1223
|
+
this.openApiSpec = generateOpenApiSpec(controllers, {
|
|
1224
|
+
title: docsOptions?.title || this.options.name || 'OneBun API',
|
|
1225
|
+
version: docsOptions?.version || '1.0.0',
|
|
1226
|
+
description: docsOptions?.description,
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Add additional OpenAPI info if provided
|
|
1230
|
+
if (this.openApiSpec && docsOptions?.contact) {
|
|
1231
|
+
(this.openApiSpec.info as Record<string, unknown>).contact = docsOptions.contact;
|
|
1232
|
+
}
|
|
1233
|
+
if (this.openApiSpec && docsOptions?.license) {
|
|
1234
|
+
(this.openApiSpec.info as Record<string, unknown>).license = docsOptions.license;
|
|
1235
|
+
}
|
|
1236
|
+
if (this.openApiSpec && docsOptions?.externalDocs) {
|
|
1237
|
+
this.openApiSpec.externalDocs = docsOptions.externalDocs;
|
|
1238
|
+
}
|
|
1239
|
+
if (this.openApiSpec && docsOptions?.servers && docsOptions.servers.length > 0) {
|
|
1240
|
+
this.openApiSpec.servers = docsOptions.servers;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Generate Swagger UI HTML
|
|
1244
|
+
const openApiPath = docsOptions?.jsonPath || '/openapi.json';
|
|
1245
|
+
this.swaggerHtml = generateSwaggerUiHtml(openApiPath);
|
|
1246
|
+
|
|
1247
|
+
const docsPath = docsOptions?.path || '/docs';
|
|
1248
|
+
this.logger.info(
|
|
1249
|
+
`Documentation available at http://${this.options.host}:${this.options.port}${docsPath}`,
|
|
1250
|
+
);
|
|
1251
|
+
this.logger.info(
|
|
1252
|
+
`OpenAPI spec available at http://${this.options.host}:${this.options.port}${openApiPath}`,
|
|
1253
|
+
);
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
this.logger.error(
|
|
1256
|
+
'Failed to initialize documentation:',
|
|
1257
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* Get the OpenAPI specification
|
|
1264
|
+
* @returns The OpenAPI spec or null if not generated
|
|
1265
|
+
*/
|
|
1266
|
+
getOpenApiSpec(): Record<string, unknown> | null {
|
|
1267
|
+
return this.openApiSpec;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1125
1270
|
/**
|
|
1126
1271
|
* Register signal handlers for graceful shutdown
|
|
1127
1272
|
* Call this after start() to enable automatic shutdown on SIGTERM/SIGINT
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -277,6 +277,11 @@ export interface ApplicationOptions {
|
|
|
277
277
|
*/
|
|
278
278
|
queue?: QueueApplicationOptions;
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Documentation configuration (OpenAPI/Swagger)
|
|
282
|
+
*/
|
|
283
|
+
docs?: DocsApplicationOptions;
|
|
284
|
+
|
|
280
285
|
/**
|
|
281
286
|
* Enable graceful shutdown on SIGTERM/SIGINT
|
|
282
287
|
* When enabled, the application will cleanly shutdown on process signals,
|
|
@@ -347,6 +352,80 @@ export interface WebSocketApplicationOptions {
|
|
|
347
352
|
maxPayload?: number;
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Documentation configuration for OneBunApplication
|
|
357
|
+
* Enables automatic OpenAPI spec generation and Swagger UI
|
|
358
|
+
*/
|
|
359
|
+
export interface DocsApplicationOptions {
|
|
360
|
+
/**
|
|
361
|
+
* Enable/disable documentation endpoints
|
|
362
|
+
* @defaultValue true
|
|
363
|
+
*/
|
|
364
|
+
enabled?: boolean;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Path for Swagger UI
|
|
368
|
+
* @defaultValue '/docs'
|
|
369
|
+
*/
|
|
370
|
+
path?: string;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Path for OpenAPI JSON specification
|
|
374
|
+
* @defaultValue '/openapi.json'
|
|
375
|
+
*/
|
|
376
|
+
jsonPath?: string;
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* API title for OpenAPI spec
|
|
380
|
+
* @defaultValue Application name or 'OneBun API'
|
|
381
|
+
*/
|
|
382
|
+
title?: string;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* API version for OpenAPI spec
|
|
386
|
+
* @defaultValue '1.0.0'
|
|
387
|
+
*/
|
|
388
|
+
version?: string;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* API description for OpenAPI spec
|
|
392
|
+
*/
|
|
393
|
+
description?: string;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Contact information
|
|
397
|
+
*/
|
|
398
|
+
contact?: {
|
|
399
|
+
name?: string;
|
|
400
|
+
email?: string;
|
|
401
|
+
url?: string;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* License information
|
|
406
|
+
*/
|
|
407
|
+
license?: {
|
|
408
|
+
name: string;
|
|
409
|
+
url?: string;
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* External documentation link
|
|
414
|
+
*/
|
|
415
|
+
externalDocs?: {
|
|
416
|
+
description?: string;
|
|
417
|
+
url: string;
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Server URLs for OpenAPI spec
|
|
422
|
+
*/
|
|
423
|
+
servers?: Array<{
|
|
424
|
+
url: string;
|
|
425
|
+
description?: string;
|
|
426
|
+
}>;
|
|
427
|
+
}
|
|
428
|
+
|
|
350
429
|
/**
|
|
351
430
|
* HTTP method types
|
|
352
431
|
*/
|