@mxweb/core 1.0.1 → 1.1.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,245 @@ 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.1.0] - 2026-01-02
9
+
10
+ ### ⚠️ Breaking Changes
11
+
12
+ - **Framework Independence**: Removed all direct dependencies on Next.js
13
+ - The framework is now fully framework-agnostic
14
+ - Next.js is still the primary target and fully compatible
15
+ - Removed `next` from `peerDependencies`
16
+ - Removed `@mxweb/utils` dependency (utilities are now built-in)
17
+ - Use `CoreRequest` interface instead of `NextRequest`
18
+ - Use `CoreResponse` class instead of `NextResponse`
19
+
20
+ - **Interceptor Redesign**: Interceptors now transform responses instead of wrapping handlers
21
+ - Old pattern: `intercept(context, next)` - wrapped handler execution
22
+ - New pattern: `transform(response)` - receives `CoreResponse` and transforms it
23
+ - Interceptors no longer have access to `CallHandler` (deprecated)
24
+ - Use `ResponseInterceptorHandler` interface instead of `InterceptorHandler`
25
+
26
+ ### Added
27
+
28
+ - **Application Options**: New configuration options for framework-agnostic response handling
29
+ - `response?: ResponseClass` - Pass response class (NextResponse, Response) for final response creation
30
+ - Defaults to standard `Response` if not provided
31
+ - Example: `Application.create({ response: NextResponse })`
32
+ - `poweredBy?: string | false` - Customize X-Powered-By header
33
+ - Defaults to `"MxWeb"`
34
+ - Set to `false` to disable the header
35
+ - Example: `Application.create({ poweredBy: "MyAPI/1.0" })`
36
+
37
+ - **Response**: Added `ResponseClass` interface for response class compatibility
38
+ - Interface for classes with static `json(body, init)` method
39
+ - Compatible with `NextResponse`, `Response`, and custom response classes
40
+ - Allows passing response class directly to `Application.create()`
41
+
42
+ - **Response**: Added `ResponseInterceptor` type and `ResponseInterceptorHandler` interface
43
+ - `ResponseInterceptor<T>` - Function type for simple response transformers
44
+ - `ResponseInterceptorHandler<T>` - Interface for class-based interceptors
45
+ - `transform(response: CoreResponse)` method for transforming responses
46
+ - Interceptors can add headers, modify body, or return new `CoreResponse`
47
+
48
+ - **Response**: Added `applyTransformer()` helper function
49
+ - Handles both class-based and function-based transformers
50
+ - Automatically detects if transformer is a class with `json()` method
51
+
52
+ - **Response**: Added `CoreResponse` class and `CoreResponseBody` interface
53
+ - Framework-agnostic response wrapper
54
+ - `CoreResponseBody<T>` - Standardized JSON response structure
55
+ - `CoreResponse.json()` - Returns the response body for custom response handling
56
+ - `ResponseTransformer<R>` - Type for custom response transformers
57
+ - All `ServerResponse` methods now return `CoreResponse` instead of `NextResponse`
58
+
59
+ - **Route**: Added fluent chain API helper methods for easier route configuration
60
+ - `decorators(...decorators)` - Add multiple decorator results directly (lazy applied on `getReflect()`)
61
+ - `middlewares(...middlewares)` - Add middleware functions
62
+ - `guards(...guards)` - Add guard classes
63
+ - `filters(...filters)` - Add exception filter classes
64
+ - `interceptors(...interceptors)` - Add interceptor classes
65
+ - `pipes(...pipes)` - Add pipe classes
66
+ - `metadata(key, value)` - Set metadata key-value pairs
67
+ - All chain methods return new Route instance (immutable pattern)
68
+ - Example: `Route.get("/users", "findAll").guards(AuthGuard).metadata("roles", ["admin"])`
69
+
70
+ - **Common**: Added `Switchable<T>` interface for safe inject facade pattern
71
+ - Provides type-safe way to expose controlled API from injects
72
+ - `ApplicationInject` and `FeatureInject` now extend `Partial<Switchable>`
73
+ - Example: `class Connection implements ApplicationInject, Switchable<DbClient> { ... }`
74
+
75
+ - **Common**: Added `Callback<R, A>` utility type (previously from `@mxweb/utils`)
76
+
77
+ - **ExecuteContext**: Added `switch()` and `switchSync()` methods for inject context switching
78
+ - `switch<T>(name)` - Async method to get inject and call its `switch()` method (throws if not found)
79
+ - `switchSync<T>(name)` - Sync version (only works if inject already initialized)
80
+ - Searches feature injects first (priority), then falls back to global injects
81
+ - Example: `const db = await this.context.switch<DatabaseClient>("db")`
82
+
83
+ - **ExecuteContext**: Added `CoreRequest` interface to abstract HTTP request
84
+ - Framework-agnostic request interface (compatible with Next.js, Express, Fastify, Fetch API)
85
+ - Properties: `nextUrl?`, `url?`, `query?`, `headers`, `body`, `json()`, `text()`, `formData()`, `blob()`, `arrayBuffer()`
86
+ - `RequestContext.req` now uses `CoreRequest` instead of `NextRequest`
87
+ - `switchHttp().getRequest<T>()` is now generic for type-safe request access
88
+
89
+ - **ExecuteContext**: Added `CoreSearchParams` class for unified query parameter handling
90
+ - Constructor accepts `URLSearchParams`, query string, or query object
91
+ - Type-safe getter methods: `get()`, `getAll()`, `getNumber()`, `getBoolean()`
92
+ - Helper methods: `has()`, `keys()`, `values()`, `entries()`, `forEach()`, `size`
93
+ - Conversion methods: `toObject()`, `toObjectAll()`, `toURLSearchParams()`, `toString()`
94
+ - `switchHttp().searchParams()` returns cached `CoreSearchParams` instance
95
+ - `switchHttp().query()` now uses `searchParams().toObject()` for backward compatibility
96
+ - Fallback chain: `nextUrl.searchParams` → `url.searchParams` → `url` string → `query` object
97
+
98
+ - **Config**: Added `switch()` method implementation
99
+ - Returns the Config instance itself for context switching
100
+ - Example: `const config = await this.context.getInject<Config>("config")`
101
+
102
+ ### Changed
103
+
104
+ - **Application**: Response handling is now centralized in `toResponse()` method
105
+ - All responses go through `Application.toResponse()` before being returned
106
+ - Adds `X-Powered-By` header automatically (configurable via `poweredBy` option)
107
+ - Converts `CoreResponse` to configured response class (NextResponse, Response, etc.)
108
+
109
+ - **Interceptor**: Complete redesign of interceptor pattern
110
+ - Interceptors now receive `CoreResponse` and return transformed `CoreResponse`
111
+ - No longer wrap handler execution - called after handler returns
112
+ - Route interceptors run first, then global interceptors
113
+ - Example:
114
+ ```ts
115
+ class LoggingInterceptor implements ResponseInterceptorHandler {
116
+ transform(response: CoreResponse): CoreResponse {
117
+ console.log(`[${response.status}] ${response.body.message}`);
118
+ response.headers.set("X-Logged", "true");
119
+ return response;
120
+ }
121
+ }
122
+ ```
123
+
124
+ - **ExecuteContext**: `getInject()` and `getLocalInject()` now return `switch()` result
125
+ - If inject implements `switch()`, returns the switched value instead of raw instance
126
+ - If inject doesn't implement `switch()`, returns the raw instance (backward compatible)
127
+ - Same behavior for `getInjectSync()` and `getLocalInjectSync()`
128
+ - This provides a safe API by default, hiding dangerous methods like `close()`, `destroy()`
129
+ - Lifecycle hooks (`onInit`, `onDestroy`) are still called on raw instance
130
+
131
+ - **Feature**: `getInject()` and `getInjectSync()` now return `switch()` result
132
+ - Same pattern as ExecuteContext for consistency
133
+ - Lifecycle hooks (`onFeature`, `onFeatureDestroy`) are still called on raw instance
134
+
135
+ - **Service**: `getInject()` and `getInjectSync()` now return `switch()` result
136
+ - Follows the same pattern as ExecuteContext for consistency
137
+
138
+ - **Hooks**: `onRequest` hook now receives `CoreRequest` instead of `NextRequest`
139
+
140
+ - **Decorator**: `Request()` function now returns `CoreRequest` and is generic
141
+ - Example: `const req = Request<NextRequest>()` for Next.js type-safe access
142
+
143
+ ### Deprecated
144
+
145
+ - **Common**: `CallHandler<T>` type is deprecated
146
+ - Interceptors no longer use the `intercept(context, next)` pattern
147
+ - Use `ResponseInterceptorHandler` with `transform(response)` instead
148
+
149
+ ### Removed
150
+
151
+ - Removed `next` from `peerDependencies` (no longer required)
152
+ - Removed `@mxweb/utils` dependency (utilities are now built-in)
153
+ - Removed `InterceptorHandler` interface (replaced by `ResponseInterceptorHandler`)
154
+
155
+ ### Fixed
156
+
157
+ - **Application**: Fixed inject lifecycle `onInit` not being called at startup
158
+ - Previously, `onInit` was only called when inject was first accessed (lazy initialization)
159
+ - Now, all registered injects are initialized during `registerInjectFactories()`
160
+ - Added `initializeInjects()` method to ensure `onInit` runs for all injects at application startup
161
+ - This is critical for injects like Migration that need to run immediately
162
+
163
+ ### Migration Guide
164
+
165
+ #### For Next.js users
166
+
167
+ The framework remains fully compatible with Next.js. Configure your application:
168
+
169
+ ```ts
170
+ import { Application } from "@mxweb/core";
171
+ import { NextResponse } from "next/server";
172
+
173
+ const app = Application.create({
174
+ response: NextResponse, // Use NextResponse for final responses
175
+ poweredBy: "MyAPI/1.0", // Optional: customize X-Powered-By header
176
+ // ... other options
177
+ });
178
+
179
+ export const GET = app.GET;
180
+ export const POST = app.POST;
181
+ ```
182
+
183
+ #### Request handling
184
+
185
+ Use `CoreRequest` interface, which is compatible with `NextRequest`:
186
+
187
+ ```ts
188
+ // Before
189
+ const req: NextRequest = ...
190
+
191
+ // After (CoreRequest is compatible)
192
+ const req: CoreRequest = nextRequest; // Works as-is
193
+
194
+ // Or for type-safe Next.js specific features
195
+ const req = Request<NextRequest>();
196
+ ```
197
+
198
+ #### Response handling
199
+
200
+ `ServerResponse` methods return `CoreResponse`, which is automatically converted:
201
+
202
+ ```ts
203
+ // The framework handles conversion internally
204
+ // For custom handling in filters:
205
+ const coreResponse = response.success(data);
206
+ return NextResponse.json(coreResponse.json(), {
207
+ status: coreResponse.status,
208
+ headers: coreResponse.headers
209
+ });
210
+ ```
211
+
212
+ #### Interceptor migration
213
+
214
+ Update interceptors to use the new `transform()` pattern:
215
+
216
+ ```ts
217
+ // Before (v1.0.x)
218
+ class LoggingInterceptor implements InterceptorHandler {
219
+ async intercept(context: ExecuteContext, next: CallHandler): Promise<unknown> {
220
+ const start = Date.now();
221
+ const result = await next();
222
+ console.log(`Request took ${Date.now() - start}ms`);
223
+ return result;
224
+ }
225
+ }
226
+
227
+ // After (v1.1.0)
228
+ class LoggingInterceptor implements ResponseInterceptorHandler {
229
+ transform(response: CoreResponse): CoreResponse {
230
+ console.log(`[${response.status}] ${response.body.message}`);
231
+ response.headers.set("X-Response-Time", Date.now().toString());
232
+ return response;
233
+ }
234
+ }
235
+ ```
236
+
237
+ ## [1.0.2] - 2024-12-18
238
+
239
+ ### Changed
240
+
241
+ - **Build**: Improved terser configuration for logger module
242
+ - Added custom `terserWithOptions()` plugin for file-specific minification
243
+ - Logger module now keeps console calls while still being minified
244
+ - Other modules continue to have console calls removed during minification
245
+ - Added `loggerMinifyOptions` with `drop_console: false` for logger
246
+
8
247
  ## [1.0.1] - 2024-12-16
