@mxweb/core 1.1.0 → 1.2.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-01-03
9
+
10
+ ### Added
11
+
12
+ - **Feature**: Added feature-level guards, filters, interceptors, and pipes support
13
+ - `FeatureInitialize` now extends `DecoratorOptions` for full decorator support
14
+ - Features can now define their own guards, filters, interceptors, and pipes
15
+ - Execution order: Application → Feature → Route for guards and pipes
16
+ - Execution order: Route → Feature → Application for filters and interceptors
17
+ - Example:
18
+ ```ts
19
+ Feature.create({
20
+ controller: ProductController,
21
+ router: productRouter,
22
+ guards: [FeatureAuthGuard],
23
+ filters: [FeatureExceptionFilter],
24
+ interceptors: [FeatureLoggingInterceptor],
25
+ pipes: [FeatureValidationPipe],
26
+ });
27
+ ```
28
+
29
+ - **Common**: Added `DecoratorOptions` interface for shared decorator configuration
30
+ - Provides a common base for guards, filters, interceptors, and pipes configuration
31
+ - Used by both `ApplicationOptions` and `FeatureInitialize`
32
+ - Properties: `guards?: Guard[]`, `filters?: Filter[]`, `interceptors?: Interceptor[]`, `pipes?: Pipe[]`
33
+
34
+ - **Common**: Added `FeatureContext` interface to avoid circular dependencies
35
+ - Abstract interface for feature context accessible from `ExecuteContext`
36
+ - Methods: `hasInject()`, `getInject()`, `getInjectSync()`
37
+ - `RequestContext.feature` now uses `FeatureContext` type
38
+
39
+ - **Common**: Moved `Guard`, `Filter`, `Interceptor` type definitions to `common.ts`
40
+ - Centralized type definitions for better code organization
41
+ - Types are no longer re-exported from `execute.ts`
42
+
43
+ - **Response**: Added `CoreResponseInterceptorHandler` interface
44
+ - Specialized interface for interceptors working with `CoreResponse`
45
+ - Separated from generic `ResponseInterceptorHandler` in `common.ts`
46
+
47
+ ### Changed
48
+
49
+ - **Application**: Updated to include feature-level decorators in execution chains
50
+ - `executeGuards()` now includes feature guards: App → Feature → Route
51
+ - `executeFilters()` now includes feature filters: Route → Feature → App
52
+ - `executeInterceptors()` now includes feature interceptors: Route → Feature → App
53
+ - `executePipes()` now includes feature pipes: App → Feature → Route
54
+
55
+ - **ExecuteContext**: Simplified `getFeature()` method signature
56
+ - Now uses generic `<Feature extends FeatureContext>` for type flexibility
57
+ - Returns `FeatureContext` by default, can be cast to specific Feature type
58
+
59
+ ### Fixed
60
+
61
+ - **Circular Dependencies**: Resolved all circular import issues
62
+ - Removed `Feature` import from `execute.ts` (was causing circular: execute → feature → controller/router → execute)
63
+ - Introduced `FeatureContext` interface in `common.ts` as abstraction
64
+ - All 6 circular dependencies have been eliminated
65
+
66
+ ### Internal
67
+
68
+ - **Type Simplification**: Simplified generic types in decorator interfaces
69
+ - `ResponseInterceptorHandler` now uses single generic `<Response>`
70
+ - `Interceptor` type simplified to single generic
71
+ - `DecoratorOptions` no longer requires generic parameters
72
+
8
73
  ## [1.1.0] - 2026-01-02
9
74
 
10
75
  ### ⚠️ Breaking Changes
@@ -1,8 +1,8 @@
1
1
  import { Feature } from "./feature";
2
2
  import { CoreResponse, ResponseClass } from "./response";
3
- import { CoreRequest, Filter, Guard, Interceptor } from "./execute";
3
+ import { CoreRequest } from "./execute";
4
4
  import { ApplicationHooksOptions } from "./hooks";
5
- import { ApplicationInject, InjectEntry, InjectFactory, InjectRegistry, Pipe, RoutePayload } from "./common";
5
+ import { ApplicationInject, DecoratorOptions, InjectEntry, InjectFactory, InjectRegistry, RoutePayload } from "./common";
6
6
  /**
7
7
  * Type for application-level dependency injection configuration.
8
8
  * An array of inject factory functions that register injects via InjectRegistry.
@@ -75,7 +75,7 @@ export type ApplicationCors = string[] | ((registry: InjectRegistry) => string[]
75
75
  * };
76
76
  * ```
77
77
  */
