@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 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
@@ -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, AsyncFn, Pipe, RoutePayload } from "./common";
5
+ import { ApplicationInject, InjectEntry, InjectFactory, InjectRegistry, Pipe, RoutePayload } from "./common";
6
6
  /**
7
7
  * Type for application-level dependency injection configuration.
8
- * Can be either a plain object or an async function that returns the injects object.
8
+ * An array of inject factory functions that register injects via InjectRegistry.
9
9
  *
10
- * @example
11
- * // Plain object
12
- * const injects: ApplicationInjects = {
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
- * // Async function
18
- * const injects: ApplicationInjects = async () => ({
19
- * db: await Connection.create(),
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 = AsyncFn<Record<string, ApplicationInject>>;
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 config
32
- * const cors: ApplicationCors = async (injects) => {
33
- * const config = injects.get("config");
34
- * return config.get("allowedOrigins");
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[] | ((injects: Map<string, ApplicationInject>) => string[] | Promise<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: { db: Connection.forRoot() },
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
- /** Global dependency injection configuration */
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
- * db: Connection.forRoot(),
94
- * config: Config.forRoot(),
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
- * const db = Application.getInject<DatabaseConnection>("db");
161
- * const config = Application.getInject<Config>("config");
185
+ * ```ts
186
+ * const config = Application.getInjectSync<Config>("config");
187
+ * ```
162
188
  */