9
248
 
10
249
  ### Changed
@@ -1,6 +1,6 @@
1
- import { NextRequest } from "next/server";
2
1
  import { Feature } from "./feature";
3
- import { Filter, Guard, Interceptor } from "./execute";
2
+ import { CoreResponse, ResponseClass } from "./response";
3
+ import { CoreRequest, Filter, Guard, Interceptor } from "./execute";
4
4
  import { ApplicationHooksOptions } from "./hooks";
5
5
  import { ApplicationInject, InjectEntry, InjectFactory, InjectRegistry, Pipe, RoutePayload } from "./common";
6
6
  /**
@@ -44,12 +44,16 @@ export type ApplicationCors = string[] | ((registry: InjectRegistry) => string[]
44
44
  /**
45
45
  * Configuration options for creating an Application instance.
46
46
  *
47
- * @template Key - The key used to extract path segments from Next.js catch-all route params
47
+ * @template Key - The key used to extract path segments from catch-all route params
48
48
  *
49
49
  * @example
50
50
  * ```ts
51
+ * // With Next.js
52
+ * import { NextResponse } from "next/server";
53
+ *
51
54
  * const options: ApplicationOptions = {
52
55
  * key: "path",
56
+ * response: NextResponse, // Use NextResponse for final response
53
57
  * injects: [
54
58
  * Config.forRoot(),
55
59
  * Database.forRoot(),
@@ -63,11 +67,44 @@ export type ApplicationCors = string[] | ((registry: InjectRegistry) => string[]
63
67
  * onRequest: (req, method) => console.log(`${method} request`),
64
68
  * onResponse: (ctx) => console.log("Response sent"),
65
69
  * };
70
+ *
71
+ * // Without response option - uses standard Response
72
+ * const options: ApplicationOptions = {
73
+ * key: "path",
74
+ * // response defaults to Response
75
+ * };
66
76
  * ```
67
77
  */