78
- export interface ApplicationOptions<Key extends string = "path"> extends ApplicationHooksOptions {
78
+ export interface ApplicationOptions<Key extends string = "path"> extends ApplicationHooksOptions, DecoratorOptions {
79
79
  /** The key used to extract path segments from catch-all route params. Defaults to "path". */
80
80
  key?: Key;
81
81
  /**
@@ -107,14 +107,6 @@ export interface ApplicationOptions<Key extends string = "path"> extends Applica
107
107
  poweredBy?: string | false;
108
108
  /** Array of inject factories for dependency injection */
109
109
  injects?: ApplicationInjects;
110
- /** Global guards applied to all routes */
111
- guards?: Guard[];
112
- /** Global exception filters applied to all routes */
113
- filters?: Filter[];
114
- /** Global interceptors applied to all routes */
115
- interceptors?: Interceptor[];
116
- /** Global pipes applied to all routes */
117
- pipes?: Pipe[];
118
110
  /** CORS configuration - allowed origins or resolver function */
119
111
  cors?: ApplicationCors;
120
112
  }
@@ -1 +1 @@
1
- "use strict";var e=require("./error.js"),t=require("./context.js"),s=require("./response.js"),r=require("./hooks.js"),o=require("./logger.js"),n=require("./common.js");const i={key:"path"};class a{static toResponse(e){return!1!==this.poweredBy&&e.headers.set("X-Powered-By",this.poweredBy),this.ResponseClass.json(e.body,{status:e.status,statusText:e.statusText,headers:e.headers})}constructor(e,t,o){this.request=e,this.method=t,this.payload=o,this.feature=null,this.route=null,this.response=new s.ServerResponse,this.requestHooks=new r.RequestHooks(a.hooks)}static getInject(e){return t.executeContext.getInject(e)}static getInjectSync(e){return t.executeContext.getInjectSync(e)}static getInjects(){return new Map(t.executeContext.injections)}static createInjectRegistry(){return this.registryCreated||(this.registry={get:async e=>t.executeContext.getInject(e),set(e,s){t.executeContext.setInject(e,s)}},t.executeContext.setRegistry(this.registry),this.registryCreated=!0),this.registry}static async registerInjectFactories(){if(this.factoriesRegistered)return;const e=this.options.injects;if(!e||!e.length)return void(this.factoriesRegistered=!0);const t=this.createInjectRegistry();for(const s of e)await s(t);this.factoriesRegistered=!0,await this.initializeInjects(),this.registerShutdown(),await this.loadCors()}static async initializeInjects(){for(const[e,s]of t.executeContext.injections)if(!s.instance)try{const e=this.createInjectRegistry();s.instance=await s.factory(e),s.instance&&"function"==typeof s.instance.onInit&&await s.instance.onInit()}catch(t){throw a.logger.error(`Error initializing inject ${e}:`,t),t}}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{a.logger.info("Shutting down...");for(const e of this.features)await e.destroyInjects();for(const[e,s]of t.executeContext.injections)if(s.instance&&"function"==typeof s.instance.onDestroy)try{await s.instance.onDestroy(),a.logger.info(`Destroyed inject: ${e}`)}catch(t){a.logger.error(`Error destroying inject ${e}:`,t)}a.logger.info("Shutdown complete"),process.exit(0)};process.on("SIGTERM",e),process.on("SIGINT",e)}static async loadCors(){const e=this.options.cors;e&&(Array.isArray(e)?this.corsOrigins=e:this.corsOrigins=await e(this.registry))}static async loadImports(){if(!this.initialized)return this.initPromise||(this.initPromise=(async()=>{this.initialized=!0})()),this.initPromise}isMatchFeature(e,t){for(const s of a.features)if(this.route=s.matchRoute(e,t),this.route)return this.feature=s,!0;return null}async executeGuards(){const e=this.route?.route.getReflect().getGuards()??new Set,s=[...a.guards,...e];if(!s.length)return!0;for(const e of s)try{const s=new e;if(!await s.canActivate(t.executeContext))return!1}catch{return!1}return!0}async executeMiddlewares(){const e=this.route.route.getMiddlewares();if(!e.length)return!0;for(const s of e){let e=!1;const r=()=>{e=!0};try{if(await s(t.executeContext,r),!e)return!1}catch{return!1}}return!0}async executeFilters(e){const r=[...this.route?.route.getReflect().getFilters()??new Set,...a.filters];for(const o of r)try{const r=new o,n=await r.catch(e,t.executeContext);if(n instanceof s.CoreResponse||n instanceof Response)return n}catch{continue}return null}async executeInterceptors(e){const t=[...this.route?.route.getReflect().getInterceptors()??new Set,...a.interceptors];if(!t.length)return e;let s=e;for(const e of t){const t=new e;s=await t.transform(s)}return s}async executePipes(e){const t=this.route?.route.getReflect().getPipes()??new Set,s=[...a.pipes,...t];if(!s.length)return e;let r=e;for(const e of s){const t=new e;r=await t.transform(r)}return r}checkCors(){const e=a.corsOrigins;if(!e.length)return!0;const t=this.request.headers.get("origin");return!t||(e.includes("*")||e.includes(t))}applyCorsHeaders(e){const t=a.corsOrigins;if(!t.length)return e;const s=this.request.headers.get("origin");return s?(t.includes("*")?e.headers.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(e.headers.set("Access-Control-Allow-Origin",s),e.headers.set("Vary","Origin")),e.headers.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),e.headers.set("Access-Control-Allow-Headers","Content-Type, Authorization"),e.headers.set("Access-Control-Max-Age","86400"),e):e}async dispatch(){const r=this.route,o=this.feature,n=t.executeContext.getContextOrThrow();try{if(!await this.executeGuards())return this.response.forbidden();if(!await this.executeMiddlewares())return this.response.forbidden();const e=r.route.getAction();let t=null;if("string"==typeof e){const s=o.getController();"function"==typeof s._initDependencies&&await s._initDependencies(),"function"==typeof s[e]&&(t=s[e].bind(s))}else"function"==typeof e&&(t=e);if(!t)return this.response.internalServer(new Error("Route action handler not found"));await this.executePipes(n);const s=await t(n);let i=this.response.success(s);return i=await this.executeInterceptors(i),i}catch(t){const r=await this.executeFilters(t);if(r)return r instanceof Response?s.CoreResponse.json({success:r.ok,message:r.statusText,code:"FILTER_RESPONSE",status:r.status,data:null,error:null},{status:r.status,statusText:r.statusText}):r;if(t instanceof e.HttpError)switch(t.statusCode){case 400:return this.response.badRequest(t.message,t.error);case 401:return this.response.unauthorized(t.message,t.error);case 403:return this.response.forbidden(t.message,t.error);case 404:return this.response.notFound(t.message,t.error);case 409:return this.response.conflict(t.message,t.error);case 422:return this.response.unprocessableEntity(t.message,t.error);case 429:return this.response.tooManyRequests(t.message,t.error);case 501:return this.response.notImplemented(t.message,t.error);case 502:return this.response.badGateway(t.message,t.error);case 503:return this.response.serviceUnavailable(t.message,t.error);case 504:return this.response.gatewayTimeout(t.message,t.error)}return a.logger.error("[dispatch] Error:",t instanceof Error?t.message:t),t instanceof Error&&t.stack&&a.logger.debug("Stack trace:",t.stack),this.response.internalServer(t)}}async match(){let e=null;try{if(!this.checkCors())return this.applyCorsHeaders(this.response.forbidden("CORS not allowed"));const s=await this.payload.params,r=a.options.key||i.key,o=s?.[r]?.join("/")||"";if(!this.isMatchFeature(this.method,o))return this.applyCorsHeaders(this.response.notFound("Route not found",{method:this.method,path:o,params:s}));await this.feature.loadInjects(),e={req:this.request,res:this.response,method:this.method,path:o,params:this.route.params,wildcards:this.route.wildcards,reflect:this.route.route.getReflect(),feature:this.feature},this.requestHooks.setFeatureHooks(this.feature.getHooks()),await this.requestHooks.featureRequest(e);const n=await t.executeContext.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(n)}catch(t){return e&&(await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e)),this.applyCorsHeaders(this.response.internalServer(t))}}static createHandler(e){return async(t,s)=>{await a.registerInjectFactories(),await a.loadImports();const r=new a(t,e,s);await r.requestHooks.appRequest(t,e);const o=await r.match.bind(r)();return a.toResponse(o)}}static create(e=i){return this.options=e,this.hooks=new r.ApplicationHooks(this.options),this.ResponseClass=e.response??Response,this.poweredBy=e.poweredBy??"MxWeb",e.guards&&e.guards.forEach(e=>this.guards.add(e)),e.filters&&e.filters.forEach(e=>this.filters.add(e)),e.interceptors&&e.interceptors.forEach(e=>this.interceptors.add(e)),e.pipes&&e.pipes.forEach(e=>this.pipes.add(e)),{[n.RouteMethod.GET]:a.createHandler(n.RouteMethod.GET),[n.RouteMethod.POST]:a.createHandler(n.RouteMethod.POST),[n.RouteMethod.PUT]:a.createHandler(n.RouteMethod.PUT),[n.RouteMethod.PATCH]:a.createHandler(n.RouteMethod.PATCH),[n.RouteMethod.DELETE]:a.createHandler(n.RouteMethod.DELETE),[n.RouteMethod.HEAD]:a.createHeadHandler(),[n.RouteMethod.OPTIONS]:a.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await a.registerInjectFactories(),await a.loadImports();const s=new a(e,n.RouteMethod.HEAD,t);await s.requestHooks.appRequest(e,n.RouteMethod.HEAD);const r=await s.match.bind(s)();return new Response(null,{status:r.status,statusText:r.statusText,headers:r.headers})}}static createOptionsHandler(){return async e=>{await a.registerInjectFactories();const t=this.corsOrigins,r=e.headers.get("origin"),o=s.ServerResponse.options();if(!t.length||!r)return o;const n=new Headers(o.headers);return t.includes("*")?n.set("Access-Control-Allow-Origin","*"):t.includes(r)&&(n.set("Access-Control-Allow-Origin",r),n.set("Vary","Origin")),n.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),n.set("Access-Control-Allow-Headers","Content-Type, Authorization"),n.set("Access-Control-Max-Age","86400"),new Response(null,{status:204,headers:n})}}static register(...e){return e.forEach(e=>this.features.add(e)),this}}a.logger=o.Logger.create("Application"),a.features=new Set,a.guards=new Set,a.filters=new Set,a.interceptors=new Set,a.pipes=new Set,a.corsOrigins=[],a.options=i,a.ResponseClass=Response,a.poweredBy="MxWeb",a.initialized=!1,a.initPromise=null,a.shutdownRegistered=!1,a.registryCreated=!1,a.factoriesRegistered=!1,exports.Application=a;
1
+ "use strict";var e=require("./error.js"),t=require("./context.js"),s=require("./response.js"),r=require("./hooks.js"),o=require("./logger.js"),n=require("./common.js");const i={key:"path"};class a{static toResponse(e){return!1!==this.poweredBy&&e.headers.set("X-Powered-By",this.poweredBy),this.ResponseClass.json(e.body,{status:e.status,statusText:e.statusText,headers:e.headers})}constructor(e,t,o){this.request=e,this.method=t,this.payload=o,this.feature=null,this.route=null,this.response=new s.ServerResponse,this.requestHooks=new r.RequestHooks(a.hooks)}static getInject(e){return t.executeContext.getInject(e)}static getInjectSync(e){return t.executeContext.getInjectSync(e)}static getInjects(){return new Map(t.executeContext.injections)}static createInjectRegistry(){return this.registryCreated||(this.registry={get:async e=>t.executeContext.getInject(e),set(e,s){t.executeContext.setInject(e,s)}},t.executeContext.setRegistry(this.registry),this.registryCreated=!0),this.registry}static async registerInjectFactories(){if(this.factoriesRegistered)return;const e=this.options.injects;if(!e||!e.length)return void(this.factoriesRegistered=!0);const t=this.createInjectRegistry();for(const s of e)await s(t);this.factoriesRegistered=!0,await this.initializeInjects(),this.registerShutdown(),await this.loadCors()}static async initializeInjects(){for(const[e,s]of t.executeContext.injections)if(!s.instance)try{const e=this.createInjectRegistry();s.instance=await s.factory(e),s.instance&&"function"==typeof s.instance.onInit&&await s.instance.onInit()}catch(t){throw a.logger.error(`Error initializing inject ${e}:`,t),t}}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{a.logger.info("Shutting down...");for(const e of this.features)await e.destroyInjects();for(const[e,s]of t.executeContext.injections)if(s.instance&&"function"==typeof s.instance.onDestroy)try{await s.instance.onDestroy(),a.logger.info(`Destroyed inject: ${e}`)}catch(t){a.logger.error(`Error destroying inject ${e}:`,t)}a.logger.info("Shutdown complete"),process.exit(0)};process.on("SIGTERM",e),process.on("SIGINT",e)}static async loadCors(){const e=this.options.cors;e&&(Array.isArray(e)?this.corsOrigins=e:this.corsOrigins=await e(this.registry))}static async loadImports(){if(!this.initialized)return this.initPromise||(this.initPromise=(async()=>{this.initialized=!0})()),this.initPromise}isMatchFeature(e,t){for(const s of a.features)if(this.route=s.matchRoute(e,t),this.route)return this.feature=s,!0;return null}async executeGuards(){const e=this.feature?.getGuards()??new Set,s=this.route?.route.getReflect().getGuards()??new Set,r=[...a.guards,...e,...s];if(!r.length)return!0;for(const e of r)try{const s=new e;if(!await s.canActivate(t.executeContext))return!1}catch{return!1}return!0}async executeMiddlewares(){const e=this.route.route.getMiddlewares();if(!e.length)return!0;for(const s of e){let e=!1;const r=()=>{e=!0};try{if(await s(t.executeContext,r),!e)return!1}catch{return!1}}return!0}async executeFilters(e){const r=[...this.route?.route.getReflect().getFilters()??new Set,...this.feature?.getFilters()??new Set,...a.filters];for(const o of r)try{const r=new o,n=await r.catch(e,t.executeContext);if(n instanceof s.CoreResponse||n instanceof Response)return n}catch{continue}return null}async executeInterceptors(e){const t=[...this.route?.route.getReflect().getInterceptors()??new Set,...this.feature?.getInterceptors()??new Set,...a.interceptors];if(!t.length)return e;let s=e;for(const e of t){const t=new e;s=await t.transform(s)}return s}async executePipes(e){const t=this.feature?.getPipes()??new Set,s=this.route?.route.getReflect().getPipes()??new Set,r=[...a.pipes,...t,...s];if(!r.length)return e;let o=e;for(const e of r){const t=new e;o=await t.transform(o)}return o}checkCors(){const e=a.corsOrigins;if(!e.length)return!0;const t=this.request.headers.get("origin");return!t||(e.includes("*")||e.includes(t))}applyCorsHeaders(e){const t=a.corsOrigins;if(!t.length)return e;const s=this.request.headers.get("origin");return s?(t.includes("*")?e.headers.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(e.headers.set("Access-Control-Allow-Origin",s),e.headers.set("Vary","Origin")),e.headers.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),e.headers.set("Access-Control-Allow-Headers","Content-Type, Authorization"),e.headers.set("Access-Control-Max-Age","86400"),e):e}async dispatch(){const r=this.route,o=this.feature,n=t.executeContext.getContextOrThrow();try{if(!await this.executeGuards())return this.response.forbidden();if(!await this.executeMiddlewares())return this.response.forbidden();const e=r.route.getAction();let t=null;if("string"==typeof e){const s=o.getController();"function"==typeof s._initDependencies&&await s._initDependencies(),"function"==typeof s[e]&&(t=s[e].bind(s))}else"function"==typeof e&&(t=e);if(!t)return this.response.internalServer(new Error("Route action handler not found"));await this.executePipes(n);const s=await t(n);let i=this.response.success(s);return i=await this.executeInterceptors(i),i}catch(t){const r=await this.executeFilters(t);if(r)return r instanceof Response?s.CoreResponse.json({success:r.ok,message:r.statusText,code:"FILTER_RESPONSE",status:r.status,data:null,error:null},{status:r.status,statusText:r.statusText}):r;if(t instanceof e.HttpError)switch(t.statusCode){case 400:return this.response.badRequest(t.message,t.error);case 401:return this.response.unauthorized(t.message,t.error);case 403:return this.response.forbidden(t.message,t.error);case 404:return this.response.notFound(t.message,t.error);case 409:return this.response.conflict(t.message,t.error);case 422:return this.response.unprocessableEntity(t.message,t.error);case 429:return this.response.tooManyRequests(t.message,t.error);case 501:return this.response.notImplemented(t.message,t.error);case 502:return this.response.badGateway(t.message,t.error);case 503:return this.response.serviceUnavailable(t.message,t.error);case 504:return this.response.gatewayTimeout(t.message,t.error)}return a.logger.error("[dispatch] Error:",t instanceof Error?t.message:t),t instanceof Error&&t.stack&&a.logger.debug("Stack trace:",t.stack),this.response.internalServer(t)}}async match(){let e=null;try{if(!this.checkCors())return this.applyCorsHeaders(this.response.forbidden("CORS not allowed"));const s=await this.payload.params,r=a.options.key||i.key,o=s?.[r]?.join("/")||"";if(!this.isMatchFeature(this.method,o))return this.applyCorsHeaders(this.response.notFound("Route not found",{method:this.method,path:o,params:s}));await this.feature.loadInjects(),e={req:this.request,res:this.response,method:this.method,path:o,params:this.route.params,wildcards:this.route.wildcards,reflect:this.route.route.getReflect(),feature:this.feature},this.requestHooks.setFeatureHooks(this.feature.getHooks()),await this.requestHooks.featureRequest(e);const n=await t.executeContext.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(n)}catch(t){return e&&(await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e)),this.applyCorsHeaders(this.response.internalServer(t))}}static createHandler(e){return async(t,s)=>{await a.registerInjectFactories(),await a.loadImports();const r=new a(t,e,s);await r.requestHooks.appRequest(t,e);const o=await r.match.bind(r)();return a.toResponse(o)}}static create(e=i){return this.options=e,this.hooks=new r.ApplicationHooks(this.options),this.ResponseClass=e.response??Response,this.poweredBy=e.poweredBy??"MxWeb",e.guards&&e.guards.forEach(e=>this.guards.add(e)),e.filters&&e.filters.forEach(e=>this.filters.add(e)),e.interceptors&&e.interceptors.forEach(e=>this.interceptors.add(e)),e.pipes&&e.pipes.forEach(e=>this.pipes.add(e)),{[n.RouteMethod.GET]:a.createHandler(n.RouteMethod.GET),[n.RouteMethod.POST]:a.createHandler(n.RouteMethod.POST),[n.RouteMethod.PUT]:a.createHandler(n.RouteMethod.PUT),[n.RouteMethod.PATCH]:a.createHandler(n.RouteMethod.PATCH),[n.RouteMethod.DELETE]:a.createHandler(n.RouteMethod.DELETE),[n.RouteMethod.HEAD]:a.createHeadHandler(),[n.RouteMethod.OPTIONS]:a.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await a.registerInjectFactories(),await a.loadImports();const s=new a(e,n.RouteMethod.HEAD,t);await s.requestHooks.appRequest(e,n.RouteMethod.HEAD);const r=await s.match.bind(s)();return new Response(null,{status:r.status,statusText:r.statusText,headers:r.headers})}}static createOptionsHandler(){return async e=>{await a.registerInjectFactories();const t=this.corsOrigins,r=e.headers.get("origin"),o=s.ServerResponse.options();if(!t.length||!r)return o;const n=new Headers(o.headers);return t.includes("*")?n.set("Access-Control-Allow-Origin","*"):t.includes(r)&&(n.set("Access-Control-Allow-Origin",r),n.set("Vary","Origin")),n.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),n.set("Access-Control-Allow-Headers","Content-Type, Authorization"),n.set("Access-Control-Max-Age","86400"),new Response(null,{status:204,headers:n})}}static register(...e){return e.forEach(e=>this.features.add(e)),this}}a.logger=o.Logger.create("Application"),a.features=new Set,a.guards=new Set,a.filters=new Set,a.interceptors=new Set,a.pipes=new Set,a.corsOrigins=[],a.options=i,a.ResponseClass=Response,a.poweredBy="MxWeb",a.initialized=!1,a.initPromise=null,a.shutdownRegistered=!1,a.registryCreated=!1,a.factoriesRegistered=!1,exports.Application=a;
@@ -1 +1 @@
1
- import{HttpError as e}from"./error.mjs";import{executeContext as t}from"./context.mjs";import{ServerResponse as s,CoreResponse as r}from"./response.mjs";import{RequestHooks as n,ApplicationHooks as i}from"./hooks.mjs";import{Logger as o}from"./logger.mjs";import{RouteMethod as a}from"./common.mjs";const c={key:"path"};class u{static toResponse(e){return!1!==this.poweredBy&&e.headers.set("X-Powered-By",this.poweredBy),this.ResponseClass.json(e.body,{status:e.status,statusText:e.statusText,headers:e.headers})}constructor(e,t,r){this.request=e,this.method=t,this.payload=r,this.feature=null,this.route=null,this.response=new s,this.requestHooks=new n(u.hooks)}static getInject(e){return t.getInject(e)}static getInjectSync(e){return t.getInjectSync(e)}static getInjects(){return new Map(t.injections)}static createInjectRegistry(){return this.registryCreated||(this.registry={get:async e=>t.getInject(e),set(e,s){t.setInject(e,s)}},t.setRegistry(this.registry),this.registryCreated=!0),this.registry}static async registerInjectFactories(){if(this.factoriesRegistered)return;const e=this.options.injects;if(!e||!e.length)return void(this.factoriesRegistered=!0);const t=this.createInjectRegistry();for(const s of e)await s(t);this.factoriesRegistered=!0,await this.initializeInjects(),this.registerShutdown(),await this.loadCors()}static async initializeInjects(){for(const[e,s]of t.injections)if(!s.instance)try{const e=this.createInjectRegistry();s.instance=await s.factory(e),s.instance&&"function"==typeof s.instance.onInit&&await s.instance.onInit()}catch(t){throw u.logger.error(`Error initializing inject ${e}:`,t),t}}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{u.logger.info("Shutting down...");for(const e of this.features)await e.destroyInjects();for(const[e,s]of t.injections)if(s.instance&&"function"==typeof s.instance.onDestroy)try{await s.instance.onDestroy(),u.logger.info(`Destroyed inject: ${e}`)}catch(t){u.logger.error(`Error destroying inject ${e}:`,t)}u.logger.info("Shutdown complete"),process.exit(0)};process.on("SIGTERM",e),process.on("SIGINT",e)}static async loadCors(){const e=this.options.cors;e&&(Array.isArray(e)?this.corsOrigins=e:this.corsOrigins=await e(this.registry))}static async loadImports(){if(!this.initialized)return this.initPromise||(this.initPromise=(async()=>{this.initialized=!0})()),this.initPromise}isMatchFeature(e,t){for(const s of u.features)if(this.route=s.matchRoute(e,t),this.route)return this.feature=s,!0;return null}async executeGuards(){const e=this.route?.route.getReflect().getGuards()??new Set,s=[...u.guards,...e];if(!s.length)return!0;for(const e of s)try{const s=new e;if(!await s.canActivate(t))return!1}catch{return!1}return!0}async executeMiddlewares(){const e=this.route.route.getMiddlewares();if(!e.length)return!0;for(const s of e){let e=!1;const r=()=>{e=!0};try{if(await s(t,r),!e)return!1}catch{return!1}}return!0}async executeFilters(e){const s=[...this.route?.route.getReflect().getFilters()??new Set,...u.filters];for(const n of s)try{const s=new n,i=await s.catch(e,t);if(i instanceof r||i instanceof Response)return i}catch{continue}return null}async executeInterceptors(e){const t=[...this.route?.route.getReflect().getInterceptors()??new Set,...u.interceptors];if(!t.length)return e;let s=e;for(const e of t){const t=new e;s=await t.transform(s)}return s}async executePipes(e){const t=this.route?.route.getReflect().getPipes()??new Set,s=[...u.pipes,...t];if(!s.length)return e;let r=e;for(const e of s){const t=new e;r=await t.transform(r)}return r}checkCors(){const e=u.corsOrigins;if(!e.length)return!0;const t=this.request.headers.get("origin");return!t||(e.includes("*")||e.includes(t))}applyCorsHeaders(e){const t=u.corsOrigins;if(!t.length)return e;const s=this.request.headers.get("origin");return s?(t.includes("*")?e.headers.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(e.headers.set("Access-Control-Allow-Origin",s),e.headers.set("Vary","Origin")),e.headers.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),e.headers.set("Access-Control-Allow-Headers","Content-Type, Authorization"),e.headers.set("Access-Control-Max-Age","86400"),e):e}async dispatch(){const s=this.route,n=this.feature,i=t.getContextOrThrow();try{if(!await this.executeGuards())return this.response.forbidden();if(!await this.executeMiddlewares())return this.response.forbidden();const e=s.route.getAction();let t=null;if("string"==typeof e){const s=n.getController();"function"==typeof s._initDependencies&&await s._initDependencies(),"function"==typeof s[e]&&(t=s[e].bind(s))}else"function"==typeof e&&(t=e);if(!t)return this.response.internalServer(new Error("Route action handler not found"));await this.executePipes(i);const r=await t(i);let o=this.response.success(r);return o=await this.executeInterceptors(o),o}catch(t){const s=await this.executeFilters(t);if(s)return s instanceof Response?r.json({success:s.ok,message:s.statusText,code:"FILTER_RESPONSE",status:s.status,data:null,error:null},{status:s.status,statusText:s.statusText}):s;if(t instanceof e)switch(t.statusCode){case 400:return this.response.badRequest(t.message,t.error);case 401:return this.response.unauthorized(t.message,t.error);case 403:return this.response.forbidden(t.message,t.error);case 404:return this.response.notFound(t.message,t.error);case 409:return this.response.conflict(t.message,t.error);case 422:return this.response.unprocessableEntity(t.message,t.error);case 429:return this.response.tooManyRequests(t.message,t.error);case 501:return this.response.notImplemented(t.message,t.error);case 502:return this.response.badGateway(t.message,t.error);case 503:return this.response.serviceUnavailable(t.message,t.error);case 504:return this.response.gatewayTimeout(t.message,t.error)}return u.logger.error("[dispatch] Error:",t instanceof Error?t.message:t),t instanceof Error&&t.stack&&u.logger.debug("Stack trace:",t.stack),this.response.internalServer(t)}}async match(){let e=null;try{if(!this.checkCors())return this.applyCorsHeaders(this.response.forbidden("CORS not allowed"));const s=await this.payload.params,r=u.options.key||c.key,n=s?.[r]?.join("/")||"";if(!this.isMatchFeature(this.method,n))return this.applyCorsHeaders(this.response.notFound("Route not found",{method:this.method,path:n,params:s}));await this.feature.loadInjects(),e={req:this.request,res:this.response,method:this.method,path:n,params:this.route.params,wildcards:this.route.wildcards,reflect:this.route.route.getReflect(),feature:this.feature},this.requestHooks.setFeatureHooks(this.feature.getHooks()),await this.requestHooks.featureRequest(e);const i=await t.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(i)}catch(t){return e&&(await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e)),this.applyCorsHeaders(this.response.internalServer(t))}}static createHandler(e){return async(t,s)=>{await u.registerInjectFactories(),await u.loadImports();const r=new u(t,e,s);await r.requestHooks.appRequest(t,e);const n=await r.match.bind(r)();return u.toResponse(n)}}static create(e=c){return this.options=e,this.hooks=new i(this.options),this.ResponseClass=e.response??Response,this.poweredBy=e.poweredBy??"MxWeb",e.guards&&e.guards.forEach(e=>this.guards.add(e)),e.filters&&e.filters.forEach(e=>this.filters.add(e)),e.interceptors&&e.interceptors.forEach(e=>this.interceptors.add(e)),e.pipes&&e.pipes.forEach(e=>this.pipes.add(e)),{[a.GET]:u.createHandler(a.GET),[a.POST]:u.createHandler(a.POST),[a.PUT]:u.createHandler(a.PUT),[a.PATCH]:u.createHandler(a.PATCH),[a.DELETE]:u.createHandler(a.DELETE),[a.HEAD]:u.createHeadHandler(),[a.OPTIONS]:u.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await u.registerInjectFactories(),await u.loadImports();const s=new u(e,a.HEAD,t);await s.requestHooks.appRequest(e,a.HEAD);const r=await s.match.bind(s)();return new Response(null,{status:r.status,statusText:r.statusText,headers:r.headers})}}static createOptionsHandler(){return async e=>{await u.registerInjectFactories();const t=this.corsOrigins,r=e.headers.get("origin"),n=s.options();if(!t.length||!r)return n;const i=new Headers(n.headers);return t.includes("*")?i.set("Access-Control-Allow-Origin","*"):t.includes(r)&&(i.set("Access-Control-Allow-Origin",r),i.set("Vary","Origin")),i.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),i.set("Access-Control-Allow-Headers","Content-Type, Authorization"),i.set("Access-Control-Max-Age","86400"),new Response(null,{status:204,headers:i})}}static register(...e){return e.forEach(e=>this.features.add(e)),this}}u.logger=o.create("Application"),u.features=new Set,u.guards=new Set,u.filters=new Set,u.interceptors=new Set,u.pipes=new Set,u.corsOrigins=[],u.options=c,u.ResponseClass=Response,u.poweredBy="MxWeb",u.initialized=!1,u.initPromise=null,u.shutdownRegistered=!1,u.registryCreated=!1,u.factoriesRegistered=!1;export{u as Application};
1
+ import{HttpError as e}from"./error.mjs";import{executeContext as t}from"./context.mjs";import{ServerResponse as s,CoreResponse as r}from"./response.mjs";import{RequestHooks as n,ApplicationHooks as i}from"./hooks.mjs";import{Logger as o}from"./logger.mjs";import{RouteMethod as a}from"./common.mjs";const c={key:"path"};class u{static toResponse(e){return!1!==this.poweredBy&&e.headers.set("X-Powered-By",this.poweredBy),this.ResponseClass.json(e.body,{status:e.status,statusText:e.statusText,headers:e.headers})}constructor(e,t,r){this.request=e,this.method=t,this.payload=r,this.feature=null,this.route=null,this.response=new s,this.requestHooks=new n(u.hooks)}static getInject(e){return t.getInject(e)}static getInjectSync(e){return t.getInjectSync(e)}static getInjects(){return new Map(t.injections)}static createInjectRegistry(){return this.registryCreated||(this.registry={get:async e=>t.getInject(e),set(e,s){t.setInject(e,s)}},t.setRegistry(this.registry),this.registryCreated=!0),this.registry}static async registerInjectFactories(){if(this.factoriesRegistered)return;const e=this.options.injects;if(!e||!e.length)return void(this.factoriesRegistered=!0);const t=this.createInjectRegistry();for(const s of e)await s(t);this.factoriesRegistered=!0,await this.initializeInjects(),this.registerShutdown(),await this.loadCors()}static async initializeInjects(){for(const[e,s]of t.injections)if(!s.instance)try{const e=this.createInjectRegistry();s.instance=await s.factory(e),s.instance&&"function"==typeof s.instance.onInit&&await s.instance.onInit()}catch(t){throw u.logger.error(`Error initializing inject ${e}:`,t),t}}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{u.logger.info("Shutting down...");for(const e of this.features)await e.destroyInjects();for(const[e,s]of t.injections)if(s.instance&&"function"==typeof s.instance.onDestroy)try{await s.instance.onDestroy(),u.logger.info(`Destroyed inject: ${e}`)}catch(t){u.logger.error(`Error destroying inject ${e}:`,t)}u.logger.info("Shutdown complete"),process.exit(0)};process.on("SIGTERM",e),process.on("SIGINT",e)}static async loadCors(){const e=this.options.cors;e&&(Array.isArray(e)?this.corsOrigins=e:this.corsOrigins=await e(this.registry))}static async loadImports(){if(!this.initialized)return this.initPromise||(this.initPromise=(async()=>{this.initialized=!0})()),this.initPromise}isMatchFeature(e,t){for(const s of u.features)if(this.route=s.matchRoute(e,t),this.route)return this.feature=s,!0;return null}async executeGuards(){const e=this.feature?.getGuards()??new Set,s=this.route?.route.getReflect().getGuards()??new Set,r=[...u.guards,...e,...s];if(!r.length)return!0;for(const e of r)try{const s=new e;if(!await s.canActivate(t))return!1}catch{return!1}return!0}async executeMiddlewares(){const e=this.route.route.getMiddlewares();if(!e.length)return!0;for(const s of e){let e=!1;const r=()=>{e=!0};try{if(await s(t,r),!e)return!1}catch{return!1}}return!0}async executeFilters(e){const s=[...this.route?.route.getReflect().getFilters()??new Set,...this.feature?.getFilters()??new Set,...u.filters];for(const n of s)try{const s=new n,i=await s.catch(e,t);if(i instanceof r||i instanceof Response)return i}catch{continue}return null}async executeInterceptors(e){const t=[...this.route?.route.getReflect().getInterceptors()??new Set,...this.feature?.getInterceptors()??new Set,...u.interceptors];if(!t.length)return e;let s=e;for(const e of t){const t=new e;s=await t.transform(s)}return s}async executePipes(e){const t=this.feature?.getPipes()??new Set,s=this.route?.route.getReflect().getPipes()??new Set,r=[...u.pipes,...t,...s];if(!r.length)return e;let n=e;for(const e of r){const t=new e;n=await t.transform(n)}return n}checkCors(){const e=u.corsOrigins;if(!e.length)return!0;const t=this.request.headers.get("origin");return!t||(e.includes("*")||e.includes(t))}applyCorsHeaders(e){const t=u.corsOrigins;if(!t.length)return e;const s=this.request.headers.get("origin");return s?(t.includes("*")?e.headers.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(e.headers.set("Access-Control-Allow-Origin",s),e.headers.set("Vary","Origin")),e.headers.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),e.headers.set("Access-Control-Allow-Headers","Content-Type, Authorization"),e.headers.set("Access-Control-Max-Age","86400"),e):e}async dispatch(){const s=this.route,n=this.feature,i=t.getContextOrThrow();try{if(!await this.executeGuards())return this.response.forbidden();if(!await this.executeMiddlewares())return this.response.forbidden();const e=s.route.getAction();let t=null;if("string"==typeof e){const s=n.getController();"function"==typeof s._initDependencies&&await s._initDependencies(),"function"==typeof s[e]&&(t=s[e].bind(s))}else"function"==typeof e&&(t=e);if(!t)return this.response.internalServer(new Error("Route action handler not found"));await this.executePipes(i);const r=await t(i);let o=this.response.success(r);return o=await this.executeInterceptors(o),o}catch(t){const s=await this.executeFilters(t);if(s)return s instanceof Response?r.json({success:s.ok,message:s.statusText,code:"FILTER_RESPONSE",status:s.status,data:null,error:null},{status:s.status,statusText:s.statusText}):s;if(t instanceof e)switch(t.statusCode){case 400:return this.response.badRequest(t.message,t.error);case 401:return this.response.unauthorized(t.message,t.error);case 403:return this.response.forbidden(t.message,t.error);case 404:return this.response.notFound(t.message,t.error);case 409:return this.response.conflict(t.message,t.error);case 422:return this.response.unprocessableEntity(t.message,t.error);case 429:return this.response.tooManyRequests(t.message,t.error);case 501:return this.response.notImplemented(t.message,t.error);case 502:return this.response.badGateway(t.message,t.error);case 503:return this.response.serviceUnavailable(t.message,t.error);case 504:return this.response.gatewayTimeout(t.message,t.error)}return u.logger.error("[dispatch] Error:",t instanceof Error?t.message:t),t instanceof Error&&t.stack&&u.logger.debug("Stack trace:",t.stack),this.response.internalServer(t)}}async match(){let e=null;try{if(!this.checkCors())return this.applyCorsHeaders(this.response.forbidden("CORS not allowed"));const s=await this.payload.params,r=u.options.key||c.key,n=s?.[r]?.join("/")||"";if(!this.isMatchFeature(this.method,n))return this.applyCorsHeaders(this.response.notFound("Route not found",{method:this.method,path:n,params:s}));await this.feature.loadInjects(),e={req:this.request,res:this.response,method:this.method,path:n,params:this.route.params,wildcards:this.route.wildcards,reflect:this.route.route.getReflect(),feature:this.feature},this.requestHooks.setFeatureHooks(this.feature.getHooks()),await this.requestHooks.featureRequest(e);const i=await t.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(i)}catch(t){return e&&(await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e)),this.applyCorsHeaders(this.response.internalServer(t))}}static createHandler(e){return async(t,s)=>{await u.registerInjectFactories(),await u.loadImports();const r=new u(t,e,s);await r.requestHooks.appRequest(t,e);const n=await r.match.bind(r)();return u.toResponse(n)}}static create(e=c){return this.options=e,this.hooks=new i(this.options),this.ResponseClass=e.response??Response,this.poweredBy=e.poweredBy??"MxWeb",e.guards&&e.guards.forEach(e=>this.guards.add(e)),e.filters&&e.filters.forEach(e=>this.filters.add(e)),e.interceptors&&e.interceptors.forEach(e=>this.interceptors.add(e)),e.pipes&&e.pipes.forEach(e=>this.pipes.add(e)),{[a.GET]:u.createHandler(a.GET),[a.POST]:u.createHandler(a.POST),[a.PUT]:u.createHandler(a.PUT),[a.PATCH]:u.createHandler(a.PATCH),[a.DELETE]:u.createHandler(a.DELETE),[a.HEAD]:u.createHeadHandler(),[a.OPTIONS]:u.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await u.registerInjectFactories(),await u.loadImports();const s=new u(e,a.HEAD,t);await s.requestHooks.appRequest(e,a.HEAD);const r=await s.match.bind(s)();return new Response(null,{status:r.status,statusText:r.statusText,headers:r.headers})}}static createOptionsHandler(){return async e=>{await u.registerInjectFactories();const t=this.corsOrigins,r=e.headers.get("origin"),n=s.options();if(!t.length||!r)return n;const i=new Headers(n.headers);return t.includes("*")?i.set("Access-Control-Allow-Origin","*"):t.includes(r)&&(i.set("Access-Control-Allow-Origin",r),i.set("Vary","Origin")),i.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),i.set("Access-Control-Allow-Headers","Content-Type, Authorization"),i.set("Access-Control-Max-Age","86400"),new Response(null,{status:204,headers:i})}}static register(...e){return e.forEach(e=>this.features.add(e)),this}}u.logger=o.create("Application"),u.features=new Set,u.guards=new Set,u.filters=new Set,u.interceptors=new Set,u.pipes=new Set,u.corsOrigins=[],u.options=c,u.ResponseClass=Response,u.poweredBy="MxWeb",u.initialized=!1,u.initPromise=null,u.shutdownRegistered=!1,u.registryCreated=!1,u.factoriesRegistered=!1;export{u as Application};
package/dist/common.d.ts CHANGED
@@ -9,6 +9,161 @@ export type Callback<R = void, A extends any[] = []> = (...args: A) => R;
9
9
  * This module provides foundational types for dependency injection, routing, and lifecycle management.
