@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.
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/application.d.ts +402 -0
- package/dist/application.js +1 -0
- package/dist/application.mjs +1 -0
- package/dist/common.d.ts +323 -0
- package/dist/common.js +1 -0
- package/dist/common.mjs +1 -0
- package/dist/config.d.ts +258 -0
- package/dist/config.js +1 -0
- package/dist/config.mjs +1 -0
- package/dist/context.d.ts +48 -0
- package/dist/context.js +1 -0
- package/dist/context.mjs +1 -0
- package/dist/controller.d.ts +238 -0
- package/dist/controller.js +1 -0
- package/dist/controller.mjs +1 -0
- package/dist/decorator.d.ts +349 -0
- package/dist/decorator.js +1 -0
- package/dist/decorator.mjs +1 -0
- package/dist/error.d.ts +301 -0
- package/dist/error.js +1 -0
- package/dist/error.mjs +1 -0
- package/dist/execute.d.ts +469 -0
- package/dist/execute.js +1 -0
- package/dist/execute.mjs +1 -0
- package/dist/feature.d.ts +239 -0
- package/dist/feature.js +1 -0
- package/dist/feature.mjs +1 -0
- package/dist/hooks.d.ts +251 -0
- package/dist/hooks.js +1 -0
- package/dist/hooks.mjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/dist/logger.d.ts +360 -0
- package/dist/logger.js +1 -0
- package/dist/logger.mjs +1 -0
- package/dist/response.d.ts +665 -0
- package/dist/response.js +1 -0
- package/dist/response.mjs +1 -0
- package/dist/route.d.ts +298 -0
- package/dist/route.js +1 -0
- package/dist/route.mjs +1 -0
- package/dist/router.d.ts +205 -0
- package/dist/router.js +1 -0
- package/dist/router.mjs +1 -0
- package/dist/service.d.ts +261 -0
- package/dist/service.js +1 -0
- package/dist/service.mjs +1 -0
- 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};
|