@onebun/core 0.2.0 → 0.2.2
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 +36 -0
- package/src/application/application.ts +258 -6
- package/src/application/multi-service-application.ts +2 -0
- package/src/application/multi-service.types.ts +1 -1
- package/src/decorators/decorators.test.ts +202 -12
- package/src/decorators/decorators.ts +228 -7
- package/src/docs-examples.test.ts +1339 -254
- package/src/file/index.ts +8 -0
- package/src/file/onebun-file.test.ts +315 -0
- package/src/file/onebun-file.ts +304 -0
- package/src/index.ts +20 -0
- package/src/module/controller.ts +96 -10
- package/src/module/index.ts +2 -1
- package/src/module/lifecycle.ts +13 -0
- package/src/module/middleware.ts +76 -0
- package/src/module/module.test.ts +138 -1
- package/src/module/module.ts +127 -2
- package/src/types.ts +169 -0
package/package.json
CHANGED
|
@@ -906,6 +906,42 @@ describe('OneBunApplication', () => {
|
|
|
906
906
|
expect(Bun.serve).toHaveBeenCalled();
|
|
907
907
|
});
|
|
908
908
|
|
|
909
|
+
test('should pass default idleTimeout (120s) to Bun.serve', async () => {
|
|
910
|
+
@Module({})
|
|
911
|
+
class TestModule {}
|
|
912
|
+
|
|
913
|
+
const app = createTestApp(TestModule);
|
|
914
|
+
await app.start();
|
|
915
|
+
|
|
916
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
917
|
+
const serveCall = (Bun.serve as any).mock.calls[0];
|
|
918
|
+
expect(serveCall[0].idleTimeout).toBe(120);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('should pass custom idleTimeout to Bun.serve', async () => {
|
|
922
|
+
@Module({})
|
|
923
|
+
class TestModule {}
|
|
924
|
+
|
|
925
|
+
const app = createTestApp(TestModule, { idleTimeout: 60 });
|
|
926
|
+
await app.start();
|
|
927
|
+
|
|
928
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
929
|
+
const serveCall = (Bun.serve as any).mock.calls[0];
|
|
930
|
+
expect(serveCall[0].idleTimeout).toBe(60);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test('should pass idleTimeout: 0 to disable timeout', async () => {
|
|
934
|
+
@Module({})
|
|
935
|
+
class TestModule {}
|
|
936
|
+
|
|
937
|
+
const app = createTestApp(TestModule, { idleTimeout: 0 });
|
|
938
|
+
await app.start();
|
|
939
|
+
|
|
940
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
941
|
+
const serveCall = (Bun.serve as any).mock.calls[0];
|
|
942
|
+
expect(serveCall[0].idleTimeout).toBe(0);
|
|
943
|
+
});
|
|
944
|
+
|
|
909
945
|
test('should start application with config initialization', async () => {
|
|
910
946
|
@Module({})
|
|
911
947
|
class TestModule {}
|
|
@@ -27,16 +27,23 @@ import { makeTraceService, TraceService } from '@onebun/trace';
|
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
getControllerMetadata,
|
|
30
|
+
getControllerMiddleware,
|
|
30
31
|
getSseMetadata,
|
|
31
32
|
type SseDecoratorOptions,
|
|
32
33
|
} from '../decorators/decorators';
|
|
34
|
+
import { OneBunFile, validateFile } from '../file/onebun-file';
|
|
33
35
|
import {
|
|
34
36
|
NotInitializedConfig,
|
|
35
37
|
type IConfig,
|
|
36
38
|
type OneBunAppConfig,
|
|
37
39
|
} from '../module/config.interface';
|
|
38
40
|
import { ConfigServiceImpl } from '../module/config.service';
|
|
39
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
createSseStream,
|
|
43
|
+
DEFAULT_IDLE_TIMEOUT,
|
|
44
|
+
DEFAULT_SSE_HEARTBEAT_MS,
|
|
45
|
+
DEFAULT_SSE_TIMEOUT,
|
|
46
|
+
} from '../module/controller';
|
|
40
47
|
import { OneBunModule } from '../module/module';
|
|
41
48
|
import { QueueService, type QueueAdapter } from '../queue';
|
|
42
49
|
import { InMemoryQueueAdapter } from '../queue/adapters/memory.adapter';
|
|
@@ -484,7 +491,7 @@ export class OneBunApplication {
|
|
|
484
491
|
|
|
485
492
|
/**
|
|
486
493
|
* Create a route handler with the full OneBun request lifecycle:
|
|
487
|
-
* tracing setup → middleware chain → executeHandler → metrics → tracing end
|
|
494
|
+
* tracing setup → per-request timeout → middleware chain → executeHandler → metrics → tracing end
|
|
488
495
|
*/
|
|
489
496
|
function createRouteHandler(
|
|
490
497
|
routeMeta: RouteMetadata,
|
|
@@ -492,10 +499,28 @@ export class OneBunApplication {
|
|
|
492
499
|
controller: Controller,
|
|
493
500
|
fullPath: string,
|
|
494
501
|
method: string,
|
|
495
|
-
): (req: OneBunRequest) => Promise<Response> {
|
|
496
|
-
|
|
502
|
+
): (req: OneBunRequest, server: ReturnType<typeof Bun.serve>) => Promise<Response> {
|
|
503
|
+
// Determine the effective timeout for this route:
|
|
504
|
+
// SSE endpoints check @Sse({ timeout }) first, then route-level, then DEFAULT_SSE_TIMEOUT
|
|
505
|
+
// Normal endpoints use route-level timeout only (undefined = use global idleTimeout)
|
|
506
|
+
const isSse = routeMeta.handler
|
|
507
|
+
? getSseMetadata(Object.getPrototypeOf(controller), routeMeta.handler) !== undefined
|
|
508
|
+
: false;
|
|
509
|
+
const sseDecoratorOptions = routeMeta.handler
|
|
510
|
+
? getSseMetadata(Object.getPrototypeOf(controller), routeMeta.handler)
|
|
511
|
+
: undefined;
|
|
512
|
+
const effectiveTimeout: number | undefined = isSse
|
|
513
|
+
? (sseDecoratorOptions?.timeout ?? routeMeta.timeout ?? DEFAULT_SSE_TIMEOUT)
|
|
514
|
+
: routeMeta.timeout;
|
|
515
|
+
|
|
516
|
+
return async (req, server) => {
|
|
497
517
|
const startTime = Date.now();
|
|
498
518
|
|
|
519
|
+
// Apply per-request idle timeout if configured
|
|
520
|
+
if (effectiveTimeout !== undefined) {
|
|
521
|
+
server.timeout(req, effectiveTimeout);
|
|
522
|
+
}
|
|
523
|
+
|
|
499
524
|
// Setup tracing context if available and enabled
|
|
500
525
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
501
526
|
let traceSpan: any = null;
|
|
@@ -700,6 +725,12 @@ export class OneBunApplication {
|
|
|
700
725
|
};
|
|
701
726
|
}
|
|
702
727
|
|
|
728
|
+
// Application-wide middleware — resolve class constructors via root module DI
|
|
729
|
+
const globalMiddlewareClasses = (this.options.middleware as Function[] | undefined) ?? [];
|
|
730
|
+
const globalMiddleware: Function[] = globalMiddlewareClasses.length > 0
|
|
731
|
+
? (this.ensureModule().resolveMiddleware?.(globalMiddlewareClasses) ?? [])
|
|
732
|
+
: [];
|
|
733
|
+
|
|
703
734
|
// Add routes from controllers
|
|
704
735
|
for (const controllerClass of controllers) {
|
|
705
736
|
const controllerMetadata = getControllerMetadata(controllerClass);
|
|
@@ -724,6 +755,15 @@ export class OneBunApplication {
|
|
|
724
755
|
|
|
725
756
|
const controllerPath = controllerMetadata.path;
|
|
726
757
|
|
|
758
|
+
// Module-level middleware (already resolved bound functions)
|
|
759
|
+
const moduleMiddleware = this.ensureModule().getModuleMiddleware?.(controllerClass) ?? [];
|
|
760
|
+
|
|
761
|
+
// Controller-level middleware — resolve class constructors via root module DI
|
|
762
|
+
const ctrlMiddlewareClasses = getControllerMiddleware(controllerClass);
|
|
763
|
+
const ctrlMiddleware: Function[] = ctrlMiddlewareClasses.length > 0
|
|
764
|
+
? (this.ensureModule().resolveMiddleware?.(ctrlMiddlewareClasses) ?? [])
|
|
765
|
+
: [];
|
|
766
|
+
|
|
727
767
|
for (const route of controllerMetadata.routes) {
|
|
728
768
|
// Combine: appPrefix + controllerPath + routePath
|
|
729
769
|
// Normalize to ensure consistent matching (e.g., '/api/users/' -> '/api/users')
|
|
@@ -733,8 +773,26 @@ export class OneBunApplication {
|
|
|
733
773
|
controller,
|
|
734
774
|
);
|
|
735
775
|
|
|
776
|
+
// Route-level middleware — resolve class constructors via root module DI
|
|
777
|
+
const routeMiddlewareClasses = route.middleware ?? [];
|
|
778
|
+
const routeMiddleware: Function[] = routeMiddlewareClasses.length > 0
|
|
779
|
+
? (this.ensureModule().resolveMiddleware?.(routeMiddlewareClasses) ?? [])
|
|
780
|
+
: [];
|
|
781
|
+
|
|
782
|
+
// Merge middleware: global → module → controller → route
|
|
783
|
+
const mergedMiddleware = [
|
|
784
|
+
...globalMiddleware,
|
|
785
|
+
...moduleMiddleware,
|
|
786
|
+
...ctrlMiddleware,
|
|
787
|
+
...routeMiddleware,
|
|
788
|
+
];
|
|
789
|
+
const routeWithMergedMiddleware: RouteMetadata = {
|
|
790
|
+
...route,
|
|
791
|
+
middleware: mergedMiddleware.length > 0 ? mergedMiddleware : undefined,
|
|
792
|
+
};
|
|
793
|
+
|
|
736
794
|
// Create wrapped handler with full OneBun lifecycle (tracing, metrics, middleware)
|
|
737
|
-
const wrappedHandler = createRouteHandler(
|
|
795
|
+
const wrappedHandler = createRouteHandler(routeWithMergedMiddleware, handler, controller, fullPath, method);
|
|
738
796
|
|
|
739
797
|
// Add to bunRoutes grouped by path and method
|
|
740
798
|
if (!bunRoutes[fullPath]) {
|
|
@@ -843,6 +901,8 @@ export class OneBunApplication {
|
|
|
843
901
|
this.server = Bun.serve<WsClientData>({
|
|
844
902
|
port: this.options.port,
|
|
845
903
|
hostname: this.options.host,
|
|
904
|
+
// Idle timeout (seconds) — default 120s to support SSE and long-running requests
|
|
905
|
+
idleTimeout: this.options.idleTimeout ?? DEFAULT_IDLE_TIMEOUT,
|
|
846
906
|
// WebSocket handlers
|
|
847
907
|
websocket: wsHandlers,
|
|
848
908
|
// Bun routes API: all endpoints are handled here
|
|
@@ -911,6 +971,43 @@ export class OneBunApplication {
|
|
|
911
971
|
throw error;
|
|
912
972
|
}
|
|
913
973
|
|
|
974
|
+
/**
|
|
975
|
+
* Extract an OneBunFile from a JSON value.
|
|
976
|
+
* Supports two formats:
|
|
977
|
+
* - String: raw base64 data
|
|
978
|
+
* - Object: { data: string, filename?: string, mimeType?: string }
|
|
979
|
+
*/
|
|
980
|
+
function extractFileFromJsonValue(value: unknown): OneBunFile | undefined {
|
|
981
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
982
|
+
return OneBunFile.fromBase64(value);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
986
|
+
const obj = value as Record<string, unknown>;
|
|
987
|
+
if (typeof obj.data === 'string' && obj.data.length > 0) {
|
|
988
|
+
return OneBunFile.fromBase64(
|
|
989
|
+
obj.data,
|
|
990
|
+
typeof obj.filename === 'string' ? obj.filename : undefined,
|
|
991
|
+
typeof obj.mimeType === 'string' ? obj.mimeType : undefined,
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return undefined;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Extract a file from a JSON body by field name
|
|
1001
|
+
*/
|
|
1002
|
+
function extractFileFromJson(
|
|
1003
|
+
jsonBody: Record<string, unknown>,
|
|
1004
|
+
fieldName: string,
|
|
1005
|
+
): OneBunFile | undefined {
|
|
1006
|
+
const fieldValue = jsonBody[fieldName];
|
|
1007
|
+
|
|
1008
|
+
return extractFileFromJsonValue(fieldValue);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
914
1011
|
/**
|
|
915
1012
|
* Execute route handler with parameter injection and validation.
|
|
916
1013
|
* Path parameters come from BunRequest.params (populated by Bun routes API).
|
|
@@ -951,6 +1048,52 @@ export class OneBunApplication {
|
|
|
951
1048
|
// Sort params by index to ensure correct order
|
|
952
1049
|
const sortedParams = [...(route.params || [])].sort((a, b) => a.index - b.index);
|
|
953
1050
|
|
|
1051
|
+
// Pre-parse body for file upload params (FormData or JSON, cached for all params)
|
|
1052
|
+
const needsFileData = sortedParams.some(
|
|
1053
|
+
(p) =>
|
|
1054
|
+
p.type === ParamType.FILE ||
|
|
1055
|
+
p.type === ParamType.FILES ||
|
|
1056
|
+
p.type === ParamType.FORM_FIELD,
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// Validate that @Body and file decorators are not used on the same method
|
|
1060
|
+
if (needsFileData) {
|
|
1061
|
+
const hasBody = sortedParams.some((p) => p.type === ParamType.BODY);
|
|
1062
|
+
if (hasBody) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
'Cannot use @Body() together with @UploadedFile/@UploadedFiles/@FormField on the same method. ' +
|
|
1065
|
+
'Both consume the request body. Use file decorators for multipart/base64 uploads.',
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1071
|
+
let formData: any = null;
|
|
1072
|
+
let jsonBody: Record<string, unknown> | null = null;
|
|
1073
|
+
let isMultipart = false;
|
|
1074
|
+
|
|
1075
|
+
if (needsFileData) {
|
|
1076
|
+
const contentType = req.headers.get('content-type') || '';
|
|
1077
|
+
|
|
1078
|
+
if (contentType.includes('multipart/form-data')) {
|
|
1079
|
+
isMultipart = true;
|
|
1080
|
+
try {
|
|
1081
|
+
formData = await req.formData();
|
|
1082
|
+
} catch {
|
|
1083
|
+
formData = null;
|
|
1084
|
+
}
|
|
1085
|
+
} else if (contentType.includes('application/json')) {
|
|
1086
|
+
try {
|
|
1087
|
+
const parsed = await req.json();
|
|
1088
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1089
|
+
jsonBody = parsed as Record<string, unknown>;
|
|
1090
|
+
}
|
|
1091
|
+
} catch {
|
|
1092
|
+
jsonBody = null;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
954
1097
|
for (const param of sortedParams) {
|
|
955
1098
|
switch (param.type) {
|
|
956
1099
|
case ParamType.PATH:
|
|
@@ -989,6 +1132,100 @@ export class OneBunApplication {
|
|
|
989
1132
|
args[param.index] = undefined;
|
|
990
1133
|
break;
|
|
991
1134
|
|
|
1135
|
+
case ParamType.FILE: {
|
|
1136
|
+
let file: OneBunFile | undefined;
|
|
1137
|
+
|
|
1138
|
+
if (isMultipart && formData && param.name) {
|
|
1139
|
+
const entry = formData.get(param.name);
|
|
1140
|
+
if (entry instanceof File) {
|
|
1141
|
+
file = new OneBunFile(entry);
|
|
1142
|
+
}
|
|
1143
|
+
} else if (jsonBody && param.name) {
|
|
1144
|
+
file = extractFileFromJson(jsonBody, param.name);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
if (file && param.fileOptions) {
|
|
1148
|
+
validateFile(file, param.fileOptions, param.name);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
args[param.index] = file;
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
case ParamType.FILES: {
|
|
1156
|
+
let files: OneBunFile[] = [];
|
|
1157
|
+
|
|
1158
|
+
if (isMultipart && formData) {
|
|
1159
|
+
if (param.name) {
|
|
1160
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1161
|
+
const entries: any[] = formData.getAll(param.name);
|
|
1162
|
+
files = entries
|
|
1163
|
+
.filter((entry: unknown): entry is File => entry instanceof File)
|
|
1164
|
+
.map((f: File) => new OneBunFile(f));
|
|
1165
|
+
} else {
|
|
1166
|
+
// Get all files from all fields
|
|
1167
|
+
for (const [, value] of formData.entries()) {
|
|
1168
|
+
if (value instanceof File) {
|
|
1169
|
+
files.push(new OneBunFile(value));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
} else if (jsonBody) {
|
|
1174
|
+
if (param.name) {
|
|
1175
|
+
const fieldValue = jsonBody[param.name];
|
|
1176
|
+
if (Array.isArray(fieldValue)) {
|
|
1177
|
+
files = fieldValue
|
|
1178
|
+
.map((item) => extractFileFromJsonValue(item))
|
|
1179
|
+
.filter((f): f is OneBunFile => f !== undefined);
|
|
1180
|
+
}
|
|
1181
|
+
} else {
|
|
1182
|
+
// Extract all file-like values from JSON
|
|
1183
|
+
for (const [, value] of Object.entries(jsonBody)) {
|
|
1184
|
+
const file = extractFileFromJsonValue(value);
|
|
1185
|
+
if (file) {
|
|
1186
|
+
files.push(file);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Validate maxCount
|
|
1193
|
+
if (param.fileOptions?.maxCount !== undefined && files.length > param.fileOptions.maxCount) {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Too many files for "${param.name || 'upload'}". Got ${files.length}, max is ${param.fileOptions.maxCount}`,
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Validate each file
|
|
1200
|
+
if (param.fileOptions) {
|
|
1201
|
+
for (const file of files) {
|
|
1202
|
+
validateFile(file, param.fileOptions, param.name);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
args[param.index] = files;
|
|
1207
|
+
break;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
case ParamType.FORM_FIELD: {
|
|
1211
|
+
let value: string | undefined;
|
|
1212
|
+
|
|
1213
|
+
if (isMultipart && formData && param.name) {
|
|
1214
|
+
const entry = formData.get(param.name);
|
|
1215
|
+
if (typeof entry === 'string') {
|
|
1216
|
+
value = entry;
|
|
1217
|
+
}
|
|
1218
|
+
} else if (jsonBody && param.name) {
|
|
1219
|
+
const jsonValue = jsonBody[param.name];
|
|
1220
|
+
if (jsonValue !== undefined && jsonValue !== null) {
|
|
1221
|
+
value = String(jsonValue);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
args[param.index] = value;
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
992
1229
|
default:
|
|
993
1230
|
args[param.index] = undefined;
|
|
994
1231
|
}
|
|
@@ -998,6 +1235,16 @@ export class OneBunApplication {
|
|
|
998
1235
|
throw new Error(`Required parameter ${param.name || param.index} is missing`);
|
|
999
1236
|
}
|
|
1000
1237
|
|
|
1238
|
+
// For FILES type, also check for empty array when required
|
|
1239
|
+
if (
|
|
1240
|
+
param.isRequired &&
|
|
1241
|
+
param.type === ParamType.FILES &&
|
|
1242
|
+
Array.isArray(args[param.index]) &&
|
|
1243
|
+
(args[param.index] as unknown[]).length === 0
|
|
1244
|
+
) {
|
|
1245
|
+
throw new Error(`Required parameter ${param.name || param.index} is missing`);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1001
1248
|
// Apply arktype schema validation if provided
|
|
1002
1249
|
if (param.schema && args[param.index] !== undefined) {
|
|
1003
1250
|
try {
|
|
@@ -1170,9 +1417,14 @@ export class OneBunApplication {
|
|
|
1170
1417
|
|
|
1171
1418
|
// Check if result is an async iterable (generator)
|
|
1172
1419
|
if (result && typeof result === 'object' && Symbol.asyncIterator in result) {
|
|
1420
|
+
// Apply default heartbeat if none specified to keep the connection alive
|
|
1421
|
+
const effectiveOptions = {
|
|
1422
|
+
...options,
|
|
1423
|
+
heartbeat: options.heartbeat ?? DEFAULT_SSE_HEARTBEAT_MS,
|
|
1424
|
+
};
|
|
1173
1425
|
const stream = createSseStream(
|
|
1174
1426
|
result as AsyncIterable<unknown>,
|
|
1175
|
-
|
|
1427
|
+
effectiveOptions,
|
|
1176
1428
|
);
|
|
1177
1429
|
|
|
1178
1430
|
return new Response(stream, {
|
|
@@ -143,6 +143,7 @@ export class MultiServiceApplication<TServices extends ServicesMap = ServicesMap
|
|
|
143
143
|
envOverrides: { ...appOptions.envOverrides, ...serviceOptions.envOverrides },
|
|
144
144
|
envSchemaExtend: serviceOptions.envSchemaExtend,
|
|
145
145
|
logger: { ...appOptions.logger, ...serviceOptions.logger },
|
|
146
|
+
middleware: serviceOptions.middleware ?? appOptions.middleware,
|
|
146
147
|
metrics: { ...appOptions.metrics, ...serviceOptions.metrics },
|
|
147
148
|
tracing: { ...appOptions.tracing, ...serviceOptions.tracing },
|
|
148
149
|
};
|
|
@@ -197,6 +198,7 @@ export class MultiServiceApplication<TServices extends ServicesMap = ServicesMap
|
|
|
197
198
|
basePath: mergedOptions.basePath,
|
|
198
199
|
// When routePrefix is true, use service name as prefix
|
|
199
200
|
routePrefix: mergedOptions.routePrefix ? name : undefined,
|
|
201
|
+
middleware: mergedOptions.middleware,
|
|
200
202
|
envSchema: mergedEnvSchema,
|
|
201
203
|
envOptions: {
|
|
202
204
|
...this.options.envOptions,
|
|
@@ -30,7 +30,7 @@ export type TracingOptions = NonNullable<ApplicationOptions['tracing']>;
|
|
|
30
30
|
* Any new shared options should be added to ApplicationOptions first.
|
|
31
31
|
*/
|
|
32
32
|
export interface BaseServiceOptions
|
|
33
|
-
extends Pick<ApplicationOptions, 'host' | 'basePath' | 'metrics' | 'tracing'> {
|
|
33
|
+
extends Pick<ApplicationOptions, 'host' | 'basePath' | 'metrics' | 'tracing' | 'middleware'> {
|
|
34
34
|
/**
|
|
35
35
|
* Add service name as prefix to all routes.
|
|
36
36
|
* When true, the service name will be used as routePrefix.
|