10
10
  * @module common
11
11
  */
12
+ /**
13
+ * Interface representing the feature context accessible from ExecuteContext.
14
+ * This abstraction allows execute.ts to work with features without importing Feature class directly,
15
+ * avoiding circular dependencies.
16
+ *
17
+ * @remarks
18
+ * Feature class implements this interface.
19
+ * Used in RequestContext and ExecuteContext methods.
20
+ */
21
+ export interface FeatureContext {
22
+ /**
23
+ * Checks if an inject exists in this feature.
24
+ * @param name - The inject name to check
25
+ * @returns true if the inject exists
26
+ */
27
+ hasInject(name: string): boolean;
28
+ /**
29
+ * Retrieves an inject by name with lazy initialization.
30
+ * @template T - Expected return type
31
+ * @param name - The name of the inject to retrieve
32
+ * @returns Promise resolving to the inject or undefined
33
+ */
34
+ getInject<T = unknown>(name: string): Promise<T | undefined>;
35
+ /**
36
+ * Retrieves an inject synchronously (only if already initialized).
37
+ * @template T - Expected return type
38
+ * @param name - The name of the inject to retrieve
39
+ * @returns The inject or undefined
40
+ */
41
+ getInjectSync<T = unknown>(name: string): T | undefined;
42
+ }
43
+ /**
44
+ * Interface for guard classes that determine if a request should proceed.
45
+ * Guards are used for authorization, authentication, and access control.
46
+ *
47
+ * @template Context - The execution context type (defaults to unknown for flexibility)
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * class AuthGuard implements CanActivate {
52
+ * canActivate(context: ExecuteContext): boolean {
53
+ * const token = context.switchHttp().headers()["authorization"];
54
+ * return !!token && isValidToken(token);
55
+ * }
56
+ * }
57
+ * ```
58
+ */
59
+ export interface CanActivate<Context = unknown> {
60
+ /**
61
+ * Determines if the request should be allowed to proceed.
62
+ *
63
+ * @param context - The execution context for the current request
64
+ * @returns true to allow, false to deny (results in 403 Forbidden)
65
+ */
66
+ canActivate(context: Context): boolean | Promise<boolean>;
67
+ }
68
+ /**
69
+ * Constructor type for guard classes.
70
+ *
71
+ * @template Context - The execution context type
72
+ */
73
+ export type Guard<Context = unknown> = new (...args: unknown[]) => CanActivate<Context>;
74
+ /**
75
+ * Interface for exception filter classes that handle errors.
76
+ * Filters can transform exceptions into custom responses.
77
+ *
78
+ * @template Response - The type of response the filter returns
79
+ * @template Context - The execution context type (defaults to unknown for flexibility)
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * class HttpExceptionFilter implements ExceptionFilter<Response> {
84
+ * catch(exception: unknown, context: ExecuteContext): Response {
85
+ * if (exception instanceof HttpError) {
86
+ * return new Response(JSON.stringify({ error: exception.message }), {
87
+ * status: exception.statusCode,
88
+ * });
89
+ * }
90
+ * throw exception; // Re-throw if not handled
91
+ * }
92
+ * }
93
+ * ```
94
+ */
95
+ export interface ExceptionFilter<Response = unknown, Context = unknown> {
96
+ /**
97
+ * Handles an exception and optionally returns a response.
98
+ *
99
+ * @param exception - The thrown exception
100
+ * @param context - The execution context for the current request
101
+ * @returns A response to send, or re-throw to pass to next filter
102
+ */
103
+ catch(exception: unknown, context: Context): Response | Promise<Response>;
104
+ }
105
+ /**
106
+ * Constructor type for exception filter classes.
107
+ *
108
+ * @template Response - The type of response the filter returns
109
+ * @template Context - The execution context type
110
+ */
111
+ export type Filter<Response = unknown, Context = unknown> = new (...args: unknown[]) => ExceptionFilter<Response, Context>;
112
+ /**
113
+ * Interface for class-based response interceptors.
114
+ * Provides more flexibility than function interceptors with access to instance state.
115
+ *
116
+ * @template T - The type of the response data
117
+ * @template Response - The response type (defaults to unknown for flexibility)
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * class LoggingInterceptor implements ResponseInterceptorHandler {
122
+ * transform(response: CoreResponse): CoreResponse {
123
+ * console.log(`[${response.status}] ${response.body.message}`);
124
+ * return response;
125
+ * }
126
+ * }
127
+ *
128
+ * class CacheHeaderInterceptor implements ResponseInterceptorHandler {
129
+ * transform(response: CoreResponse): CoreResponse {
130
+ * if (response.status >= 200 && response.status < 300) {
131
+ * response.headers.set("Cache-Control", "max-age=3600");
132
+ * }
133
+ * return response;
134
+ * }
135
+ * }
136
+ * ```
137
+ */
138
+ export interface ResponseInterceptorHandler<Response = unknown> {
139
+ /**
140
+ * Transforms the response.
141
+ * Called after the handler returns, allowing post-processing of the response.
142
+ *
143
+ * @param response - The response to transform
144
+ * @returns The transformed response (can be the same instance or a new one)
145
+ */
146
+ transform(response: Response): Response | Promise<Response>;
147
+ }
148
+ /**
149
+ * Constructor type for response interceptor classes.
150
+ * Interceptors transform the response after the handler returns.
151
+ *
152
+ * @template Response - The response type
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * import { ResponseInterceptorHandler, CoreResponse } from "@mxweb/core";
157
+ *
158
+ * class LoggingInterceptor implements ResponseInterceptorHandler {
159
+ * transform(response: CoreResponse): CoreResponse {
160
+ * console.log(`Response: ${response.status} ${response.body.message}`);
161
+ * return response;
162
+ * }
163
+ * }
164
+ * ```
165
+ */
166
+ export type Interceptor<Response = unknown> = new (...args: unknown[]) => ResponseInterceptorHandler<Response>;
12
167
  /**
13
168
  * Interface for Application-level dependency injects.
14
169
  * All global injects must implement this interface to ensure consistent lifecycle management.
@@ -275,6 +430,56 @@ export interface PipeTransform<Input = unknown, Output = unknown> {
275
430
  * @template Output - The type of output value the pipe produces
276
431
  */
