@onebun/core 0.2.5 → 0.2.7
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 +6 -6
- package/src/application/application.test.ts +865 -1
- package/src/application/application.ts +247 -48
- package/src/application/multi-service-application.test.ts +15 -0
- package/src/application/multi-service-application.ts +2 -0
- package/src/application/multi-service.types.ts +7 -1
- package/src/decorators/decorators.test.ts +28 -0
- package/src/decorators/decorators.ts +26 -0
- package/src/docs-examples.test.ts +29 -1
- package/src/index.ts +1 -0
- package/src/module/module.test.ts +60 -7
- package/src/module/module.ts +36 -0
- package/src/queue/docs-examples.test.ts +72 -12
- package/src/queue/index.ts +4 -0
- package/src/queue/queue-service-proxy.test.ts +82 -0
- package/src/queue/queue-service-proxy.ts +114 -0
- package/src/queue/types.ts +2 -2
- package/src/types.ts +62 -3
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type Context,
|
|
5
|
+
Effect,
|
|
6
|
+
type Layer,
|
|
7
|
+
} from 'effect';
|
|
2
8
|
|
|
3
9
|
import type { Controller } from '../module/controller';
|
|
4
10
|
import type { WsClientData } from '../websocket/ws.types';
|
|
@@ -45,7 +51,13 @@ import {
|
|
|
45
51
|
DEFAULT_SSE_TIMEOUT,
|
|
46
52
|
} from '../module/controller';
|
|
47
53
|
import { OneBunModule } from '../module/module';
|
|
48
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
QueueService,
|
|
56
|
+
QueueServiceProxy,
|
|
57
|
+
QueueServiceTag,
|
|
58
|
+
type QueueAdapter,
|
|
59
|
+
type QueueConfig,
|
|
60
|
+
} from '../queue';
|
|
49
61
|
import { InMemoryQueueAdapter } from '../queue/adapters/memory.adapter';
|
|
50
62
|
import { RedisQueueAdapter } from '../queue/adapters/redis.adapter';
|
|
51
63
|
import { hasQueueDecorators } from '../queue/decorators';
|
|
@@ -88,6 +100,17 @@ try {
|
|
|
88
100
|
// Docs module not available - this is optional
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
// Optional CacheService for static file existence caching
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
let cacheServiceClass: new (...args: unknown[]) => any = null as any;
|
|
106
|
+
try {
|
|
107
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
108
|
+
const cacheModule = require('@onebun/cache');
|
|
109
|
+
cacheServiceClass = cacheModule.CacheService;
|
|
110
|
+
} catch {
|
|
111
|
+
// @onebun/cache not available - use in-memory Map fallback
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
// Import tracing modules directly
|
|
92
115
|
|
|
93
116
|
// Global trace context for current request
|
|
@@ -113,12 +136,12 @@ function clearGlobalTraceContext(): void {
|
|
|
113
136
|
* normalizePath('/') // => '/'
|
|
114
137
|
* normalizePath('/api/v1/') // => '/api/v1'
|
|
115
138
|
*/
|
|
116
|
-
function normalizePath(
|
|
117
|
-
if (
|
|
118
|
-
return
|
|
139
|
+
function normalizePath(pathStr: string): string {
|
|
140
|
+
if (pathStr === '/' || pathStr.length <= 1) {
|
|
141
|
+
return pathStr;
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
return
|
|
144
|
+
return pathStr.endsWith('/') ? pathStr.slice(0, -1) : pathStr;
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
/**
|
|
@@ -191,6 +214,29 @@ function resolveHost(explicitHost: string | undefined): string {
|
|
|
191
214
|
return '0.0.0.0';
|
|
192
215
|
}
|
|
193
216
|
|
|
217
|
+
/** Default TTL for static file existence cache (ms) when not specified */
|
|
218
|
+
const DEFAULT_STATIC_FILE_EXISTENCE_CACHE_TTL_MS = 60_000;
|
|
219
|
+
|
|
220
|
+
/** Cache key prefix for static file existence in CacheService */
|
|
221
|
+
const STATIC_EXISTS_CACHE_PREFIX = 'onebun:static:exists:';
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolve a relative path under a root directory and ensure the result stays inside the root (path traversal protection).
|
|
225
|
+
* @param rootDir - Absolute path to the static root directory
|
|
226
|
+
* @param relativePath - URL path segment (e.g. from request path after prefix); must not contain '..' that escapes root
|
|
227
|
+
* @returns Absolute path if under root, otherwise null
|
|
228
|
+
*/
|
|
229
|
+
function resolvePathUnderRoot(rootDir: string, relativePath: string): string | null {
|
|
230
|
+
const normalized = path.join(rootDir, relativePath.startsWith('/') ? relativePath.slice(1) : relativePath);
|
|
231
|
+
const resolved = path.resolve(normalized);
|
|
232
|
+
const rootResolved = path.resolve(rootDir);
|
|
233
|
+
if (resolved === rootResolved || resolved.startsWith(rootResolved + path.sep)) {
|
|
234
|
+
return resolved;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
194
240
|
/**
|
|
195
241
|
* OneBun Application
|
|
196
242
|
*/
|
|
@@ -210,6 +256,7 @@ export class OneBunApplication {
|
|
|
210
256
|
private wsHandler: WsHandler | null = null;
|
|
211
257
|
private queueService: QueueService | null = null;
|
|
212
258
|
private queueAdapter: QueueAdapter | null = null;
|
|
259
|
+
private queueServiceProxy: QueueServiceProxy | null = null;
|
|
213
260
|
// Docs (OpenAPI/Swagger) - generated on start()
|
|
214
261
|
private openApiSpec: Record<string, unknown> | null = null;
|
|
215
262
|
private swaggerHtml: string | null = null;
|
|
@@ -364,11 +411,11 @@ export class OneBunApplication {
|
|
|
364
411
|
* const port = app.getConfigValue('server.port'); // number
|
|
365
412
|
* const host = app.getConfigValue('server.host'); // string
|
|
366
413
|
*/
|
|
367
|
-
getConfigValue<P extends DeepPaths<OneBunAppConfig>>(
|
|
414
|
+
getConfigValue<P extends DeepPaths<OneBunAppConfig>>(pathKey: P): DeepValue<OneBunAppConfig, P>;
|
|
368
415
|
/** Fallback for dynamic paths */
|
|
369
|
-
getConfigValue<T = unknown>(
|
|
370
|
-
getConfigValue(
|
|
371
|
-
return this.getConfig().get(
|
|
416
|
+
getConfigValue<T = unknown>(pathKey: string): T;
|
|
417
|
+
getConfigValue(pathKey: string): unknown {
|
|
418
|
+
return this.getConfig().get(pathKey);
|
|
372
419
|
}
|
|
373
420
|
|
|
374
421
|
/**
|
|
@@ -441,6 +488,16 @@ export class OneBunApplication {
|
|
|
441
488
|
// so services can safely use this.config.get() in their constructors
|
|
442
489
|
this.rootModule = OneBunModule.create(this.moduleClass, this.loggerLayer, this.config);
|
|
443
490
|
|
|
491
|
+
// Register QueueService proxy in DI so controllers/providers/middleware can inject QueueService.
|
|
492
|
+
// After initializeQueue(), setDelegate(real) is called when queue is enabled.
|
|
493
|
+
this.queueServiceProxy = new QueueServiceProxy();
|
|
494
|
+
if (this.ensureModule().registerService) {
|
|
495
|
+
this.ensureModule().registerService!(
|
|
496
|
+
QueueServiceTag as Context.Tag<unknown, QueueService>,
|
|
497
|
+
this.queueServiceProxy as unknown as QueueService,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
444
501
|
// Start metrics collection if enabled
|
|
445
502
|
if (this.metricsService && this.metricsService.startSystemMetricsCollection) {
|
|
446
503
|
this.metricsService.startSystemMetricsCollection();
|
|
@@ -758,10 +815,14 @@ export class OneBunApplication {
|
|
|
758
815
|
// Module-level middleware (already resolved bound functions)
|
|
759
816
|
const moduleMiddleware = this.ensureModule().getModuleMiddleware?.(controllerClass) ?? [];
|
|
760
817
|
|
|
761
|
-
//
|
|
818
|
+
// Resolve controller-level and route-level middleware with the owner module's DI
|
|
819
|
+
const ownerModule =
|
|
820
|
+
this.ensureModule().getOwnerModuleForController?.(controllerClass) ?? this.ensureModule();
|
|
821
|
+
|
|
822
|
+
// Controller-level middleware — resolve via owner module DI
|
|
762
823
|
const ctrlMiddlewareClasses = getControllerMiddleware(controllerClass);
|
|
763
824
|
const ctrlMiddleware: Function[] = ctrlMiddlewareClasses.length > 0
|
|
764
|
-
? (
|
|
825
|
+
? (ownerModule.resolveMiddleware?.(ctrlMiddlewareClasses) ?? [])
|
|
765
826
|
: [];
|
|
766
827
|
|
|
767
828
|
for (const route of controllerMetadata.routes) {
|
|
@@ -773,10 +834,10 @@ export class OneBunApplication {
|
|
|
773
834
|
controller,
|
|
774
835
|
);
|
|
775
836
|
|
|
776
|
-
// Route-level middleware — resolve
|
|
837
|
+
// Route-level middleware — resolve via owner module DI
|
|
777
838
|
const routeMiddlewareClasses = route.middleware ?? [];
|
|
778
839
|
const routeMiddleware: Function[] = routeMiddlewareClasses.length > 0
|
|
779
|
-
? (
|
|
840
|
+
? (ownerModule.resolveMiddleware?.(routeMiddlewareClasses) ?? [])
|
|
780
841
|
: [];
|
|
781
842
|
|
|
782
843
|
// Merge middleware: global → module → controller → route
|
|
@@ -897,6 +958,69 @@ export class OneBunApplication {
|
|
|
897
958
|
|
|
898
959
|
drain() { /* no-op */ },
|
|
899
960
|
};
|
|
961
|
+
|
|
962
|
+
// Static file serving: resolve root and setup existence cache (CacheService or in-memory Map)
|
|
963
|
+
const staticOpts = this.options.static;
|
|
964
|
+
let staticRootResolved: string | null = null;
|
|
965
|
+
let staticPathPrefix: string | undefined;
|
|
966
|
+
let staticFallbackFile: string | undefined;
|
|
967
|
+
let staticCacheTtlMs = 0;
|
|
968
|
+
// Cache interface: get(key) => value | undefined, set(key, value, ttlMs)
|
|
969
|
+
type StaticExistsCache = {
|
|
970
|
+
get(key: string): Promise<boolean | undefined>;
|
|
971
|
+
set(key: string, value: boolean, ttlMs: number): Promise<void>;
|
|
972
|
+
};
|
|
973
|
+
let staticExistsCache: StaticExistsCache | null = null;
|
|
974
|
+
|
|
975
|
+
if (staticOpts?.root) {
|
|
976
|
+
staticRootResolved = path.resolve(staticOpts.root);
|
|
977
|
+
staticPathPrefix = staticOpts.pathPrefix;
|
|
978
|
+
staticFallbackFile = staticOpts.fallbackFile;
|
|
979
|
+
staticCacheTtlMs = staticOpts.fileExistenceCacheTtlMs ?? DEFAULT_STATIC_FILE_EXISTENCE_CACHE_TTL_MS;
|
|
980
|
+
|
|
981
|
+
if (staticCacheTtlMs > 0) {
|
|
982
|
+
if (cacheServiceClass) {
|
|
983
|
+
try {
|
|
984
|
+
const cacheService = this.ensureModule().getServiceByClass?.(cacheServiceClass);
|
|
985
|
+
if (cacheService && typeof cacheService.get === 'function' && typeof cacheService.set === 'function') {
|
|
986
|
+
staticExistsCache = {
|
|
987
|
+
get: (key: string) => cacheService.get(STATIC_EXISTS_CACHE_PREFIX + key),
|
|
988
|
+
set: (key: string, value: boolean, ttlMs: number) =>
|
|
989
|
+
cacheService.set(STATIC_EXISTS_CACHE_PREFIX + key, value, { ttl: ttlMs }),
|
|
990
|
+
};
|
|
991
|
+
this.logger.debug('Static file existence cache using CacheService');
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
// CacheService not in module, use fallback
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!staticExistsCache) {
|
|
998
|
+
const map = new Map<string, { exists: boolean; expiresAt: number }>();
|
|
999
|
+
staticExistsCache = {
|
|
1000
|
+
async get(key: string): Promise<boolean | undefined> {
|
|
1001
|
+
const entry = map.get(key);
|
|
1002
|
+
if (!entry) {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
if (staticCacheTtlMs > 0 && Date.now() > entry.expiresAt) {
|
|
1006
|
+
map.delete(key);
|
|
1007
|
+
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return entry.exists;
|
|
1012
|
+
},
|
|
1013
|
+
async set(key: string, value: boolean, ttlMs: number): Promise<void> {
|
|
1014
|
+
map.set(key, {
|
|
1015
|
+
exists: value,
|
|
1016
|
+
expiresAt: ttlMs > 0 ? Date.now() + ttlMs : Number.MAX_SAFE_INTEGER,
|
|
1017
|
+
});
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
this.logger.debug('Static file existence cache using in-memory Map');
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
900
1024
|
|
|
901
1025
|
this.server = Bun.serve<WsClientData>({
|
|
902
1026
|
port: this.options.port,
|
|
@@ -916,8 +1040,8 @@ export class OneBunApplication {
|
|
|
916
1040
|
const socketioPath = app.options.websocket?.socketio?.path ?? '/socket.io';
|
|
917
1041
|
|
|
918
1042
|
const url = new URL(req.url);
|
|
919
|
-
const
|
|
920
|
-
const isSocketIoPath = socketioEnabled &&
|
|
1043
|
+
const requestPath = normalizePath(url.pathname);
|
|
1044
|
+
const isSocketIoPath = socketioEnabled && requestPath.startsWith(socketioPath);
|
|
921
1045
|
if (upgradeHeader === 'websocket' || isSocketIoPath) {
|
|
922
1046
|
const response = await app.wsHandler.handleUpgrade(req, server);
|
|
923
1047
|
if (response === undefined) {
|
|
@@ -928,6 +1052,60 @@ export class OneBunApplication {
|
|
|
928
1052
|
}
|
|
929
1053
|
}
|
|
930
1054
|
|
|
1055
|
+
// Static file serving (GET/HEAD only)
|
|
1056
|
+
if (staticRootResolved && (req.method === 'GET' || req.method === 'HEAD')) {
|
|
1057
|
+
const requestPath = normalizePath(new URL(req.url).pathname);
|
|
1058
|
+
const prefix = staticPathPrefix ?? '/';
|
|
1059
|
+
const hasPrefix = prefix !== '' && prefix !== '/';
|
|
1060
|
+
if (hasPrefix && !requestPath.startsWith(prefix)) {
|
|
1061
|
+
return new Response('Not Found', { status: HttpStatusCode.NOT_FOUND });
|
|
1062
|
+
}
|
|
1063
|
+
const relativePath = hasPrefix ? requestPath.slice(prefix.length) || '/' : requestPath;
|
|
1064
|
+
const resolvedPath = resolvePathUnderRoot(staticRootResolved, relativePath);
|
|
1065
|
+
if (resolvedPath === null) {
|
|
1066
|
+
return new Response('Not Found', { status: HttpStatusCode.NOT_FOUND });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const cache = staticCacheTtlMs > 0 ? staticExistsCache : null;
|
|
1070
|
+
const cacheKey = resolvedPath;
|
|
1071
|
+
if (cache) {
|
|
1072
|
+
const cached = await cache.get(cacheKey);
|
|
1073
|
+
if (cached === true) {
|
|
1074
|
+
return new Response(Bun.file(resolvedPath));
|
|
1075
|
+
}
|
|
1076
|
+
if (cached === false) {
|
|
1077
|
+
if (staticFallbackFile) {
|
|
1078
|
+
const fallbackResolved = resolvePathUnderRoot(staticRootResolved, staticFallbackFile);
|
|
1079
|
+
if (fallbackResolved !== null) {
|
|
1080
|
+
return new Response(Bun.file(fallbackResolved));
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return new Response('Not Found', { status: HttpStatusCode.NOT_FOUND });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const file = Bun.file(resolvedPath);
|
|
1089
|
+
const exists = await file.exists();
|
|
1090
|
+
if (cache && staticCacheTtlMs > 0) {
|
|
1091
|
+
await cache.set(cacheKey, exists, staticCacheTtlMs);
|
|
1092
|
+
}
|
|
1093
|
+
if (exists) {
|
|
1094
|
+
return new Response(file);
|
|
1095
|
+
}
|
|
1096
|
+
if (staticFallbackFile) {
|
|
1097
|
+
const fallbackResolved = resolvePathUnderRoot(staticRootResolved, staticFallbackFile);
|
|
1098
|
+
if (fallbackResolved !== null) {
|
|
1099
|
+
const fallbackFile = Bun.file(fallbackResolved);
|
|
1100
|
+
if (await fallbackFile.exists()) {
|
|
1101
|
+
return new Response(fallbackFile);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return new Response('Not Found', { status: HttpStatusCode.NOT_FOUND });
|
|
1107
|
+
}
|
|
1108
|
+
|
|
931
1109
|
// 404 for everything not matched by routes
|
|
932
1110
|
return new Response('Not Found', { status: HttpStatusCode.NOT_FOUND });
|
|
933
1111
|
},
|
|
@@ -949,6 +1127,9 @@ export class OneBunApplication {
|
|
|
949
1127
|
}
|
|
950
1128
|
|
|
951
1129
|
this.logger.info(`Server started on http://${this.options.host}:${this.options.port}`);
|
|
1130
|
+
if (staticRootResolved) {
|
|
1131
|
+
this.logger.info(`Static files served from ${staticRootResolved}`);
|
|
1132
|
+
}
|
|
952
1133
|
if (this.metricsService) {
|
|
953
1134
|
this.logger.info(
|
|
954
1135
|
`Metrics available at http://${this.options.host}:${this.options.port}${metricsPath}`,
|
|
@@ -1477,6 +1658,7 @@ export class OneBunApplication {
|
|
|
1477
1658
|
await this.queueService.stop();
|
|
1478
1659
|
this.queueService = null;
|
|
1479
1660
|
}
|
|
1661
|
+
this.queueServiceProxy?.setDelegate(null);
|
|
1480
1662
|
|
|
1481
1663
|
// Disconnect queue adapter
|
|
1482
1664
|
if (this.queueAdapter) {
|
|
@@ -1538,43 +1720,57 @@ export class OneBunApplication {
|
|
|
1538
1720
|
}
|
|
1539
1721
|
|
|
1540
1722
|
// Create the appropriate adapter
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1723
|
+
const adapterOpt = queueOptions?.adapter ?? 'memory';
|
|
1724
|
+
|
|
1725
|
+
if (typeof adapterOpt === 'function') {
|
|
1726
|
+
// Custom adapter constructor (e.g. NATS JetStream)
|
|
1727
|
+
const adapterCtor = adapterOpt;
|
|
1728
|
+
this.queueAdapter = new adapterCtor(queueOptions?.options);
|
|
1729
|
+
await this.queueAdapter.connect();
|
|
1730
|
+
this.logger.info(`Queue system initialized with custom adapter: ${this.queueAdapter.name}`);
|
|
1731
|
+
} else {
|
|
1732
|
+
const adapterType = adapterOpt;
|
|
1733
|
+
if (adapterType === 'memory') {
|
|
1734
|
+
this.queueAdapter = new InMemoryQueueAdapter();
|
|
1735
|
+
this.logger.info('Queue system initialized with in-memory adapter');
|
|
1736
|
+
} else if (adapterType === 'redis') {
|
|
1737
|
+
const redisOptions = queueOptions?.redis ?? {};
|
|
1738
|
+
if (redisOptions.useSharedProvider !== false) {
|
|
1739
|
+
// Use shared Redis provider
|
|
1740
|
+
this.queueAdapter = new RedisQueueAdapter({
|
|
1741
|
+
useSharedClient: true,
|
|
1742
|
+
keyPrefix: redisOptions.prefix ?? 'onebun:queue:',
|
|
1743
|
+
});
|
|
1744
|
+
this.logger.info('Queue system initialized with Redis adapter (shared provider)');
|
|
1745
|
+
} else if (redisOptions.url) {
|
|
1746
|
+
// Create dedicated Redis connection
|
|
1747
|
+
this.queueAdapter = new RedisQueueAdapter({
|
|
1748
|
+
useSharedClient: false,
|
|
1749
|
+
url: redisOptions.url,
|
|
1750
|
+
keyPrefix: redisOptions.prefix ?? 'onebun:queue:',
|
|
1751
|
+
});
|
|
1752
|
+
this.logger.info('Queue system initialized with Redis adapter (dedicated connection)');
|
|
1753
|
+
} else {
|
|
1754
|
+
throw new Error('Redis queue adapter requires either useSharedProvider: true or a url');
|
|
1755
|
+
}
|
|
1563
1756
|
} else {
|
|
1564
|
-
throw new Error(
|
|
1757
|
+
throw new Error(`Unknown queue adapter type: ${adapterType}`);
|
|
1565
1758
|
}
|
|
1566
|
-
} else {
|
|
1567
|
-
throw new Error(`Unknown queue adapter type: ${adapterType}`);
|
|
1568
|
-
}
|
|
1569
1759
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1760
|
+
// Connect the adapter
|
|
1761
|
+
await this.queueAdapter.connect();
|
|
1762
|
+
}
|
|
1572
1763
|
|
|
1573
1764
|
// Create queue service with config
|
|
1574
|
-
|
|
1575
|
-
adapter:
|
|
1576
|
-
|
|
1577
|
-
|
|
1765
|
+
const queueServiceConfig: QueueConfig = {
|
|
1766
|
+
adapter:
|
|
1767
|
+
typeof adapterOpt === 'function' ? this.queueAdapter.type : adapterOpt,
|
|
1768
|
+
options:
|
|
1769
|
+
typeof adapterOpt === 'function'
|
|
1770
|
+
? (queueOptions?.options as Record<string, unknown> | undefined)
|
|
1771
|
+
: queueOptions?.redis,
|
|
1772
|
+
};
|
|
1773
|
+
this.queueService = new QueueService(queueServiceConfig);
|
|
1578
1774
|
|
|
1579
1775
|
// Initialize with the adapter
|
|
1580
1776
|
await this.queueService.initialize(this.queueAdapter);
|
|
@@ -1599,6 +1795,9 @@ export class OneBunApplication {
|
|
|
1599
1795
|
// Start the queue service
|
|
1600
1796
|
await this.queueService.start();
|
|
1601
1797
|
this.logger.info('Queue service started');
|
|
1798
|
+
|
|
1799
|
+
// Wire the real QueueService into the DI proxy so injected consumers use it
|
|
1800
|
+
this.queueServiceProxy?.setDelegate(this.queueService);
|
|
1602
1801
|
}
|
|
1603
1802
|
|
|
1604
1803
|
/**
|
|
@@ -181,6 +181,21 @@ describe('MultiServiceApplication', () => {
|
|
|
181
181
|
});
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
describe('queue option', () => {
|
|
185
|
+
test('should accept queue option and pass it to child applications', () => {
|
|
186
|
+
const app = new MultiServiceApplication({
|
|
187
|
+
services: {
|
|
188
|
+
serviceA: { module: TestModuleA, port: 3001 },
|
|
189
|
+
},
|
|
190
|
+
queue: {
|
|
191
|
+
enabled: true,
|
|
192
|
+
adapter: 'memory',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
expect(app).toBeDefined();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
184
199
|
describe('filtering configuration', () => {
|
|
185
200
|
test('should accept enabledServices option', () => {
|
|
186
201
|
const app = new MultiServiceApplication({
|
|
@@ -219,6 +219,8 @@ export class MultiServiceApplication<TServices extends ServicesMap = ServicesMap
|
|
|
219
219
|
...mergedOptions.tracing,
|
|
220
220
|
serviceName: name,
|
|
221
221
|
},
|
|
222
|
+
queue: this.options.queue,
|
|
223
|
+
static: mergedOptions.static ?? serviceConfig.static,
|
|
222
224
|
});
|
|
223
225
|
|
|
224
226
|
this.applications.set(name, app);
|
|
@@ -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' | 'middleware'> {
|
|
33
|
+
extends Pick<ApplicationOptions, 'host' | 'basePath' | 'metrics' | 'tracing' | 'middleware' | 'static'> {
|
|
34
34
|
/**
|
|
35
35
|
* Add service name as prefix to all routes.
|
|
36
36
|
* When true, the service name will be used as routePrefix.
|
|
@@ -132,4 +132,10 @@ export interface MultiServiceApplicationOptions<TServices extends ServicesMap =
|
|
|
132
132
|
* ```
|
|
133
133
|
*/
|
|
134
134
|
externalServiceUrls?: Partial<Record<keyof TServices, string>>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Queue configuration applied to all services.
|
|
138
|
+
* When set, each service's OneBunApplication receives this queue config.
|
|
139
|
+
*/
|
|
140
|
+
queue?: ApplicationOptions['queue'];
|
|
135
141
|
}
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
Req,
|
|
53
53
|
Res,
|
|
54
54
|
UseMiddleware,
|
|
55
|
+
Middleware,
|
|
55
56
|
Module,
|
|
56
57
|
getModuleMetadata,
|
|
57
58
|
ApiResponse,
|
|
@@ -301,6 +302,33 @@ describe('decorators', () => {
|
|
|
301
302
|
});
|
|
302
303
|
});
|
|
303
304
|
|
|
305
|
+
describe('Middleware decorator', () => {
|
|
306
|
+
test('should emit design:paramtypes for automatic DI', () => {
|
|
307
|
+
@Service()
|
|
308
|
+
class HelperService {
|
|
309
|
+
getValue() {
|
|
310
|
+
return 'ok';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@Middleware()
|
|
315
|
+
class TestMiddleware extends BaseMiddleware {
|
|
316
|
+
constructor(private readonly helper: HelperService) {
|
|
317
|
+
super();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async use(_req: OneBunRequest, next: () => Promise<OneBunResponse>) {
|
|
321
|
+
return await next();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const deps = getConstructorParamTypes(TestMiddleware);
|
|
326
|
+
expect(deps).toBeDefined();
|
|
327
|
+
expect(deps).toHaveLength(1);
|
|
328
|
+
expect(deps?.[0]).toBe(HelperService);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
304
332
|
describe('HTTP method decorators', () => {
|
|
305
333
|
test('should register GET route', () => {
|
|
306
334
|
@Controller('test')
|
|
@@ -240,6 +240,32 @@ export function Inject<T>(serviceType: new (...args: any[]) => T) {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Class decorator for middleware. Apply to classes that extend BaseMiddleware so that
|
|
245
|
+
* TypeScript emits design:paramtypes and constructor dependencies are resolved automatically
|
|
246
|
+
* by the framework (no need for @Inject on each parameter). You can still use @Inject when needed.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* @Middleware()
|
|
251
|
+
* class AuthMiddleware extends BaseMiddleware {
|
|
252
|
+
* constructor(private authService: AuthService) {
|
|
253
|
+
* super();
|
|
254
|
+
* }
|
|
255
|
+
* async use(req, next) { ... }
|
|
256
|
+
* }
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
260
|
+
export function Middleware(): ClassDecorator {
|
|
261
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
262
|
+
return (target: any): any => {
|
|
263
|
+
injectable()(target);
|
|
264
|
+
|
|
265
|
+
return target;
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
243
269
|
/**
|
|
244
270
|
* Register dependencies manually (fallback method)
|
|
245
271
|
*/
|
|
@@ -1648,7 +1648,7 @@ describe('Lifecycle Hooks API Documentation Examples (docs/api/services.md)', ()
|
|
|
1648
1648
|
|
|
1649
1649
|
describe('Controller Lifecycle Hooks', () => {
|
|
1650
1650
|
/**
|
|
1651
|
-
* @source docs/api/
|
|
1651
|
+
* @source docs/api/services.md#lifecycle-hooks (controllers support the same hooks)
|
|
1652
1652
|
*/
|
|
1653
1653
|
it('should implement lifecycle hooks in controllers', () => {
|
|
1654
1654
|
// From docs: Controller lifecycle hooks example
|
|
@@ -2406,6 +2406,34 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
2406
2406
|
const app = new OneBunApplication(AppModule, { tracing: tracingOptions, loggerLayer: makeMockLoggerLayer() });
|
|
2407
2407
|
expect(app).toBeDefined();
|
|
2408
2408
|
});
|
|
2409
|
+
|
|
2410
|
+
/**
|
|
2411
|
+
* @source docs/api/core.md#staticapplicationoptions
|
|
2412
|
+
*/
|
|
2413
|
+
it('should accept static file serving configuration (SPA on same host)', async () => {
|
|
2414
|
+
const fs = await import('node:fs');
|
|
2415
|
+
const path = await import('node:path');
|
|
2416
|
+
const os = await import('node:os');
|
|
2417
|
+
|
|
2418
|
+
const tmpDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'onebun-docs-static-'));
|
|
2419
|
+
try {
|
|
2420
|
+
fs.writeFileSync(path.join(tmpDir, 'index.html'), '<!DOCTYPE html><html><body>SPA</body></html>', 'utf8');
|
|
2421
|
+
|
|
2422
|
+
@Module({ controllers: [] })
|
|
2423
|
+
class AppModule {}
|
|
2424
|
+
|
|
2425
|
+
// From docs: Static files (SPA on same host)
|
|
2426
|
+
const app = new OneBunApplication(AppModule, {
|
|
2427
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2428
|
+
static: { root: tmpDir, fallbackFile: 'index.html' },
|
|
2429
|
+
});
|
|
2430
|
+
expect(app).toBeDefined();
|
|
2431
|
+
await app.start();
|
|
2432
|
+
await app.stop();
|
|
2433
|
+
} finally {
|
|
2434
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
2435
|
+
}
|
|
2436
|
+
});
|
|
2409
2437
|
});
|
|
2410
2438
|
|
|
2411
2439
|
describe('MultiServiceApplication (docs/api/core.md)', () => {
|