68
78
  export interface ApplicationOptions<Key extends string = "path"> extends ApplicationHooksOptions {
69
79
  /** The key used to extract path segments from catch-all route params. Defaults to "path". */
70
80
  key?: Key;
81
+ /**
82
+ * Response class to use for creating final responses.
83
+ * Pass NextResponse for Next.js, or any class with static json() method.
84
+ * Defaults to standard Response.
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * import { NextResponse } from "next/server";
89
+ * Application.create({ response: NextResponse });
90
+ * ```
91
+ */
92
+ response?: ResponseClass;
93
+ /**
94
+ * Custom value for X-Powered-By header.
95
+ * Set to false to disable the header.
96
+ * Defaults to "MxWeb".
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * // Custom value
101
+ * Application.create({ poweredBy: "MyAPI/1.0" });
102
+ *
103
+ * // Disable header
104
+ * Application.create({ poweredBy: false });
105
+ * ```
106
+ */
107
+ poweredBy?: string | false;
71
108
  /** Array of inject factories for dependency injection */
72
109
  injects?: ApplicationInjects;
73
110
  /** Global guards applied to all routes */
@@ -82,7 +119,8 @@ export interface ApplicationOptions<Key extends string = "path"> extends Applica
82
119
  cors?: ApplicationCors;
83
120
  }