277
432
  export type Pipe<Input = unknown, Output = unknown> = new (...args: unknown[]) => PipeTransform<Input, Output>;
433
+ /**
434
+ * Generic constructor type for class instantiation.
435
+ */
436
+ export type ClassConstructor<T = unknown> = new (...args: any[]) => T;
437
+ /**
438
+ * Shared options for guards, filters, interceptors, and pipes.
439
+ * Used by both Application and Feature configurations.
440
+ *
441
+ * @remarks
442
+ * This interface provides a common base for decorator options across
443
+ * different levels of the application hierarchy (Application, Feature, Route).
444
+ *
445
+ * Execution order:
446
+ * - Guards: Application → Feature → Route
447
+ * - Filters: Route → Feature → Application (reverse for catch)
448
+ * - Interceptors: Route → Feature → Application (for transform)
449
+ * - Pipes: Application → Feature → Route
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * const options: DecoratorOptions = {
454
+ * guards: [AuthGuard, RolesGuard],
455
+ * filters: [HttpExceptionFilter],
456
+ * interceptors: [LoggingInterceptor],
457
+ * pipes: [ValidationPipe],
458
+ * };
459
+ * ```
460
+ */
461
+ export interface DecoratorOptions {
462
+ /**
463
+ * Guards to apply at this level.
464
+ * Guards determine if a request should be processed.
465
+ */
466
+ guards?: Guard[];
467
+ /**
468
+ * Exception filters to apply at this level.
469
+ * Filters handle exceptions thrown during request processing.
470
+ */
471
+ filters?: Filter[];
472
+ /**
473
+ * Interceptors to apply at this level.
474
+ * Interceptors can transform the response.
475
+ */
476
+ interceptors?: Interceptor[];
477
+ /**
478
+ * Pipes to apply at this level.
479
+ * Pipes transform input data before it reaches the handler.
480
+ */
481
+ pipes?: Pipe[];
482
+ }
278
483
  /**
279
484
  * Constructor type for inject classes.
280
485
  * Used when registering injects that need to be instantiated.
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * @module decorator
10
10
  */
