@mxweb/core 1.0.0 → 1.0.2
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 +82 -0
- package/dist/application.d.ts +90 -45
- package/dist/application.js +1 -1
- package/dist/application.mjs +1 -1
- package/dist/common.d.ts +149 -0
- package/dist/config.d.ts +44 -27
- package/dist/config.js +1 -1
- package/dist/config.mjs +1 -1
- package/dist/decorator.d.ts +21 -5
- package/dist/decorator.js +1 -1
- package/dist/decorator.mjs +1 -1
- package/dist/execute.d.ts +74 -22
- package/dist/execute.js +1 -1
- package/dist/execute.mjs +1 -1
- package/dist/feature.d.ts +46 -20
- package/dist/feature.js +1 -1
- package/dist/feature.mjs +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.mjs +1 -1
- package/dist/service.d.ts +22 -4
- package/dist/service.js +1 -1
- package/dist/service.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.2] - 2024-12-18
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Build**: Improved terser configuration for logger module
|
|
13
|
+
- Added custom `terserWithOptions()` plugin for file-specific minification
|
|
14
|
+
- Logger module now keeps console calls while still being minified
|
|
15
|
+
- Other modules continue to have console calls removed during minification
|
|
16
|
+
- Added `loggerMinifyOptions` with `drop_console: false` for logger
|
|
17
|
+
|
|
18
|
+
## [1.0.1] - 2024-12-16
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **Config**: Refactored `forRoot()` to return a factory function for lazy initialization via `InjectRegistry`
|
|
23
|
+
- `forRoot()` now accepts optional `name` parameter for custom inject naming
|
|
24
|
+
- Updated to use new `InjectRegistry` pattern with `registry.set(name, factory)`
|
|
25
|
+
- Improved type safety for `get()` and `getAsync()` methods with conditional return types
|
|
26
|
+
|
|
27
|
+
- **Application**: Major refactoring of dependency injection system
|
|
28
|
+
- Changed `ApplicationInjects` from object/async function to `InjectFactory[]` array
|
|
29
|
+
- Added `InjectRegistry` wrapper for safe inject registration (`get`/`set` only)
|
|
30
|
+
- Implemented lazy initialization - inject instances created on first `getInject()` call
|
|
31
|
+
- Added `getInject()` (async) and `getInjectSync()` methods for accessing injects
|
|
32
|
+
- Injects are now stored as `InjectEntry` with factory and lazy instance
|
|
33
|
+
|
|
34
|
+
- **ExecuteContext**: Added support for lazy inject initialization
|
|
35
|
+
- Added `setRegistry()` method to set registry reference
|
|
36
|
+
- Changed `getInject()` to async method for lazy initialization
|
|
37
|
+
- Added `getInjectSync()` for synchronous access (only if already initialized)
|
|
38
|
+
- Added `isInjectInitialized()` method to check inject state
|
|
39
|
+
- Injects stored as `InjectEntry` with factory and optional instance
|
|
40
|
+
- Changed `getLocalInject()` to async method for lazy initialization
|
|
41
|
+
- Added `getLocalInjectSync()` for synchronous access to feature injects
|
|
42
|
+
|
|
43
|
+
- **Feature**: Refactored feature inject system to use factory pattern
|
|
44
|
+
- Changed `FeatureInjects` from object to `FeatureInjectFactory[]` array
|
|
45
|
+
- Added `FeatureInjectRegistry` wrapper for safe inject registration
|
|
46
|
+
- Implemented lazy initialization - inject instances created on first `getInject()` call
|
|
47
|
+
- Added `getInjectSync()` for synchronous access (only if already initialized)
|
|
48
|
+
- Added `isInjectInitialized()` method to check inject state
|
|
49
|
+
- Lifecycle hooks (`onFeature`, `onFeatureDestroy`) now called at appropriate times
|
|
50
|
+
|
|
51
|
+
- **Service**: Updated inject methods for async pattern
|
|
52
|
+
- Changed `getInject()` to async method returning `Promise<T | undefined>`
|
|
53
|
+
- Added `getInjectSync()` for synchronous access (only if already initialized)
|
|
54
|
+
|
|
55
|
+
- **Decorator**: Updated inject decorators for async pattern
|
|
56
|
+
- Changed `Inject()` to async function returning `Promise<T | undefined>`
|
|
57
|
+
- Added `InjectSync()` for synchronous access (only if already initialized)
|
|
58
|
+
|
|
59
|
+
- **Common**: Added new types for improved dependency injection
|
|
60
|
+
- Added `InjectInstanceFactory<T>` - factory function type that receives `InjectRegistry`
|
|
61
|
+
- Added `InjectEntry<T>` - entry containing factory and lazily-initialized instance
|
|
62
|
+
- Added `InjectRegistry` interface - wrapper with `get`/`set` methods only
|
|
63
|
+
- Updated `InjectFactory` type to work with new `InjectRegistry`
|
|
64
|
+
- Added `FeatureInjectInstanceFactory<T>` - factory function for feature injects
|
|
65
|
+
- Added `FeatureInjectEntry<T>` - entry for feature inject factory and instance
|
|
66
|
+
- Added `FeatureInjectRegistry` interface - wrapper for feature inject registration
|
|
67
|
+
- Added `FeatureInjectFactory` type for feature inject factories
|
|
68
|
+
|
|
69
|
+
## [1.0.0] - 2024-12-16
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- Initial release of `@mxweb/core` framework
|
|
74
|
+
- Configuration management system (`Config` class) with:
|
|
75
|
+
- Synchronous and asynchronous configuration providers
|
|
76
|
+
- Lazy evaluation of configuration factories
|
|
77
|
+
- Singleton pattern for consistent configuration
|
|
78
|
+
- Support for default values in `get()` and `getAsync()` methods
|
|
79
|
+
- `getOrThrow()` and `getAsyncOrThrow()` methods for required configurations
|
|
80
|
+
- `has()` method to check configuration existence
|
|
81
|
+
- `getAll()` method to retrieve all built configurations
|
|
82
|
+
- `reset()` method for testing purposes
|
package/dist/application.d.ts
CHANGED
|
@@ -2,48 +2,58 @@ import { NextRequest } from "next/server";
|
|
|
2
2
|
import { Feature } from "./feature";
|
|
3
3
|
import { Filter, Guard, Interceptor } from "./execute";
|
|
4
4
|
import { ApplicationHooksOptions } from "./hooks";
|
|
5
|
-
import { ApplicationInject,
|
|
5
|
+
import { ApplicationInject, InjectEntry, InjectFactory, InjectRegistry, Pipe, RoutePayload } from "./common";
|
|
6
6
|
/**
|
|
7
7
|
* Type for application-level dependency injection configuration.
|
|
8
|
-
*
|
|
8
|
+
* An array of inject factory functions that register injects via InjectRegistry.
|
|
9
9
|
*
|
|
10
|
-
* @
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* db: new DatabaseConnection(),
|
|
14
|
-
* config: Config.forRoot(),
|
|
15
|
-
* };
|
|
10
|
+
* @remarks
|
|
11
|
+
* Uses the new factory pattern for lazy initialization of injects.
|
|
12
|
+
* Each factory receives an InjectRegistry to register its inject.
|
|
16
13
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const injects: ApplicationInjects = [
|
|
17
|
+
* Config.forRoot(), // Registers "config" inject
|
|
18
|
+
* Database.forRoot("db"), // Registers "db" inject
|
|
19
|
+
* async (registry) => { // Custom factory
|
|
20
|
+
* const config = await registry.get<Config>("config");
|
|
21
|
+
* registry.set("cache", () => new Cache(config));
|
|
22
|
+
* },
|
|
23
|
+
* ];
|
|
24
|
+
* ```
|
|
21
25
|
*/
|
|
22
|
-
export type ApplicationInjects =
|
|
26
|
+
export type ApplicationInjects = InjectFactory[];
|
|
23
27
|
/**
|
|
24
28
|
* Type for CORS origins configuration.
|
|
25
29
|
* Can be a static array of allowed origins or a function that dynamically resolves origins.
|
|
26
30
|
*
|
|
27
31
|
* @example
|
|
32
|
+
* ```ts
|
|
28
33
|
* // Static origins
|
|
29
34
|
* const cors: ApplicationCors = ["https://example.com", "https://api.example.com"];
|
|
30
35
|
*
|
|
31
|
-
* // Dynamic origins from
|
|
32
|
-
* const cors: ApplicationCors = async (
|
|
33
|
-
* const config =
|
|
34
|
-
* return config
|
|
36
|
+
* // Dynamic origins from registry
|
|
37
|
+
* const cors: ApplicationCors = async (registry) => {
|
|
38
|
+
* const config = await registry.get<Config>("config");
|
|
39
|
+
* return config?.get<string[]>("allowedOrigins") ?? [];
|
|
35
40
|
* };
|
|
41
|
+
* ```
|
|
36
42
|
*/
|
|
37
|
-
export type ApplicationCors = string[] | ((
|
|
43
|
+
export type ApplicationCors = string[] | ((registry: InjectRegistry) => string[] | Promise<string[]>);
|
|
38
44
|
/**
|
|
39
45
|
* Configuration options for creating an Application instance.
|
|
40
46
|
*
|
|
41
47
|
* @template Key - The key used to extract path segments from Next.js catch-all route params
|
|
42
48
|
*
|
|
43
49
|
* @example
|
|
50
|
+
* ```ts
|
|
44
51
|
* const options: ApplicationOptions = {
|
|
45
52
|
* key: "path",
|
|
46
|
-
* injects:
|
|
53
|
+
* injects: [
|
|
54
|
+
* Config.forRoot(),
|
|
55
|
+
* Database.forRoot(),
|
|
56
|
+
* ],
|
|
47
57
|
* guards: [AuthGuard],
|
|
48
58
|
* filters: [GlobalExceptionFilter],
|
|
49
59
|
* interceptors: [LoggingInterceptor],
|
|
@@ -53,11 +63,12 @@ export type ApplicationCors = string[] | ((injects: Map<string, ApplicationInjec
|
|
|
53
63
|
* onRequest: (req, method) => console.log(`${method} request`),
|
|
54
64
|
* onResponse: (ctx) => console.log("Response sent"),
|
|
55
65
|
* };
|
|
66
|
+
* ```
|
|
56
67
|
*/
|
|
57
68
|
export interface ApplicationOptions<Key extends string = "path"> extends ApplicationHooksOptions {
|
|
58
69
|
/** The key used to extract path segments from catch-all route params. Defaults to "path". */
|
|
59
70
|
key?: Key;
|
|
60
|
-
/**
|
|
71
|
+
/** Array of inject factories for dependency injection */
|
|
61
72
|
injects?: ApplicationInjects;
|
|
62
73
|
/** Global guards applied to all routes */
|
|
63
74
|
guards?: Guard[];
|
|
@@ -89,10 +100,10 @@ export interface ApplicationOptions<Key extends string = "path"> extends Applica
|
|
|
89
100
|
* import "@/features/products/product.feature";
|
|
90
101
|
*
|
|
91
102
|
* const app = Application.create({
|
|
92
|
-
* injects:
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
103
|
+
* injects: [
|
|
104
|
+
* Config.forRoot(),
|
|
105
|
+
* Database.forRoot(),
|
|
106
|
+
* ],
|
|
96
107
|
* guards: [AuthGuard],
|
|
97
108
|
* cors: ["https://example.com"],
|
|
98
109
|
* });
|
|
@@ -150,42 +161,73 @@ export declare class Application<Key extends string = "path"> {
|
|
|
150
161
|
*/
|
|
151
162
|
private constructor();
|
|
152
163
|
/**
|
|
153
|
-
* Retrieves a global inject instance by name.
|
|
164
|
+
* Retrieves a global inject instance by name (async for lazy initialization).
|
|
165
|
+
*
|
|
166
|
+
* @template T - The type of the inject instance
|
|
167
|
+
* @param name - The name of the inject to retrieve
|
|
168
|
+
* @returns Promise resolving to the inject instance if found, undefined otherwise
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* const db = await Application.getInject<DatabaseConnection>("db");
|
|
173
|
+
* const config = await Application.getInject<Config>("config");
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
static getInject<T extends ApplicationInject = ApplicationInject>(name: string): Promise<T | undefined>;
|
|
177
|
+
/**
|
|
178
|
+
* Retrieves a global inject instance by name (sync - only if already initialized).
|
|
154
179
|
*
|
|
155
180
|
* @template T - The type of the inject instance
|
|
156
181
|
* @param name - The name of the inject to retrieve
|
|
157
|
-
* @returns The inject instance if found, undefined otherwise
|
|
182
|
+
* @returns The inject instance if found and initialized, undefined otherwise
|
|
158
183
|
*
|
|
159
184
|
* @example
|
|
160
|
-
*
|
|
161
|
-
* const config = Application.
|
|
185
|
+
* ```ts
|
|
186
|
+
* const config = Application.getInjectSync<Config>("config");
|
|
187
|
+
* ```
|
|
162
188
|
*/
|
|
163
|
-
static
|
|
189
|
+
static getInjectSync<T extends ApplicationInject = ApplicationInject>(name: string): T | undefined;
|
|
164
190
|
/**
|
|
165
|
-
* Retrieves all registered global
|
|
191
|
+
* Retrieves all registered global inject entries.
|
|
166
192
|
*
|
|
167
|
-
* @returns A new Map containing all inject name-
|
|
193
|
+
* @returns A new Map containing all inject name-entry pairs
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* Entries contain factories and optionally initialized instances.
|
|
168
197
|
*
|
|
169
198
|
* @example
|
|
199
|
+
* ```ts
|
|
170
200
|
* const injects = Application.getInjects();
|
|
171
|
-
* for (const [name,
|
|
172
|
-
* console.log(`Inject: ${name}`);
|
|
201
|
+
* for (const [name, entry] of injects) {
|
|
202
|
+
* console.log(`Inject: ${name}, initialized: ${!!entry.instance}`);
|
|
173
203
|
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
static getInjects(): Map<string, InjectEntry>;
|
|
207
|
+
/** Flag indicating if inject registry has been created */
|
|
208
|
+
private static registryCreated;
|
|
209
|
+
/** The InjectRegistry instance */
|
|
210
|
+
private static registry;
|
|
211
|
+
/**
|
|
212
|
+
* Creates the InjectRegistry wrapper for safe inject registration.
|
|
213
|
+
* This method is idempotent - subsequent calls return the same registry.
|
|
214
|
+
*
|
|
215
|
+
* @returns The InjectRegistry instance
|
|
174
216
|
*/
|
|
175
|
-
static
|
|
176
|
-
/** Flag indicating if
|
|
177
|
-
private static
|
|
217
|
+
private static createInjectRegistry;
|
|
218
|
+
/** Flag indicating if inject factories have been registered */
|
|
219
|
+
private static factoriesRegistered;
|
|
178
220
|
/**
|
|
179
|
-
*
|
|
180
|
-
* This method is idempotent - subsequent calls will not
|
|
221
|
+
* Registers all inject factories from options.
|
|
222
|
+
* This method is idempotent - subsequent calls will not re-register.
|
|
181
223
|
*
|
|
182
224
|
* @remarks
|
|
183
|
-
* -
|
|
184
|
-
* -
|
|
185
|
-
* - Registers shutdown handlers after
|
|
186
|
-
* - Loads CORS configuration
|
|
225
|
+
* - Creates the InjectRegistry wrapper
|
|
226
|
+
* - Processes each factory in order (factories just register, don't create instances)
|
|
227
|
+
* - Registers shutdown handlers after all factories are registered
|
|
228
|
+
* - Loads CORS configuration (may need injects)
|
|
187
229
|
*/
|
|
188
|
-
private static
|
|
230
|
+
private static registerInjectFactories;
|
|
189
231
|
/**
|
|
190
232
|
* Registers process shutdown handlers for graceful cleanup.
|
|
191
233
|
* Handles SIGTERM and SIGINT signals to properly destroy injects.
|
|
@@ -323,7 +365,7 @@ export declare class Application<Key extends string = "path"> {
|
|
|
323
365
|
*
|
|
324
366
|
* @remarks
|
|
325
367
|
* The returned handler:
|
|
326
|
-
* 1.
|
|
368
|
+
* 1. Registers inject factories (lazy - instances created on first get)
|
|
327
369
|
* 2. Initializes features
|
|
328
370
|
* 3. Creates Application instance
|
|
329
371
|
* 4. Executes application onRequest hook
|
|
@@ -341,7 +383,10 @@ export declare class Application<Key extends string = "path"> {
|
|
|
341
383
|
* ```ts
|
|
342
384
|
* const app = Application.create({
|
|
343
385
|
* key: "path",
|
|
344
|
-
* injects:
|
|
386
|
+
* injects: [
|
|
387
|
+
* Config.forRoot(),
|
|
388
|
+
* Database.forRoot(),
|
|
389
|
+
* ],
|
|
345
390
|
* guards: [AuthGuard],
|
|
346
391
|
* cors: ["https://example.com"],
|
|
347
392
|
* onStart: () => console.log("App started"),
|
package/dist/application.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e=require("./error.js"),t=require("./context.js"),s=require("./response.js"),r=require("./hooks.js"),
|
|
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;
|
package/dist/application.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{HttpError as e}from"./error.mjs";import{executeContext as t}from"./context.mjs";import{ServerResponse as s}from"./response.mjs";import{RequestHooks as r,ApplicationHooks as n}from"./hooks.mjs";import{RouteMethod as
|
|
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};
|
package/dist/common.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Callback } from "@mxweb/utils";
|
|
1
2
|
/**
|
|
2
3
|
* @fileoverview Common types, interfaces, and utilities used throughout the @mxweb/core framework.
|
|
3
4
|
* This module provides foundational types for dependency injection, routing, and lifecycle management.
|
|
@@ -73,6 +74,77 @@ export interface FeatureInject {
|
|
|
73
74
|
*/
|
|
74
75
|
onFeatureDestroy?(): void | Promise<void>;
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Factory function that creates a feature inject instance.
|
|
79
|
+
* Receives the registry to access other feature injects.
|
|
80
|
+
*
|
|
81
|
+
* @template T - The type of the inject instance (must extend FeatureInject)
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* const repoFactory: FeatureInjectInstanceFactory<ProductRepository> = async (registry) => {
|
|
86
|
+
* const db = await registry.get<DatabaseConnection>("db");
|
|
87
|
+
* return new ProductRepository(db!);
|
|
88
|
+
* };
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export type FeatureInjectInstanceFactory<T extends FeatureInject = FeatureInject> = (registry: FeatureInjectRegistry) => T | Promise<T>;
|
|
92
|
+
/**
|
|
93
|
+
* Entry stored in the feature injection map.
|
|
94
|
+
* Contains factory and lazily-initialized instance.
|
|
95
|
+
*
|
|
96
|
+
* @template T - The type of the inject instance (must extend FeatureInject)
|
|
97
|
+
*/
|
|
98
|
+
export interface FeatureInjectEntry<T extends FeatureInject = FeatureInject> {
|
|
99
|
+
/** Factory function to create the instance */
|
|
100
|
+
factory: FeatureInjectInstanceFactory<T>;
|
|
101
|
+
/** Lazily-initialized instance (undefined until first access) */
|
|
102
|
+
instance?: T;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Wrapper object for feature inject registration.
|
|
106
|
+
* Only exposes get/set to prevent accidental removal.
|
|
107
|
+
*
|
|
108
|
+
* @remarks
|
|
109
|
+
* This interface is passed to feature inject factories to allow them to
|
|
110
|
+
* access other injects and register themselves.
|
|
111
|
+
*/
|
|
112
|
+
export interface FeatureInjectRegistry {
|
|
113
|
+
/**
|
|
114
|
+
* Retrieves a feature inject by name (async for lazy initialization).
|
|
115
|
+
*
|
|
116
|
+
* @template T - The expected type of the inject
|
|
117
|
+
* @param name - The name of the inject to retrieve
|
|
118
|
+
* @returns Promise resolving to the inject instance or undefined
|
|
119
|
+
*/
|
|
120
|
+
get<T extends FeatureInject = FeatureInject>(name: string): Promise<T | undefined>;
|
|
121
|
+
/**
|
|
122
|
+
* Registers a feature inject factory by name.
|
|
123
|
+
*
|
|
124
|
+
* @template T - The type of the inject instance
|
|
125
|
+
* @param name - The name to register the inject under
|
|
126
|
+
* @param factory - Factory function that creates the inject instance
|
|
127
|
+
*/
|
|
128
|
+
set<T extends FeatureInject = FeatureInject>(name: string, factory: FeatureInjectInstanceFactory<T>): void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Factory function that registers feature injects via the registry.
|
|
132
|
+
* Used in Feature.create({ injects: [...] }) array.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const feature = Feature.create({
|
|
137
|
+
* controller: ProductController,
|
|
138
|
+
* router: Router.register(...),
|
|
139
|
+
* injects: [
|
|
140
|
+
* (registry) => {
|
|
141
|
+
* registry.set("productRepo", () => new ProductRepository());
|
|
142
|
+
* },
|
|
143
|
+
* ],
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export type FeatureInjectFactory = Callback<void | Promise<void>, [FeatureInjectRegistry]>;
|
|
76
148
|
/**
|
|
77
149
|
* Utility type for values that can be either synchronous or async.
|
|
78
150
|
* Commonly used for configuration that may need async resolution.
|
|
@@ -301,6 +373,83 @@ export declare class InjectionRegistry {
|
|
|
301
373
|
*/
|
|
302
374
|
static hasInstance(name: string): boolean;
|
|
303
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Factory function that creates an inject instance.
|
|
378
|
+
* Receives the registry to access other injects.
|
|
379
|
+
*
|
|
380
|
+
* @template T - The type of the inject instance (must extend ApplicationInject)
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```ts
|
|
384
|
+
* const dbFactory: InjectInstanceFactory<DatabaseConnection> = async (registry) => {
|
|
385
|
+
* const config = await registry.get<Config>("config");
|
|
386
|
+
* return new DatabaseConnection(config?.get("dbUrl"));
|
|
387
|
+
* };
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
export type InjectInstanceFactory<T extends ApplicationInject = ApplicationInject> = (registry: InjectRegistry) => T | Promise<T>;
|
|
391
|
+
/**
|
|
392
|
+
* Entry stored in the injection map.
|
|
393
|
+
* Contains factory and lazily-initialized instance.
|
|
394
|
+
*
|
|
395
|
+
* @template T - The type of the inject instance (must extend ApplicationInject)
|
|
396
|
+
*/
|
|
397
|
+
export interface InjectEntry<T extends ApplicationInject = ApplicationInject> {
|
|
398
|
+
/** Factory function to create the instance */
|
|
399
|
+
factory: InjectInstanceFactory<T>;
|
|
400
|
+
/** Lazily-initialized instance (undefined until first access) */
|
|
401
|
+
instance?: T;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Wrapper object for inject registration.
|
|
405
|
+
* Only exposes get/set to prevent accidental removal.
|
|
406
|
+
*
|
|
407
|
+
* @remarks
|
|
408
|
+
* This interface is passed to inject factories to allow them to
|
|
409
|
+
* access other injects and register themselves.
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```ts
|
|
413
|
+
* Config.forRoot("config"); // Returns (registry: InjectRegistry) => void
|
|
414
|
+
*
|
|
415
|
+
* // Inside the factory:
|
|
416
|
+
* registry.set("config", () => new Config());
|
|
417
|
+
* const db = await registry.get<Database>("db");
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
export interface InjectRegistry {
|
|
421
|
+
/**
|
|
422
|
+
* Retrieves an inject by name (async for lazy initialization).
|
|
423
|
+
*
|
|
424
|
+
* @template T - The expected type of the inject
|
|
425
|
+
* @param name - The name of the inject to retrieve
|
|
426
|
+
* @returns Promise resolving to the inject instance or undefined
|
|
427
|
+
*/
|
|
428
|
+
get<T extends ApplicationInject = ApplicationInject>(name: string): Promise<T | undefined>;
|
|
429
|
+
/**
|
|
430
|
+
* Registers an inject factory by name.
|
|
431
|
+
*
|
|
432
|
+
* @template T - The type of the inject instance
|
|
433
|
+
* @param name - The name to register the inject under
|
|
434
|
+
* @param factory - Factory function that creates the inject instance
|
|
435
|
+
*/
|
|
436
|
+
set<T extends ApplicationInject = ApplicationInject>(name: string, factory: InjectInstanceFactory<T>): void;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Factory function that registers injects via the registry.
|
|
440
|
+
* Used in Application.create({ injects: [...] }) array.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* const app = Application.create({
|
|
445
|
+
* injects: [
|
|
446
|
+
* Config.forRoot(), // Returns InjectFactory
|
|
447
|
+
* Database.forRoot(), // Returns InjectFactory
|
|
448
|
+
* ],
|
|
449
|
+
* });
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
export type InjectFactory = Callback<void | Promise<void>, [InjectRegistry]>;
|
|
304
453
|
/**
|
|
305
454
|
* Enumeration of supported HTTP methods.
|
|
306
455
|
* Used throughout the framework for route matching and handler creation.
|