@onebun/core 0.2.6 → 0.2.8
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 +350 -7
- package/src/application/application.ts +537 -254
- 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.ts +213 -0
- package/src/docs-examples.test.ts +386 -3
- package/src/exception-filters/exception-filters.test.ts +172 -0
- package/src/exception-filters/exception-filters.ts +129 -0
- package/src/exception-filters/http-exception.ts +22 -0
- package/src/exception-filters/index.ts +2 -0
- package/src/file/onebun-file.ts +8 -2
- package/src/http-guards/http-guards.test.ts +230 -0
- package/src/http-guards/http-guards.ts +173 -0
- package/src/http-guards/index.ts +1 -0
- package/src/index.ts +10 -0
- package/src/module/module.test.ts +78 -0
- package/src/module/module.ts +55 -7
- 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/security/cors-middleware.ts +212 -0
- package/src/security/index.ts +19 -0
- package/src/security/rate-limit-middleware.ts +276 -0
- package/src/security/security-headers-middleware.ts +188 -0
- package/src/security/security.test.ts +285 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/testing-module.test.ts +199 -0
- package/src/testing/testing-module.ts +252 -0
- package/src/types.ts +153 -3
|
@@ -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
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import './metadata'; // Import polyfill first
|
|
2
2
|
import { type, type Type } from 'arktype';
|
|
3
3
|
|
|
4
|
+
import type { ExceptionFilter } from '../exception-filters/exception-filters';
|
|
5
|
+
|
|
4
6
|
import {
|
|
5
7
|
type ControllerMetadata,
|
|
6
8
|
type FileUploadOptions,
|
|
7
9
|
type FilesUploadOptions,
|
|
8
10
|
HttpMethod,
|
|
11
|
+
type HttpGuard,
|
|
9
12
|
type ParamDecoratorOptions,
|
|
10
13
|
type ParamMetadata,
|
|
11
14
|
ParamType,
|
|
@@ -213,6 +216,30 @@ export function controllerDecorator(basePath: string = '') {
|
|
|
213
216
|
);
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
// Copy controller-level guards from original class to wrapped class
|
|
220
|
+
// This ensures @UseGuards works regardless of decorator order
|
|
221
|
+
const existingControllerGuards: (Function | HttpGuard)[] | undefined =
|
|
222
|
+
Reflect.getMetadata(HTTP_CONTROLLER_GUARDS_METADATA, target);
|
|
223
|
+
if (existingControllerGuards) {
|
|
224
|
+
Reflect.defineMetadata(
|
|
225
|
+
HTTP_CONTROLLER_GUARDS_METADATA,
|
|
226
|
+
existingControllerGuards,
|
|
227
|
+
WrappedController,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Copy controller-level exception filters from original class to wrapped class
|
|
232
|
+
// This ensures @UseFilters works regardless of decorator order
|
|
233
|
+
const existingControllerFilters: ExceptionFilter[] | undefined =
|
|
234
|
+
Reflect.getMetadata(CONTROLLER_EXCEPTION_FILTERS_METADATA, target);
|
|
235
|
+
if (existingControllerFilters) {
|
|
236
|
+
Reflect.defineMetadata(
|
|
237
|
+
CONTROLLER_EXCEPTION_FILTERS_METADATA,
|
|
238
|
+
existingControllerFilters,
|
|
239
|
+
WrappedController,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
216
243
|
return WrappedController as T;
|
|
217
244
|
};
|
|
218
245
|
}
|
|
@@ -330,6 +357,14 @@ function createRouteDecorator(method: HttpMethod) {
|
|
|
330
357
|
const middleware: Function[] =
|
|
331
358
|
Reflect.getMetadata(MIDDLEWARE_METADATA, target, propertyKey) || [];
|
|
332
359
|
|
|
360
|
+
// Get HTTP guards metadata if exists
|
|
361
|
+
const guards: (Function | HttpGuard)[] =
|
|
362
|
+
Reflect.getMetadata(HTTP_GUARDS_METADATA, target, propertyKey) || [];
|
|
363
|
+
|
|
364
|
+
// Get exception filters metadata if exists
|
|
365
|
+
const filters: ExceptionFilter[] =
|
|
366
|
+
Reflect.getMetadata(EXCEPTION_FILTERS_METADATA, target, propertyKey) || [];
|
|
367
|
+
|
|
333
368
|
// Get response schemas metadata if exists
|
|
334
369
|
const responseSchemas: Array<{
|
|
335
370
|
statusCode: number;
|
|
@@ -343,6 +378,8 @@ function createRouteDecorator(method: HttpMethod) {
|
|
|
343
378
|
handler: propertyKey,
|
|
344
379
|
params,
|
|
345
380
|
middleware,
|
|
381
|
+
guards,
|
|
382
|
+
filters,
|
|
346
383
|
responseSchemas: responseSchemas.map((rs) => ({
|
|
347
384
|
statusCode: rs.statusCode,
|
|
348
385
|
schema: rs.schema,
|
|
@@ -679,6 +716,26 @@ export function FormField(fieldName: string, options?: ParamDecoratorOptions): P
|
|
|
679
716
|
*/
|
|
680
717
|
const CONTROLLER_MIDDLEWARE_METADATA = 'onebun:controller_middleware';
|
|
681
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Metadata key for route-level HTTP guards
|
|
721
|
+
*/
|
|
722
|
+
const HTTP_GUARDS_METADATA = 'onebun:http_guards';
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Metadata key for controller-level HTTP guards
|
|
726
|
+
*/
|
|
727
|
+
const HTTP_CONTROLLER_GUARDS_METADATA = 'onebun:controller_http_guards';
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Metadata key for route-level exception filters
|
|
731
|
+
*/
|
|
732
|
+
const EXCEPTION_FILTERS_METADATA = 'onebun:exception_filters';
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Metadata key for controller-level exception filters
|
|
736
|
+
*/
|
|
737
|
+
const CONTROLLER_EXCEPTION_FILTERS_METADATA = 'onebun:controller_exception_filters';
|
|
738
|
+
|
|
682
739
|
/**
|
|
683
740
|
* Middleware decorator — can be applied to both controllers (class) and individual routes (method).
|
|
684
741
|
*
|
|
@@ -765,6 +822,162 @@ export function getControllerMiddleware(target: Function): Function[] {
|
|
|
765
822
|
return Reflect.getMetadata(CONTROLLER_MIDDLEWARE_METADATA, target) || [];
|
|
766
823
|
}
|
|
767
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Get controller-level HTTP guards for a controller class.
|
|
827
|
+
* Returns guards registered via @UseGuards() applied to the class.
|
|
828
|
+
*
|
|
829
|
+
* @param target - Controller class (constructor)
|
|
830
|
+
* @returns Array of guard class constructors or instances
|
|
831
|
+
*/
|
|
832
|
+
export function getControllerGuards(target: Function): (Function | HttpGuard)[] {
|
|
833
|
+
return Reflect.getMetadata(HTTP_CONTROLLER_GUARDS_METADATA, target) || [];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Guards decorator — can be applied to both controllers (class) and individual routes (method).
|
|
838
|
+
*
|
|
839
|
+
* Pass guard **class constructors** or **instances**. Class constructors are instantiated
|
|
840
|
+
* for each request; instances are reused.
|
|
841
|
+
*
|
|
842
|
+
* Guards run after the middleware chain but before the route handler.
|
|
843
|
+
* If any guard returns false, the request is rejected with a 403 Forbidden response.
|
|
844
|
+
*
|
|
845
|
+
* Execution order: middleware → guards → handler
|
|
846
|
+
*
|
|
847
|
+
* @example Class-level (all routes)
|
|
848
|
+
* ```typescript
|
|
849
|
+
* \@Controller('/admin')
|
|
850
|
+
* \@UseGuards(AuthGuard)
|
|
851
|
+
* class AdminController extends BaseController { ... }
|
|
852
|
+
* ```
|
|
853
|
+
*
|
|
854
|
+
* @example Method-level (single route)
|
|
855
|
+
* ```typescript
|
|
856
|
+
* \@Get('/dashboard')
|
|
857
|
+
* \@UseGuards(new RolesGuard(['admin']))
|
|
858
|
+
* getDashboard() { ... }
|
|
859
|
+
* ```
|
|
860
|
+
*
|
|
861
|
+
* @example Combined (class guard + route guard)
|
|
862
|
+
* ```typescript
|
|
863
|
+
* \@Controller('/api')
|
|
864
|
+
* \@UseGuards(AuthGuard) // runs on every route
|
|
865
|
+
* class ApiController extends BaseController {
|
|
866
|
+
* \@Get('/admin')
|
|
867
|
+
* \@UseGuards(new RolesGuard(['admin'])) // runs only here, after AuthGuard
|
|
868
|
+
* getAdmin() { ... }
|
|
869
|
+
* }
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
873
|
+
export function UseGuards(...guards: (Function | HttpGuard)[]): any {
|
|
874
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
875
|
+
return function useGuardsDecorator(...args: any[]): any {
|
|
876
|
+
// ---- Class decorator: target is a constructor function ----
|
|
877
|
+
if (args.length === 1 && typeof args[0] === 'function') {
|
|
878
|
+
const target = args[0] as Function;
|
|
879
|
+
const existing: (Function | HttpGuard)[] =
|
|
880
|
+
Reflect.getMetadata(HTTP_CONTROLLER_GUARDS_METADATA, target) || [];
|
|
881
|
+
Reflect.defineMetadata(
|
|
882
|
+
HTTP_CONTROLLER_GUARDS_METADATA,
|
|
883
|
+
[...existing, ...guards],
|
|
884
|
+
target,
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
return target;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ---- Method decorator: (target, propertyKey, descriptor) ----
|
|
891
|
+
const [target, propertyKey, descriptor] = args as [
|
|
892
|
+
object,
|
|
893
|
+
string | symbol,
|
|
894
|
+
PropertyDescriptor,
|
|
895
|
+
];
|
|
896
|
+
const existingGuards: (Function | HttpGuard)[] =
|
|
897
|
+
Reflect.getMetadata(HTTP_GUARDS_METADATA, target, propertyKey) || [];
|
|
898
|
+
Reflect.defineMetadata(
|
|
899
|
+
HTTP_GUARDS_METADATA,
|
|
900
|
+
[...existingGuards, ...guards],
|
|
901
|
+
target,
|
|
902
|
+
propertyKey,
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
return descriptor;
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Get controller-level exception filters for a controller class.
|
|
911
|
+
* Returns filters registered via @UseFilters() applied to the class.
|
|
912
|
+
*
|
|
913
|
+
* @param target - Controller class (constructor)
|
|
914
|
+
* @returns Array of exception filter instances
|
|
915
|
+
*/
|
|
916
|
+
export function getControllerFilters(target: Function): ExceptionFilter[] {
|
|
917
|
+
return Reflect.getMetadata(CONTROLLER_EXCEPTION_FILTERS_METADATA, target) || [];
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Exception Filters decorator — can be applied to both controllers (class) and individual routes (method).
|
|
922
|
+
*
|
|
923
|
+
* Pass filter **instances** (from `createExceptionFilter()` or implementing `ExceptionFilter`).
|
|
924
|
+
* Route-level filters take priority over controller-level filters.
|
|
925
|
+
*
|
|
926
|
+
* Filters run in order: controller-level first, then route-level.
|
|
927
|
+
* The first filter in the combined list catches the error.
|
|
928
|
+
* If no filters are registered, the default filter handles the error.
|
|
929
|
+
*
|
|
930
|
+
* @example Class-level (all routes)
|
|
931
|
+
* ```typescript
|
|
932
|
+
* \@Controller('/api')
|
|
933
|
+
* \@UseFilters(new HttpExceptionFilter())
|
|
934
|
+
* class ApiController extends BaseController { ... }
|
|
935
|
+
* ```
|
|
936
|
+
*
|
|
937
|
+
* @example Method-level (single route)
|
|
938
|
+
* ```typescript
|
|
939
|
+
* \@Get('/risky')
|
|
940
|
+
* \@UseFilters(createExceptionFilter((err, ctx) => new Response('Custom error', { status: 500 })))
|
|
941
|
+
* riskyRoute() { ... }
|
|
942
|
+
* ```
|
|
943
|
+
*/
|
|
944
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
945
|
+
export function UseFilters(...filters: ExceptionFilter[]): any {
|
|
946
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
947
|
+
return function useFiltersDecorator(...args: any[]): any {
|
|
948
|
+
// ---- Class decorator: target is a constructor function ----
|
|
949
|
+
if (args.length === 1 && typeof args[0] === 'function') {
|
|
950
|
+
const target = args[0] as Function;
|
|
951
|
+
const existing: ExceptionFilter[] =
|
|
952
|
+
Reflect.getMetadata(CONTROLLER_EXCEPTION_FILTERS_METADATA, target) || [];
|
|
953
|
+
Reflect.defineMetadata(
|
|
954
|
+
CONTROLLER_EXCEPTION_FILTERS_METADATA,
|
|
955
|
+
[...existing, ...filters],
|
|
956
|
+
target,
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
return target;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// ---- Method decorator: (target, propertyKey, descriptor) ----
|
|
963
|
+
const [target, propertyKey, descriptor] = args as [
|
|
964
|
+
object,
|
|
965
|
+
string | symbol,
|
|
966
|
+
PropertyDescriptor,
|
|
967
|
+
];
|
|
968
|
+
const existingFilters: ExceptionFilter[] =
|
|
969
|
+
Reflect.getMetadata(EXCEPTION_FILTERS_METADATA, target, propertyKey) || [];
|
|
970
|
+
Reflect.defineMetadata(
|
|
971
|
+
EXCEPTION_FILTERS_METADATA,
|
|
972
|
+
[...existingFilters, ...filters],
|
|
973
|
+
target,
|
|
974
|
+
propertyKey,
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
return descriptor;
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
768
981
|
/**
|
|
769
982
|
* HTTP GET decorator
|
|
770
983
|
*/
|