@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.
Files changed (33) hide show
  1. package/package.json +6 -6
  2. package/src/application/application.test.ts +350 -7
  3. package/src/application/application.ts +537 -254
  4. package/src/application/multi-service-application.test.ts +15 -0
  5. package/src/application/multi-service-application.ts +2 -0
  6. package/src/application/multi-service.types.ts +7 -1
  7. package/src/decorators/decorators.ts +213 -0
  8. package/src/docs-examples.test.ts +386 -3
  9. package/src/exception-filters/exception-filters.test.ts +172 -0
  10. package/src/exception-filters/exception-filters.ts +129 -0
  11. package/src/exception-filters/http-exception.ts +22 -0
  12. package/src/exception-filters/index.ts +2 -0
  13. package/src/file/onebun-file.ts +8 -2
  14. package/src/http-guards/http-guards.test.ts +230 -0
  15. package/src/http-guards/http-guards.ts +173 -0
  16. package/src/http-guards/index.ts +1 -0
  17. package/src/index.ts +10 -0
  18. package/src/module/module.test.ts +78 -0
  19. package/src/module/module.ts +55 -7
  20. package/src/queue/docs-examples.test.ts +72 -12
  21. package/src/queue/index.ts +4 -0
  22. package/src/queue/queue-service-proxy.test.ts +82 -0
  23. package/src/queue/queue-service-proxy.ts +114 -0
  24. package/src/queue/types.ts +2 -2
  25. package/src/security/cors-middleware.ts +212 -0
  26. package/src/security/index.ts +19 -0
  27. package/src/security/rate-limit-middleware.ts +276 -0
  28. package/src/security/security-headers-middleware.ts +188 -0
  29. package/src/security/security.test.ts +285 -0
  30. package/src/testing/index.ts +1 -0
  31. package/src/testing/testing-module.test.ts +199 -0
  32. package/src/testing/testing-module.ts +252 -0
  33. 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
  */