@mxweb/core 1.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/application.d.ts +402 -0
  4. package/dist/application.js +1 -0
  5. package/dist/application.mjs +1 -0
  6. package/dist/common.d.ts +323 -0
  7. package/dist/common.js +1 -0
  8. package/dist/common.mjs +1 -0
  9. package/dist/config.d.ts +258 -0
  10. package/dist/config.js +1 -0
  11. package/dist/config.mjs +1 -0
  12. package/dist/context.d.ts +48 -0
  13. package/dist/context.js +1 -0
  14. package/dist/context.mjs +1 -0
  15. package/dist/controller.d.ts +238 -0
  16. package/dist/controller.js +1 -0
  17. package/dist/controller.mjs +1 -0
  18. package/dist/decorator.d.ts +349 -0
  19. package/dist/decorator.js +1 -0
  20. package/dist/decorator.mjs +1 -0
  21. package/dist/error.d.ts +301 -0
  22. package/dist/error.js +1 -0
  23. package/dist/error.mjs +1 -0
  24. package/dist/execute.d.ts +469 -0
  25. package/dist/execute.js +1 -0
  26. package/dist/execute.mjs +1 -0
  27. package/dist/feature.d.ts +239 -0
  28. package/dist/feature.js +1 -0
  29. package/dist/feature.mjs +1 -0
  30. package/dist/hooks.d.ts +251 -0
  31. package/dist/hooks.js +1 -0
  32. package/dist/hooks.mjs +1 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.js +1 -0
  35. package/dist/index.mjs +1 -0
  36. package/dist/logger.d.ts +360 -0
  37. package/dist/logger.js +1 -0
  38. package/dist/logger.mjs +1 -0
  39. package/dist/response.d.ts +665 -0
  40. package/dist/response.js +1 -0
  41. package/dist/response.mjs +1 -0
  42. package/dist/route.d.ts +298 -0
  43. package/dist/route.js +1 -0
  44. package/dist/route.mjs +1 -0
  45. package/dist/router.d.ts +205 -0
  46. package/dist/router.js +1 -0
  47. package/dist/router.mjs +1 -0
  48. package/dist/service.d.ts +261 -0
  49. package/dist/service.js +1 -0
  50. package/dist/service.mjs +1 -0
  51. package/package.json +168 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 MXWeb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # @mxweb/core