84
121
  /**
85
- * Main Application class for handling HTTP requests in Next.js App Router.
122
+ * Main Application class for handling HTTP requests.
123
+ * Designed to be framework-agnostic with primary support for Next.js App Router.
86
124
  *
87
125
  * This class provides a NestJS-inspired framework for building APIs with:
88
126
  * - Dependency injection
@@ -91,11 +129,11 @@ export interface ApplicationOptions<Key extends string = "path"> extends Applica
91
129
  * - Lifecycle hooks
92
130
  * - CORS support
93
131
  *
94
- * @template Key - The key used to extract path segments from Next.js catch-all route params
132
+ * @template Key - The key used to extract path segments from catch-all route params
95
133
  *
96
134
  * @example
97
135
  * ```ts
98
- * // In app/api/[[...path]]/route.ts
136
+ * // In app/api/[[...path]]/route.ts (Next.js)
99
137
  * import { Application } from "@mxweb/core";
100
138
  * import "@/features/products/product.feature";
101
139
  *
@@ -121,6 +159,8 @@ export declare class Application<Key extends string = "path"> {
121
159
  private readonly request;
122
160
  private readonly method;
123
161
  private readonly payload;
162
+ /** Logger instance for Application */
163
+ private static logger;
124
164
  /** Registered features (modules) */
