Skip to content

Commit 0a8aad6

Browse files
committed
feat(di): implement DIContainer with Injectable and Inject decorators for dependency injection
1 parent 8158a7a commit 0a8aad6

2 files changed

Lines changed: 284 additions & 54 deletions

File tree

src/utils/decorators.utils.ts

Lines changed: 142 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -152,18 +152,100 @@ class RateLimiterCache {
152152
// Global cache instance
153153
const rateLimiterCache = new RateLimiterCache();
154154

155+
// di.container.ts
156+
type Constructor<T = any> = new (...args: any[]) => T;
157+
158+
export class DIContainer {
159+
private instances = new Map<Constructor, any>();
160+
private constructing = new Map<Constructor, any>();
161+
162+
register<T>(target: Constructor<T>) {
163+
// Mark as injectable, but do not instantiate yet
164+
if (!this.instances.has(target) && !this.constructing.has(target)) {
165+
// No-op: instantiation is deferred until get()
166+
}
167+
}
168+
169+
get<T>(target: Constructor<T>): T {
170+
// Return existing instance if available
171+
if (this.instances.has(target)) {
172+
return this.instances.get(target);
173+
}
174+
175+
// If currently constructing, return the proxy (for circular refs)
176+
if (this.constructing.has(target)) {
177+
return this.constructing.get(target);
178+
}
179+
180+
// Mark as constructing (for circular dependency support)
181+
let proxy: any = {};
182+
this.constructing.set(target, proxy);
183+
184+
// Resolve constructor dependencies
185+
const paramTypes: Constructor[] = Reflect.getMetadata('design:paramtypes', target as object) || [];
186+
const dependencies = paramTypes.map(dep => this.get(dep));
187+
const instance = new target(...dependencies);
188+
189+
// Copy instance properties to proxy (for circular refs)
190+
Object.assign(proxy, instance);
191+
192+
// Replace proxy with real instance
193+
this.instances.set(target, proxy);
194+
this.constructing.delete(target);
195+
196+
// Copy prototype (for instanceof checks)
197+
Object.setPrototypeOf(proxy, target.prototype);
198+
199+
return proxy;
200+
}
201+
202+
clear() {
203+
this.instances.clear();
204+
this.constructing.clear();
205+
}
206+
}
207+
208+
const diContainer = new DIContainer();
209+
155210
function normalizeHeaderValue(value: unknown): string | string[] | undefined {
156211
if (typeof value === 'undefined') return undefined;
157-
158212
if (typeof value === 'string') return value;
159-
160213
if (Array.isArray(value) && value.every(item => typeof item === 'string')) {
161214
return value;
162215
}
163-
164216
return String(value);
165217
}
166218

219+
/**
220+
* Injectable decorator for marking classes as injectable.
221+
*
222+
* @returns Class decorator that marks a class as injectable and registers it with the DI container.
223+
*/
224+
export function Injectable(): ClassDecorator {
225+
return target => {
226+
Reflect.defineMetadata('injectable', true, target);
227+
diContainer.register(target as any);
228+
};
229+
}
230+
231+
/**
232+
* Inject decorator for injecting dependencies into class properties.
233+
*
234+
* @param targetClass - The class to inject
235+
* @returns Property decorator that injects the specified class into the property
236+
*/
237+
export function Inject<T>(targetClass: new (...args: any[]) => T): PropertyDecorator {
238+
return (target, propertyKey) => {
239+
Object.defineProperty(target, propertyKey, {
240+
get: function () {
241+
return diContainer.get(targetClass);
242+
},
243+
enumerable: true,
244+
configurable: true
245+
});
246+
};
247+
}
248+
167249
/**
168250
* Factory function that creates HTTP method decorators.
169251
*
@@ -173,13 +255,13 @@ function normalizeHeaderValue(value: unknown): string | string[] | undefined {
173255
function createRouteDecorator(method: HttpMethod) {
174256
return (path: string): MethodDecorator => {
175257
return (target, propertyKey, descriptor) => {
176-
const routes: RouteDefinition[] = Reflect.getMetadata(ROUTES_KEY, target.constructor) || [];
258+
const routes: RouteDefinition[] = Reflect.getMetadata(ROUTES_KEY, (target as object).constructor) || [];
177259
routes.push({
178260
path,
179261
method,
180262
handlerName: propertyKey as string
181263
});
182-
Reflect.defineMetadata(ROUTES_KEY, routes, target.constructor);
264+
Reflect.defineMetadata(ROUTES_KEY, routes, (target as object).constructor);
183265
};
184266
};
185267
}
@@ -311,8 +393,9 @@ export function Controller(basePath: string): ClassDecorator {
311393
*/
312394
export function Use(...middlewares: RequestHandler[]): MethodDecorator {
313395
return (target, propertyKey, _descriptor) => {
314-
const existing: RequestHandler[] = Reflect.getMetadata(MIDDLEWARE_KEY, target, propertyKey as string) || [];
315-
Reflect.defineMetadata(MIDDLEWARE_KEY, [...existing, ...middlewares], target, propertyKey as string);
396+
const existing: RequestHandler[] =
397+
Reflect.getMetadata(MIDDLEWARE_KEY, target as object, propertyKey as string) || [];
398+
Reflect.defineMetadata(MIDDLEWARE_KEY, [...existing, ...middlewares], target as object, propertyKey as string);
316399
};
317400
}
318401

@@ -326,11 +409,11 @@ export function Use(...middlewares: RequestHandler[]): MethodDecorator {
326409
function createParamDecorator(type: ParamDefinition['type'], key?: string) {
327410
return (paramKey?: string): ParameterDecorator => {
328411
return (target, propertyKey, parameterIndex) => {
329-
const params: ParamDefinition[] = Reflect.getMetadata(PARAMS_KEY, target, propertyKey as string) || [];
412+
const params: ParamDefinition[] = Reflect.getMetadata(PARAMS_KEY, target as object, propertyKey as string) || [];
330413
// Ensure parameters are ordered by index
331414
params.push({ index: parameterIndex, type, key: paramKey || key });
332415
params.sort((a, b) => a.index - b.index);
333-
Reflect.defineMetadata(PARAMS_KEY, params, target, propertyKey as string);
416+
Reflect.defineMetadata(PARAMS_KEY, params, target as object, propertyKey as string);
334417
};
335418
};
336419
}
@@ -495,14 +578,14 @@ export function Headers(headers: Record<string, string> | string, value?: string
495578
return (target: any, propertyKey?: string | symbol) => {
496579
if (typeof propertyKey === 'undefined') {
497580
// Class decorator
498-
const existing: Record<string, string> = Reflect.getMetadata(HEADER_KEY, target) || {};
581+
const existing: Record<string, string> = Reflect.getMetadata(HEADER_KEY, target as object) || {};
499582
const newHeaders = typeof headers === 'string' ? { [headers]: value! } : headers;
500-
Reflect.defineMetadata(HEADER_KEY, { ...existing, ...newHeaders }, target);
583+
Reflect.defineMetadata(HEADER_KEY, { ...existing, ...newHeaders }, target as object);
501584
} else {
502585
// Method decorator
503-
const existing: Record<string, string> = Reflect.getMetadata(HEADER_KEY, target, propertyKey) || {};
586+
const existing: Record<string, string> = Reflect.getMetadata(HEADER_KEY, target as object, propertyKey) || {};
504587
const newHeaders = typeof headers === 'string' ? { [headers]: value! } : headers;
505-
Reflect.defineMetadata(HEADER_KEY, { ...existing, ...newHeaders }, target, propertyKey);
588+
Reflect.defineMetadata(HEADER_KEY, { ...existing, ...newHeaders }, target as object, propertyKey);
506589
}
507590
};
508591
}
@@ -525,9 +608,9 @@ export function Headers(headers: Record<string, string> | string, value?: string
525608
*/
526609
export function Before(fn: Function): MethodDecorator {
527610
return (target, propertyKey, _descriptor) => {
528-
const hooks: Function[] = Reflect.getMetadata(BEFORE_KEY, target, propertyKey as string) || [];
611+
const hooks: Function[] = Reflect.getMetadata(BEFORE_KEY, target as object, propertyKey as string) || [];
529612
hooks.push(fn);
530-
Reflect.defineMetadata(BEFORE_KEY, hooks, target, propertyKey as string);
613+
Reflect.defineMetadata(BEFORE_KEY, hooks, target as object, propertyKey as string);
531614
};
532615
}
533616

@@ -550,9 +633,9 @@ export function Before(fn: Function): MethodDecorator {
550633
*/
551634
export function After(fn: Function): MethodDecorator {
552635
return (target, propertyKey, _descriptor) => {
553-
const hooks: Function[] = Reflect.getMetadata(AFTER_KEY, target, propertyKey as string) || [];
636+
const hooks: Function[] = Reflect.getMetadata(AFTER_KEY, target as object, propertyKey as string) || [];
554637
hooks.push(fn);
555-
Reflect.defineMetadata(AFTER_KEY, hooks, target, propertyKey as string);
638+
Reflect.defineMetadata(AFTER_KEY, hooks, target as object, propertyKey as string);
556639
};
557640
}
558641

@@ -579,10 +662,10 @@ export function Roles(...roles: string[]): MethodDecorator & ClassDecorator {
579662
return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {
580663
if (typeof propertyKey === 'undefined') {
581664
// Class decorator
582-
Reflect.defineMetadata(ROLES_KEY, roles, target);
665+
Reflect.defineMetadata(ROLES_KEY, roles, target as object);
583666
} else {
584667
// Method decorator
585-
Reflect.defineMetadata(ROLES_KEY, roles, target, propertyKey as string);
668+
Reflect.defineMetadata(ROLES_KEY, roles, target as object, propertyKey as string);
586669
}
587670
};
588671
}
@@ -637,10 +720,10 @@ export function Cache(ttlSeconds: number): MethodDecorator & ClassDecorator {
637720
return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {
638721
if (typeof propertyKey === 'undefined') {
639722
// Class decorator
640-
Reflect.defineMetadata(CACHE_KEY, { ttlSeconds }, target);
723+
Reflect.defineMetadata(CACHE_KEY, { ttlSeconds }, target as object);
641724
} else {
642725
// Method decorator
643-
Reflect.defineMetadata(CACHE_KEY, { ttlSeconds }, target, propertyKey as string);
726+
Reflect.defineMetadata(CACHE_KEY, { ttlSeconds }, target as object, propertyKey as string);
644727
}
645728
};
646729
}
@@ -680,10 +763,10 @@ export function RateLimit(options: {
680763
return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {
681764
if (typeof propertyKey === 'undefined') {
682765
// Class decorator
683-
Reflect.defineMetadata(RATE_LIMIT_KEY, opts, target);
766+
Reflect.defineMetadata(RATE_LIMIT_KEY, opts, target as object);
684767
} else {
685768
// Method decorator
686-
Reflect.defineMetadata(RATE_LIMIT_KEY, opts, target, propertyKey as string);
769+
Reflect.defineMetadata(RATE_LIMIT_KEY, opts, target as object, propertyKey as string);
687770
}
688771
};
689772
}
@@ -752,10 +835,10 @@ export function Version(
752835
return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {
753836
if (typeof propertyKey === 'undefined') {
754837
// Class decorator
755-
Reflect.defineMetadata(VERSION_KEY, { version, options: opts }, target);
838+
Reflect.defineMetadata(VERSION_KEY, { version, options: opts }, target as object);
756839
} else {
757840
// Method decorator
758-
Reflect.defineMetadata(VERSION_KEY, { version, options: opts }, target, propertyKey as string);
841+
Reflect.defineMetadata(VERSION_KEY, { version, options: opts }, target as object, propertyKey as string);
759842
}
760843
};
761844
}
@@ -779,10 +862,10 @@ export function Timeout(ms: number): MethodDecorator & ClassDecorator {
779862
return (target: any, propertyKey?: string | symbol, _descriptor?: PropertyDescriptor) => {
780863
if (typeof propertyKey === 'undefined') {
781864
// Class decorator
782-
Reflect.defineMetadata(TIMEOUT_KEY, { ms }, target);
865+
Reflect.defineMetadata(TIMEOUT_KEY, { ms }, target as object);
783866
} else {
784867
// Method decorator
785-
Reflect.defineMetadata(TIMEOUT_KEY, { ms }, target, propertyKey as string);
868+
Reflect.defineMetadata(TIMEOUT_KEY, { ms }, target as object, propertyKey as string);
786869
}
787870
};
788871
}
@@ -833,10 +916,10 @@ export function Log(options?: {
833916
};
834917
if (typeof propertyKey === 'undefined') {
835918
// Class decorator
836-
Reflect.defineMetadata(LOG_KEY, config, target);
919+
Reflect.defineMetadata(LOG_KEY, config, target as object);
837920
} else {
838921
// Method decorator
839-
Reflect.defineMetadata(LOG_KEY, config, target, propertyKey as string);
922+
Reflect.defineMetadata(LOG_KEY, config, target as object, propertyKey as string);
840923
}
841924
};
842925
}
@@ -850,50 +933,56 @@ export function Log(options?: {
850933
*/
851934
export function registerControllers(router: Router, controllers: any[]) {
852935
controllers.forEach(ControllerClass => {
853-
const instance = new ControllerClass();
854-
const basePath: string = Reflect.getMetadata('basePath', ControllerClass) || '';
855-
const routes: RouteDefinition[] = Reflect.getMetadata(ROUTES_KEY, ControllerClass) || [];
936+
// Use DI container to resolve controller (constructor injection + property injection)
937+
const instance = diContainer.get(ControllerClass);
938+
const basePath: string = Reflect.getMetadata('basePath', ControllerClass as object) || '';
939+
const routes: RouteDefinition[] = Reflect.getMetadata(ROUTES_KEY, ControllerClass as object) || [];
856940

857941
// Get controller-level decorators (fallback values)
858-
const controllerRateLimit = Reflect.getMetadata(RATE_LIMIT_KEY, ControllerClass);
859-
const controllerCache = Reflect.getMetadata(CACHE_KEY, ControllerClass);
860-
const controllerTimeout = Reflect.getMetadata(TIMEOUT_KEY, ControllerClass);
861-
const controllerVersion = Reflect.getMetadata(VERSION_KEY, ControllerClass);
862-
const controllerRoles = Reflect.getMetadata(ROLES_KEY, ControllerClass);
863-
const controllerLogConfig = Reflect.getMetadata(LOG_KEY, ControllerClass);
864-
const controllerHeaders = Reflect.getMetadata(HEADER_KEY, ControllerClass) || {};
942+
const controllerRateLimit = Reflect.getMetadata(RATE_LIMIT_KEY, ControllerClass as object);
943+
const controllerCache = Reflect.getMetadata(CACHE_KEY, ControllerClass as object);
944+
const controllerTimeout = Reflect.getMetadata(TIMEOUT_KEY, ControllerClass as object);
945+
const controllerVersion = Reflect.getMetadata(VERSION_KEY, ControllerClass as object);
946+
const controllerRoles = Reflect.getMetadata(ROLES_KEY, ControllerClass as object);
947+
const controllerLogConfig = Reflect.getMetadata(LOG_KEY, ControllerClass as object);
948+
const controllerHeaders = Reflect.getMetadata(HEADER_KEY, ControllerClass as object) || {};
865949

866950
routes.forEach(({ path, method, handlerName }) => {
867-
const middlewares: RequestHandler[] = Reflect.getMetadata(MIDDLEWARE_KEY, instance, handlerName) || [];
868-
const params: ParamDefinition[] = Reflect.getMetadata(PARAMS_KEY, instance, handlerName) || [];
951+
const middlewares: RequestHandler[] = Reflect.getMetadata(MIDDLEWARE_KEY, instance as object, handlerName) || [];
952+
const params: ParamDefinition[] = Reflect.getMetadata(PARAMS_KEY, instance as object, handlerName) || [];
869953

870-
const httpCode: number | undefined = Reflect.getMetadata(HTTP_CODE_KEY, instance, handlerName);
954+
const httpCode: number | undefined = Reflect.getMetadata(HTTP_CODE_KEY, instance as object, handlerName);
871955

872956
// Merge controller-level and method-level headers
873-
const methodHeaders: Record<string, string> = Reflect.getMetadata(HEADER_KEY, instance, handlerName) || {};
957+
const methodHeaders: Record<string, string> =
958+
Reflect.getMetadata(HEADER_KEY, instance as object, handlerName) || {};
874959
const headers = { ...controllerHeaders, ...methodHeaders };
875960

876-
const beforeHooks: Function[] = Reflect.getMetadata(BEFORE_KEY, instance, handlerName) || [];
877-
const afterHooks: Function[] = Reflect.getMetadata(AFTER_KEY, instance, handlerName) || [];
961+
const beforeHooks: Function[] = Reflect.getMetadata(BEFORE_KEY, instance as object, handlerName) || [];
962+
const afterHooks: Function[] = Reflect.getMetadata(AFTER_KEY, instance as object, handlerName) || [];
878963
const redirect: { url?: string; statusCode: number } | undefined = Reflect.getMetadata(
879964
REDIRECT_KEY,
880-
instance,
965+
instance as object,
966+
handlerName
967+
);
968+
const contentType: { type: string } | undefined = Reflect.getMetadata(
969+
CONTENT_TYPE_KEY,
970+
instance as object,
881971
handlerName
882972
);
883-
const contentType: { type: string } | undefined = Reflect.getMetadata(CONTENT_TYPE_KEY, instance, handlerName);
884973

885974
// Use method-level decorators if present, otherwise fall back to controller-level
886-
const roles: string[] = Reflect.getMetadata(ROLES_KEY, instance, handlerName) || controllerRoles || [];
975+
const roles: string[] = Reflect.getMetadata(ROLES_KEY, instance as object, handlerName) || controllerRoles || [];
887976
const cache: { ttlSeconds: number } | undefined =
888-
Reflect.getMetadata(CACHE_KEY, instance, handlerName) || controllerCache;
977+
Reflect.getMetadata(CACHE_KEY, instance as object, handlerName) || controllerCache;
889978
const rateLimitOptions:
890979
| { max: number; windowMs: number; standardHeaders: boolean; legacyHeaders: boolean }
891-
| undefined = Reflect.getMetadata(RATE_LIMIT_KEY, instance, handlerName) || controllerRateLimit;
980+
| undefined = Reflect.getMetadata(RATE_LIMIT_KEY, instance as object, handlerName) || controllerRateLimit;
892981
const version:
893982
| { version: string; options: { addPrefix: boolean; addHeader: boolean; headerName: string } }
894-
| undefined = Reflect.getMetadata(VERSION_KEY, instance, handlerName) || controllerVersion;
983+
| undefined = Reflect.getMetadata(VERSION_KEY, instance as object, handlerName) || controllerVersion;
895984
const timeout: { ms: number } | undefined =
896-
Reflect.getMetadata(TIMEOUT_KEY, instance, handlerName) || controllerTimeout;
985+
Reflect.getMetadata(TIMEOUT_KEY, instance as object, handlerName) || controllerTimeout;
897986
const logConfig:
898987
| {
899988
logEntry?: boolean;
@@ -902,7 +991,7 @@ export function registerControllers(router: Router, controllers: any[]) {
902991
logParams?: boolean;
903992
logResponse?: boolean;
904993
}
905-
| undefined = Reflect.getMetadata(LOG_KEY, instance, handlerName) || controllerLogConfig;
994+
| undefined = Reflect.getMetadata(LOG_KEY, instance as object, handlerName) || controllerLogConfig;
906995

907996
// Create rate limiter for this specific route if needed
908997
let rateLimiter: any = null;

0 commit comments

Comments
 (0)