11
- import { CoreRequest, ExecuteContext, Filter, Guard, Interceptor } from "./execute";
12
- import { FeatureInject, Pipe } from "./common";
11
+ import { CoreRequest, ExecuteContext } from "./execute";
12
+ import { FeatureInject, Filter, Guard, Interceptor, Pipe } from "./common";
13
13
  /**
14
14
  * Result object from decorator functions.
15
15
  * Contains sets of guards, filters, interceptors, pipes, and metadata
package/dist/execute.d.ts CHANGED
@@ -9,8 +9,7 @@
9
9
  * @module execute
10
10
  */
11
11
  import { ServerResponse } from "./response";
12
- import { Feature } from "./feature";
13
- import { ApplicationInject, InjectEntry, InjectInstanceFactory, InjectRegistry, Pipe, RouteMethod, RouteNextHandler } from "./common";
12
+ import { ApplicationInject, FeatureContext, Filter, Guard, InjectEntry, InjectInstanceFactory, InjectRegistry, Interceptor, Pipe, RouteMethod, RouteNextHandler } from "./common";
14
13
  /**
15
14
  * Core request interface defining the minimal contract for HTTP requests.
16
15
  * This abstraction allows @mxweb/core to work with any request implementation
@@ -264,12 +263,12 @@ export declare class Reflect {
264
263
  * Returns the set of exception filter classes attached to this route.
265
264
  * @returns Set of filter class constructors
266
265
  */