163
- static getInject<T extends ApplicationInject = ApplicationInject>(name: string): T | undefined;
189
+ static getInjectSync<T extends ApplicationInject = ApplicationInject>(name: string): T | undefined;
164
190
  /**
165
- * Retrieves all registered global injects.
191
+ * Retrieves all registered global inject entries.
166
192
  *
167
- * @returns A new Map containing all inject name-instance pairs
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, instance] of injects) {
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 getInjects(): Map<string, ApplicationInject>;
176
- /** Flag indicating if injects have been loaded */
177
- private static injectsLoaded;
217
+ private static createInjectRegistry;
218
+ /** Flag indicating if inject factories have been registered */
219
+ private static factoriesRegistered;
178
220
  /**
179
- * Loads and initializes all application-level injects.
180
- * This method is idempotent - subsequent calls will not reload injects.
221
+ * Registers all inject factories from options.
222
+ * This method is idempotent - subsequent calls will not re-register.
181
223
  *
182
224
  * @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
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 loadInjects;
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. Loads application injects
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: { db: Connection.forRoot() },
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"),
@@ -1 +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;
1
+ "use strict";var e=require("./error.js"),t=require("./context.js"),s=require("./response.js"),r=require("./hooks.js"),n=require("./common.js");const o={key:"path"};class i{constructor(e,t,n){this.request=e,this.method=t,this.payload=n,this.feature=null,this.route=null,this.response=new s.ServerResponse,this.requestHooks=new r.RequestHooks(i.hooks)}static getInject(e){return t.executeContext.getInject(e)}static getInjectSync(e){return t.executeContext.getInjectSync(e)}static getInjects(){return new Map(t.executeContext.injections)}static createInjectRegistry(){return this.registryCreated||(this.registry={get:async e=>t.executeContext.getInject(e),set(e,s){t.executeContext.setInject(e,s)}},t.executeContext.setRegistry(this.registry),this.registryCreated=!0),this.registry}static async registerInjectFactories(){if(this.factoriesRegistered)return;const e=this.options.injects;if(!e||!e.length)return void(this.factoriesRegistered=!0);const t=this.createInjectRegistry();for(const s of e)await s(t);this.factoriesRegistered=!0,this.registerShutdown(),await this.loadCors()}static registerShutdown(){if(this.shutdownRegistered)return;this.shutdownRegistered=!0;const e=async()=>{for(const e of this.features)await e.destroyInjects();for(const[e,s]of t.executeContext.injections)if(s.instance&&"function"==typeof s.instance.onDestroy)try{await s.instance.onDestroy()}catch(e){}process.exit(0)};process.on("SIGTERM",e),process.on("SIGINT",e)}static async loadCors(){const e=this.options.cors;e&&(Array.isArray(e)?this.corsOrigins=e:this.corsOrigins=await e(this.registry))}static async loadImports(){if(!this.initialized)return this.initPromise||(this.initPromise=(async()=>{this.initialized=!0})()),this.initPromise}isMatchFeature(e,t){for(const s of i.features)if(this.route=s.matchRoute(e,t),this.route)return this.feature=s,!0;return null}async executeGuards(){const e=this.route?.route.getReflect().getGuards()??new Set,s=[...i.guards,...e];if(!s.length)return!0;for(const e of s)try{const s=new e;if(!await s.canActivate(t.executeContext))return!1}catch{return!1}return!0}async executeMiddlewares(){const e=this.route.route.getMiddlewares();if(!e.length)return!0;for(const s of e){let e=!1;const r=()=>{e=!0};try{if(await s(t.executeContext,r),!e)return!1}catch{return!1}}return!0}async executeFilters(e){const s=[...this.route?.route.getReflect().getFilters()??new Set,...i.filters];for(const r of s)try{const s=new r,n=await s.catch(e,t.executeContext);if(n instanceof Response)return n}catch{continue}return null}async executeInterceptors(e){const s=[...this.route?.route.getReflect().getInterceptors()??new Set,...i.interceptors];if(!s.length)return e();let r=e;for(let e=s.length-1;e>=0;e--){const n=new(0,s[e]),o=r;r=()=>n.intercept(t.executeContext,o)}return r()}async executePipes(e){const t=this.route?.route.getReflect().getPipes()??new Set,s=[...i.pipes,...t];if(!s.length)return e;let r=e;for(const e of s){const t=new e;r=await t.transform(r)}return r}checkCors(){const e=i.corsOrigins;if(!e.length)return!0;const t=this.request.headers.get("origin");return!t||(e.includes("*")||e.includes(t))}applyCorsHeaders(e){const t=i.corsOrigins;if(!t.length)return e;const s=this.request.headers.get("origin");if(!s)return e;const r=new Headers(e.headers);return t.includes("*")?r.set("Access-Control-Allow-Origin","*"):t.includes(s)&&(r.set("Access-Control-Allow-Origin",s),r.set("Vary","Origin")),r.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),r.set("Access-Control-Allow-Headers","Content-Type, Authorization"),r.set("Access-Control-Max-Age","86400"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})}async dispatch(){const s=this.route,r=this.feature,n=t.executeContext.getContextOrThrow();try{if(!await this.executeGuards())return this.response.forbidden();if(!await this.executeMiddlewares())return this.response.forbidden();const e=s.route.getAction();let t=null;if("string"==typeof e){const s=r.getController();"function"==typeof s._initDependencies&&await s._initDependencies(),"function"==typeof s[e]&&(t=s[e].bind(s))}else"function"==typeof e&&(t=e);if(!t)return this.response.internalServer(new Error("Route action handler not found"));await this.executePipes(n);const o=await this.executeInterceptors(()=>t(n));return this.response.success(o)}catch(t){const s=await this.executeFilters(t);if(s)return s;if(t instanceof e.HttpError){switch(t.statusCode){case 400:return this.response.badRequest(t.message,t.error);case 401:return this.response.unauthorized(t.message,t.error);case 403:return this.response.forbidden(t.message,t.error);case 404:return this.response.notFound(t.message,t.error);case 409:return this.response.conflict(t.message,t.error);case 422:return this.response.unprocessableEntity(t.message,t.error);case 429:return this.response.tooManyRequests(t.message,t.error);case 501:return this.response.notImplemented(t.message,t.error);case 502:return this.response.badGateway(t.message,t.error);case 503:return this.response.serviceUnavailable(t.message,t.error);case 504:return this.response.gatewayTimeout(t.message,t.error)}return this.response.internalServer(t)}return this.response.internalServer(t)}}async match(){let e=null;try{if(!this.checkCors())return this.applyCorsHeaders(this.response.forbidden("CORS not allowed"));const s=await this.payload.params,r=i.options.key||o.key,n=s?.[r]?.join("/")||"";if(!this.isMatchFeature(this.method,n))return this.applyCorsHeaders(this.response.notFound("Route not found",{method:this.method,path:n,params:s}));await this.feature.loadInjects(),e={req:this.request,res:this.response,method:this.method,path:n,params:this.route.params,wildcards:this.route.wildcards,reflect:this.route.route.getReflect(),feature:this.feature},this.requestHooks.setFeatureHooks(this.feature.getHooks()),await this.requestHooks.featureRequest(e);const a=await t.executeContext.run(e,()=>this.dispatch());return await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e),this.applyCorsHeaders(a)}catch(t){return e&&(await this.requestHooks.featureResponse(e),await this.requestHooks.appResponse(e)),this.applyCorsHeaders(this.response.internalServer(t))}}static createHandler(e){return async(t,s)=>{await i.registerInjectFactories(),await i.loadImports();const r=new i(t,e,s);return await r.requestHooks.appRequest(t,e),await r.match.bind(r)()}}static create(e=o){return this.options=e,this.hooks=new r.ApplicationHooks(this.options),e.guards&&e.guards.forEach(e=>this.guards.add(e)),e.filters&&e.filters.forEach(e=>this.filters.add(e)),e.interceptors&&e.interceptors.forEach(e=>this.interceptors.add(e)),e.pipes&&e.pipes.forEach(e=>this.pipes.add(e)),{[n.RouteMethod.GET]:i.createHandler(n.RouteMethod.GET),[n.RouteMethod.POST]:i.createHandler(n.RouteMethod.POST),[n.RouteMethod.PUT]:i.createHandler(n.RouteMethod.PUT),[n.RouteMethod.PATCH]:i.createHandler(n.RouteMethod.PATCH),[n.RouteMethod.DELETE]:i.createHandler(n.RouteMethod.DELETE),[n.RouteMethod.HEAD]:i.createHeadHandler(),[n.RouteMethod.OPTIONS]:i.createOptionsHandler()}}static createHeadHandler(){return async(e,t)=>{await i.registerInjectFactories(),await i.loadImports();const s=new i(e,n.RouteMethod.HEAD,t);await s.requestHooks.appRequest(e,n.RouteMethod.HEAD);const r=await s.match.bind(s)();return new Response(null,{status:r.status,statusText:r.statusText,headers:r.headers})}}static createOptionsHandler(){return async e=>{await i.registerInjectFactories();const t=this.corsOrigins,r=e.headers.get("origin"),n=s.ServerResponse.options();if(!t.length||!r)return n;const o=new Headers(n.headers);return t.includes("*")?o.set("Access-Control-Allow-Origin","*"):t.includes(r)&&(o.set("Access-Control-Allow-Origin",r),o.set("Vary","Origin")),o.set("Access-Control-Allow-Methods","GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS"),o.set("Access-Control-Allow-Headers","Content-Type, Authorization"),o.set("Access-Control-Max-Age","86400"),new Response(null,{status:204,headers:o})}}static register(...e){return e.forEach(e=>this.features.add(e)),this}}i.features=new Set,i.guards=new Set,i.filters=new Set,i.interceptors=new Set,i.pipes=new Set,i.corsOrigins=[],i.options=o,i.initialized=!1,i.initPromise=null,i.shutdownRegistered=!1,i.registryCreated=!1,i.factoriesRegistered=!1,exports.Application=i;
@@ -1 +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};
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.