125
165
  private static features;
126
166
  /** Global guards */
@@ -135,6 +175,10 @@ export declare class Application<Key extends string = "path"> {
135
175
  private static corsOrigins;
136
176
  /** Application configuration options */
137
177
  private static options;
178
+ /** Response class for creating final responses */
179
+ private static ResponseClass;
180
+ /** X-Powered-By header value (false to disable) */
181
+ private static poweredBy;
138
182
  /** Application-level hooks manager */
139
183
  private static hooks;
140
184
  /** Flag indicating if features are initialized */
@@ -151,11 +195,18 @@ export declare class Application<Key extends string = "path"> {
151
195
  private route;
152
196
  /** Request-scoped hooks manager */
153
197
  private requestHooks;
198
+ /**
199
+ * Converts a CoreResponse to the configured Response class.
200
+ *
201
+ * @param coreResponse - The CoreResponse to convert
202
+ * @returns Response instance (NextResponse, Response, etc.)
203
+ */
204
+ private static toResponse;
154
205
  /**
155
206
  * Creates a new Application instance for handling a request.
156
207
  * This constructor is private - use Application.create() to set up the application.
157
208
  *
158
- * @param request - The incoming Next.js request
209
+ * @param request - The incoming CoreRequest (compatible with NextRequest)
159
210
  * @param method - The HTTP method of the request
160
211
  * @param payload - The route payload containing path params
161
212
  */
@@ -228,6 +279,16 @@ export declare class Application<Key extends string = "path"> {
228
279
  * - Loads CORS configuration (may need injects)
229
280
  */
230
281
  private static registerInjectFactories;
282
+ /**
283
+ * Initializes all registered injects by calling their onInit lifecycle hook.
284
+ * This ensures injects like Migration run at application startup.
285
+ *
286
+ * @remarks
287
+ * - Injects are initialized in registration order
288
+ * - Each inject's factory is called to create the instance
289
+ * - onInit is called if the inject implements it
290
+ */
291
+ private static initializeInjects;
231
292
  /**
232
293
  * Registers process shutdown handlers for graceful cleanup.
233
294
  * Handles SIGTERM and SIGINT signals to properly destroy injects.
@@ -284,24 +345,23 @@ export declare class Application<Key extends string = "path"> {
284
345
  * Route-level filters run first, then global filters.
285
346
  *
286
347
  * @param error - The error to handle
287
- * @returns A Response if a filter handles the error, null otherwise
348
+ * @returns A CoreResponse or Response if a filter handles the error, null otherwise
288
349
  *
289
350
  * @remarks
290
- * Filters are tried in order until one returns a Response.
351
+ * Filters are tried in order until one returns a Response or CoreResponse.
291
352
  * If no filter handles the error, null is returned and default error handling applies.
292
353
  */
293
354
  private executeFilters;
294
355
  /**
295
- * Wraps the handler with interceptor chain.
296
- * Route-level interceptors are outermost, global interceptors are innermost.
356
+ * Applies interceptors to transform the response.
357
+ * Route-level interceptors run first, then global interceptors.
297
358
  *
298
- * @template T - The return type of the handler
299
- * @param handler - The original handler function to wrap
300
- * @returns The result of executing the interceptor chain
359
+ * @param response - The CoreResponse to transform
360
+ * @returns The transformed CoreResponse after passing through all interceptors
301
361
  *
302
362
  * @remarks
303
- * Interceptors form a chain where each can modify the request/response.
304
- * The chain is built from right to left (last interceptor wraps handler first).
363
+ * Interceptors are applied in order: route interceptors first, then global.
364
+ * Each interceptor receives the response and can transform headers or body.
305
365
  */
306
366
  private executeInterceptors;
307
367
  /**
@@ -320,10 +380,11 @@ export declare class Application<Key extends string = "path"> {
320
380
  */
321
381
  private checkCors;
322
382
  /**
323
- * Applies CORS headers to the response.
383
+ * Applies CORS headers to the CoreResponse.
384
+ * Does not convert to Response - that's the responsibility of the response transformer.
324
385
  *
325
- * @param response - The response to apply headers to
326
- * @returns A new Response with CORS headers applied
386
+ * @param response - The CoreResponse to apply CORS headers to
387
+ * @returns The same CoreResponse with CORS headers applied
327
388
  */
328
389
  private applyCorsHeaders;
329
390
  /**
@@ -361,7 +422,7 @@ export declare class Application<Key extends string = "path"> {
361
422
  *
362
423
  * @template Key - The key used to extract path segments from route params
363
424
  * @param method - The HTTP method this handler will process
364
- * @returns An async function that handles Next.js requests
425
+ * @returns An async function that handles HTTP requests
365
426
  *
366
427
  * @remarks
367
428
  * The returned handler:
@@ -398,13 +459,13 @@ export declare class Application<Key extends string = "path"> {
398
459
  * ```
399
460
  */
400
461
  static create<Key extends string = "path">(options?: ApplicationOptions<Key>): {
401
- GET: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
402
- POST: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
403
- PUT: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
404
- PATCH: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
405
- DELETE: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
406
- HEAD: (req: NextRequest, payload: RoutePayload) => Promise<Response>;
407
- OPTIONS: (req: NextRequest) => Promise<Response>;
462
+ GET: (req: CoreRequest, payload: RoutePayload<"path">) => Promise<Response>;
463
+ POST: (req: CoreRequest, payload: RoutePayload<"path">) => Promise<Response>;
464
+ PUT: (req: CoreRequest, payload: RoutePayload<"path">) => Promise<Response>;
465
+ PATCH: (req: CoreRequest, payload: RoutePayload<"path">) => Promise<Response>;
466
+ DELETE: (req: CoreRequest, payload: RoutePayload<"path">) => Promise<Response>;
467
+ HEAD: (req: CoreRequest, payload: RoutePayload) => Promise<Response>;
468
+ OPTIONS: (req: CoreRequest) => Promise<Response | CoreResponse<unknown>>;
408
469
  };
409
470
  /**
410
471
  * Creates a handler for HEAD requests.
@@ -1 +1 @@
1
- "use strict";var e=require("./error.js"),t=require("./context.js"),s=require("./response.js"),r=require("./hooks.js"),n=require("./common.js");const o={key:"path"};class i{constructor(e,t,n){this.request=e,this.method=t,this.payload=n,this.feature=null,this.route=null,this.response=new s.ServerResponse,this.requestHooks=new r.RequestHooks(i.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,this.registerShutdown(),await this.loadCors()}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{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()}catch(e){}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 i.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=[...i.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 s=[...this.route?.route.getReflect().getFilters()??new Set,...i.filters];for(const r of s)try{const s=new r,n=await s.catch(e,t.executeContext);if(n instanceof Response)return n}catch{continue}return null}async executeInterceptors(e){const s=[...this.route?.route.getReflect().getInterceptors()??new Set,...i.interceptors];if(!s.length)return e();let r=e;for(let e=s.length-1;e>=0;e--){const n=new(0,s[e]),o=r;r=()=>n.intercept(t.executeContext,o)}return r()}async executePipes(e){const t=this.route?.route.getReflect().getPipes()??new Set,s=[...i.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=i.corsOrigins;if(!e.length)return!0;const t=this.request.headers.get("origin");return!t||(e.includes("*")||e.includes(t))}applyCorsHeaders(e){const t=i.corsOrigins;if(!t.length)return e;const s=this.request.headers.get("origin");if(!s)return e;const r=new Headers(e.headers);return t.includes("*")?r.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(r.set("Access-Control-Allow-Origin",s),r.set("Vary","Origin")),r.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),r.set("Access-Control-Allow-Headers","Content-Type, Authorization"),r.set("Access-Control-Max-Age","86400"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}async dispatch(){const s=this.route,r=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=s.route.getAction();let t=null;if("string"==typeof e){const s=r.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 o=await this.executeInterceptors(()=>t(n));return this.response.success(o)}catch(t){const s=await this.executeFilters(t);if(s)return s;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 this.response.internalServer(t)}return 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=i.options.key||o.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 a=await t.executeContext.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(a)}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 i.registerInjectFactories(),await i.loadImports();const r=new i(t,e,s);return await r.requestHooks.appRequest(t,e),await r.match.bind(r)()}}static create(e=o){return this.options=e,this.hooks=new r.ApplicationHooks(this.options),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]:i.createHandler(n.RouteMethod.GET),[n.RouteMethod.POST]:i.createHandler(n.RouteMethod.POST),[n.RouteMethod.PUT]:i.createHandler(n.RouteMethod.PUT),[n.RouteMethod.PATCH]:i.createHandler(n.RouteMethod.PATCH),[n.RouteMethod.DELETE]:i.createHandler(n.RouteMethod.DELETE),[n.RouteMethod.HEAD]:i.createHeadHandler(),[n.RouteMethod.OPTIONS]:i.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await i.registerInjectFactories(),await i.loadImports();const s=new i(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 i.registerInjectFactories();const t=this.corsOrigins,r=e.headers.get("origin"),n=s.ServerResponse.options();if(!t.length||!r)return n;const o=new Headers(n.headers);return t.includes("*")?o.set("Access-Control-Allow-Origin","*"):t.includes(r)&&(o.set("Access-Control-Allow-Origin",r),o.set("Vary","Origin")),o.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),o.set("Access-Control-Allow-Headers","Content-Type, Authorization"),o.set("Access-Control-Max-Age","86400"),new Response(null,{status:204,headers:o})}}static register(...e){return e.forEach(e=>this.features.add(e)),this}}i.features=new Set,i.guards=new Set,i.filters=new Set,i.interceptors=new Set,i.pipes=new Set,i.corsOrigins=[],i.options=o,i.initialized=!1,i.initPromise=null,i.shutdownRegistered=!1,i.registryCreated=!1,i.factoriesRegistered=!1,exports.Application=i;
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 +1 @@
1
- import{HttpError as e}from"./error.mjs";import{executeContext as t}from"./context.mjs";import{ServerResponse as s}from"./response.mjs";import{RequestHooks as r,ApplicationHooks as n}from"./hooks.mjs";import{RouteMethod as i}from"./common.mjs";const o={key:"path"};class a{constructor(e,t,n){this.request=e,this.method=t,this.payload=n,this.feature=null,this.route=null,this.response=new s,this.requestHooks=new r(a.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,this.registerShutdown(),await this.loadCors()}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{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()}catch(e){}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))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,...a.filters];for(const r of s)try{const s=new r,n=await s.catch(e,t);if(n instanceof Response)return n}catch{continue}return null}async executeInterceptors(e){const s=[...this.route?.route.getReflect().getInterceptors()??new Set,...a.interceptors];if(!s.length)return e();let r=e;for(let e=s.length-1;e>=0;e--){const n=new(0,s[e]),i=r;r=()=>n.intercept(t,i)}return r()}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");if(!s)return e;const r=new Headers(e.headers);return t.includes("*")?r.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(r.set("Access-Control-Allow-Origin",s),r.set("Vary","Origin")),r.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),r.set("Access-Control-Allow-Headers","Content-Type, Authorization"),r.set("Access-Control-Max-Age","86400"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}async dispatch(){const s=this.route,r=this.feature,n=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=r.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 i=await this.executeInterceptors(()=>t(n));return this.response.success(i)}catch(t){const s=await this.executeFilters(t);if(s)return 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 this.response.internalServer(t)}return 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||o.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 a.registerInjectFactories(),await a.loadImports();const r=new a(t,e,s);return await r.requestHooks.appRequest(t,e),await r.match.bind(r)()}}static create(e=o){return this.options=e,this.hooks=new n(this.options),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)),{[i.GET]:a.createHandler(i.GET),[i.POST]:a.createHandler(i.POST),[i.PUT]:a.createHandler(i.PUT),[i.PATCH]:a.createHandler(i.PATCH),[i.DELETE]:a.createHandler(i.DELETE),[i.HEAD]:a.createHeadHandler(),[i.OPTIONS]:a.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await a.registerInjectFactories(),await a.loadImports();const s=new a(e,i.HEAD,t);await s.requestHooks.appRequest(e,i.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"),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}}a.features=new Set,a.guards=new Set,a.filters=new Set,a.interceptors=new Set,a.pipes=new Set,a.corsOrigins=[],a.options=o,a.initialized=!1,a.initPromise=null,a.shutdownRegistered=!1,a.registryCreated=!1,a.factoriesRegistered=!1;export{a 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.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};
package/dist/common.d.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { Callback } from "@mxweb/utils";
1
+ /**
2
+ * Generic callback function type.
3
+ * @template R - Return type
4
+ * @template A - Arguments tuple type
5
+ */
6
+ export type Callback<R = void, A extends any[] = []> = (...args: A) => R;
2
7
  /**
3
8
  * @fileoverview Common types, interfaces, and utilities used throughout the @mxweb/core framework.
4
9
  * This module provides foundational types for dependency injection, routing, and lifecycle management.
@@ -29,7 +34,46 @@ import { Callback } from "@mxweb/utils";
29
34
  * }
30
35
  * ```
31
36
  */
32
- export interface ApplicationInject {
37
+ /**
38
+ * Interface for injects that support context switching.
39
+ * Implement this interface to expose a safe, controlled API via `context.switch()`.
40
+ *
41
+ * @template T - The type of object returned by the switch() method
42
+ *
43
+ * @remarks
44
+ * This interface is used to expose a **safe facade** instead of the raw inject instance.
45
+ * The switch() method should return an object that only exposes safe operations,
46
+ * hiding dangerous methods like close(), destroy(), or direct state manipulation.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * interface DbSwitchable {
51
+ * query<T>(sql: string): Promise<T>;
52
+ * getRepo<T>(entity: EntityClass<T>): Repository<T>;
53
+ * }
54
+ *
55
+ * class Connection implements ApplicationInject, Switchable<DbSwitchable> {
56
+ * private client: DatabaseClient;
57
+ *
58
+ * switch(): DbSwitchable {
59
+ * return {
60
+ * query: (sql) => this.client.query(sql),
61
+ * getRepo: (entity) => this.getRepo(entity),
62
+ * };
63
+ * }
64
+ * }
65
+ * ```
66
+ */
67
+ export interface Switchable<T = unknown> {
68
+ /**
69
+ * Returns a safe, controlled interface for this inject.
70
+ * Called by `context.switch()` to get the public API.
71
+ *
72
+ * @returns The switched context or utility object
73
+ */
74
+ switch(): T;
75
+ }
76
+ export interface ApplicationInject extends Partial<Switchable> {
33
77
  /**
34
78
  * Called when the inject is initialized (on first request).
35
79
  * Use this to establish connections, load resources, or perform async setup.
@@ -62,7 +106,7 @@ export interface ApplicationInject {
62
106
  * }
63
107
  * ```
64
108
  */
65
- export interface FeatureInject {
109
+ export interface FeatureInject extends Partial<Switchable> {
66
110
  /**
67
111
  * Called when the feature is loaded.
68
112
  * Use this for feature-specific initialization.
@@ -192,6 +236,9 @@ export type RouteNextHandler = () => void;
192
236
  * Function type for call handlers used in interceptors.
193
237
  * Returns a promise of the handler result.
194
238
  *
239
+ * @deprecated Since v1.1.0, interceptors no longer wrap handlers.
240
+ * Use `ResponseInterceptorHandler` from `response.ts` instead.
241
+ *
195
242
  * @template T - The type of the resolved value
196
243
  */
197
244
  export type CallHandler<T = unknown> = () => Promise<T>;