267
- getFilters(): Set<Filter<unknown>>;
266
+ getFilters(): Set<Filter>;
268
267
  /**
269
268
  * Returns the set of interceptor classes attached to this route.
270
269
  * @returns Set of interceptor class constructors
271
270
  */
272
- getInterceptors(): Set<Interceptor<unknown>>;
271
+ getInterceptors(): Set<Interceptor>;
273
272
  /**
274
273
  * Returns the set of pipe classes attached to this route.
275
274
  * @returns Set of pipe class constructors
@@ -314,7 +313,7 @@ export interface RequestContext {
314
313
  /** Route metadata and decorators */
315
314
  reflect: Reflect;
316
315
  /** The feature that matched this request */
317
- feature: Feature;
316
+ feature: FeatureContext;
318
317
  }
319
318
  /**
320
319
  * Request-scoped execution context using AsyncLocalStorage.
@@ -561,7 +560,7 @@ export declare class ExecuteContext {
561
560
  *
562
561
  * @returns The Feature instance or undefined if no context
563
562
  */
564
- getFeature(): Feature | undefined;
563
+ getFeature<Feature extends FeatureContext>(): Feature | undefined;
565
564
  /**
566
565
  * Retrieves a feature-local inject by name with lazy initialization.
567
566
  * Falls back to global injects if not found in the feature.
@@ -639,33 +638,6 @@ export declare class ExecuteContext {
639
638
  */
640
639
  switchSync<T = unknown>(name: string): T | undefined;
641
640
  }
