@mxweb/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -0
  3. package/dist/application.d.ts +402 -0
  4. package/dist/application.js +1 -0
  5. package/dist/application.mjs +1 -0
  6. package/dist/common.d.ts +323 -0
  7. package/dist/common.js +1 -0
  8. package/dist/common.mjs +1 -0
  9. package/dist/config.d.ts +258 -0
  10. package/dist/config.js +1 -0
  11. package/dist/config.mjs +1 -0
  12. package/dist/context.d.ts +48 -0
  13. package/dist/context.js +1 -0
  14. package/dist/context.mjs +1 -0
  15. package/dist/controller.d.ts +238 -0
  16. package/dist/controller.js +1 -0
  17. package/dist/controller.mjs +1 -0
  18. package/dist/decorator.d.ts +349 -0
  19. package/dist/decorator.js +1 -0
  20. package/dist/decorator.mjs +1 -0
  21. package/dist/error.d.ts +301 -0
  22. package/dist/error.js +1 -0
  23. package/dist/error.mjs +1 -0
  24. package/dist/execute.d.ts +469 -0
  25. package/dist/execute.js +1 -0
  26. package/dist/execute.mjs +1 -0
  27. package/dist/feature.d.ts +239 -0
  28. package/dist/feature.js +1 -0
  29. package/dist/feature.mjs +1 -0
  30. package/dist/hooks.d.ts +251 -0
  31. package/dist/hooks.js +1 -0
  32. package/dist/hooks.mjs +1 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.js +1 -0
  35. package/dist/index.mjs +1 -0
  36. package/dist/logger.d.ts +360 -0
  37. package/dist/logger.js +1 -0
  38. package/dist/logger.mjs +1 -0
  39. package/dist/response.d.ts +665 -0
  40. package/dist/response.js +1 -0
  41. package/dist/response.mjs +1 -0
  42. package/dist/route.d.ts +298 -0
  43. package/dist/route.js +1 -0
  44. package/dist/route.mjs +1 -0
  45. package/dist/router.d.ts +205 -0
  46. package/dist/router.js +1 -0
  47. package/dist/router.mjs +1 -0
  48. package/dist/service.d.ts +261 -0
  49. package/dist/service.js +1 -0
  50. package/dist/service.mjs +1 -0
  51. package/package.json +168 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * @fileoverview Execution context and request-scoped state management.