2
+
3
+ A NestJS-inspired backend framework for Next.js App Router.
4
+
5
+ > ⚠️ **Security Notice (CVE-2025-55182)**
6
+ >
7
+ > This vulnerability affects Next.js. Please ensure you are using a patched version of Next.js:
8
+ > - Next.js 16.x: Use version `16.0.7` or later
9
+ > - Next.js 15.x: Use version `15.1.7` or later
10
+ > - Next.js 14.x: Use version `14.2.25` or later
11
+ >
12
+ > ```bash
13
+ > npm install next@latest
14
+ > ```
15
+ >
16
+ > For more details, see the [Next.js security advisory](https://github.com/vercel/next.js/security/advisories).
17
+
18
+ ## Features
19
+
20
+ - 🏗️ **Feature-based Architecture** - Modular design with Features, Controllers, and Services
21
+ - 💉 **Dependency Injection** - Built-in DI container for managing dependencies
22
+ - 🛡️ **Guards & Interceptors** - Request lifecycle hooks for auth, logging, and more
23
+ - 🔄 **Pipes & Filters** - Data transformation and exception handling
24
+ - 🎯 **Decorators** - Intuitive decorators for clean, declarative code
25
+ - 📦 **Request Scoping** - AsyncLocalStorage-based request context
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ npm install @mxweb/core
31
+ # or
32
+ yarn add @mxweb/core
33
+ # or
34
+ pnpm add @mxweb/core
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```ts
40
+ // app/api/[[...path]]/route.ts
41
+ import { Application } from "@mxweb/core";
42
+ import "@/features/product.feature";
43
+
44
+ const app = Application.create({});
45
+
46
+ export const GET = app.GET;
47
+ export const POST = app.POST;
48
+ export const PUT = app.PUT;
49
+ export const PATCH = app.PATCH;
50
+ export const DELETE = app.DELETE;
51
+ ```
52
+
53
+ ## Documentation
54
+
55
+ For detailed documentation, guides, and API reference, visit:
56
+
57
+ 👉 **[https://docs.mxweb.io](https://docs.mxweb.io)**
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,402 @@
1
+ import { NextRequest } from "next/server";
2
+ import { Feature } from "./feature";
3
+ import { Filter, Guard, Interceptor } from "./execute";
4
+ import { ApplicationHooksOptions } from "./hooks";
5
+ import { ApplicationInject, AsyncFn, Pipe, RoutePayload } from "./common";
6
+ /**
7
+ * Type for application-level dependency injection configuration.
8
+ * Can be either a plain object or an async function that returns the injects object.
9
+ *
10
+ * @example
11
+ * // Plain object
12
+ * const injects: ApplicationInjects = {
13
+ * db: new DatabaseConnection(),
14
+ * config: Config.forRoot(),
15
+ * };
16
+ *
17
+ * // Async function
18
+ * const injects: ApplicationInjects = async () => ({
19
+ * db: await Connection.create(),
20
+ * });
21
+ */
22
+ export type ApplicationInjects = AsyncFn<Record<string, ApplicationInject>>;
23
+ /**
24
+ * Type for CORS origins configuration.
25
+ * Can be a static array of allowed origins or a function that dynamically resolves origins.
26
+ *
27
+ * @example
28
+ * // Static origins
29
+ * const cors: ApplicationCors = ["https://example.com", "https://api.example.com"];
30
+ *
31
+ * // Dynamic origins from config
32
+ * const cors: ApplicationCors = async (injects) => {
33
+ * const config = injects.get("config");
34
+ * return config.get("allowedOrigins");
35
+ * };
36
+ */
37
+ export type ApplicationCors = string[] | ((injects: Map<string, ApplicationInject>) => string[] | Promise<string[]>);
38
+ /**
39
+ * Configuration options for creating an Application instance.
40
+ *
41
+ * @template Key - The key used to extract path segments from Next.js catch-all route params
42
+ *
43
+ * @example
44
+ * const options: ApplicationOptions = {
45
+ * key: "path",
46
+ * injects: { db: Connection.forRoot() },
47
+ * guards: [AuthGuard],
48
+ * filters: [GlobalExceptionFilter],
49
+ * interceptors: [LoggingInterceptor],
50
+ * pipes: [ValidationPipe],
51
+ * cors: ["https://example.com"],
52
+ * onStart: () => console.log("App started"),
53
+ * onRequest: (req, method) => console.log(`${method} request`),
54
+ * onResponse: (ctx) => console.log("Response sent"),
55
+ * };
56
+ */
57
+ export interface ApplicationOptions<Key extends string = "path"> extends ApplicationHooksOptions {
58
+ /** The key used to extract path segments from catch-all route params. Defaults to "path". */
59
+ key?: Key;
60
+ /** Global dependency injection configuration */
61
+ injects?: ApplicationInjects;
62
+ /** Global guards applied to all routes */
63
+ guards?: Guard[];
64
+ /** Global exception filters applied to all routes */
65
+ filters?: Filter[];
66
+ /** Global interceptors applied to all routes */
67
+ interceptors?: Interceptor[];
68
+ /** Global pipes applied to all routes */
69
+ pipes?: Pipe[];
70
+ /** CORS configuration - allowed origins or resolver function */
71
+ cors?: ApplicationCors;
72
+ }
73
+ /**
74
+ * Main Application class for handling HTTP requests in Next.js App Router.
75
+ *
76
+ * This class provides a NestJS-inspired framework for building APIs with:
77
+ * - Dependency injection
78
+ * - Guards, filters, interceptors, and pipes
79
+ * - Feature-based modular architecture
80
+ * - Lifecycle hooks
81
+ * - CORS support
82
+ *
83
+ * @template Key - The key used to extract path segments from Next.js catch-all route params
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * // In app/api/[[...path]]/route.ts
88
+ * import { Application } from "@mxweb/core";
89
+ * import "@/features/products/product.feature";
90
+ *
91
+ * const app = Application.create({
92
+ * injects: {
93
+ * db: Connection.forRoot(),
94
+ * config: Config.forRoot(),
95
+ * },
96
+ * guards: [AuthGuard],
97
+ * cors: ["https://example.com"],
98
+ * });
99
+ *
100
+ * export const GET = app.GET;
101
+ * export const POST = app.POST;
102
+ * export const PUT = app.PUT;
103
+ * export const PATCH = app.PATCH;
104
+ * export const DELETE = app.DELETE;
105
+ * export const HEAD = app.HEAD;
106
+ * export const OPTIONS = app.OPTIONS;
107
+ * ```
108
+ */
109
+ export declare class Application<Key extends string = "path"> {
110
+ private readonly request;
111
+ private readonly method;
112
+ private readonly payload;
113
+ /** Registered features (modules) */
114
+ private static features;
115
+ /** Global guards */
116
+ private static guards;
117
+ /** Global exception filters */
118
+ private static filters;
119
+ /** Global interceptors */
120
+ private static interceptors;
121
+ /** Global pipes */
122
+ private static pipes;
123
+ /** Allowed CORS origins */
124
+ private static corsOrigins;
125
+ /** Application configuration options */
126
+ private static options;
127
+ /** Application-level hooks manager */
128
+ private static hooks;
129
+ /** Flag indicating if features are initialized */
130
+ private static initialized;
131
+ /** Promise for pending initialization */
132
+ private static initPromise;
133
+ /** Flag indicating if shutdown handlers are registered */
134
+ private static shutdownRegistered;
135
+ /** Response builder for this request */
136
+ private response;
137
+ /** Matched feature for this request */
138
+ private feature;
139
+ /** Matched route for this request */
140
+ private route;
141
+ /** Request-scoped hooks manager */
142
+ private requestHooks;
143
+ /**
144
+ * Creates a new Application instance for handling a request.
145
+ * This constructor is private - use Application.create() to set up the application.
146
+ *
147
+ * @param request - The incoming Next.js request
148
+ * @param method - The HTTP method of the request
149
+ * @param payload - The route payload containing path params
150
+ */
151
+ private constructor();
152
+ /**
153
+ * Retrieves a global inject instance by name.
154
+ *
155
+ * @template T - The type of the inject instance
156
+ * @param name - The name of the inject to retrieve
157
+ * @returns The inject instance if found, undefined otherwise
158
+ *
159
+ * @example
160
+ * const db = Application.getInject<DatabaseConnection>("db");
161
+ * const config = Application.getInject<Config>("config");
162
+ */
163
+ static getInject<T extends ApplicationInject = ApplicationInject>(name: string): T | undefined;
164
+ /**
165
+ * Retrieves all registered global injects.
166
+ *
167
+ * @returns A new Map containing all inject name-instance pairs
168
+ *
169
+ * @example
170
+ * const injects = Application.getInjects();
171
+ * for (const [name, instance] of injects) {
172
+ * console.log(`Inject: ${name}`);
173
+ * }
174
+ */
175
+ static getInjects(): Map<string, ApplicationInject>;
176
+ /** Flag indicating if injects have been loaded */
177
+ private static injectsLoaded;
178
+ /**
179
+ * Loads and initializes all application-level injects.
180
+ * This method is idempotent - subsequent calls will not reload injects.
181
+ *
182
+ * @remarks
183
+ * - Supports both plain object and async function for injects configuration
184
+ * - Calls onInit lifecycle hook on each inject
185
+ * - Registers shutdown handlers after loading
186
+ * - Loads CORS configuration after injects are ready
187
+ */
188
+ private static loadInjects;
189
+ /**
190
+ * Registers process shutdown handlers for graceful cleanup.
191
+ * Handles SIGTERM and SIGINT signals to properly destroy injects.
192
+ *
193
+ * @remarks
194
+ * - This method is idempotent - subsequent calls will not re-register handlers
195
+ * - Destroys feature injects first, then global injects
196
+ * - Calls onDestroy lifecycle hook on each inject
197
+ */
198
+ private static registerShutdown;
199
+ /**
200
+ * Loads CORS configuration from options.
201
+ * Supports both static array and dynamic resolver function.
202
+ */
203
+ private static loadCors;
204
+ /**
205
+ * Initializes the application by loading features.
206
+ * This method is idempotent and ensures initialization only happens once.
207
+ *
208
+ * @remarks
209
+ * Features are imported statically in route.ts, this method just marks initialization complete.
210
+ */
211
+ private static loadImports;
212
+ /**
213
+ * Attempts to match the request path against registered features.
214
+ *
215
+ * @param method - The HTTP method of the request
216
+ * @param path - The request path to match
217
+ * @returns true if a matching route was found, null otherwise
218
+ */
219
+ private isMatchFeature;
220
+ /**
221
+ * Executes all guards (global + route-level) for the current request.
222
+ * Global guards run first, then route-specific guards.
223
+ *
224
+ * @returns true if all guards pass, false if any guard denies access
225
+ *
226
+ * @remarks
227
+ * If any guard returns false or throws an error, the request is denied.
228
+ */
229
+ private executeGuards;
230
+ /**
231
+ * Executes all route middlewares sequentially.
232
+ * Each middleware must call next() to continue the chain.
233
+ *
234
+ * @returns true if all middlewares pass, false if any middleware stops the chain
235
+ *
236
+ * @remarks
237
+ * If a middleware doesn't call next() or throws an error, the request is denied.
238
+ */
239
+ private executeMiddlewares;
240
+ /**
241
+ * Executes exception filters to handle errors.
242
+ * Route-level filters run first, then global filters.
243
+ *
244
+ * @param error - The error to handle
245
+ * @returns A Response if a filter handles the error, null otherwise
246
+ *
247
+ * @remarks
248
+ * Filters are tried in order until one returns a Response.
249
+ * If no filter handles the error, null is returned and default error handling applies.
250
+ */
251
+ private executeFilters;
252
+ /**
253
+ * Wraps the handler with interceptor chain.
254
+ * Route-level interceptors are outermost, global interceptors are innermost.
255
+ *
256
+ * @template T - The return type of the handler
257
+ * @param handler - The original handler function to wrap
258
+ * @returns The result of executing the interceptor chain
259
+ *
260
+ * @remarks
261
+ * Interceptors form a chain where each can modify the request/response.
262
+ * The chain is built from right to left (last interceptor wraps handler first).
263
+ */
264
+ private executeInterceptors;
265
+ /**
266
+ * Executes all pipes to transform the input value.
267
+ * Global pipes run first, then route-level pipes.
268
+ *
269
+ * @template T - The type of the value being transformed
270
+ * @param value - The value to transform
271
+ * @returns The transformed value after passing through all pipes
272
+ */
273
+ private executePipes;
274
+ /**
275
+ * Checks if the request origin is allowed by CORS configuration.
276
+ *
277
+ * @returns true if the origin is allowed or no CORS is configured, false otherwise
278
+ */
279
+ private checkCors;
280
+ /**
281
+ * Applies CORS headers to the response.
282
+ *
283
+ * @param response - The response to apply headers to
284
+ * @returns A new Response with CORS headers applied
285
+ */
286
+ private applyCorsHeaders;
287
+ /**
288
+ * Dispatches the request to the matched route handler.
289
+ * Executes the full request lifecycle: guards → middlewares → interceptors → handler.
290
+ *
291
+ * @returns The HTTP response from the handler or error response
292
+ *
293
+ * @remarks
294
+ * This method:
295
+ * 1. Executes guards (returns 403 if denied)
296
+ * 2. Executes middlewares (returns 403 if stopped)
297
+ * 3. Resolves the route action (controller method or function)
298
+ * 4. Wraps handler with interceptors
299
+ * 5. Handles errors with filters and default error handling
300
+ */
301
+ private dispatch;
302
+ /**
303
+ * Matches the incoming request to a feature and route, then dispatches.
304
+ *
305
+ * @returns The HTTP response with CORS headers applied
306
+ *
307
+ * @remarks
308
+ * This method:
309
+ * 1. Checks CORS (returns 403 if not allowed)
310
+ * 2. Matches request path against registered features
311
+ * 3. Loads feature-specific injects
312
+ * 4. Creates RequestContext
313
+ * 5. Runs dispatch within executeContext scope
314
+ * 6. Ensures onResponse hooks are called even on error
315
+ */
316
+ private match;
317
+ /**
318
+ * Creates a request handler for a specific HTTP method.
319
+ *
320
+ * @template Key - The key used to extract path segments from route params
321
+ * @param method - The HTTP method this handler will process
322
+ * @returns An async function that handles Next.js requests
323
+ *
324
+ * @remarks
325
+ * The returned handler:
326
+ * 1. Loads application injects
327
+ * 2. Initializes features
328
+ * 3. Creates Application instance
329
+ * 4. Executes application onRequest hook
330
+ * 5. Matches and dispatches the request
331
+ */
332
+ private static createHandler;
333
+ /**
334
+ * Creates and configures the Application with handlers for all HTTP methods.
335
+ *
336
+ * @template Key - The key used to extract path segments from catch-all route params
337
+ * @param options - Configuration options for the application
338
+ * @returns An object with handlers for each HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * const app = Application.create({
343
+ * key: "path",
344
+ * injects: { db: Connection.forRoot() },
345
+ * guards: [AuthGuard],
346
+ * cors: ["https://example.com"],
347
+ * onStart: () => console.log("App started"),
348
+ * });
349
+ *
350
+ * export const GET = app.GET;
351
+ * export const POST = app.POST;
352
+ * // ... etc
353
+ * ```
354
+ */
355
+ static create<Key extends string = "path">(options?: ApplicationOptions<Key>): {
356
+ GET: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
357
+ POST: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
358
+ PUT: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
359
+ PATCH: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
360
+ DELETE: (req: NextRequest, payload: RoutePayload<"path">) => Promise<Response>;
361
+ HEAD: (req: NextRequest, payload: RoutePayload) => Promise<Response>;
362
+ OPTIONS: (req: NextRequest) => Promise<Response>;
363
+ };
364
+ /**
365
+ * Creates a handler for HEAD requests.
366
+ * HEAD requests use the same logic as GET but return only headers (no body).
367
+ *
368
+ * @returns An async function that handles HEAD requests
369
+ */
370
+ private static createHeadHandler;
371
+ /**
372
+ * Creates a handler for OPTIONS requests (CORS preflight).
373
+ * Returns allowed methods and CORS headers without processing routes.
374
+ *
375
+ * @returns An async function that handles OPTIONS requests
376
+ */
377
+ private static createOptionsHandler;
378
+ /**
379
+ * Registers one or more features (modules) with the application.
380
+ * Features define controllers, routes, and feature-specific dependencies.
381
+ * This method supports chaining.
382
+ *
383
+ * @param features - One or more features to register
384
+ * @returns The Application class for method chaining
385
+ *
386
+ * @example
387
+ * ```ts
388
+ * // Register a single feature
389
+ * Application.register(productFeature);
390
+ *
391
+ * // Register multiple features
392
+ * Application.register(productFeature, userFeature, orderFeature);
393
+ *
394
+ * // Method chaining
395
+ * Application
396
+ * .register(productFeature)
397
+ * .register(userFeature)
398
+ * .register(orderFeature);
399
+ * ```
400
+ */
401
+ static register(...features: Feature[]): typeof Application;
402
+ }
@@ -0,0 +1 @@
1
+ "use strict";var e=require("./error.js"),t=require("./context.js"),s=require("./response.js"),r=require("./hooks.js"),o=require("./common.js");const n={key:"path"};class i{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(i.hooks)}static getInject(e){return t.executeContext.getInject(e)}static getInjects(){return new Map(t.executeContext.injections)}static async loadInjects(){if(this.injectsLoaded)return;const e=this.options.injects;if(!e)return void(this.injectsLoaded=!0);const s="function"==typeof e?await e():e;for(const[e,r]of Object.entries(s))"function"==typeof r.onInit&&await r.onInit(),t.executeContext.setInject(e,r);this.injectsLoaded=!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("function"==typeof s.onDestroy)try{await s.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(t.executeContext.injections))}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,o=await s.catch(e,t.executeContext);if(o instanceof Response)return o}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 o=new(0,s[e]),n=r;r=()=>o.intercept(t.executeContext,n)}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,o=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(o);const n=await this.executeInterceptors(()=>t(o));return this.response.success(n)}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||n.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 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.loadInjects(),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=n){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)),{[o.RouteMethod.GET]:i.createHandler(o.RouteMethod.GET),[o.RouteMethod.POST]:i.createHandler(o.RouteMethod.POST),[o.RouteMethod.PUT]:i.createHandler(o.RouteMethod.PUT),[o.RouteMethod.PATCH]:i.createHandler(o.RouteMethod.PATCH),[o.RouteMethod.DELETE]:i.createHandler(o.RouteMethod.DELETE),[o.RouteMethod.HEAD]:i.createHeadHandler(),[o.RouteMethod.OPTIONS]:i.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await i.loadInjects(),await i.loadImports();const s=new i(e,o.RouteMethod.HEAD,t);await s.requestHooks.appRequest(e,o.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.loadInjects();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}}i.features=new Set,i.guards=new Set,i.filters=new Set,i.interceptors=new Set,i.pipes=new Set,i.corsOrigins=[],i.options=n,i.initialized=!1,i.initPromise=null,i.shutdownRegistered=!1,i.injectsLoaded=!1,exports.Application=i;
@@ -0,0 +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 o}from"./common.mjs";const i={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 getInjects(){return new Map(t.injections)}static async loadInjects(){if(this.injectsLoaded)return;const e=this.options.injects;if(!e)return void(this.injectsLoaded=!0);const s="function"==typeof e?await e():e;for(const[e,r]of Object.entries(s))"function"==typeof r.onInit&&await r.onInit(),t.setInject(e,r);this.injectsLoaded=!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("function"==typeof s.onDestroy)try{await s.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(t.injections))}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]),o=r;r=()=>n.intercept(t,o)}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 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){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||i.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 o=await t.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(o)}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.loadInjects(),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=i){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)),{[o.GET]:a.createHandler(o.GET),[o.POST]:a.createHandler(o.POST),[o.PUT]:a.createHandler(o.PUT),[o.PATCH]:a.createHandler(o.PATCH),[o.DELETE]:a.createHandler(o.DELETE),[o.HEAD]:a.createHeadHandler(),[o.OPTIONS]:a.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await a.loadInjects(),await a.loadImports();const s=new a(e,o.HEAD,t);await s.requestHooks.appRequest(e,o.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.loadInjects();const t=this.corsOrigins,r=e.headers.get("origin"),n=s.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}}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.initialized=!1,a.initPromise=null,a.shutdownRegistered=!1,a.injectsLoaded=!1;export{a as Application};