642
- /**
643
- * Interface for guard classes that determine if a request should proceed.
644
- * Guards are used for authorization, authentication, and access control.
645
- *
646
- * @example
647
- * ```ts
648
- * class AuthGuard implements CanActivate {
649
- * canActivate(context: ExecuteContext): boolean {
650
- * const token = context.switchHttp().headers()["authorization"];
651
- * return !!token && isValidToken(token);
652
- * }
653
- * }
654
- * ```
655
- */
656
- export interface CanActivate {
657
- /**
658
- * Determines if the request should be allowed to proceed.
659
- *
660
- * @param context - The execution context for the current request
661
- * @returns true to allow, false to deny (results in 403 Forbidden)
662
- */
663
- canActivate(context: ExecuteContext): boolean | Promise<boolean>;
664
- }
665
- /**
666
- * Constructor type for guard classes.
667
- */
668
- export type Guard = new (...args: unknown[]) => CanActivate;
669
641
  /**
670
642
  * Function type for route action handlers (inline handlers).
671
643
  *
@@ -705,71 +677,3 @@ export type RouteHandler<Data = unknown> = (context: ExecuteContext) => Data;
705
677
  * ```
706
678
  */
707
679
  export type RouteMiddleware = (context: ExecuteContext, next: RouteNextHandler) => void | Promise<void>;
708
- /**
709
- * Interface for exception filter classes that handle errors.
710
- * Filters can transform exceptions into custom responses.
711
- *
712
- * @template Response - The type of response the filter returns
713
- *
714
- * @example
715
- * ```ts
716
- * class HttpExceptionFilter implements ExceptionFilter<Response> {
717
- * catch(exception: unknown, context: ExecuteContext): Response {
718
- * if (exception instanceof HttpError) {
719
- * return new Response(JSON.stringify({ error: exception.message }), {
720
- * status: exception.statusCode,
721
- * });
722
- * }
723
- * throw exception; // Re-throw if not handled
724
- * }
725
- * }
726
- * ```
727
- */
728
- export interface ExceptionFilter<Response = unknown> {
729
- /**
730
- * Handles an exception and optionally returns a response.
731
- *
732
- * @param exception - The thrown exception
733
- * @param context - The execution context for the current request
734
- * @returns A response to send, or re-throw to pass to next filter
735
- */
736
- catch(exception: unknown, context: ExecuteContext): Response | Promise<Response>;
737
- }
738
- /**
739
- * Constructor type for exception filter classes.
740
- *
741
- * @template Response - The type of response the filter returns
742
- */
743
- export type Filter<Response = unknown> = new (...args: unknown[]) => ExceptionFilter<Response>;
744
- /**
745
- * Constructor type for response interceptor classes.
746
- * Interceptors transform the CoreResponse after the handler returns.
747
- *
748
- * @template T - The type of the response data
749
- *
750
- * @remarks
751
- * The old `InterceptorHandler` interface with `intercept(context, next)` is deprecated.
752
- * Use `ResponseInterceptorHandler` from `response.ts` instead for the new pattern.
753
- *
754
- * @example
755
- * ```ts
756
- * import { ResponseInterceptorHandler, CoreResponse } from "@mxweb/core";
757
- *
758
- * class LoggingInterceptor implements ResponseInterceptorHandler {
759
- * transform(response: CoreResponse): CoreResponse {
760
- * console.log(`Response: ${response.status} ${response.body.message}`);
761
- * return response;
762
- * }
763
- * }
764
- *
765
- * class CacheHeaderInterceptor implements ResponseInterceptorHandler {
766
- * transform(response: CoreResponse): CoreResponse {
767
- * if (response.status >= 200 && response.status < 300) {
768
- * response.headers.set("Cache-Control", "max-age=3600");
769
- * }
770
- * return response;
771
- * }
772
- * }
773
- * ```
774
- */
775
- export type Interceptor<T = unknown> = new (...args: unknown[]) => import("./response").ResponseInterceptorHandler<T>;
package/dist/feature.d.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  import { ControllerConstructor } from "./controller";
11
11
  import { RouteMatched, Router } from "./router";
12
12
  import { FeatureHooks, FeatureHooksOptions } from "./hooks";
13
- import { FeatureInjectFactory, RouteMethod } from "./common";
13
+ import { DecoratorOptions, FeatureInjectFactory, Filter, Guard, Interceptor, Pipe, RouteMethod } from "./common";
14
14
  /**
15
15
  * Type for feature-specific dependency injection configuration.
16
16
  * Array of factory functions that register injects.
@@ -41,7 +41,7 @@ export type FeatureInjects = FeatureInjectFactory[];
41
41
  export type FeatureImports = unknown[] | (() => unknown[]) | (() => Promise<unknown[]>);
42
42
  /**
43
43
  * Configuration options for creating a Feature.
44
- * Extends FeatureHooksOptions for lifecycle hooks.
44
+ * Extends FeatureHooksOptions for lifecycle hooks and DecoratorOptions for guards/filters/interceptors/pipes.
45
45
  *
46
46
  * @example
47
47
  * ```ts
@@ -52,13 +52,17 @@ export type FeatureImports = unknown[] | (() => unknown[]) | (() => Promise<unkn
52
52
  * Route.post("/products", "create"),
53
53
  * ),
54
54
  * imports: [Connection.forFeature([ProductEntity])],
55
- * injects: { productRepo: new ProductRepository() },
55
+ * injects: [(registry) => registry.set("productRepo", () => new ProductRepository())],
56
+ * guards: [AuthGuard],
57
+ * filters: [HttpExceptionFilter],
58
+ * interceptors: [LoggingInterceptor],
59
+ * pipes: [ValidationPipe],
56
60
  * onStart: () => console.log("Product feature started"),
57
61
  * onRequest: (ctx) => console.log("Product feature request"),
58
62
  * };
59
63
  * ```
60
64
  */