3
+ *
4
+ * This module provides:
5
+ * - {@link ExecuteContext}: Request-scoped context using AsyncLocalStorage
6
+ * - {@link Reflect}: Route metadata and decorator access
7
+ * - Guard, Filter, Interceptor interfaces for request pipeline
8
+ *
9
+ * @module execute
10
+ */
11
+ import { NextRequest } from "next/server";
12
+ import { ServerResponse } from "./response";
13
+ import { Feature } from "./feature";
14
+ import { ApplicationInject, CallHandler, FeatureInject, Pipe, RouteMethod, RouteNextHandler } from "./common";
15
+ /**
16
+ * Container for route-level metadata, guards, filters, interceptors, and pipes.
17
+ * Created from decorator results and attached to each route.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // Access in a guard or interceptor
22
+ * const roles = context.getReflect()?.getMetadata<string[]>("roles");
23
+ * const guards = context.getReflect()?.getGuards();
24
+ * ```
25
+ */
26
+ export declare class Reflect {
27
+ private readonly metadata;
28
+ private readonly guards;
29
+ private readonly filters;
30
+ private readonly interceptors;
31
+ private readonly pipes;
32
+ /**
33
+ * Creates a new Reflect instance with the specified metadata and decorators.
34
+ *
35
+ * @param metadata - Map of metadata key-value pairs from SetMetadata
36
+ * @param guards - Set of guard classes from UseGuards
37
+ * @param filters - Set of filter classes from UseFilters
38
+ * @param interceptors - Set of interceptor classes from UseInterceptors
39
+ * @param pipes - Set of pipe classes from UsePipes
40
+ */
41
+ constructor(metadata: Map<string, unknown>, guards: Set<Guard>, filters: Set<Filter>, interceptors: Set<Interceptor>, pipes: Set<Pipe>);
42
+ /**
43
+ * Returns the set of guard classes attached to this route.
44
+ * @returns Set of guard class constructors
45
+ */
46
+ getGuards(): Set<Guard>;
47
+ /**
48
+ * Returns the set of exception filter classes attached to this route.
49
+ * @returns Set of filter class constructors
50
+ */
51
+ getFilters(): Set<Filter<unknown>>;
52
+ /**
53
+ * Returns the set of interceptor classes attached to this route.
54
+ * @returns Set of interceptor class constructors
55
+ */
56
+ getInterceptors(): Set<Interceptor<unknown>>;
57
+ /**
58
+ * Returns the set of pipe classes attached to this route.
59
+ * @returns Set of pipe class constructors
60
+ */
61
+ getPipes(): Set<Pipe>;
62
+ /**
63
+ * Retrieves a metadata value by key.
64
+ *
65
+ * @template T - Expected type of the metadata value
66
+ * @param key - The metadata key to retrieve
67
+ * @returns The metadata value or undefined if not found
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const roles = reflect.getMetadata<string[]>("roles");
72
+ * ```
73
+ */
74
+ getMetadata<T = unknown>(key: string): T | undefined;
75
+ /**
76
+ * Returns all metadata as a Map.
77
+ * @returns Map of all metadata key-value pairs
78
+ */
79
+ getAllMetadata(): Map<string, unknown>;
80
+ }
81
+ /**
82
+ * Context object containing all request-related data.
83
+ * Created for each request and available throughout the request lifecycle.
84
+ */
85
+ export interface RequestContext {
86
+ /** The incoming Next.js request */
87
+ req: NextRequest;
88
+ /** The response builder */
89
+ res: ServerResponse;
90
+ /** HTTP method of the request */
91
+ method: RouteMethod;
92
+ /** The matched path (without base API path) */
93
+ path: string;
94
+ /** Route parameters extracted from the path */
95
+ params: Record<string, string>;
96
+ /** Wildcard segments from the path */
97
+ wildcards: string[];
98
+ /** Route metadata and decorators */
99
+ reflect: Reflect;
100
+ /** The feature that matched this request */
101
+ feature: Feature;
102
+ }
103
+ /**
104
+ * Request-scoped execution context using AsyncLocalStorage.
105
+ *
106
+ * This class provides access to request data and global injects from anywhere
107
+ * in the application without explicitly passing context through function parameters.
108
+ *
109
+ * Uses the singleton pattern - access via {@link ExecuteContext.instance}.
110
+ *
111
+ * @remarks
112
+ * - Uses Node.js AsyncLocalStorage to maintain request scope across async operations
113
+ * - Manages both request-scoped context and global application injects
114
+ * - Provides HTTP helpers similar to NestJS's ExecutionContext
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * // In a service (accessed via executeContext singleton)
119
+ * const params = this.context.switchHttp().params();
120
+ * const body = await this.context.switchHttp().json<CreateDto>();
121
+ *
122
+ * // Access global injects
123
+ * const db = this.context.getInject<Database>("db");
124
+ *
125
+ * // Access feature-local injects
126
+ * const repo = this.context.getLocalInject<Repository>("productRepo");
127
+ * ```
128
+ */
129
+ export declare class ExecuteContext {
130
+ /** Singleton instance */
131
+ private static _instance;
132
+ /** AsyncLocalStorage for request-scoped context */
133
+ private storage;
134
+ /** Map of global application injects */
135
+ private _injections;
136
+ /**
137
+ * Private constructor to enforce singleton pattern.
138
+ * @private
139
+ */
140
+ private constructor();
141
+ /**
142
+ * Returns the singleton ExecuteContext instance.
143
+ * Creates the instance on first access.
144
+ *
145
+ * @returns The singleton ExecuteContext instance
146
+ */
147
+ static get instance(): ExecuteContext;
148
+ /**
149
+ * Returns the map of global application injects.
150
+ * Injects are set by Application.loadInjects() during initialization.
151
+ *
152
+ * @returns Map of inject name to instance
153
+ */
154
+ get injections(): Map<string, ApplicationInject>;
155
+ /**
156
+ * Registers a global inject by name.
157
+ *
158
+ * @param name - The name to register the inject under
159
+ * @param instance - The inject instance
160
+ */
161
+ setInject(name: string, instance: ApplicationInject): void;
162
+ /**
163
+ * Retrieves a global inject by name.
164
+ *
165
+ * @template T - Expected type of the inject (must extend ApplicationInject)
166
+ * @param name - The name of the inject to retrieve
167
+ * @returns The inject instance or undefined if not found
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * const db = context.getInject<DatabaseConnection>("db");
172
+ * const config = context.getInject<Config>("config");
173
+ * ```
174
+ */
175
+ getInject<T extends ApplicationInject = ApplicationInject>(name: string): T | undefined;
176
+ /**
177
+ * Checks if a global inject exists by name.
178
+ *
179
+ * @param name - The inject name to check
180
+ * @returns true if the inject exists, false otherwise
181
+ */
182
+ hasInject(name: string): boolean;
183
+ /**
184
+ * Clears all global injects.
185
+ * Primarily used for testing to ensure clean state between tests.
186
+ */
187
+ clearInjections(): void;
188
+ /**
189
+ * Runs a callback within the scope of a request context.
190
+ * All code executed within the callback can access the context
191
+ * via getContext() or switchHttp().
192
+ *
193
+ * @template T - Return type of the callback
194
+ * @param context - The request context to make available
195
+ * @param callback - The callback to execute within the context scope
196
+ * @returns The result of the callback (sync or async)
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * await executeContext.run(requestContext, async () => {
201
+ * // context is available here
202
+ * const ctx = executeContext.getContext();
203
+ * return await handler();
204
+ * });
205
+ * ```
206
+ */
207
+ run<T>(context: RequestContext, callback: () => T | Promise<T>): T | Promise<T>;
208
+ /**
209
+ * Retrieves the current request context.
210
+ * Returns undefined if called outside of a request scope.
211
+ *
212
+ * @returns The current RequestContext or undefined
213
+ */
214
+ getContext(): RequestContext | undefined;
215
+ /**
216
+ * Retrieves the current request context or throws an error.
217
+ * Use this when you expect to always be within a request scope.
218
+ *
219
+ * @returns The current RequestContext
220
+ * @throws {Error} If called outside of a request scope
221
+ */
222
+ getContextOrThrow(): RequestContext;
223
+ /**
224
+ * Returns an object with helper methods for accessing HTTP request data.
225
+ * Similar to NestJS's switchToHttp() but as a function factory.
226
+ *
227
+ * @returns Object with HTTP helper methods
228
+ * @throws {Error} If called outside of a request scope
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * const http = context.switchHttp();
233
+ *
234
+ * // Access request/response
235
+ * const request = http.getRequest();
236
+ * const response = http.getResponse();
237
+ *
238
+ * // Access route info
239
+ * const method = http.getMethod();
240
+ * const path = http.getPath();
241
+ * const params = http.params();
242
+ *
243
+ * // Access request data
244
+ * const query = http.query();
245
+ * const headers = http.headers();
246
+ * const body = await http.json<MyDto>();
247
+ * const formData = await http.formData();
248
+ * ```
249
+ */
250
+ switchHttp(): {
251
+ /** Returns the Next.js request object */
252
+ getRequest: () => NextRequest;
253
+ /** Returns the ServerResponse builder */
254
+ getResponse: () => ServerResponse;
255
+ /** Returns the HTTP method */
256
+ getMethod: () => RouteMethod;
257
+ /** Returns the matched path */
258
+ getPath: () => string;
259
+ /** Returns route parameters */
260
+ params: () => Record<string, string>;
261
+ /** Returns wildcard segments */
262
+ wildcards: () => string[];
263
+ /** Returns query parameters as object */
264
+ query: () => {
265
+ [k: string]: string;
266
+ };
267
+ /** Returns headers as object */
268
+ headers: () => {
269
+ [k: string]: string;
270
+ };
271
+ /** Returns the raw body ReadableStream */
272
+ body: () => ReadableStream<Uint8Array<ArrayBuffer>> | null;
273
+ /** Parses and returns the JSON body */
274
+ json: <T = unknown>() => Promise<T>;
275
+ /** Returns the body as text */
276
+ text: () => Promise<string>;
277
+ /** Parses and returns FormData */
278
+ formData: () => Promise<FormData>;
279
+ /** Returns the body as Blob */
280
+ blob: () => Promise<Blob>;
281
+ /** Returns the body as ArrayBuffer */
282
+ arrayBuffer: () => Promise<ArrayBuffer>;
283
+ };
284
+ /**
285
+ * Returns the Reflect instance for the current route.
286
+ * Contains metadata and decorator information.
287
+ *
288
+ * @returns The Reflect instance or undefined if no context
289
+ */
290
+ getReflect(): Reflect | undefined;
291
+ /**
292
+ * Retrieves route metadata by key.
293
+ * Shorthand for getReflect()?.getMetadata().
294
+ *
295
+ * @template T - Expected type of the metadata value
296
+ * @param key - The metadata key to retrieve
297
+ * @returns The metadata value or undefined
298
+ */
299
+ getMetadata<T>(key: string): T | undefined;
300
+ /**
301
+ * Returns the Feature that matched the current request.
302
+ *
303
+ * @returns The Feature instance or undefined if no context
304
+ */
305
+ getFeature(): Feature | undefined;
306
+ /**
307
+ * Retrieves a feature-local inject by name.
308
+ * Falls back to global injects if not found in the feature.
309
+ *
310
+ * @template T - Expected type of the inject (must extend FeatureInject)
311
+ * @param name - The name of the inject to retrieve
312
+ * @returns The inject instance or undefined if not found
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * // First checks feature injects, then global injects
317
+ * const repo = context.getLocalInject<ProductRepository>("productRepo");
318
+ * ```
319
+ */
320
+ getLocalInject<T extends FeatureInject>(name: string): T | undefined;
321
+ }
322
+ /**
323
+ * Interface for guard classes that determine if a request should proceed.
324
+ * Guards are used for authorization, authentication, and access control.
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * class AuthGuard implements CanActivate {
329
+ * canActivate(context: ExecuteContext): boolean {
330
+ * const token = context.switchHttp().headers()["authorization"];
331
+ * return !!token && isValidToken(token);
332
+ * }
333
+ * }
334
+ * ```
335
+ */
336
+ export interface CanActivate {
337
+ /**
338
+ * Determines if the request should be allowed to proceed.
339
+ *
340
+ * @param context - The execution context for the current request
341
+ * @returns true to allow, false to deny (results in 403 Forbidden)
342
+ */
343
+ canActivate(context: ExecuteContext): boolean | Promise<boolean>;
344
+ }
345
+ /**
346
+ * Constructor type for guard classes.
347
+ */
348
+ export type Guard = new (...args: unknown[]) => CanActivate;
349
+ /**
350
+ * Function type for route action handlers (inline handlers).
351
+ *
352
+ * RouteHandler is used when defining routes with inline functions instead of
353
+ * controller method names. The handler receives the ExecuteContext and returns
354
+ * data that will be serialized as the JSON response.
355
+ *
356
+ * @template Data - The type of data returned by the handler
357
+ * @param context - The ExecuteContext providing access to request, response, params, etc.
358
+ * @returns Data to be serialized as JSON response, or a Promise resolving to data
359
+ *
360
+ * @example
361
+ * ```ts
362
+ * // Inline handler in route definition
363
+ * Route.get("/health", async (context) => {
364
+ * return context.response.success({ status: "ok", timestamp: Date.now() });
365
+ * });
366
+ *
367
+ * // With typed response
368
+ * const handler: RouteHandler<{ message: string }> = (ctx) => {
369
+ * return { message: "Hello World" };
370
+ * };
371
+ * Route.get("/hello", handler);
372
+ * ```
373
+ */
374
+ export type RouteHandler<Data = unknown> = (context: ExecuteContext) => Data;
375
+ /**
376
+ * Function type for route middlewares.
377
+ * Middlewares can perform actions before the handler and must call next() to continue.
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * const loggingMiddleware: RouteMiddleware = (context, next) => {
382
+ * console.log(`Request: ${context.switchHttp().getMethod()} ${context.switchHttp().getPath()}`);
383
+ * next();
384
+ * };
385
+ * ```
386
+ */
387
+ export type RouteMiddleware = (context: ExecuteContext, next: RouteNextHandler) => void | Promise<void>;
388
+ /**
389
+ * Interface for exception filter classes that handle errors.
390
+ * Filters can transform exceptions into custom responses.
391
+ *
392
+ * @template Response - The type of response the filter returns
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * class HttpExceptionFilter implements ExceptionFilter<Response> {
397
+ * catch(exception: unknown, context: ExecuteContext): Response {
398
+ * if (exception instanceof HttpError) {
399
+ * return new Response(JSON.stringify({ error: exception.message }), {
400
+ * status: exception.statusCode,
401
+ * });
402
+ * }
403
+ * throw exception; // Re-throw if not handled
404
+ * }
405
+ * }
406
+ * ```
407
+ */
408
+ export interface ExceptionFilter<Response = unknown> {
409
+ /**
410
+ * Handles an exception and optionally returns a response.
411
+ *
412
+ * @param exception - The thrown exception
413
+ * @param context - The execution context for the current request
414
+ * @returns A response to send, or re-throw to pass to next filter
415
+ */
416
+ catch(exception: unknown, context: ExecuteContext): Response | Promise<Response>;
417
+ }
418
+ /**
419
+ * Constructor type for exception filter classes.
420
+ *
421
+ * @template Response - The type of response the filter returns
422
+ */
423
+ export type Filter<Response = unknown> = new (...args: unknown[]) => ExceptionFilter<Response>;
424
+ /**
425
+ * Interface for interceptor classes that wrap handler execution.
426
+ * Interceptors can transform requests/responses, add caching, logging, etc.
427
+ *
428
+ * @template T - The type of the handler result
429
+ *
430
+ * @example
431
+ * ```ts
432
+ * class LoggingInterceptor implements InterceptorHandler {
433
+ * async intercept(context: ExecuteContext, next: CallHandler): Promise<unknown> {
434
+ * const start = Date.now();
435
+ * const result = await next();
436
+ * console.log(`Request took ${Date.now() - start}ms`);
437
+ * return result;
438
+ * }
439
+ * }
440
+ *
441
+ * class CacheInterceptor implements InterceptorHandler {
442
+ * async intercept(context: ExecuteContext, next: CallHandler): Promise<unknown> {
443
+ * const key = getCacheKey(context);
444
+ * const cached = await cache.get(key);
445
+ * if (cached) return cached;
446
+ *
447
+ * const result = await next();
448
+ * await cache.set(key, result);
449
+ * return result;
450
+ * }
451
+ * }
452
+ * ```
453
+ */
454
+ export interface InterceptorHandler<T = unknown> {
455
+ /**
456
+ * Intercepts the handler execution.
457
+ *
458
+ * @param context - The execution context for the current request
459
+ * @param next - Function to call the next interceptor or handler
460
+ * @returns The (possibly transformed) result
461
+ */
462
+ intercept(context: ExecuteContext, next: CallHandler<T>): Promise<T>;
463
+ }
464
+ /**
465
+ * Constructor type for interceptor classes.
466
+ *
467
+ * @template T - The type of the handler result
468
+ */
469
+ export type Interceptor<T = unknown> = new (...args: unknown[]) => InterceptorHandler<T>;
@@ -0,0 +1 @@
1
+ "use strict";var t=require("async_hooks");class e{constructor(){this.storage=new t.AsyncLocalStorage,this._injections=new Map}static get instance(){return e._instance||(e._instance=new e),e._instance}get injections(){return this._injections}setInject(t,e){this._injections.set(t,e)}getInject(t){return this._injections.get(t)}hasInject(t){return this._injections.has(t)}clearInjections(){this._injections.clear()}run(t,e){return this.storage.run(t,e)}getContext(){return this.storage.getStore()}getContextOrThrow(){const t=this.storage.getStore();if(!t)throw new Error("[ExecuteContext] No context available. Make sure you're inside a request handler.");return t}switchHttp(){const t=this.getContextOrThrow();return{getRequest:()=>t.req,getResponse:()=>t.res,getMethod:()=>t.method,getPath:()=>t.path,params:()=>t.params,wildcards:()=>t.wildcards,query:()=>Object.fromEntries(t.req.nextUrl.searchParams),headers:()=>Object.fromEntries(t.req.headers),body:()=>t.req.body,json:()=>t.req.json(),text:()=>t.req.text(),formData:()=>t.req.formData(),blob:()=>t.req.blob(),arrayBuffer:()=>t.req.arrayBuffer()}}getReflect(){return this.getContext()?.reflect}getMetadata(t){return this.getContext()?.reflect.getMetadata(t)}getFeature(){return this.getContext()?.feature}getLocalInject(t){const e=this.getContext()?.feature;return e?.hasInject(t)?e.getInject(t):this._injections.get(t)}}e._instance=null,exports.ExecuteContext=e,exports.Reflect=class{constructor(t,e,r,s,n){this.metadata=t,this.guards=e,this.filters=r,this.interceptors=s,this.pipes=n}getGuards(){return this.guards}getFilters(){return this.filters}getInterceptors(){return this.interceptors}getPipes(){return this.pipes}getMetadata(t){return this.metadata.get(t)}getAllMetadata(){return this.metadata}};
@@ -0,0 +1 @@
1
+ import{AsyncLocalStorage as t}from"async_hooks";class e{constructor(t,e,r,s,n){this.metadata=t,this.guards=e,this.filters=r,this.interceptors=s,this.pipes=n}getGuards(){return this.guards}getFilters(){return this.filters}getInterceptors(){return this.interceptors}getPipes(){return this.pipes}getMetadata(t){return this.metadata.get(t)}getAllMetadata(){return this.metadata}}class r{constructor(){this.storage=new t,this._injections=new Map}static get instance(){return r._instance||(r._instance=new r),r._instance}get injections(){return this._injections}setInject(t,e){this._injections.set(t,e)}getInject(t){return this._injections.get(t)}hasInject(t){return this._injections.has(t)}clearInjections(){this._injections.clear()}run(t,e){return this.storage.run(t,e)}getContext(){return this.storage.getStore()}getContextOrThrow(){const t=this.storage.getStore();if(!t)throw new Error("[ExecuteContext] No context available. Make sure you're inside a request handler.");return t}switchHttp(){const t=this.getContextOrThrow();return{getRequest:()=>t.req,getResponse:()=>t.res,getMethod:()=>t.method,getPath:()=>t.path,params:()=>t.params,wildcards:()=>t.wildcards,query:()=>Object.fromEntries(t.req.nextUrl.searchParams),headers:()=>Object.fromEntries(t.req.headers),body:()=>t.req.body,json:()=>t.req.json(),text:()=>t.req.text(),formData:()=>t.req.formData(),blob:()=>t.req.blob(),arrayBuffer:()=>t.req.arrayBuffer()}}getReflect(){return this.getContext()?.reflect}getMetadata(t){return this.getContext()?.reflect.getMetadata(t)}getFeature(){return this.getContext()?.feature}getLocalInject(t){const e=this.getContext()?.feature;return e?.hasInject(t)?e.getInject(t):this._injections.get(t)}}r._instance=null;export{r as ExecuteContext,e as Reflect};