@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 +65 -0
- package/dist/application.d.ts +3 -11
- package/dist/application.js +1 -1
- package/dist/application.mjs +1 -1
- package/dist/common.d.ts +205 -0
- package/dist/decorator.d.ts +2 -2
- package/dist/execute.d.ts +5 -101
- package/dist/feature.d.ts +40 -4
- package/dist/feature.js +1 -1
- package/dist/feature.mjs +1 -1
- package/dist/response.d.ts +4 -4
- package/dist/route.d.ts +2 -2
- package/package.json +1 -1
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
|
package/dist/application.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Feature } from "./feature";
|
|
2
2
|
import { CoreResponse, ResponseClass } from "./response";
|
|
3
|
-
import { CoreRequest
|
|
3
|
+
import { CoreRequest } from "./execute";
|
|
4
4
|
import { ApplicationHooksOptions } from "./hooks";
|
|
5
|
-
import { ApplicationInject, InjectEntry, InjectFactory, InjectRegistry,
|
|
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
|
}
|
package/dist/application.js
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/application.mjs
CHANGED
|
@@ -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,
|
|
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.
|
package/dist/decorator.d.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module decorator
|
|
10
10
|
*/
|
|
11
|
-
import { CoreRequest, ExecuteContext
|
|
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 {
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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};
|
package/dist/response.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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 {
|
|
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