61
- export interface FeatureInitialize extends FeatureHooksOptions {
65
+ export interface FeatureInitialize extends FeatureHooksOptions, DecoratorOptions {
62
66
  /** The controller class that handles requests for this feature */
63
67
  controller: ControllerConstructor<any>;
64
68
  /** Router with registered routes for this feature */
@@ -148,6 +152,14 @@ export declare class Feature {
148
152
  private factoriesRegistered;
149
153
  /** Flag indicating if imports have been loaded */
150
154
  private importsLoaded;
155
+ /** Feature-level guards */
156
+ private guards;
157
+ /** Feature-level exception filters */
158
+ private filters;
159
+ /** Feature-level interceptors */
160
+ private interceptors;
161
+ /** Feature-level pipes */
162
+ private pipes;
151
163
  /**
152
164
  * Private constructor - use Feature.create() to instantiate.
153
165
  *
@@ -256,6 +268,30 @@ export declare class Feature {
256
268
  * @returns The FeatureHooks instance
257
269
  */
258
270
  getHooks(): FeatureHooks;
271
+ /**
272
+ * Returns the feature-level guards.
273
+ *
274
+ * @returns Set of guard classes for this feature
275
+ */
276
+ getGuards(): Set<Guard>;
277
+ /**
278
+ * Returns the feature-level exception filters.
279
+ *
280
+ * @returns Set of filter classes for this feature
281
+ */
282
+ getFilters(): Set<Filter>;
283
+ /**
284
+ * Returns the feature-level interceptors.
285
+ *
286
+ * @returns Set of interceptor classes for this feature
287
+ */
288
+ getInterceptors(): Set<Interceptor>;
289
+ /**
290
+ * Returns the feature-level pipes.
291
+ *
292
+ * @returns Set of pipe classes for this feature
293
+ */
294
+ getPipes(): Set<Pipe>;
259
295
  /**
260
296
  * Attempts to match a request against this feature's routes.
261
297
  *
package/dist/feature.js CHANGED
@@ -1 +1 @@
1
- "use strict";var t=require("./hooks.js");class e{constructor(e,s,i){this.options=e,this.ControllerContructor=s,this.router=i,this.routeMatched=null,this.controller=null,this.injects=new Map,this.factoriesRegistered=!1,this.importsLoaded=!1,this.hooks=new t.FeatureHooks(this.options),this.registry=this.createInjectRegistry()}createInjectRegistry(){return{get:t=>this.getInject(t),set:(t,e)=>{this.injects.set(t,{factory:e})}}}static create(t){return new e(t,t.controller,t.router)}async loadImports(){if(this.importsLoaded)return;this.importsLoaded=!0;const{imports:t}=this.options;if(!t)return;"function"==typeof t&&await t()}async loadInjects(){if(await this.loadImports(),this.factoriesRegistered)return;this.factoriesRegistered=!0;const{injects:t}=this.options;if(t&&t.length)for(const e of t)await e(this.registry)}async destroyInjects(){for(const[t,e]of this.injects)if(e.instance&&e.instance.onFeatureDestroy)try{await e.instance.onFeatureDestroy()}catch(t){}this.injects.clear(),this.factoriesRegistered=!1}async getInject(t){const e=this.injects.get(t);if(e)return e.instance||(e.instance=await e.factory(this.registry),"function"==typeof e.instance.onFeature&&await e.instance.onFeature()),"function"==typeof e.instance.switch?e.instance.switch():e.instance}getInjectSync(t){const e=this.injects.get(t);if(e?.instance)return"function"==typeof e.instance.switch?e.instance.switch():e.instance}hasInject(t){return this.injects.has(t)}isInjectInitialized(t){const e=this.injects.get(t);return!!e?.instance}getController(){if(!this.controller){const t=this.ControllerContructor;this.controller=new t}return this.controller}getHooks(){return this.hooks}matchRoute(t,e){return this.routeMatched=this.router.match(t,e),this.routeMatched}}exports.Feature=e;
1
+ "use strict";var t=require("./hooks.js");class e{constructor(e,s,i){this.options=e,this.ControllerContructor=s,this.router=i,this.routeMatched=null,this.controller=null,this.injects=new Map,this.factoriesRegistered=!1,this.importsLoaded=!1,this.guards=new Set,this.filters=new Set,this.interceptors=new Set,this.pipes=new Set,this.hooks=new t.FeatureHooks(this.options),this.registry=this.createInjectRegistry(),e.guards&&e.guards.forEach(t=>this.guards.add(t)),e.filters&&e.filters.forEach(t=>this.filters.add(t)),e.interceptors&&e.interceptors.forEach(t=>this.interceptors.add(t)),e.pipes&&e.pipes.forEach(t=>this.pipes.add(t))}createInjectRegistry(){return{get:t=>this.getInject(t),set:(t,e)=>{this.injects.set(t,{factory:e})}}}static create(t){return new e(t,t.controller,t.router)}async loadImports(){if(this.importsLoaded)return;this.importsLoaded=!0;const{imports:t}=this.options;if(!t)return;"function"==typeof t&&await t()}async loadInjects(){if(await this.loadImports(),this.factoriesRegistered)return;this.factoriesRegistered=!0;const{injects:t}=this.options;if(t&&t.length)for(const e of t)await e(this.registry)}async destroyInjects(){for(const[t,e]of this.injects)if(e.instance&&e.instance.onFeatureDestroy)try{await e.instance.onFeatureDestroy()}catch(t){}this.injects.clear(),this.factoriesRegistered=!1}async getInject(t){const e=this.injects.get(t);if(e)return e.instance||(e.instance=await e.factory(this.registry),"function"==typeof e.instance.onFeature&&await e.instance.onFeature()),"function"==typeof e.instance.switch?e.instance.switch():e.instance}getInjectSync(t){const e=this.injects.get(t);if(e?.instance)return"function"==typeof e.instance.switch?e.instance.switch():e.instance}hasInject(t){return this.injects.has(t)}isInjectInitialized(t){const e=this.injects.get(t);return!!e?.instance}getController(){if(!this.controller){const t=this.ControllerContructor;this.controller=new t}return this.controller}getHooks(){return this.hooks}getGuards(){return this.guards}getFilters(){return this.filters}getInterceptors(){return this.interceptors}getPipes(){return this.pipes}matchRoute(t,e){return this.routeMatched=this.router.match(t,e),this.routeMatched}}exports.Feature=e;
package/dist/feature.mjs CHANGED
@@ -1 +1 @@
1
- import{FeatureHooks as t}from"./hooks.mjs";class e{constructor(e,s,i){this.options=e,this.ControllerContructor=s,this.router=i,this.routeMatched=null,this.controller=null,this.injects=new Map,this.factoriesRegistered=!1,this.importsLoaded=!1,this.hooks=new t(this.options),this.registry=this.createInjectRegistry()}createInjectRegistry(){return{get:t=>this.getInject(t),set:(t,e)=>{this.injects.set(t,{factory:e})}}}static create(t){return new e(t,t.controller,t.router)}async loadImports(){if(this.importsLoaded)return;this.importsLoaded=!0;const{imports:t}=this.options;if(!t)return;"function"==typeof t&&await t()}async loadInjects(){if(await this.loadImports(),this.factoriesRegistered)return;this.factoriesRegistered=!0;const{injects:t}=this.options;if(t&&t.length)for(const e of t)await e(this.registry)}async destroyInjects(){for(const[t,e]of this.injects)if(e.instance&&e.instance.onFeatureDestroy)try{await e.instance.onFeatureDestroy()}catch(t){}this.injects.clear(),this.factoriesRegistered=!1}async getInject(t){const e=this.injects.get(t);if(e)return e.instance||(e.instance=await e.factory(this.registry),"function"==typeof e.instance.onFeature&&await e.instance.onFeature()),"function"==typeof e.instance.switch?e.instance.switch():e.instance}getInjectSync(t){const e=this.injects.get(t);if(e?.instance)return"function"==typeof e.instance.switch?e.instance.switch():e.instance}hasInject(t){return this.injects.has(t)}isInjectInitialized(t){const e=this.injects.get(t);return!!e?.instance}getController(){if(!this.controller){const t=this.ControllerContructor;this.controller=new t}return this.controller}getHooks(){return this.hooks}matchRoute(t,e){return this.routeMatched=this.router.match(t,e),this.routeMatched}}export{e as Feature};
1
+ import{FeatureHooks as t}from"./hooks.mjs";class e{constructor(e,s,i){this.options=e,this.ControllerContructor=s,this.router=i,this.routeMatched=null,this.controller=null,this.injects=new Map,this.factoriesRegistered=!1,this.importsLoaded=!1,this.guards=new Set,this.filters=new Set,this.interceptors=new Set,this.pipes=new Set,this.hooks=new t(this.options),this.registry=this.createInjectRegistry(),e.guards&&e.guards.forEach(t=>this.guards.add(t)),e.filters&&e.filters.forEach(t=>this.filters.add(t)),e.interceptors&&e.interceptors.forEach(t=>this.interceptors.add(t)),e.pipes&&e.pipes.forEach(t=>this.pipes.add(t))}createInjectRegistry(){return{get:t=>this.getInject(t),set:(t,e)=>{this.injects.set(t,{factory:e})}}}static create(t){return new e(t,t.controller,t.router)}async loadImports(){if(this.importsLoaded)return;this.importsLoaded=!0;const{imports:t}=this.options;if(!t)return;"function"==typeof t&&await t()}async loadInjects(){if(await this.loadImports(),this.factoriesRegistered)return;this.factoriesRegistered=!0;const{injects:t}=this.options;if(t&&t.length)for(const e of t)await e(this.registry)}async destroyInjects(){for(const[t,e]of this.injects)if(e.instance&&e.instance.onFeatureDestroy)try{await e.instance.onFeatureDestroy()}catch(t){}this.injects.clear(),this.factoriesRegistered=!1}async getInject(t){const e=this.injects.get(t);if(e)return e.instance||(e.instance=await e.factory(this.registry),"function"==typeof e.instance.onFeature&&await e.instance.onFeature()),"function"==typeof e.instance.switch?e.instance.switch():e.instance}getInjectSync(t){const e=this.injects.get(t);if(e?.instance)return"function"==typeof e.instance.switch?e.instance.switch():e.instance}hasInject(t){return this.injects.has(t)}isInjectInitialized(t){const e=this.injects.get(t);return!!e?.instance}getController(){if(!this.controller){const t=this.ControllerContructor;this.controller=new t}return this.controller}getHooks(){return this.hooks}getGuards(){return this.guards}getFilters(){return this.filters}getInterceptors(){return this.interceptors}getPipes(){return this.pipes}matchRoute(t,e){return this.routeMatched=this.router.match(t,e),this.routeMatched}}export{e as Feature};
@@ -245,21 +245,21 @@ export declare function applyTransformer<R = Response>(transformer: ResponseTran
245
245
  */
246
246
  export type ResponseInterceptor<T = unknown> = (response: CoreResponse<T>) => CoreResponse<T> | Promise<CoreResponse<T>>;
247
247
  /**
248
- * Interface for class-based response interceptors.
248
+ * Interface for class-based response interceptors with CoreResponse.
249
249
  * Provides more flexibility than function interceptors with access to instance state.
250
250
  *
251
251
  * @template T - The type of the response data
252
252
  *
253
253
  * @example
254
254
  * ```ts
255
- * class LoggingInterceptor implements ResponseInterceptorHandler {
255
+ * class LoggingInterceptor implements CoreResponseInterceptorHandler {
256
256
  * transform(response: CoreResponse): CoreResponse {
257
257
  * console.log(`[${response.status}] ${response.body.message}`);
258
258
  * return response;
259
259
  * }
260
260
  * }
261
261
  *
262
- * class CacheHeaderInterceptor implements ResponseInterceptorHandler {
262
+ * class CacheHeaderInterceptor implements CoreResponseInterceptorHandler {
263
263
  * transform(response: CoreResponse): CoreResponse {
264
264
  * if (response.status >= 200 && response.status < 300) {
265
265
  * response.headers.set("Cache-Control", "max-age=3600");
@@ -269,7 +269,7 @@ export type ResponseInterceptor<T = unknown> = (response: CoreResponse<T>) => Co
269
269
  * }
270
270
  * ```
271
271
  */
272
- export interface ResponseInterceptorHandler<T = unknown> {
272
+ export interface CoreResponseInterceptorHandler<T = unknown> {
273
273
  /**
274
274
  * Transforms the response.
275
275
  * Called after the handler returns, allowing post-processing of the response.
package/dist/route.d.ts CHANGED
@@ -33,9 +33,9 @@
33
33
  * })
34
34
  * ```
35
35
  */
36
- import { Pipe, RouteMethod } from "./common";
36
+ import { Filter, Guard, Interceptor, Pipe, RouteMethod } from "./common";
37
37
  import { DecoratorResult } from "./decorator";
38
- import { Filter, Guard, Interceptor, Reflect, RouteHandler, RouteMiddleware } from "./execute";
38
+ import { Reflect, RouteHandler, RouteMiddleware } from "./execute";
39
39
  /**
40
40
  * Represents a single route definition in the application.
41
41
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxweb/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A NestJS-inspired backend framework for Next.js App Router with dependency injection, guards, interceptors, and modular architecture",
5
5
  "keywords": [
6
6
  "nextjs",