@adimm/x-injection 2.1.2 → 3.0.1
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/README.md +2837 -860
- package/dist/index.cjs +232 -245
- package/dist/index.d.cts +53 -42
- package/dist/index.d.ts +53 -42
- package/dist/index.js +255 -266
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,860 +1,2837 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img width="260px" height="auto" alt="xInjection Logo" src="https://raw.githubusercontent.com/AdiMarianMutu/x-injection/main/assets/logo.png"><br /><a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/v/@adimm/x-injection"></a>
|
|
3
|
-
<a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection" target="__blank"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection"></a>
|
|
4
|
-
<img src="https://badgen.net/npm/license/@adimm/x-injection">
|
|
5
|
-
</p>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<a href="https://github.com/AdiMarianMutu/x-injection/actions/workflows/ci.yml?query=branch%3Amain" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection/actions/workflows/ci.yml/badge.svg?branch=main"></a>
|
|
9
|
-
<a href="https://github.com/AdiMarianMutu/x-injection/actions/workflows/publish.yml" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection/actions/workflows/publish.yml/badge.svg"></a>
|
|
10
|
-
<br>
|
|
11
|
-
<img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection">
|
|
12
|
-
<a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/dm/@adimm/x-injection"></a>
|
|
13
|
-
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- [AppModule](#appmodule
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
39
|
-
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
- [
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
48
|
-
- [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- [
|
|
58
|
-
- [
|
|
59
|
-
- [
|
|
60
|
-
- [
|
|
61
|
-
- [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
MyModule.
|
|
473
|
-
MyModule.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
MyModule.
|
|
477
|
-
MyModule.
|
|
478
|
-
MyModule.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
})
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
> [!
|
|
659
|
-
>
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
{
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img width="260px" height="auto" alt="xInjection Logo" src="https://raw.githubusercontent.com/AdiMarianMutu/x-injection/main/assets/logo.png"><br /><a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/v/@adimm/x-injection"></a>
|
|
3
|
+
<a href="https://app.codecov.io/gh/AdiMarianMutu/x-injection" target="__blank"><img src="https://badgen.net/codecov/c/github/AdiMarianMutu/x-injection"></a>
|
|
4
|
+
<img src="https://badgen.net/npm/license/@adimm/x-injection">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://github.com/AdiMarianMutu/x-injection/actions/workflows/ci.yml?query=branch%3Amain" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection/actions/workflows/ci.yml/badge.svg?branch=main"></a>
|
|
9
|
+
<a href="https://github.com/AdiMarianMutu/x-injection/actions/workflows/publish.yml" target="__blank"><img src="https://github.com/AdiMarianMutu/x-injection/actions/workflows/publish.yml/badge.svg"></a>
|
|
10
|
+
<br>
|
|
11
|
+
<img src="https://badgen.net/bundlephobia/minzip/@adimm/x-injection">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@adimm/x-injection" target="__blank"><img src="https://badgen.net/npm/dm/@adimm/x-injection"></a>
|
|
13
|
+
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
**A powerful, modular dependency injection library for TypeScript** — Built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s elegant module architecture.
|
|
17
|
+
|
|
18
|
+
## Table of Contents
|
|
19
|
+
|
|
20
|
+
- [Table of Contents](#table-of-contents)
|
|
21
|
+
- [What Problems Does This Solve?](#what-problems-does-this-solve)
|
|
22
|
+
- [Problem 1: Manual Dependency Wiring](#problem-1-manual-dependency-wiring)
|
|
23
|
+
- [Problem 2: Tight Coupling and Testing Difficulty](#problem-2-tight-coupling-and-testing-difficulty)
|
|
24
|
+
- [Problem 3: Lack of Encapsulation](#problem-3-lack-of-encapsulation)
|
|
25
|
+
- [Problem 4: Lifecycle Management Complexity](#problem-4-lifecycle-management-complexity)
|
|
26
|
+
- [Overview](#overview)
|
|
27
|
+
- [Features](#features)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick Start](#quick-start)
|
|
30
|
+
- [Core Concepts](#core-concepts)
|
|
31
|
+
- [Services with @Injectable](#services-with-injectable)
|
|
32
|
+
- [Modules](#modules)
|
|
33
|
+
- [Blueprints](#blueprints)
|
|
34
|
+
- [AppModule](#appmodule)
|
|
35
|
+
- [OOP-Style Modules with ProviderModuleClass](#oop-style-modules-with-providermoduleclass)
|
|
36
|
+
- [Basic OOP Module](#basic-oop-module)
|
|
37
|
+
- [Advanced OOP Patterns](#advanced-oop-patterns)
|
|
38
|
+
- [When to Use OOP vs Functional](#when-to-use-oop-vs-functional)
|
|
39
|
+
- [Provider Tokens](#provider-tokens)
|
|
40
|
+
- [1. Class Token](#1-class-token)
|
|
41
|
+
- [2. Class Token with Substitution](#2-class-token-with-substitution)
|
|
42
|
+
- [3. Value Token](#3-value-token)
|
|
43
|
+
- [4. Factory Token](#4-factory-token)
|
|
44
|
+
- [Injection Scopes](#injection-scopes)
|
|
45
|
+
- [Singleton (Default)](#singleton-default)
|
|
46
|
+
- [Transient](#transient)
|
|
47
|
+
- [Request](#request)
|
|
48
|
+
- [Scope Priority Order](#scope-priority-order)
|
|
49
|
+
- [Module System](#module-system)
|
|
50
|
+
- [Import/Export Pattern](#importexport-pattern)
|
|
51
|
+
- [Re-exporting Modules](#re-exporting-modules)
|
|
52
|
+
- [Dynamic Module Updates](#dynamic-module-updates)
|
|
53
|
+
- [Global Modules](#global-modules)
|
|
54
|
+
- [Dependency Injection](#dependency-injection)
|
|
55
|
+
- [Constructor Injection](#constructor-injection)
|
|
56
|
+
- [@Inject Decorator](#inject-decorator)
|
|
57
|
+
- [@MultiInject Decorator](#multiinject-decorator)
|
|
58
|
+
- [Optional Dependencies](#optional-dependencies)
|
|
59
|
+
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
60
|
+
- [onReady Hook](#onready-hook)
|
|
61
|
+
- [onReset Hook](#onreset-hook)
|
|
62
|
+
- [onDispose Hook](#ondispose-hook)
|
|
63
|
+
- [Events System](#events-system)
|
|
64
|
+
- [Subscribing to Events](#subscribing-to-events)
|
|
65
|
+
- [Available Event Types](#available-event-types)
|
|
66
|
+
- [Event Use Cases](#event-use-cases)
|
|
67
|
+
- [Middlewares](#middlewares)
|
|
68
|
+
- [BeforeGet Middleware](#beforeget-middleware)
|
|
69
|
+
- [BeforeAddProvider Middleware](#beforeaddprovider-middleware)
|
|
70
|
+
- [BeforeAddImport Middleware](#beforeaddimport-middleware)
|
|
71
|
+
- [OnExportAccess Middleware](#onexportaccess-middleware)
|
|
72
|
+
- [BeforeRemoveImport Middleware](#beforeremoveimport-middleware)
|
|
73
|
+
- [BeforeRemoveProvider Middleware](#beforeremoveprovider-middleware)
|
|
74
|
+
- [BeforeRemoveExport Middleware](#beforeremoveexport-middleware)
|
|
75
|
+
- [All Available Middleware Types](#all-available-middleware-types)
|
|
76
|
+
- [Testing](#testing)
|
|
77
|
+
- [Blueprint Cloning](#blueprint-cloning)
|
|
78
|
+
- [Provider Substitution](#provider-substitution)
|
|
79
|
+
- [Mocking Services](#mocking-services)
|
|
80
|
+
- [Advanced Module API](#advanced-module-api)
|
|
81
|
+
- [Query Methods](#query-methods)
|
|
82
|
+
- [Multiple Provider Binding](#multiple-provider-binding)
|
|
83
|
+
- [Batch Resolution with getMany()](#batch-resolution-with-getmany)
|
|
84
|
+
- [Resources](#resources)
|
|
85
|
+
- [Contributing](#contributing)
|
|
86
|
+
- [Credits](#credits)
|
|
87
|
+
- [License](#license)
|
|
88
|
+
|
|
89
|
+
## What Problems Does This Solve?
|
|
90
|
+
|
|
91
|
+
Modern applications face several dependency management challenges. Let's examine these problems and how xInjection solves them.
|
|
92
|
+
|
|
93
|
+
### Problem 1: Manual Dependency Wiring
|
|
94
|
+
|
|
95
|
+
**Without xInjection:**
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Manually creating and wiring dependencies
|
|
99
|
+
class DatabaseService {
|
|
100
|
+
constructor(private config: ConfigService) {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class UserRepository {
|
|
104
|
+
constructor(private db: DatabaseService) {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class AuthService {
|
|
108
|
+
constructor(private userRepo: UserRepository) {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Manual instantiation nightmare
|
|
112
|
+
const config = new ConfigService();
|
|
113
|
+
const database = new DatabaseService(config);
|
|
114
|
+
const userRepo = new UserRepository(database);
|
|
115
|
+
const authService = new AuthService(userRepo);
|
|
116
|
+
|
|
117
|
+
// Every file needs to repeat this setup
|
|
118
|
+
// Changes to constructors require updating all instantiation sites
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**With xInjection:**
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
@Injectable()
|
|
125
|
+
class DatabaseService {
|
|
126
|
+
constructor(private config: ConfigService) {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Injectable()
|
|
130
|
+
class UserRepository {
|
|
131
|
+
constructor(private db: DatabaseService) {}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Injectable()
|
|
135
|
+
class AuthService {
|
|
136
|
+
constructor(private userRepo: UserRepository) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const AuthModule = ProviderModule.create({
|
|
140
|
+
id: 'AuthModule',
|
|
141
|
+
providers: [ConfigService, DatabaseService, UserRepository, AuthService],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Automatic dependency resolution
|
|
145
|
+
const authService = AuthModule.get(AuthService);
|
|
146
|
+
// All dependencies automatically injected!
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Problem 2: Tight Coupling and Testing Difficulty
|
|
150
|
+
|
|
151
|
+
**Without xInjection:**
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
class PaymentService {
|
|
155
|
+
// Hardcoded dependency - impossible to mock
|
|
156
|
+
private stripe = new StripeClient('api-key');
|
|
157
|
+
|
|
158
|
+
async charge(amount: number) {
|
|
159
|
+
return this.stripe.charge(amount);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Testing requires hitting the real Stripe API
|
|
164
|
+
// No way to inject a mock without changing production code
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**With xInjection:**
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
@Injectable()
|
|
171
|
+
class PaymentService {
|
|
172
|
+
constructor(private paymentGateway: PaymentGateway) {}
|
|
173
|
+
|
|
174
|
+
async charge(amount: number) {
|
|
175
|
+
return this.paymentGateway.charge(amount);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Production: Use real Stripe
|
|
180
|
+
const ProductionModule = ProviderModule.create({
|
|
181
|
+
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Testing: Use mock (no production code changes needed!)
|
|
185
|
+
const TestModule = ProviderModule.create({
|
|
186
|
+
providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }, PaymentService],
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Problem 3: Lack of Encapsulation
|
|
191
|
+
|
|
192
|
+
**Without xInjection:**
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// Internal implementation details exposed
|
|
196
|
+
class CacheService {
|
|
197
|
+
// Should be private but other modules need access
|
|
198
|
+
public internalCache = new Map();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
class DatabaseModule {
|
|
202
|
+
// Everything is public - no control over what gets used
|
|
203
|
+
public connection = createConnection();
|
|
204
|
+
public cache = new CacheService();
|
|
205
|
+
public queryBuilder = new QueryBuilder();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Other modules can access internals they shouldn't
|
|
209
|
+
const cache = databaseModule.internalCache; // Bad!
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**With xInjection:**
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
const DatabaseModule = ProviderModule.create({
|
|
216
|
+
id: 'DatabaseModule',
|
|
217
|
+
providers: [ConnectionPool, CacheService, QueryBuilder],
|
|
218
|
+
exports: [QueryBuilder], // Only expose public API
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Other modules can only access QueryBuilder
|
|
222
|
+
// ConnectionPool and CacheService remain internal
|
|
223
|
+
const ApiModule = ProviderModule.create({
|
|
224
|
+
imports: [DatabaseModule],
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ✅ Works - QueryBuilder is exported
|
|
228
|
+
const queryBuilder = ApiModule.get(QueryBuilder);
|
|
229
|
+
|
|
230
|
+
// ❌ Error - CacheService not exported (properly encapsulated!)
|
|
231
|
+
const cache = ApiModule.get(CacheService);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Problem 4: Lifecycle Management Complexity
|
|
235
|
+
|
|
236
|
+
**Without xInjection:**
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
class AppServices {
|
|
240
|
+
database: DatabaseService;
|
|
241
|
+
cache: CacheService;
|
|
242
|
+
|
|
243
|
+
async initialize() {
|
|
244
|
+
this.database = new DatabaseService();
|
|
245
|
+
await this.database.connect();
|
|
246
|
+
|
|
247
|
+
this.cache = new CacheService();
|
|
248
|
+
await this.cache.initialize();
|
|
249
|
+
|
|
250
|
+
// Manually track initialization order and cleanup
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async cleanup() {
|
|
254
|
+
// Must remember to clean up in reverse order
|
|
255
|
+
await this.cache.dispose();
|
|
256
|
+
await this.database.disconnect();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Easy to forget cleanup, leading to resource leaks
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**With xInjection:**
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
const AppModule = ProviderModule.create({
|
|
267
|
+
id: 'AppModule',
|
|
268
|
+
providers: [DatabaseService, CacheService],
|
|
269
|
+
onReady: async (module) => {
|
|
270
|
+
// Initialization logic - called immediately after module creation
|
|
271
|
+
const db = module.get(DatabaseService);
|
|
272
|
+
await db.connect();
|
|
273
|
+
},
|
|
274
|
+
onDispose: () => {
|
|
275
|
+
return {
|
|
276
|
+
before: async (mod) => {
|
|
277
|
+
// Automatic cleanup in proper order
|
|
278
|
+
const db = mod.get(DatabaseService);
|
|
279
|
+
await db.disconnect();
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Lifecycle automatically managed
|
|
286
|
+
await AppModule.dispose(); // Everything cleaned up properly
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
xInjection transforms these pain points into elegant, maintainable code through:
|
|
290
|
+
|
|
291
|
+
- **Automatic dependency resolution** - No manual wiring
|
|
292
|
+
- **Inversion of Control** - Easy testing and flexibility
|
|
293
|
+
- **Encapsulation** - Fine-grained control over module boundaries
|
|
294
|
+
- **Lifecycle hooks** - Proper initialization and cleanup
|
|
295
|
+
- **Modular architecture** - Scalable application structure
|
|
296
|
+
|
|
297
|
+
## Overview
|
|
298
|
+
|
|
299
|
+
**xInjection** is a powerful [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) library built on [InversifyJS](https://github.com/inversify/InversifyJS), inspired by [NestJS](https://github.com/nestjs/nest)'s modular architecture. It provides fine-grained control over dependency encapsulation through a module-based system where each module manages its own container with explicit import/export boundaries.
|
|
300
|
+
|
|
301
|
+
## Features
|
|
302
|
+
|
|
303
|
+
- **Modular Architecture** - NestJS-style import/export system for clean dependency boundaries
|
|
304
|
+
- **Isolated Containers** - Each module manages its own InversifyJS container
|
|
305
|
+
- **Flexible Scopes** - Singleton, Transient, and Request-scoped providers
|
|
306
|
+
- **Lazy Loading** - Blueprint pattern for deferred module instantiation
|
|
307
|
+
- **Lifecycle Hooks** - `onReady`, `onReset`, `onDispose` for module lifecycle management
|
|
308
|
+
- **Events & Middlewares** - Deep customization through event subscriptions and middleware chains
|
|
309
|
+
- **OOP Support** - `ProviderModuleClass` for class-based module architecture
|
|
310
|
+
- **Framework Agnostic** - Works in Node.js and browser environments
|
|
311
|
+
- **TypeScript First** - Full type safety with decorator support
|
|
312
|
+
|
|
313
|
+
## Installation
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
npm install @adimm/x-injection reflect-metadata
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**TypeScript Configuration** (`tsconfig.json`):
|
|
320
|
+
|
|
321
|
+
```json
|
|
322
|
+
{
|
|
323
|
+
"compilerOptions": {
|
|
324
|
+
"experimentalDecorators": true,
|
|
325
|
+
"emitDecoratorMetadata": true
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Import `reflect-metadata` at your application's entry point:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import 'reflect-metadata';
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Quick Start
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { Injectable, ProviderModule } from '@adimm/x-injection';
|
|
340
|
+
|
|
341
|
+
@Injectable()
|
|
342
|
+
class UserService {
|
|
343
|
+
getUser(id: string) {
|
|
344
|
+
return { id, name: 'John Doe' };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@Injectable()
|
|
349
|
+
class AuthService {
|
|
350
|
+
constructor(private userService: UserService) {}
|
|
351
|
+
|
|
352
|
+
login(userId: string) {
|
|
353
|
+
const user = this.userService.getUser(userId);
|
|
354
|
+
return `Logged in as ${user.name}`;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const AuthModule = ProviderModule.create({
|
|
359
|
+
id: 'AuthModule',
|
|
360
|
+
providers: [UserService, AuthService],
|
|
361
|
+
exports: [AuthService],
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const authService = AuthModule.get(AuthService);
|
|
365
|
+
console.log(authService.login('123')); // "Logged in as John Doe"
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Core Concepts
|
|
369
|
+
|
|
370
|
+
### Services with @Injectable
|
|
371
|
+
|
|
372
|
+
The `@Injectable()` decorator marks a class as available for dependency injection. It enables automatic constructor parameter resolution.
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { Injectable, InjectionScope } from '@adimm/x-injection';
|
|
376
|
+
|
|
377
|
+
// Basic injectable service (Singleton by default)
|
|
378
|
+
@Injectable()
|
|
379
|
+
class LoggerService {
|
|
380
|
+
log(message: string) {
|
|
381
|
+
console.log(`[LOG] ${message}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Injectable with scope specification
|
|
386
|
+
@Injectable(InjectionScope.Request)
|
|
387
|
+
class RequestContext {
|
|
388
|
+
requestId = Math.random();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Complex service with dependencies
|
|
392
|
+
@Injectable()
|
|
393
|
+
class ApiService {
|
|
394
|
+
constructor(
|
|
395
|
+
private logger: LoggerService,
|
|
396
|
+
private context: RequestContext
|
|
397
|
+
) {}
|
|
398
|
+
|
|
399
|
+
async fetchData() {
|
|
400
|
+
this.logger.log(`Fetching data for request ${this.context.requestId}`);
|
|
401
|
+
return { data: 'example' };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
> [!IMPORTANT]
|
|
407
|
+
> The `@Injectable()` decorator is **required** for any class that:
|
|
408
|
+
>
|
|
409
|
+
> - Has constructor dependencies
|
|
410
|
+
> - Needs to be resolved from a module container
|
|
411
|
+
> - Should be managed by the dependency injection system
|
|
412
|
+
|
|
413
|
+
### Modules
|
|
414
|
+
|
|
415
|
+
Modules are the fundamental building blocks of xInjection. Each module encapsulates providers with explicit control over imports and exports.
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { ProviderModule } from '@adimm/x-injection';
|
|
419
|
+
|
|
420
|
+
// Define services
|
|
421
|
+
@Injectable()
|
|
422
|
+
class DatabaseService {
|
|
423
|
+
query(sql: string) {
|
|
424
|
+
return [{ id: 1, name: 'Result' }];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@Injectable()
|
|
429
|
+
class InternalCacheService {
|
|
430
|
+
// Internal-only service
|
|
431
|
+
private cache = new Map();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@Injectable()
|
|
435
|
+
class UserRepository {
|
|
436
|
+
constructor(
|
|
437
|
+
private db: DatabaseService,
|
|
438
|
+
private cache: InternalCacheService
|
|
439
|
+
) {}
|
|
440
|
+
|
|
441
|
+
findById(id: string) {
|
|
442
|
+
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Create module with encapsulation
|
|
447
|
+
const DatabaseModule = ProviderModule.create({
|
|
448
|
+
id: 'DatabaseModule',
|
|
449
|
+
providers: [DatabaseService, InternalCacheService, UserRepository],
|
|
450
|
+
exports: [UserRepository], // Only UserRepository accessible to importers
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Use the module
|
|
454
|
+
const ApiModule = ProviderModule.create({
|
|
455
|
+
id: 'ApiModule',
|
|
456
|
+
imports: [DatabaseModule],
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// ✅ Works - UserRepository is exported
|
|
460
|
+
const userRepo = ApiModule.get(UserRepository);
|
|
461
|
+
|
|
462
|
+
// ❌ Throws error - InternalCacheService not exported
|
|
463
|
+
// const cache = ApiModule.get(InternalCacheService);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Key Module Methods:**
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
const MyModule = ProviderModule.create({ id: 'MyModule' });
|
|
470
|
+
|
|
471
|
+
// Resolution
|
|
472
|
+
MyModule.get(ServiceClass); // Get provider instance
|
|
473
|
+
MyModule.getMany(Service1, Service2); // Get multiple providers
|
|
474
|
+
|
|
475
|
+
// Queries
|
|
476
|
+
MyModule.hasProvider(ServiceClass); // Check if provider exists
|
|
477
|
+
MyModule.isImportingModule('ModuleId'); // Check if importing module
|
|
478
|
+
MyModule.isExportingProvider(ServiceClass); // Check if exporting provider
|
|
479
|
+
MyModule.isExportingModule('ModuleId'); // Check if exporting module
|
|
480
|
+
|
|
481
|
+
// Dynamic updates
|
|
482
|
+
MyModule.update.addProvider(NewService); // Add provider
|
|
483
|
+
MyModule.update.addImport(OtherModule); // Add import
|
|
484
|
+
MyModule.update.removeProvider(ServiceClass); // Remove provider
|
|
485
|
+
MyModule.update.removeImport(OtherModule); // Remove import
|
|
486
|
+
MyModule.update.removeFromExports(Service); // Remove from exports
|
|
487
|
+
|
|
488
|
+
// Lifecycle
|
|
489
|
+
await MyModule.reset(); // Reset module state
|
|
490
|
+
await MyModule.dispose(); // Clean up and dispose
|
|
491
|
+
MyModule.isDisposed; // Check disposal state
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Blueprints
|
|
495
|
+
|
|
496
|
+
Blueprints allow you to define module configurations without instantiating them, enabling lazy loading and template reuse.
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
import { ProviderModule } from '@adimm/x-injection';
|
|
500
|
+
|
|
501
|
+
@Injectable()
|
|
502
|
+
class ConfigService {
|
|
503
|
+
getConfig() {
|
|
504
|
+
return { apiUrl: 'https://api.example.com' };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Define blueprint (not instantiated yet)
|
|
509
|
+
const ConfigModuleBp = ProviderModule.blueprint({
|
|
510
|
+
id: 'ConfigModule',
|
|
511
|
+
providers: [ConfigService],
|
|
512
|
+
exports: [ConfigService],
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Use blueprint in imports (auto-converts to module)
|
|
516
|
+
const ApiModule = ProviderModule.create({
|
|
517
|
+
id: 'ApiModule',
|
|
518
|
+
imports: [ConfigModuleBp], // Instantiated automatically when needed
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Or create module from blueprint explicitly
|
|
522
|
+
const ConfigModule = ProviderModule.create(ConfigModuleBp);
|
|
523
|
+
|
|
524
|
+
// Clone blueprints for testing
|
|
525
|
+
const ConfigModuleMock = ConfigModuleBp.clone().updateDefinition({
|
|
526
|
+
id: 'ConfigModuleMock',
|
|
527
|
+
providers: [{ provide: ConfigService, useValue: { getConfig: () => ({ apiUrl: 'mock' }) } }],
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Benefits of Blueprints:**
|
|
532
|
+
|
|
533
|
+
- **Deferred Instantiation** - Only create modules when needed
|
|
534
|
+
- **Reusable Templates** - Define once, use in multiple places
|
|
535
|
+
- **Testing** - Clone and modify for test scenarios
|
|
536
|
+
- **Scoped Singletons** - Each importer gets its own module instance
|
|
537
|
+
|
|
538
|
+
> [!TIP]
|
|
539
|
+
> Use blueprints when you need the same module configuration in multiple places, or when you want to delay module creation until runtime.
|
|
540
|
+
|
|
541
|
+
### AppModule
|
|
542
|
+
|
|
543
|
+
The `AppModule` is a special global root module that's automatically available throughout your application. Global modules are auto-imported into `AppModule`.
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
import { AppModule, ProviderModule } from '@adimm/x-injection';
|
|
547
|
+
|
|
548
|
+
@Injectable()
|
|
549
|
+
class LoggerService {
|
|
550
|
+
log(message: string) {
|
|
551
|
+
console.log(message);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Add global providers to AppModule
|
|
556
|
+
AppModule.update.addProvider(LoggerService);
|
|
557
|
+
|
|
558
|
+
// Access from any module without explicit import
|
|
559
|
+
const FeatureModule = ProviderModule.create({
|
|
560
|
+
id: 'FeatureModule',
|
|
561
|
+
// No need to import AppModule
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const logger = FeatureModule.get(LoggerService);
|
|
565
|
+
logger.log('Hello from FeatureModule!');
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**Global Module Pattern:**
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
// Create a global module (auto-imports into AppModule)
|
|
572
|
+
const LoggerModule = ProviderModule.create({
|
|
573
|
+
id: 'LoggerModule',
|
|
574
|
+
isGlobal: true,
|
|
575
|
+
providers: [LoggerService],
|
|
576
|
+
exports: [LoggerService],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Now all modules have access to LoggerService
|
|
580
|
+
const AnyModule = ProviderModule.create({
|
|
581
|
+
id: 'AnyModule',
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const logger = AnyModule.get(LoggerService); // Works!
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
> [!WARNING]
|
|
588
|
+
>
|
|
589
|
+
> - Cannot create a module with `id: 'AppModule'` - this is reserved
|
|
590
|
+
> - Cannot import `AppModule` into other modules
|
|
591
|
+
> - Use global modules sparingly to avoid implicit dependencies
|
|
592
|
+
|
|
593
|
+
## OOP-Style Modules with ProviderModuleClass
|
|
594
|
+
|
|
595
|
+
For developers who prefer class-based architecture, xInjection provides `ProviderModuleClass` - a composition-based wrapper that prevents naming conflicts between your custom methods and the DI container methods.
|
|
596
|
+
|
|
597
|
+
### Basic OOP Module
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
import { Injectable, ProviderModuleClass } from '@adimm/x-injection';
|
|
601
|
+
|
|
602
|
+
@Injectable()
|
|
603
|
+
class UserService {
|
|
604
|
+
get(id: string) {
|
|
605
|
+
return { id, name: 'John Doe' };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
@Injectable()
|
|
610
|
+
class AuthService {
|
|
611
|
+
constructor(private userService: UserService) {}
|
|
612
|
+
|
|
613
|
+
login(userId: string) {
|
|
614
|
+
const user = this.userService.get(userId);
|
|
615
|
+
return `Logged in as ${user.name}`;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// OOP-style module extending ProviderModuleClass
|
|
620
|
+
class AuthModule extends ProviderModuleClass {
|
|
621
|
+
constructor() {
|
|
622
|
+
super({
|
|
623
|
+
id: 'AuthModule',
|
|
624
|
+
providers: [UserService, AuthService],
|
|
625
|
+
exports: [AuthService],
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Custom business logic methods
|
|
630
|
+
authenticateUser(userId: string): string {
|
|
631
|
+
const authService = this.module.get(AuthService);
|
|
632
|
+
return authService.login(userId);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
getUserById(userId: string) {
|
|
636
|
+
const userService = this.module.get(UserService);
|
|
637
|
+
return userService.get(userId);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Custom method named 'get' - no conflict!
|
|
641
|
+
get(): string {
|
|
642
|
+
return 'custom-get-value';
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Instantiate and use
|
|
647
|
+
const authModule = new AuthModule();
|
|
648
|
+
|
|
649
|
+
// Use custom methods
|
|
650
|
+
console.log(authModule.authenticateUser('123')); // "Logged in as John Doe"
|
|
651
|
+
console.log(authModule.get()); // "custom-get-value"
|
|
652
|
+
|
|
653
|
+
// Access DI container through .module property
|
|
654
|
+
const authService = authModule.module.get(AuthService);
|
|
655
|
+
authModule.module.update.addProvider(NewService);
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
> [!IMPORTANT]
|
|
659
|
+
> All `ProviderModule` methods are available through the `.module` property to prevent naming conflicts with your custom methods.
|
|
660
|
+
|
|
661
|
+
### Advanced OOP Patterns
|
|
662
|
+
|
|
663
|
+
**Module with Initialization Logic:**
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
@Injectable()
|
|
667
|
+
class DatabaseService {
|
|
668
|
+
private connected = false;
|
|
669
|
+
|
|
670
|
+
async connect(): Promise<void> {
|
|
671
|
+
// Connection logic
|
|
672
|
+
this.connected = true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
isConnected(): boolean {
|
|
676
|
+
return this.connected;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
class DatabaseModule extends ProviderModuleClass {
|
|
681
|
+
private isModuleConnected = false;
|
|
682
|
+
|
|
683
|
+
constructor() {
|
|
684
|
+
super({
|
|
685
|
+
id: 'DatabaseModule',
|
|
686
|
+
providers: [DatabaseService],
|
|
687
|
+
exports: [DatabaseService],
|
|
688
|
+
onReady: async (module) => {
|
|
689
|
+
console.log('DatabaseModule ready!');
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async connect(): Promise<void> {
|
|
695
|
+
const dbService = this.module.get(DatabaseService);
|
|
696
|
+
await dbService.connect();
|
|
697
|
+
this.isModuleConnected = true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
getConnectionStatus(): boolean {
|
|
701
|
+
return this.isModuleConnected;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const dbModule = new DatabaseModule();
|
|
706
|
+
await dbModule.connect();
|
|
707
|
+
console.log(dbModule.getConnectionStatus()); // true
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**Module with Computed Properties:**
|
|
711
|
+
|
|
712
|
+
```ts
|
|
713
|
+
@Injectable()
|
|
714
|
+
class ApiService {
|
|
715
|
+
makeRequest() {
|
|
716
|
+
return 'response';
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
@Injectable()
|
|
721
|
+
class HttpClient {
|
|
722
|
+
get(url: string) {
|
|
723
|
+
return `GET ${url}`;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
class ApiModule extends ProviderModuleClass {
|
|
728
|
+
constructor() {
|
|
729
|
+
super({
|
|
730
|
+
id: 'ApiModule',
|
|
731
|
+
providers: [ApiService, HttpClient],
|
|
732
|
+
exports: [ApiService],
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Computed properties - lazy evaluation
|
|
737
|
+
get apiService(): ApiService {
|
|
738
|
+
return this.module.get(ApiService);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
get httpClient(): HttpClient {
|
|
742
|
+
return this.module.get(HttpClient);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Business logic using multiple services
|
|
746
|
+
async makeAuthenticatedRequest(url: string, token: string) {
|
|
747
|
+
const client = this.httpClient;
|
|
748
|
+
return client.get(url) + ` with token ${token}`;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const apiModule = new ApiModule();
|
|
753
|
+
const response = await apiModule.makeAuthenticatedRequest('/users', 'token123');
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### When to Use OOP vs Functional
|
|
757
|
+
|
|
758
|
+
**Use OOP-style (`extends ProviderModuleClass`) when:**
|
|
759
|
+
|
|
760
|
+
- You need custom business logic methods on the module itself
|
|
761
|
+
- You prefer class-based architecture and inheritance patterns
|
|
762
|
+
- You want computed properties or getters for providers
|
|
763
|
+
- You need initialization logic or state management in the module
|
|
764
|
+
- You're building a complex module with multiple related operations
|
|
765
|
+
- You want to prevent naming conflicts (e.g., custom `get()` method)
|
|
766
|
+
|
|
767
|
+
**Use Functional-style (`ProviderModule.create()`) when:**
|
|
768
|
+
|
|
769
|
+
- You only need dependency injection without custom logic
|
|
770
|
+
- You prefer functional composition and simplicity
|
|
771
|
+
- You want more concise code
|
|
772
|
+
- You're creating straightforward provider containers
|
|
773
|
+
- You don't need module-level state or behavior
|
|
774
|
+
|
|
775
|
+
**Key Point:** Both styles are fully compatible and can be mixed within the same application. `ProviderModuleClass` uses composition (contains a `ProviderModule` as `this.module`), providing identical DI functionality while preventing method name conflicts.
|
|
776
|
+
|
|
777
|
+
## Provider Tokens
|
|
778
|
+
|
|
779
|
+
xInjection supports four types of provider tokens, each serving different use cases.
|
|
780
|
+
|
|
781
|
+
### 1. Class Token
|
|
782
|
+
|
|
783
|
+
The simplest form - just provide the class directly.
|
|
784
|
+
|
|
785
|
+
```ts
|
|
786
|
+
@Injectable()
|
|
787
|
+
class UserService {
|
|
788
|
+
getUsers() {
|
|
789
|
+
return [{ id: '1', name: 'Alice' }];
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const MyModule = ProviderModule.create({
|
|
794
|
+
id: 'MyModule',
|
|
795
|
+
providers: [UserService], // Class token
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const userService = MyModule.get(UserService);
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### 2. Class Token with Substitution
|
|
802
|
+
|
|
803
|
+
Use one class as the token but instantiate a different class. Perfect for polymorphism and testing.
|
|
804
|
+
|
|
805
|
+
```ts
|
|
806
|
+
@Injectable()
|
|
807
|
+
abstract class PaymentGateway {
|
|
808
|
+
abstract charge(amount: number): Promise<void>;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
@Injectable()
|
|
812
|
+
class StripePaymentGateway extends PaymentGateway {
|
|
813
|
+
async charge(amount: number) {
|
|
814
|
+
console.log(`Charging $${amount} via Stripe`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
@Injectable()
|
|
819
|
+
class MockPaymentGateway extends PaymentGateway {
|
|
820
|
+
async charge(amount: number) {
|
|
821
|
+
console.log(`Mock charge: $${amount}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Production
|
|
826
|
+
const ProductionModule = ProviderModule.create({
|
|
827
|
+
id: 'ProductionModule',
|
|
828
|
+
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }],
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// Testing
|
|
832
|
+
const TestModule = ProviderModule.create({
|
|
833
|
+
id: 'TestModule',
|
|
834
|
+
providers: [{ provide: PaymentGateway, useClass: MockPaymentGateway }],
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const prodGateway = ProductionModule.get(PaymentGateway); // StripePaymentGateway
|
|
838
|
+
const testGateway = TestModule.get(PaymentGateway); // MockPaymentGateway
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### 3. Value Token
|
|
842
|
+
|
|
843
|
+
Provide constant values or pre-instantiated objects.
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
// Configuration values
|
|
847
|
+
const ConfigModule = ProviderModule.create({
|
|
848
|
+
id: 'ConfigModule',
|
|
849
|
+
providers: [
|
|
850
|
+
{ provide: 'API_KEY', useValue: 'secret-key-123' },
|
|
851
|
+
{ provide: 'API_URL', useValue: 'https://api.example.com' },
|
|
852
|
+
{ provide: 'MAX_RETRIES', useValue: 3 },
|
|
853
|
+
],
|
|
854
|
+
exports: ['API_KEY', 'API_URL', 'MAX_RETRIES'],
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
const apiKey = ConfigModule.get('API_KEY'); // 'secret-key-123'
|
|
858
|
+
const apiUrl = ConfigModule.get('API_URL'); // 'https://api.example.com'
|
|
859
|
+
const maxRetries = ConfigModule.get('MAX_RETRIES'); // 3
|
|
860
|
+
|
|
861
|
+
// Pre-instantiated objects
|
|
862
|
+
const existingLogger = new Logger();
|
|
863
|
+
const LoggerModule = ProviderModule.create({
|
|
864
|
+
id: 'LoggerModule',
|
|
865
|
+
providers: [{ provide: Logger, useValue: existingLogger }],
|
|
866
|
+
});
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
### 4. Factory Token
|
|
870
|
+
|
|
871
|
+
Use a factory function to create providers dynamically. The `inject` parameter specifies dependencies.
|
|
872
|
+
|
|
873
|
+
```ts
|
|
874
|
+
@Injectable()
|
|
875
|
+
class ConfigService {
|
|
876
|
+
dbUrl = 'postgres://localhost:5432/mydb';
|
|
877
|
+
dbPort = 5432;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
interface DatabaseConnection {
|
|
881
|
+
url: string;
|
|
882
|
+
port: number;
|
|
883
|
+
connected: boolean;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const DatabaseModule = ProviderModule.create({
|
|
887
|
+
id: 'DatabaseModule',
|
|
888
|
+
providers: [
|
|
889
|
+
ConfigService,
|
|
890
|
+
{
|
|
891
|
+
provide: 'DATABASE_CONNECTION',
|
|
892
|
+
useFactory: (config: ConfigService) => {
|
|
893
|
+
// Factory receives injected dependencies
|
|
894
|
+
return {
|
|
895
|
+
url: config.dbUrl,
|
|
896
|
+
port: config.dbPort,
|
|
897
|
+
connected: true,
|
|
898
|
+
};
|
|
899
|
+
},
|
|
900
|
+
inject: [ConfigService], // Dependencies to inject into factory
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
exports: ['DATABASE_CONNECTION'],
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
const connection = DatabaseModule.get<DatabaseConnection>('DATABASE_CONNECTION');
|
|
907
|
+
console.log(connection.url); // 'postgres://localhost:5432/mydb'
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
**Complex Factory Example with Multiple Dependencies:**
|
|
911
|
+
|
|
912
|
+
```ts
|
|
913
|
+
@Injectable()
|
|
914
|
+
class LoggerService {
|
|
915
|
+
log(message: string) {
|
|
916
|
+
console.log(message);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
@Injectable()
|
|
921
|
+
class MetricsService {
|
|
922
|
+
track(event: string) {
|
|
923
|
+
console.log(`Tracking: ${event}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
interface ApiClient {
|
|
928
|
+
logger: LoggerService;
|
|
929
|
+
metrics: MetricsService;
|
|
930
|
+
baseUrl: string;
|
|
931
|
+
makeRequest(endpoint: string): void;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const ApiModule = ProviderModule.create({
|
|
935
|
+
id: 'ApiModule',
|
|
936
|
+
providers: [
|
|
937
|
+
LoggerService,
|
|
938
|
+
MetricsService,
|
|
939
|
+
{ provide: 'BASE_URL', useValue: 'https://api.example.com' },
|
|
940
|
+
{
|
|
941
|
+
provide: 'API_CLIENT',
|
|
942
|
+
useFactory: (logger: LoggerService, metrics: MetricsService, baseUrl: string): ApiClient => {
|
|
943
|
+
return {
|
|
944
|
+
logger,
|
|
945
|
+
metrics,
|
|
946
|
+
baseUrl,
|
|
947
|
+
makeRequest(endpoint: string) {
|
|
948
|
+
this.logger.log(`Making request to ${this.baseUrl}${endpoint}`);
|
|
949
|
+
this.metrics.track('api_request');
|
|
950
|
+
},
|
|
951
|
+
};
|
|
952
|
+
},
|
|
953
|
+
inject: [LoggerService, MetricsService, 'BASE_URL'],
|
|
954
|
+
},
|
|
955
|
+
],
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
const apiClient = ApiModule.get<ApiClient>('API_CLIENT');
|
|
959
|
+
apiClient.makeRequest('/users');
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
> [!TIP]
|
|
963
|
+
> Use factory tokens when:
|
|
964
|
+
>
|
|
965
|
+
> - Provider creation requires complex logic
|
|
966
|
+
> - You need to inject dependencies into the factory
|
|
967
|
+
> - You're creating providers that depend on runtime configuration
|
|
968
|
+
> - You need to create multiple instances with different configurations
|
|
969
|
+
|
|
970
|
+
## Injection Scopes
|
|
971
|
+
|
|
972
|
+
Control provider lifecycle with three scope types. Scope priority order: **token scope > decorator scope > module default scope**.
|
|
973
|
+
|
|
974
|
+
### Singleton (Default)
|
|
975
|
+
|
|
976
|
+
Cached after first resolution - same instance returned every time.
|
|
977
|
+
|
|
978
|
+
```ts
|
|
979
|
+
@Injectable() // Singleton by default
|
|
980
|
+
class DatabaseService {
|
|
981
|
+
connectionId = Math.random();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const MyModule = ProviderModule.create({
|
|
985
|
+
id: 'MyModule',
|
|
986
|
+
providers: [DatabaseService],
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const db1 = MyModule.get(DatabaseService);
|
|
990
|
+
const db2 = MyModule.get(DatabaseService);
|
|
991
|
+
|
|
992
|
+
console.log(db1 === db2); // true
|
|
993
|
+
console.log(db1.connectionId === db2.connectionId); // true
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### Transient
|
|
997
|
+
|
|
998
|
+
New instance created on every resolution.
|
|
999
|
+
|
|
1000
|
+
```ts
|
|
1001
|
+
@Injectable(InjectionScope.Transient)
|
|
1002
|
+
class RequestLogger {
|
|
1003
|
+
requestId = Math.random();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const MyModule = ProviderModule.create({
|
|
1007
|
+
id: 'MyModule',
|
|
1008
|
+
providers: [RequestLogger],
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
const logger1 = MyModule.get(RequestLogger);
|
|
1012
|
+
const logger2 = MyModule.get(RequestLogger);
|
|
1013
|
+
|
|
1014
|
+
console.log(logger1 === logger2); // false
|
|
1015
|
+
console.log(logger1.requestId === logger2.requestId); // false
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
### Request
|
|
1019
|
+
|
|
1020
|
+
Single instance per resolution tree. All dependencies resolved in the same `get()` call share the same instance.
|
|
1021
|
+
|
|
1022
|
+
```ts
|
|
1023
|
+
@Injectable(InjectionScope.Request)
|
|
1024
|
+
class RequestContext {
|
|
1025
|
+
requestId = Math.random();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
@Injectable(InjectionScope.Transient)
|
|
1029
|
+
class ServiceA {
|
|
1030
|
+
constructor(public ctx: RequestContext) {}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
@Injectable(InjectionScope.Transient)
|
|
1034
|
+
class ServiceB {
|
|
1035
|
+
constructor(public ctx: RequestContext) {}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
@Injectable(InjectionScope.Transient)
|
|
1039
|
+
class Controller {
|
|
1040
|
+
constructor(
|
|
1041
|
+
public serviceA: ServiceA,
|
|
1042
|
+
public serviceB: ServiceB
|
|
1043
|
+
) {}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const MyModule = ProviderModule.create({
|
|
1047
|
+
id: 'MyModule',
|
|
1048
|
+
providers: [RequestContext, ServiceA, ServiceB, Controller],
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
// First resolution tree
|
|
1052
|
+
const controller1 = MyModule.get(Controller);
|
|
1053
|
+
console.log(controller1.serviceA.ctx === controller1.serviceB.ctx); // true
|
|
1054
|
+
// ServiceA and ServiceB share the same RequestContext
|
|
1055
|
+
|
|
1056
|
+
// Second resolution tree
|
|
1057
|
+
const controller2 = MyModule.get(Controller);
|
|
1058
|
+
console.log(controller2.serviceA.ctx === controller2.serviceB.ctx); // true
|
|
1059
|
+
// New resolution, both services get a new shared RequestContext
|
|
1060
|
+
|
|
1061
|
+
// Different resolution trees get different contexts
|
|
1062
|
+
console.log(controller1.serviceA.ctx === controller2.serviceA.ctx); // false
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Visual Representation:**
|
|
1066
|
+
|
|
1067
|
+
```
|
|
1068
|
+
First module.get(Controller):
|
|
1069
|
+
Controller (new) ──┬──> ServiceA (new) ──┐
|
|
1070
|
+
│ ├──> RequestContext (SAME instance)
|
|
1071
|
+
└──> ServiceB (new) ──┘
|
|
1072
|
+
|
|
1073
|
+
Second module.get(Controller):
|
|
1074
|
+
Controller (new) ──┬──> ServiceA (new) ──┐
|
|
1075
|
+
│ ├──> RequestContext (NEW instance)
|
|
1076
|
+
└──> ServiceB (new) ──┘
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Scope Priority Order
|
|
1080
|
+
|
|
1081
|
+
Scopes are resolved in the following priority order (highest to lowest):
|
|
1082
|
+
|
|
1083
|
+
1. **Token scope** (highest priority)
|
|
1084
|
+
2. **Decorator scope**
|
|
1085
|
+
3. **Module default scope** (lowest priority)
|
|
1086
|
+
|
|
1087
|
+
```ts
|
|
1088
|
+
@Injectable(InjectionScope.Singleton) // Priority 2
|
|
1089
|
+
class MyService {}
|
|
1090
|
+
|
|
1091
|
+
const MyModule = ProviderModule.create({
|
|
1092
|
+
id: 'MyModule',
|
|
1093
|
+
defaultScope: InjectionScope.Singleton, // Priority 3 (lowest)
|
|
1094
|
+
providers: [
|
|
1095
|
+
{
|
|
1096
|
+
provide: MyService,
|
|
1097
|
+
useClass: MyService,
|
|
1098
|
+
scope: InjectionScope.Transient, // Priority 1 (highest) - WINS!
|
|
1099
|
+
},
|
|
1100
|
+
],
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Token scope wins: new instance every time
|
|
1104
|
+
const s1 = MyModule.get(MyService);
|
|
1105
|
+
const s2 = MyModule.get(MyService);
|
|
1106
|
+
console.log(s1 === s2); // false
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
**Examples of Each Priority:**
|
|
1110
|
+
|
|
1111
|
+
```ts
|
|
1112
|
+
// Priority 1: Token scope
|
|
1113
|
+
const Module1 = ProviderModule.create({
|
|
1114
|
+
id: 'Module1',
|
|
1115
|
+
defaultScope: InjectionScope.Singleton,
|
|
1116
|
+
providers: [
|
|
1117
|
+
{
|
|
1118
|
+
provide: MyService,
|
|
1119
|
+
useClass: MyService,
|
|
1120
|
+
scope: InjectionScope.Transient, // Token wins
|
|
1121
|
+
},
|
|
1122
|
+
],
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// Priority 2: Decorator scope (no token scope)
|
|
1126
|
+
@Injectable(InjectionScope.Request)
|
|
1127
|
+
class DecoratedService {}
|
|
1128
|
+
|
|
1129
|
+
const Module2 = ProviderModule.create({
|
|
1130
|
+
id: 'Module2',
|
|
1131
|
+
defaultScope: InjectionScope.Singleton,
|
|
1132
|
+
providers: [DecoratedService], // Decorator wins
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Priority 3: Module default (no token or decorator scope)
|
|
1136
|
+
@Injectable() // No scope specified
|
|
1137
|
+
class PlainService {}
|
|
1138
|
+
|
|
1139
|
+
const Module3 = ProviderModule.create({
|
|
1140
|
+
id: 'Module3',
|
|
1141
|
+
defaultScope: InjectionScope.Transient, // Module default wins
|
|
1142
|
+
providers: [PlainService],
|
|
1143
|
+
});
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
> [!IMPORTANT]
|
|
1147
|
+
> Request scope is useful for scenarios like:
|
|
1148
|
+
>
|
|
1149
|
+
> - HTTP request tracking (same request ID across all services in one request)
|
|
1150
|
+
> - Transaction contexts (same database transaction across all repositories)
|
|
1151
|
+
> - User context (same user data across all services in one operation)
|
|
1152
|
+
|
|
1153
|
+
## Module System
|
|
1154
|
+
|
|
1155
|
+
### Import/Export Pattern
|
|
1156
|
+
|
|
1157
|
+
Modules explicitly control dependency boundaries through imports and exports, providing encapsulation and clear interfaces.
|
|
1158
|
+
|
|
1159
|
+
```ts
|
|
1160
|
+
@Injectable()
|
|
1161
|
+
class DatabaseService {
|
|
1162
|
+
query(sql: string) {
|
|
1163
|
+
return [{ result: 'data' }];
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
@Injectable()
|
|
1168
|
+
class InternalCacheService {
|
|
1169
|
+
// Private to DatabaseModule
|
|
1170
|
+
cache = new Map();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const DatabaseModule = ProviderModule.create({
|
|
1174
|
+
id: 'DatabaseModule',
|
|
1175
|
+
providers: [DatabaseService, InternalCacheService],
|
|
1176
|
+
exports: [DatabaseService], // Only DatabaseService is accessible
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
const ApiModule = ProviderModule.create({
|
|
1180
|
+
id: 'ApiModule',
|
|
1181
|
+
imports: [DatabaseModule],
|
|
1182
|
+
providers: [ApiService],
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// ✅ Works - DatabaseService is exported
|
|
1186
|
+
const dbService = ApiModule.get(DatabaseService);
|
|
1187
|
+
|
|
1188
|
+
// ❌ Error - InternalCacheService not exported
|
|
1189
|
+
// const cache = ApiModule.get(InternalCacheService);
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
**Nested Imports:**
|
|
1193
|
+
|
|
1194
|
+
```ts
|
|
1195
|
+
const LayerA = ProviderModule.create({
|
|
1196
|
+
id: 'LayerA',
|
|
1197
|
+
providers: [ServiceA],
|
|
1198
|
+
exports: [ServiceA],
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
const LayerB = ProviderModule.create({
|
|
1202
|
+
id: 'LayerB',
|
|
1203
|
+
imports: [LayerA],
|
|
1204
|
+
providers: [ServiceB],
|
|
1205
|
+
exports: [ServiceB, LayerA], // Re-export LayerA
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
const LayerC = ProviderModule.create({
|
|
1209
|
+
id: 'LayerC',
|
|
1210
|
+
imports: [LayerB],
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
// ✅ Works - ServiceA accessible through LayerB's re-export
|
|
1214
|
+
const serviceA = LayerC.get(ServiceA);
|
|
1215
|
+
|
|
1216
|
+
// ✅ Works - ServiceB exported by LayerB
|
|
1217
|
+
const serviceB = LayerC.get(ServiceB);
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
### Re-exporting Modules
|
|
1221
|
+
|
|
1222
|
+
Modules can re-export imported modules to create aggregation modules.
|
|
1223
|
+
|
|
1224
|
+
```ts
|
|
1225
|
+
const DatabaseModule = ProviderModule.create({
|
|
1226
|
+
id: 'DatabaseModule',
|
|
1227
|
+
providers: [DatabaseService],
|
|
1228
|
+
exports: [DatabaseService],
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
const ConfigModule = ProviderModule.create({
|
|
1232
|
+
id: 'ConfigModule',
|
|
1233
|
+
providers: [ConfigService],
|
|
1234
|
+
exports: [ConfigService],
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
const LoggerModule = ProviderModule.create({
|
|
1238
|
+
id: 'LoggerModule',
|
|
1239
|
+
providers: [LoggerService],
|
|
1240
|
+
exports: [LoggerService],
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
// CoreModule aggregates common modules
|
|
1244
|
+
const CoreModule = ProviderModule.create({
|
|
1245
|
+
id: 'CoreModule',
|
|
1246
|
+
imports: [DatabaseModule, ConfigModule, LoggerModule],
|
|
1247
|
+
exports: [DatabaseModule, ConfigModule, LoggerModule], // Re-export all
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// Consumers import CoreModule and get all three modules
|
|
1251
|
+
const FeatureModule = ProviderModule.create({
|
|
1252
|
+
id: 'FeatureModule',
|
|
1253
|
+
imports: [CoreModule], // Just import one module
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// Access all re-exported providers
|
|
1257
|
+
const db = FeatureModule.get(DatabaseService);
|
|
1258
|
+
const config = FeatureModule.get(ConfigService);
|
|
1259
|
+
const logger = FeatureModule.get(LoggerService);
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
> [!TIP]
|
|
1263
|
+
> Create "barrel" or "core" modules that re-export commonly used modules to simplify imports throughout your application.
|
|
1264
|
+
|
|
1265
|
+
### Dynamic Module Updates
|
|
1266
|
+
|
|
1267
|
+
Modules support runtime modifications for flexibility. Use sparingly as it can impact performance.
|
|
1268
|
+
|
|
1269
|
+
```ts
|
|
1270
|
+
const DynamicModule = ProviderModule.create({
|
|
1271
|
+
id: 'DynamicModule',
|
|
1272
|
+
providers: [ServiceA],
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// Add providers dynamically
|
|
1276
|
+
DynamicModule.update.addProvider(ServiceB);
|
|
1277
|
+
DynamicModule.update.addProvider(ServiceC, true); // true = also export
|
|
1278
|
+
|
|
1279
|
+
// Add imports dynamically
|
|
1280
|
+
const DatabaseModule = ProviderModule.create({
|
|
1281
|
+
id: 'DatabaseModule',
|
|
1282
|
+
providers: [DatabaseService],
|
|
1283
|
+
exports: [DatabaseService],
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
DynamicModule.update.addImport(DatabaseModule, true); // true = also export
|
|
1287
|
+
|
|
1288
|
+
// Check what's available
|
|
1289
|
+
console.log(DynamicModule.hasProvider(ServiceB)); // true
|
|
1290
|
+
console.log(DynamicModule.isImportingModule('DatabaseModule')); // true
|
|
1291
|
+
console.log(DynamicModule.isExportingProvider(ServiceC)); // true
|
|
1292
|
+
|
|
1293
|
+
// Remove providers and imports
|
|
1294
|
+
DynamicModule.update.removeProvider(ServiceB);
|
|
1295
|
+
DynamicModule.update.removeImport(DatabaseModule);
|
|
1296
|
+
DynamicModule.update.removeFromExports(ServiceC);
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
**Dynamic Import Propagation:**
|
|
1300
|
+
|
|
1301
|
+
```ts
|
|
1302
|
+
const ModuleA = ProviderModule.create({
|
|
1303
|
+
id: 'ModuleA',
|
|
1304
|
+
providers: [ServiceA],
|
|
1305
|
+
exports: [ServiceA],
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const ModuleB = ProviderModule.create({
|
|
1309
|
+
id: 'ModuleB',
|
|
1310
|
+
imports: [ModuleA],
|
|
1311
|
+
exports: [ModuleA],
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
const ModuleC = ProviderModule.create({
|
|
1315
|
+
id: 'ModuleC',
|
|
1316
|
+
providers: [ServiceC],
|
|
1317
|
+
exports: [ServiceC],
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
// Initially, ModuleB doesn't have ServiceC
|
|
1321
|
+
console.log(ModuleB.hasProvider(ServiceC)); // false
|
|
1322
|
+
|
|
1323
|
+
// Dynamically import ModuleC into ModuleA and export it
|
|
1324
|
+
ModuleA.update.addImport(ModuleC, true);
|
|
1325
|
+
|
|
1326
|
+
// Now ModuleB automatically has ServiceC (import propagation!)
|
|
1327
|
+
console.log(ModuleB.hasProvider(ServiceC)); // true
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
> [!WARNING]
|
|
1331
|
+
> Dynamic module updates:
|
|
1332
|
+
>
|
|
1333
|
+
> - Can impact performance if used frequently
|
|
1334
|
+
> - Should be used primarily for testing or plugin systems
|
|
1335
|
+
> - May make dependency graphs harder to understand
|
|
1336
|
+
> - Are propagated automatically to importing modules
|
|
1337
|
+
|
|
1338
|
+
### Global Modules
|
|
1339
|
+
|
|
1340
|
+
Mark modules as global to auto-import into `AppModule`, making them available everywhere.
|
|
1341
|
+
|
|
1342
|
+
```ts
|
|
1343
|
+
@Injectable()
|
|
1344
|
+
class LoggerService {
|
|
1345
|
+
log(message: string) {
|
|
1346
|
+
console.log(`[LOG] ${message}`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Create global module
|
|
1351
|
+
const LoggerModule = ProviderModule.create({
|
|
1352
|
+
id: 'LoggerModule',
|
|
1353
|
+
isGlobal: true, // Auto-imports into AppModule
|
|
1354
|
+
providers: [LoggerService],
|
|
1355
|
+
exports: [LoggerService],
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
// Now any module can access LoggerService without explicit import
|
|
1359
|
+
const FeatureModule = ProviderModule.create({
|
|
1360
|
+
id: 'FeatureModule',
|
|
1361
|
+
// No imports needed!
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
const logger = FeatureModule.get(LoggerService); // Works!
|
|
1365
|
+
logger.log('Hello from FeatureModule');
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
**Global Module with Blueprint:**
|
|
1369
|
+
|
|
1370
|
+
```ts
|
|
1371
|
+
@Injectable()
|
|
1372
|
+
class ConfigService {
|
|
1373
|
+
apiUrl = 'https://api.example.com';
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Blueprint with global flag
|
|
1377
|
+
const ConfigModuleBp = ProviderModule.blueprint({
|
|
1378
|
+
id: 'ConfigModule',
|
|
1379
|
+
isGlobal: true,
|
|
1380
|
+
providers: [ConfigService],
|
|
1381
|
+
exports: [ConfigService],
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
// Automatically imports into AppModule
|
|
1385
|
+
console.log(AppModule.isImportingModule('ConfigModule')); // true
|
|
1386
|
+
console.log(AppModule.hasProvider(ConfigService)); // true
|
|
1387
|
+
|
|
1388
|
+
// Available in all modules
|
|
1389
|
+
const AnyModule = ProviderModule.create({ id: 'AnyModule' });
|
|
1390
|
+
const config = AnyModule.get(ConfigService); // Works!
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
> [!CAUTION]
|
|
1394
|
+
> Use global modules sparingly:
|
|
1395
|
+
>
|
|
1396
|
+
> - They create implicit dependencies that can make code harder to understand
|
|
1397
|
+
> - They reduce encapsulation and explicit dependency graphs
|
|
1398
|
+
> - Best used for true cross-cutting concerns (logging, configuration, telemetry)
|
|
1399
|
+
> - Prefer explicit imports when possible for better maintainability
|
|
1400
|
+
|
|
1401
|
+
## Dependency Injection
|
|
1402
|
+
|
|
1403
|
+
### Constructor Injection
|
|
1404
|
+
|
|
1405
|
+
The primary way to inject dependencies. TypeScript metadata handles it automatically with `@Injectable()`.
|
|
1406
|
+
|
|
1407
|
+
```ts
|
|
1408
|
+
@Injectable()
|
|
1409
|
+
class DatabaseService {
|
|
1410
|
+
query(sql: string) {
|
|
1411
|
+
return [{ data: 'result' }];
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
@Injectable()
|
|
1416
|
+
class LoggerService {
|
|
1417
|
+
log(message: string) {
|
|
1418
|
+
console.log(message);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
@Injectable()
|
|
1423
|
+
class UserRepository {
|
|
1424
|
+
// Dependencies automatically injected via constructor
|
|
1425
|
+
constructor(
|
|
1426
|
+
private db: DatabaseService,
|
|
1427
|
+
private logger: LoggerService
|
|
1428
|
+
) {}
|
|
1429
|
+
|
|
1430
|
+
findAll() {
|
|
1431
|
+
this.logger.log('Finding all users');
|
|
1432
|
+
return this.db.query('SELECT * FROM users');
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const UserModule = ProviderModule.create({
|
|
1437
|
+
id: 'UserModule',
|
|
1438
|
+
providers: [DatabaseService, LoggerService, UserRepository],
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
// UserRepository automatically receives DatabaseService and LoggerService
|
|
1442
|
+
const userRepo = UserModule.get(UserRepository);
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
### @Inject Decorator
|
|
1446
|
+
|
|
1447
|
+
Use `@Inject` for explicit injection when automatic resolution doesn't work (e.g., string tokens, interfaces).
|
|
1448
|
+
|
|
1449
|
+
```ts
|
|
1450
|
+
import { Inject, Injectable } from '@adimm/x-injection';
|
|
1451
|
+
|
|
1452
|
+
@Injectable()
|
|
1453
|
+
class ApiService {
|
|
1454
|
+
constructor(
|
|
1455
|
+
@Inject('API_KEY') private apiKey: string,
|
|
1456
|
+
@Inject('API_URL') private apiUrl: string,
|
|
1457
|
+
@Inject('MAX_RETRIES') private maxRetries: number
|
|
1458
|
+
) {}
|
|
1459
|
+
|
|
1460
|
+
makeRequest() {
|
|
1461
|
+
console.log(`Calling ${this.apiUrl} with key ${this.apiKey}`);
|
|
1462
|
+
console.log(`Max retries: ${this.maxRetries}`);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const ApiModule = ProviderModule.create({
|
|
1467
|
+
id: 'ApiModule',
|
|
1468
|
+
providers: [
|
|
1469
|
+
{ provide: 'API_KEY', useValue: 'secret-123' },
|
|
1470
|
+
{ provide: 'API_URL', useValue: 'https://api.example.com' },
|
|
1471
|
+
{ provide: 'MAX_RETRIES', useValue: 3 },
|
|
1472
|
+
ApiService,
|
|
1473
|
+
],
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
const apiService = ApiModule.get(ApiService);
|
|
1477
|
+
apiService.makeRequest();
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
**Injecting Abstract Classes:**
|
|
1481
|
+
|
|
1482
|
+
```ts
|
|
1483
|
+
@Injectable()
|
|
1484
|
+
abstract class PaymentGateway {
|
|
1485
|
+
abstract charge(amount: number): Promise<void>;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
@Injectable()
|
|
1489
|
+
class StripePaymentGateway extends PaymentGateway {
|
|
1490
|
+
async charge(amount: number) {
|
|
1491
|
+
console.log(`Stripe: Charging $${amount}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
@Injectable()
|
|
1496
|
+
class PaymentService {
|
|
1497
|
+
constructor(@Inject(PaymentGateway) private gateway: PaymentGateway) {}
|
|
1498
|
+
|
|
1499
|
+
async processPayment(amount: number) {
|
|
1500
|
+
await this.gateway.charge(amount);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const PaymentModule = ProviderModule.create({
|
|
1505
|
+
id: 'PaymentModule',
|
|
1506
|
+
providers: [{ provide: PaymentGateway, useClass: StripePaymentGateway }, PaymentService],
|
|
1507
|
+
});
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
### @MultiInject Decorator
|
|
1511
|
+
|
|
1512
|
+
Inject multiple providers bound to the same token as an array.
|
|
1513
|
+
|
|
1514
|
+
```ts
|
|
1515
|
+
import { Injectable, MultiInject } from '@adimm/x-injection';
|
|
1516
|
+
|
|
1517
|
+
@Injectable()
|
|
1518
|
+
class EmailNotifier {
|
|
1519
|
+
notify() {
|
|
1520
|
+
console.log('Email notification sent');
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
@Injectable()
|
|
1525
|
+
class SmsNotifier {
|
|
1526
|
+
notify() {
|
|
1527
|
+
console.log('SMS notification sent');
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
@Injectable()
|
|
1532
|
+
class PushNotifier {
|
|
1533
|
+
notify() {
|
|
1534
|
+
console.log('Push notification sent');
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
abstract class Notifier {
|
|
1539
|
+
abstract notify(): void;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
@Injectable()
|
|
1543
|
+
class NotificationService {
|
|
1544
|
+
constructor(@MultiInject(Notifier) private notifiers: Notifier[]) {}
|
|
1545
|
+
|
|
1546
|
+
notifyAll() {
|
|
1547
|
+
this.notifiers.forEach((notifier) => notifier.notify());
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const NotificationModule = ProviderModule.create({
|
|
1552
|
+
id: 'NotificationModule',
|
|
1553
|
+
providers: [
|
|
1554
|
+
{ provide: Notifier, useClass: EmailNotifier },
|
|
1555
|
+
{ provide: Notifier, useClass: SmsNotifier },
|
|
1556
|
+
{ provide: Notifier, useClass: PushNotifier },
|
|
1557
|
+
NotificationService,
|
|
1558
|
+
],
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
const service = NotificationModule.get(NotificationService);
|
|
1562
|
+
service.notifyAll();
|
|
1563
|
+
// Output:
|
|
1564
|
+
// Email notification sent
|
|
1565
|
+
// SMS notification sent
|
|
1566
|
+
// Push notification sent
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
**Alternative with module.get():**
|
|
1570
|
+
|
|
1571
|
+
```ts
|
|
1572
|
+
const MyModule = ProviderModule.create({
|
|
1573
|
+
id: 'MyModule',
|
|
1574
|
+
providers: [
|
|
1575
|
+
{ provide: 'Handler', useValue: 'Handler1' },
|
|
1576
|
+
{ provide: 'Handler', useValue: 'Handler2' },
|
|
1577
|
+
{ provide: 'Handler', useValue: 'Handler3' },
|
|
1578
|
+
],
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// Get all providers bound to 'Handler'
|
|
1582
|
+
const handlers = MyModule.get('Handler', false, true); // Third param = asList
|
|
1583
|
+
console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
|
|
1584
|
+
```
|
|
1585
|
+
|
|
1586
|
+
### Optional Dependencies
|
|
1587
|
+
|
|
1588
|
+
Use the `isOptional` flag to handle missing dependencies gracefully.
|
|
1589
|
+
|
|
1590
|
+
```ts
|
|
1591
|
+
@Injectable()
|
|
1592
|
+
class ServiceA {
|
|
1593
|
+
value = 'A';
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
@Injectable()
|
|
1597
|
+
class ServiceB {
|
|
1598
|
+
constructor(
|
|
1599
|
+
private serviceA: ServiceA,
|
|
1600
|
+
@Inject('OPTIONAL_CONFIG') private config?: any
|
|
1601
|
+
) {}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const MyModule = ProviderModule.create({
|
|
1605
|
+
id: 'MyModule',
|
|
1606
|
+
providers: [ServiceA, ServiceB],
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// Get with optional flag
|
|
1610
|
+
const optionalService = MyModule.get('NOT_EXISTS', true); // isOptional = true
|
|
1611
|
+
console.log(optionalService); // undefined (no error thrown)
|
|
1612
|
+
|
|
1613
|
+
// Without optional flag (throws error)
|
|
1614
|
+
try {
|
|
1615
|
+
const service = MyModule.get('NOT_EXISTS'); // Throws!
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
console.error('Provider not found');
|
|
1618
|
+
}
|
|
1619
|
+
```
|
|
1620
|
+
|
|
1621
|
+
> [!TIP]
|
|
1622
|
+
> Use `@Inject` when:
|
|
1623
|
+
>
|
|
1624
|
+
> - Injecting string tokens or symbols
|
|
1625
|
+
> - Injecting abstract classes
|
|
1626
|
+
> - TypeScript's automatic injection doesn't work (interfaces, etc.)
|
|
1627
|
+
>
|
|
1628
|
+
> Use `@MultiInject` when:
|
|
1629
|
+
>
|
|
1630
|
+
> - You want to collect all providers bound to a single token
|
|
1631
|
+
> - Implementing plugin systems
|
|
1632
|
+
> - Working with strategy patterns
|
|
1633
|
+
|
|
1634
|
+
## Lifecycle Hooks
|
|
1635
|
+
|
|
1636
|
+
Lifecycle hooks allow you to execute code at specific points in a module's lifecycle.
|
|
1637
|
+
|
|
1638
|
+
### onReady Hook
|
|
1639
|
+
|
|
1640
|
+
Invoked immediately after module creation. Perfect for initialization logic.
|
|
1641
|
+
|
|
1642
|
+
```ts
|
|
1643
|
+
@Injectable()
|
|
1644
|
+
class DatabaseService {
|
|
1645
|
+
connected = false;
|
|
1646
|
+
|
|
1647
|
+
async connect() {
|
|
1648
|
+
console.log('Connecting to database...');
|
|
1649
|
+
this.connected = true;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const DatabaseModule = ProviderModule.create({
|
|
1654
|
+
id: 'DatabaseModule',
|
|
1655
|
+
providers: [DatabaseService],
|
|
1656
|
+
onReady: async (module) => {
|
|
1657
|
+
console.log('DatabaseModule is ready!');
|
|
1658
|
+
|
|
1659
|
+
// Initialize services
|
|
1660
|
+
const db = module.get(DatabaseService);
|
|
1661
|
+
await db.connect();
|
|
1662
|
+
|
|
1663
|
+
console.log('Database connected:', db.connected);
|
|
1664
|
+
},
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
// Output:
|
|
1668
|
+
// DatabaseModule is ready!
|
|
1669
|
+
// Connecting to database...
|
|
1670
|
+
// Database connected: true
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
### onReset Hook
|
|
1674
|
+
|
|
1675
|
+
Invoked when `module.reset()` is called. Provides `before` and `after` callbacks for cleanup and reinitialization.
|
|
1676
|
+
|
|
1677
|
+
```ts
|
|
1678
|
+
@Injectable()
|
|
1679
|
+
class CacheService {
|
|
1680
|
+
cache = new Map();
|
|
1681
|
+
|
|
1682
|
+
clear() {
|
|
1683
|
+
this.cache.clear();
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const CacheModule = ProviderModule.create({
|
|
1688
|
+
id: 'CacheModule',
|
|
1689
|
+
providers: [CacheService],
|
|
1690
|
+
onReset: () => {
|
|
1691
|
+
return {
|
|
1692
|
+
before: async (mod) => {
|
|
1693
|
+
console.log('Before reset - clearing cache');
|
|
1694
|
+
const cache = mod.get(CacheService);
|
|
1695
|
+
cache.clear();
|
|
1696
|
+
},
|
|
1697
|
+
after: async () => {
|
|
1698
|
+
console.log('After reset - cache reinitialized');
|
|
1699
|
+
},
|
|
1700
|
+
};
|
|
1701
|
+
},
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
// Trigger reset
|
|
1705
|
+
await CacheModule.reset();
|
|
1706
|
+
// Output:
|
|
1707
|
+
// Before reset - clearing cache
|
|
1708
|
+
// After reset - cache reinitialized
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
### onDispose Hook
|
|
1712
|
+
|
|
1713
|
+
Invoked when `module.dispose()` is called. Perfect for cleanup tasks like closing connections.
|
|
1714
|
+
|
|
1715
|
+
```ts
|
|
1716
|
+
@Injectable()
|
|
1717
|
+
class DatabaseService {
|
|
1718
|
+
connected = true;
|
|
1719
|
+
|
|
1720
|
+
async disconnect() {
|
|
1721
|
+
console.log('Disconnecting from database...');
|
|
1722
|
+
this.connected = false;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
@Injectable()
|
|
1727
|
+
class FileService {
|
|
1728
|
+
async closeFiles() {
|
|
1729
|
+
console.log('Closing open files...');
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
const AppModule = ProviderModule.create({
|
|
1734
|
+
id: 'AppModule',
|
|
1735
|
+
providers: [DatabaseService, FileService],
|
|
1736
|
+
onDispose: () => {
|
|
1737
|
+
return {
|
|
1738
|
+
before: async (mod) => {
|
|
1739
|
+
console.log('Cleanup started');
|
|
1740
|
+
const db = mod.get(DatabaseService);
|
|
1741
|
+
const files = mod.get(FileService);
|
|
1742
|
+
|
|
1743
|
+
await db.disconnect();
|
|
1744
|
+
await files.closeFiles();
|
|
1745
|
+
},
|
|
1746
|
+
after: async () => {
|
|
1747
|
+
console.log('Cleanup completed');
|
|
1748
|
+
},
|
|
1749
|
+
};
|
|
1750
|
+
},
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
// Dispose module
|
|
1754
|
+
await AppModule.dispose();
|
|
1755
|
+
// Output:
|
|
1756
|
+
// Cleanup started
|
|
1757
|
+
// Disconnecting from database...
|
|
1758
|
+
// Closing open files...
|
|
1759
|
+
// Cleanup completed
|
|
1760
|
+
|
|
1761
|
+
// Module is now disposed
|
|
1762
|
+
console.log(AppModule.isDisposed); // true
|
|
1763
|
+
|
|
1764
|
+
// Subsequent operations throw error
|
|
1765
|
+
try {
|
|
1766
|
+
AppModule.get(DatabaseService);
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
console.error('Cannot access disposed module');
|
|
1769
|
+
}
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
**Complete Lifecycle Example:**
|
|
1773
|
+
|
|
1774
|
+
```ts
|
|
1775
|
+
@Injectable()
|
|
1776
|
+
class ResourceService {
|
|
1777
|
+
initialized = false;
|
|
1778
|
+
|
|
1779
|
+
async initialize() {
|
|
1780
|
+
console.log('Initializing resource...');
|
|
1781
|
+
this.initialized = true;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
async cleanup() {
|
|
1785
|
+
console.log('Cleaning up resource...');
|
|
1786
|
+
this.initialized = false;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const ResourceModule = ProviderModule.create({
|
|
1791
|
+
id: 'ResourceModule',
|
|
1792
|
+
providers: [ResourceService],
|
|
1793
|
+
|
|
1794
|
+
onReady: async (module) => {
|
|
1795
|
+
console.log('[READY] Module created');
|
|
1796
|
+
const service = module.get(ResourceService);
|
|
1797
|
+
await service.initialize();
|
|
1798
|
+
},
|
|
1799
|
+
|
|
1800
|
+
onReset: () => {
|
|
1801
|
+
console.log('[RESET] Resetting module');
|
|
1802
|
+
return {
|
|
1803
|
+
before: async (mod) => {
|
|
1804
|
+
console.log('[RESET:BEFORE] Cleaning up before reset');
|
|
1805
|
+
const service = mod.get(ResourceService);
|
|
1806
|
+
await service.cleanup();
|
|
1807
|
+
},
|
|
1808
|
+
after: async () => {
|
|
1809
|
+
console.log('[RESET:AFTER] Reinitializing after reset');
|
|
1810
|
+
const service = mod.get(ResourceService);
|
|
1811
|
+
await service.initialize();
|
|
1812
|
+
},
|
|
1813
|
+
};
|
|
1814
|
+
},
|
|
1815
|
+
|
|
1816
|
+
onDispose: () => {
|
|
1817
|
+
console.log('[DISPOSE] Disposing module');
|
|
1818
|
+
return {
|
|
1819
|
+
before: async (mod) => {
|
|
1820
|
+
console.log('[DISPOSE:BEFORE] Final cleanup');
|
|
1821
|
+
const service = mod.get(ResourceService);
|
|
1822
|
+
await service.cleanup();
|
|
1823
|
+
},
|
|
1824
|
+
after: async () => {
|
|
1825
|
+
console.log('[DISPOSE:AFTER] Module fully disposed');
|
|
1826
|
+
},
|
|
1827
|
+
};
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
|
|
1831
|
+
// Usage
|
|
1832
|
+
await ResourceModule.reset();
|
|
1833
|
+
await ResourceModule.dispose();
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
> [!IMPORTANT]
|
|
1837
|
+
> Lifecycle hook execution order:
|
|
1838
|
+
>
|
|
1839
|
+
> 1. **onReady** - Immediately after module creation
|
|
1840
|
+
> 2. **onReset** (before) → module reset → **onReset** (after)
|
|
1841
|
+
> 3. **onDispose** (before) → module disposal → **onDispose** (after)
|
|
1842
|
+
|
|
1843
|
+
> [!WARNING]
|
|
1844
|
+
> After calling `dispose()`:
|
|
1845
|
+
>
|
|
1846
|
+
> - All module operations will throw errors
|
|
1847
|
+
> - The module cannot be reused
|
|
1848
|
+
> - Internal resources are cleaned up
|
|
1849
|
+
> - Use for application shutdown or when modules are truly finished
|
|
1850
|
+
|
|
1851
|
+
## Events System
|
|
1852
|
+
|
|
1853
|
+
The events system allows you to observe and react to module changes in real-time.
|
|
1854
|
+
|
|
1855
|
+
### Subscribing to Events
|
|
1856
|
+
|
|
1857
|
+
```ts
|
|
1858
|
+
import { DefinitionEventType } from '@adimm/x-injection';
|
|
1859
|
+
|
|
1860
|
+
const MyModule = ProviderModule.create({
|
|
1861
|
+
id: 'MyModule',
|
|
1862
|
+
providers: [ServiceA],
|
|
1863
|
+
});
|
|
1864
|
+
|
|
1865
|
+
// Subscribe to all events
|
|
1866
|
+
const unsubscribe = MyModule.update.subscribe(({ type, change }) => {
|
|
1867
|
+
console.log(`Event: ${DefinitionEventType[type]}`, change);
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
// Trigger events
|
|
1871
|
+
MyModule.update.addProvider(ServiceB); // Event: Provider
|
|
1872
|
+
MyModule.update.addImport(OtherModule); // Event: Import
|
|
1873
|
+
const service = MyModule.get(ServiceA); // Event: GetProvider
|
|
1874
|
+
|
|
1875
|
+
// Clean up
|
|
1876
|
+
unsubscribe();
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
### Available Event Types
|
|
1880
|
+
|
|
1881
|
+
```ts
|
|
1882
|
+
enum DefinitionEventType {
|
|
1883
|
+
Noop, // No operation
|
|
1884
|
+
Import, // Module/blueprint added
|
|
1885
|
+
Provider, // Provider added
|
|
1886
|
+
GetProvider, // Provider resolved
|
|
1887
|
+
Export, // Export added
|
|
1888
|
+
ExportModule, // Module added to exports
|
|
1889
|
+
ExportProvider, // Provider added to exports
|
|
1890
|
+
ImportRemoved, // Module removed
|
|
1891
|
+
ProviderRemoved, // Provider removed
|
|
1892
|
+
ExportRemoved, // Export removed
|
|
1893
|
+
ExportModuleRemoved, // Module removed from exports
|
|
1894
|
+
ExportProviderRemoved, // Provider removed from exports
|
|
1895
|
+
}
|
|
1896
|
+
```
|
|
1897
|
+
|
|
1898
|
+
### Event Use Cases
|
|
1899
|
+
|
|
1900
|
+
**Monitoring Provider Resolution:**
|
|
1901
|
+
|
|
1902
|
+
```ts
|
|
1903
|
+
const MonitoredModule = ProviderModule.create({
|
|
1904
|
+
id: 'MonitoredModule',
|
|
1905
|
+
providers: [DatabaseService, CacheService],
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1908
|
+
MonitoredModule.update.subscribe(({ type, change }) => {
|
|
1909
|
+
if (type === DefinitionEventType.GetProvider) {
|
|
1910
|
+
console.log('Provider accessed:', change.constructor.name);
|
|
1911
|
+
console.log('Access time:', new Date().toISOString());
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
// Logs access
|
|
1916
|
+
const db = MonitoredModule.get(DatabaseService);
|
|
1917
|
+
// Output: Provider accessed: DatabaseService
|
|
1918
|
+
// Access time: 2024-01-15T10:30:00.000Z
|
|
1919
|
+
```
|
|
1920
|
+
|
|
1921
|
+
**Tracking Module Composition:**
|
|
1922
|
+
|
|
1923
|
+
```ts
|
|
1924
|
+
const RootModule = ProviderModule.create({
|
|
1925
|
+
id: 'RootModule',
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
const compositionLog: string[] = [];
|
|
1929
|
+
|
|
1930
|
+
RootModule.update.subscribe(({ type, change }) => {
|
|
1931
|
+
switch (type) {
|
|
1932
|
+
case DefinitionEventType.Import:
|
|
1933
|
+
compositionLog.push(`Imported: ${change.id}`);
|
|
1934
|
+
break;
|
|
1935
|
+
case DefinitionEventType.Provider:
|
|
1936
|
+
const providerName = typeof change === 'function' ? change.name : change.provide;
|
|
1937
|
+
compositionLog.push(`Added provider: ${providerName}`);
|
|
1938
|
+
break;
|
|
1939
|
+
case DefinitionEventType.Export:
|
|
1940
|
+
compositionLog.push(`Exported: ${JSON.stringify(change)}`);
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
RootModule.update.addImport(DatabaseModule);
|
|
1946
|
+
RootModule.update.addProvider(ServiceA);
|
|
1947
|
+
RootModule.update.addProvider(ServiceB, true);
|
|
1948
|
+
|
|
1949
|
+
console.log(compositionLog);
|
|
1950
|
+
// [
|
|
1951
|
+
// 'Imported: DatabaseModule',
|
|
1952
|
+
// 'Added provider: ServiceA',
|
|
1953
|
+
// 'Added provider: ServiceB',
|
|
1954
|
+
// 'Exported: ServiceB'
|
|
1955
|
+
// ]
|
|
1956
|
+
```
|
|
1957
|
+
|
|
1958
|
+
**Debugging Dynamic Changes:**
|
|
1959
|
+
|
|
1960
|
+
```ts
|
|
1961
|
+
const DebugModule = ProviderModule.create({
|
|
1962
|
+
id: 'DebugModule',
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
DebugModule.update.subscribe(({ type, change }) => {
|
|
1966
|
+
const eventName = DefinitionEventType[type];
|
|
1967
|
+
|
|
1968
|
+
if (type === DefinitionEventType.ImportRemoved) {
|
|
1969
|
+
console.warn(`⚠️ Module removed: ${change.id}`);
|
|
1970
|
+
} else if (type === DefinitionEventType.ProviderRemoved) {
|
|
1971
|
+
console.warn(`⚠️ Provider removed:`, change);
|
|
1972
|
+
} else {
|
|
1973
|
+
console.log(`✅ ${eventName}:`, change);
|
|
1974
|
+
}
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
DebugModule.update.addProvider(ServiceA);
|
|
1978
|
+
DebugModule.update.removeProvider(ServiceA);
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
**Building a Module Activity Logger:**
|
|
1982
|
+
|
|
1983
|
+
```ts
|
|
1984
|
+
class ModuleActivityLogger {
|
|
1985
|
+
private events: Array<{ timestamp: number; type: string; change: any }> = [];
|
|
1986
|
+
|
|
1987
|
+
constructor(module: ProviderModule) {
|
|
1988
|
+
module.update.subscribe(({ type, change }) => {
|
|
1989
|
+
this.events.push({
|
|
1990
|
+
timestamp: Date.now(),
|
|
1991
|
+
type: DefinitionEventType[type],
|
|
1992
|
+
change,
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
getReport() {
|
|
1998
|
+
return {
|
|
1999
|
+
totalEvents: this.events.length,
|
|
2000
|
+
events: this.events,
|
|
2001
|
+
summary: this.events.reduce(
|
|
2002
|
+
(acc, event) => {
|
|
2003
|
+
acc[event.type] = (acc[event.type] || 0) + 1;
|
|
2004
|
+
return acc;
|
|
2005
|
+
},
|
|
2006
|
+
{} as Record<string, number>
|
|
2007
|
+
),
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
const TrackedModule = ProviderModule.create({ id: 'TrackedModule' });
|
|
2013
|
+
const logger = new ModuleActivityLogger(TrackedModule);
|
|
2014
|
+
|
|
2015
|
+
TrackedModule.update.addProvider(ServiceA);
|
|
2016
|
+
TrackedModule.update.addProvider(ServiceB);
|
|
2017
|
+
TrackedModule.get(ServiceA);
|
|
2018
|
+
TrackedModule.get(ServiceB);
|
|
2019
|
+
|
|
2020
|
+
console.log(logger.getReport());
|
|
2021
|
+
// {
|
|
2022
|
+
// totalEvents: 4,
|
|
2023
|
+
// events: [...],
|
|
2024
|
+
// summary: { Provider: 2, GetProvider: 2 }
|
|
2025
|
+
// }
|
|
2026
|
+
```
|
|
2027
|
+
|
|
2028
|
+
> [!WARNING]
|
|
2029
|
+
>
|
|
2030
|
+
> - Always call `unsubscribe()` to prevent memory leaks
|
|
2031
|
+
> - Events fire **after** middlewares have executed
|
|
2032
|
+
> - Event handlers are synchronous - avoid heavy operations
|
|
2033
|
+
> - High-frequency events (like `GetProvider`) can impact performance
|
|
2034
|
+
|
|
2035
|
+
## Middlewares
|
|
2036
|
+
|
|
2037
|
+
Middlewares intercept and transform module operations before they complete. They provide powerful customization capabilities.
|
|
2038
|
+
|
|
2039
|
+
### BeforeGet Middleware
|
|
2040
|
+
|
|
2041
|
+
Transform provider values before they're returned to consumers.
|
|
2042
|
+
|
|
2043
|
+
```ts
|
|
2044
|
+
import { MiddlewareType } from '@adimm/x-injection';
|
|
2045
|
+
|
|
2046
|
+
@Injectable()
|
|
2047
|
+
class UserService {
|
|
2048
|
+
getUser() {
|
|
2049
|
+
return { id: 1, name: 'Alice' };
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
const MyModule = ProviderModule.create({
|
|
2054
|
+
id: 'MyModule',
|
|
2055
|
+
providers: [UserService],
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
// Wrap resolved providers with metadata
|
|
2059
|
+
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
|
|
2060
|
+
// Return true to pass through unchanged
|
|
2061
|
+
if (!(provider instanceof UserService)) return true;
|
|
2062
|
+
|
|
2063
|
+
// Transform the value
|
|
2064
|
+
return {
|
|
2065
|
+
timestamp: Date.now(),
|
|
2066
|
+
instance: provider,
|
|
2067
|
+
metadata: { cached: false },
|
|
2068
|
+
};
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
const result = MyModule.get(UserService);
|
|
2072
|
+
console.log(result);
|
|
2073
|
+
// {
|
|
2074
|
+
// timestamp: 1705320000000,
|
|
2075
|
+
// instance: UserService { ... },
|
|
2076
|
+
// metadata: { cached: false }
|
|
2077
|
+
// }
|
|
2078
|
+
```
|
|
2079
|
+
|
|
2080
|
+
**Conditional Transformation:**
|
|
2081
|
+
|
|
2082
|
+
```ts
|
|
2083
|
+
@Injectable()
|
|
2084
|
+
class ServiceA {}
|
|
2085
|
+
|
|
2086
|
+
@Injectable()
|
|
2087
|
+
class ServiceB {}
|
|
2088
|
+
|
|
2089
|
+
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token) => {
|
|
2090
|
+
// Only transform ServiceA
|
|
2091
|
+
if (provider instanceof ServiceA) {
|
|
2092
|
+
return { wrapped: provider, type: 'A' };
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// Pass through everything else unchanged
|
|
2096
|
+
return true;
|
|
2097
|
+
});
|
|
2098
|
+
|
|
2099
|
+
const serviceA = MyModule.get(ServiceA); // { wrapped: ServiceA, type: 'A' }
|
|
2100
|
+
const serviceB = MyModule.get(ServiceB); // ServiceB (unchanged)
|
|
2101
|
+
```
|
|
2102
|
+
|
|
2103
|
+
**Using inject() to avoid infinite loops:**
|
|
2104
|
+
|
|
2105
|
+
```ts
|
|
2106
|
+
@Injectable()
|
|
2107
|
+
class LoggerService {
|
|
2108
|
+
log(message: string) {
|
|
2109
|
+
console.log(message);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
@Injectable()
|
|
2114
|
+
class PaymentService {}
|
|
2115
|
+
|
|
2116
|
+
MyModule.middlewares.add(MiddlewareType.BeforeGet, (provider, token, inject) => {
|
|
2117
|
+
if (!(provider instanceof PaymentService)) return true;
|
|
2118
|
+
|
|
2119
|
+
// Use inject() instead of module.get() to avoid infinite loop
|
|
2120
|
+
const logger = inject(LoggerService);
|
|
2121
|
+
logger.log('Payment service accessed');
|
|
2122
|
+
|
|
2123
|
+
return provider; // Or transform it
|
|
2124
|
+
});
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
### BeforeAddProvider Middleware
|
|
2128
|
+
|
|
2129
|
+
Block specific providers:
|
|
2130
|
+
|
|
2131
|
+
```ts
|
|
2132
|
+
MyModule.middlewares.add(MiddlewareType.BeforeAddProvider, (provider) => {
|
|
2133
|
+
// Block ServiceB from being added
|
|
2134
|
+
if ((provider as any).name === 'ServiceB') {
|
|
2135
|
+
return false; // Abort
|
|
2136
|
+
}
|
|
2137
|
+
return true; // Allow
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
MyModule.update.addProvider(ServiceA);
|
|
2141
|
+
MyModule.update.addProvider(ServiceB); // Silently rejected
|
|
2142
|
+
MyModule.update.addProvider(ServiceC);
|
|
2143
|
+
|
|
2144
|
+
console.log(MyModule.hasProvider(ServiceA)); // true
|
|
2145
|
+
console.log(MyModule.hasProvider(ServiceB)); // false
|
|
2146
|
+
console.log(MyModule.hasProvider(ServiceC)); // true
|
|
2147
|
+
```
|
|
2148
|
+
|
|
2149
|
+
### BeforeAddImport Middleware
|
|
2150
|
+
|
|
2151
|
+
Intercept modules before they're imported.
|
|
2152
|
+
|
|
2153
|
+
```ts
|
|
2154
|
+
const Module1 = ProviderModule.create({ id: 'Module1' });
|
|
2155
|
+
const Module2 = ProviderModule.create({ id: 'Module2' });
|
|
2156
|
+
const RestrictedModule = ProviderModule.create({ id: 'RestrictedModule' });
|
|
2157
|
+
|
|
2158
|
+
const MainModule = ProviderModule.create({ id: 'MainModule' });
|
|
2159
|
+
|
|
2160
|
+
// Block specific modules
|
|
2161
|
+
MainModule.middlewares.add(MiddlewareType.BeforeAddImport, (module) => {
|
|
2162
|
+
if (module.id === 'RestrictedModule') {
|
|
2163
|
+
console.warn(`❌ Cannot import ${module.id}`);
|
|
2164
|
+
return false; // Block
|
|
2165
|
+
}
|
|
2166
|
+
return true; // Allow
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
MainModule.update.addImport(Module1); // ✅ Allowed
|
|
2170
|
+
MainModule.update.addImport(Module2); // ✅ Allowed
|
|
2171
|
+
MainModule.update.addImport(RestrictedModule); // ❌ Blocked
|
|
2172
|
+
|
|
2173
|
+
console.log(MainModule.isImportingModule('Module1')); // true
|
|
2174
|
+
console.log(MainModule.isImportingModule('RestrictedModule')); // false
|
|
2175
|
+
```
|
|
2176
|
+
|
|
2177
|
+
**Auto-add providers to imported modules:**
|
|
2178
|
+
|
|
2179
|
+
```ts
|
|
2180
|
+
MyModule.middlewares.add(MiddlewareType.BeforeAddImport, (importedModule) => {
|
|
2181
|
+
// Add logger to every imported module
|
|
2182
|
+
importedModule.update.addProvider(LoggerService, true);
|
|
2183
|
+
return importedModule; // Return modified module
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
MyModule.update.addImport(FeatureModule);
|
|
2187
|
+
// FeatureModule now has LoggerService
|
|
2188
|
+
```
|
|
2189
|
+
|
|
2190
|
+
### OnExportAccess Middleware
|
|
2191
|
+
|
|
2192
|
+
Control which importing modules can access exports.
|
|
2193
|
+
|
|
2194
|
+
```ts
|
|
2195
|
+
@Injectable()
|
|
2196
|
+
class SensitiveService {}
|
|
2197
|
+
|
|
2198
|
+
@Injectable()
|
|
2199
|
+
class PublicService {}
|
|
2200
|
+
|
|
2201
|
+
const SecureModule = ProviderModule.create({
|
|
2202
|
+
id: 'SecureModule',
|
|
2203
|
+
providers: [SensitiveService, PublicService],
|
|
2204
|
+
exports: [SensitiveService, PublicService],
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
// Restrict access based on importer
|
|
2208
|
+
SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importerModule, exportToken) => {
|
|
2209
|
+
// Block untrusted modules from accessing SensitiveService
|
|
2210
|
+
if (importerModule.id === 'UntrustedModule' && exportToken === SensitiveService) {
|
|
2211
|
+
console.warn(`❌ ${importerModule.id} denied access to SensitiveService`);
|
|
2212
|
+
return false; // Deny
|
|
2213
|
+
}
|
|
2214
|
+
return true; // Allow
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
const TrustedModule = ProviderModule.create({
|
|
2218
|
+
id: 'TrustedModule',
|
|
2219
|
+
imports: [SecureModule],
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
const UntrustedModule = ProviderModule.create({
|
|
2223
|
+
id: 'UntrustedModule',
|
|
2224
|
+
imports: [SecureModule],
|
|
2225
|
+
});
|
|
2226
|
+
|
|
2227
|
+
// Trusted module can access both
|
|
2228
|
+
console.log(TrustedModule.hasProvider(SensitiveService)); // true
|
|
2229
|
+
console.log(TrustedModule.hasProvider(PublicService)); // true
|
|
2230
|
+
|
|
2231
|
+
// Untrusted module blocked from SensitiveService
|
|
2232
|
+
console.log(UntrustedModule.hasProvider(SensitiveService)); // false
|
|
2233
|
+
console.log(UntrustedModule.hasProvider(PublicService)); // true
|
|
2234
|
+
```
|
|
2235
|
+
|
|
2236
|
+
**Complete access control:**
|
|
2237
|
+
|
|
2238
|
+
```ts
|
|
2239
|
+
SecureModule.middlewares.add(MiddlewareType.OnExportAccess, (importer, exportToken) => {
|
|
2240
|
+
const allowlist = ['TrustedModule1', 'TrustedModule2'];
|
|
2241
|
+
|
|
2242
|
+
if (!allowlist.includes(String(importer.id))) {
|
|
2243
|
+
console.warn(`Access denied for ${importer.id}`);
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
return true;
|
|
2248
|
+
});
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
### BeforeRemoveImport Middleware
|
|
2252
|
+
|
|
2253
|
+
Prevent specific modules from being removed.
|
|
2254
|
+
|
|
2255
|
+
```ts
|
|
2256
|
+
const PermanentModule = ProviderModule.create({ id: 'PermanentModule' });
|
|
2257
|
+
const TemporaryModule = ProviderModule.create({ id: 'TemporaryModule' });
|
|
2258
|
+
|
|
2259
|
+
const MainModule = ProviderModule.create({ id: 'MainModule' });
|
|
2260
|
+
|
|
2261
|
+
// Protect PermanentModule
|
|
2262
|
+
MainModule.middlewares.add(MiddlewareType.BeforeRemoveImport, (module) => {
|
|
2263
|
+
if (module.id === 'PermanentModule') {
|
|
2264
|
+
console.warn(`⚠️ Cannot remove ${module.id}`);
|
|
2265
|
+
return false; // Block removal
|
|
2266
|
+
}
|
|
2267
|
+
return true; // Allow removal
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
MainModule.update.addImport(PermanentModule);
|
|
2271
|
+
MainModule.update.addImport(TemporaryModule);
|
|
2272
|
+
|
|
2273
|
+
// Try to remove
|
|
2274
|
+
MainModule.update.removeImport(PermanentModule); // ❌ Blocked
|
|
2275
|
+
MainModule.update.removeImport(TemporaryModule); // ✅ Removed
|
|
2276
|
+
|
|
2277
|
+
console.log(MainModule.isImportingModule('PermanentModule')); // true
|
|
2278
|
+
console.log(MainModule.isImportingModule('TemporaryModule')); // false
|
|
2279
|
+
```
|
|
2280
|
+
|
|
2281
|
+
### BeforeRemoveProvider Middleware
|
|
2282
|
+
|
|
2283
|
+
Prevent specific providers from being removed.
|
|
2284
|
+
|
|
2285
|
+
```ts
|
|
2286
|
+
MyModule.middlewares.add(MiddlewareType.BeforeRemoveProvider, (provider) => {
|
|
2287
|
+
// Block removal of critical services
|
|
2288
|
+
if (provider === DatabaseService) {
|
|
2289
|
+
console.warn('⚠️ Cannot remove DatabaseService');
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
return true;
|
|
2293
|
+
});
|
|
2294
|
+
|
|
2295
|
+
MyModule.update.addProvider(DatabaseService);
|
|
2296
|
+
MyModule.update.addProvider(CacheService);
|
|
2297
|
+
|
|
2298
|
+
MyModule.update.removeProvider(DatabaseService); // ❌ Blocked
|
|
2299
|
+
MyModule.update.removeProvider(CacheService); // ✅ Removed
|
|
2300
|
+
|
|
2301
|
+
console.log(MyModule.hasProvider(DatabaseService)); // true
|
|
2302
|
+
console.log(MyModule.hasProvider(CacheService)); // false
|
|
2303
|
+
```
|
|
2304
|
+
|
|
2305
|
+
### BeforeRemoveExport Middleware
|
|
2306
|
+
|
|
2307
|
+
Prevent specific exports from being removed.
|
|
2308
|
+
|
|
2309
|
+
```ts
|
|
2310
|
+
import { ProviderModuleHelpers } from '@adimm/x-injection';
|
|
2311
|
+
|
|
2312
|
+
const MyModule = ProviderModule.create({
|
|
2313
|
+
id: 'MyModule',
|
|
2314
|
+
providers: [ServiceA, ServiceB],
|
|
2315
|
+
exports: [ServiceA, ServiceB],
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
MyModule.middlewares.add(MiddlewareType.BeforeRemoveExport, (exportDef) => {
|
|
2319
|
+
// Check if it's a module or provider
|
|
2320
|
+
if (ProviderModuleHelpers.isModule(exportDef)) {
|
|
2321
|
+
// Block module removal
|
|
2322
|
+
return exportDef.id !== 'ProtectedModule';
|
|
2323
|
+
} else {
|
|
2324
|
+
// Block ServiceA removal
|
|
2325
|
+
return exportDef !== ServiceA;
|
|
2326
|
+
}
|
|
2327
|
+
});
|
|
2328
|
+
|
|
2329
|
+
MyModule.update.removeFromExports(ServiceA); // ❌ Blocked
|
|
2330
|
+
MyModule.update.removeFromExports(ServiceB); // ✅ Removed
|
|
2331
|
+
|
|
2332
|
+
console.log(MyModule.isExportingProvider(ServiceA)); // true
|
|
2333
|
+
console.log(MyModule.isExportingProvider(ServiceB)); // false
|
|
2334
|
+
```
|
|
2335
|
+
|
|
2336
|
+
### All Available Middleware Types
|
|
2337
|
+
|
|
2338
|
+
```ts
|
|
2339
|
+
enum MiddlewareType {
|
|
2340
|
+
BeforeAddImport, // Before importing a module
|
|
2341
|
+
BeforeAddProvider, // Before adding a provider
|
|
2342
|
+
BeforeGet, // Before returning provider to consumer
|
|
2343
|
+
BeforeRemoveImport, // Before removing an import
|
|
2344
|
+
BeforeRemoveProvider, // Before removing a provider
|
|
2345
|
+
BeforeRemoveExport, // Before removing an export
|
|
2346
|
+
OnExportAccess, // When importer accesses exports
|
|
2347
|
+
}
|
|
2348
|
+
```
|
|
2349
|
+
|
|
2350
|
+
**Middleware Return Values:**
|
|
2351
|
+
|
|
2352
|
+
- `false` - Abort the operation (block it)
|
|
2353
|
+
- `true` - Pass through unchanged
|
|
2354
|
+
- Modified value - Transform and continue
|
|
2355
|
+
- For `BeforeGet`: Can return any value (transformation)
|
|
2356
|
+
|
|
2357
|
+
> [!CAUTION]
|
|
2358
|
+
> Middleware best practices:
|
|
2359
|
+
>
|
|
2360
|
+
> - Returning `false` aborts the chain (no value returned)
|
|
2361
|
+
> - Middlewares execute in registration order
|
|
2362
|
+
> - Always handle errors in middleware chains
|
|
2363
|
+
> - Use `inject()` parameter in BeforeGet to avoid infinite loops
|
|
2364
|
+
> - Be careful with performance - middlewares run on every operation
|
|
2365
|
+
> - Events fire **after** middlewares complete
|
|
2366
|
+
|
|
2367
|
+
## Testing
|
|
2368
|
+
|
|
2369
|
+
xInjection makes testing easy through blueprint cloning and provider substitution.
|
|
2370
|
+
|
|
2371
|
+
### Blueprint Cloning
|
|
2372
|
+
|
|
2373
|
+
Clone blueprints to create test-specific configurations without affecting production code.
|
|
2374
|
+
|
|
2375
|
+
```ts
|
|
2376
|
+
// Production blueprint
|
|
2377
|
+
const DatabaseModuleBp = ProviderModule.blueprint({
|
|
2378
|
+
id: 'DatabaseModule',
|
|
2379
|
+
providers: [DatabaseService, ConnectionPool],
|
|
2380
|
+
exports: [DatabaseService],
|
|
2381
|
+
});
|
|
2382
|
+
|
|
2383
|
+
// Test blueprint - clone and modify
|
|
2384
|
+
const DatabaseModuleMock = DatabaseModuleBp.clone().updateDefinition({
|
|
2385
|
+
id: 'DatabaseModuleMock',
|
|
2386
|
+
providers: [
|
|
2387
|
+
{ provide: DatabaseService, useClass: MockDatabaseService },
|
|
2388
|
+
{ provide: ConnectionPool, useClass: MockConnectionPool },
|
|
2389
|
+
],
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
// Use in tests
|
|
2393
|
+
const TestModule = ProviderModule.create({
|
|
2394
|
+
id: 'TestModule',
|
|
2395
|
+
imports: [DatabaseModuleMock],
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
const db = TestModule.get(DatabaseService); // MockDatabaseService
|
|
2399
|
+
```
|
|
2400
|
+
|
|
2401
|
+
**Deep Blueprint Cloning:**
|
|
2402
|
+
|
|
2403
|
+
```ts
|
|
2404
|
+
const OriginalBp = ProviderModule.blueprint({
|
|
2405
|
+
id: 'Original',
|
|
2406
|
+
providers: [ServiceA, ServiceB, ServiceC],
|
|
2407
|
+
exports: [ServiceA, ServiceB],
|
|
2408
|
+
onReady: (module) => console.log('Original ready'),
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
// Clone and completely override
|
|
2412
|
+
const ClonedBp = OriginalBp.clone().updateDefinition({
|
|
2413
|
+
id: 'Cloned',
|
|
2414
|
+
providers: [MockServiceA, MockServiceB], // Different providers
|
|
2415
|
+
exports: [MockServiceA], // Different exports
|
|
2416
|
+
onReady: undefined, // Remove lifecycle hooks
|
|
2417
|
+
});
|
|
2418
|
+
|
|
2419
|
+
// Original blueprint unchanged
|
|
2420
|
+
console.log(OriginalBp.providers?.length); // 3
|
|
2421
|
+
console.log(ClonedBp.providers?.length); // 2
|
|
2422
|
+
```
|
|
2423
|
+
|
|
2424
|
+
### Provider Substitution
|
|
2425
|
+
|
|
2426
|
+
Replace real services with mocks for testing.
|
|
2427
|
+
|
|
2428
|
+
```ts
|
|
2429
|
+
// Production services
|
|
2430
|
+
@Injectable()
|
|
2431
|
+
class ApiService {
|
|
2432
|
+
async fetchData() {
|
|
2433
|
+
return fetch('https://api.example.com/data').then((r) => r.json());
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
@Injectable()
|
|
2438
|
+
class UserService {
|
|
2439
|
+
constructor(private api: ApiService) {}
|
|
2440
|
+
|
|
2441
|
+
async getUsers() {
|
|
2442
|
+
return this.api.fetchData();
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
// Mock service
|
|
2447
|
+
class MockApiService {
|
|
2448
|
+
async fetchData() {
|
|
2449
|
+
return { users: [{ id: 1, name: 'Mock User' }] };
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// Production module
|
|
2454
|
+
const ProductionModule = ProviderModule.create({
|
|
2455
|
+
id: 'ProductionModule',
|
|
2456
|
+
providers: [ApiService, UserService],
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
// Test module with substitution
|
|
2460
|
+
const TestModule = ProviderModule.create({
|
|
2461
|
+
id: 'TestModule',
|
|
2462
|
+
providers: [
|
|
2463
|
+
{ provide: ApiService, useClass: MockApiService },
|
|
2464
|
+
UserService, // Uses MockApiService automatically
|
|
2465
|
+
],
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
const userService = TestModule.get(UserService);
|
|
2469
|
+
const users = await userService.getUsers();
|
|
2470
|
+
console.log(users); // Mock data
|
|
2471
|
+
```
|
|
2472
|
+
|
|
2473
|
+
### Mocking Services
|
|
2474
|
+
|
|
2475
|
+
**Using useValue for simple mocks:**
|
|
2476
|
+
|
|
2477
|
+
```ts
|
|
2478
|
+
const mockPaymentGateway = {
|
|
2479
|
+
charge: jest.fn().mockResolvedValue({ success: true }),
|
|
2480
|
+
refund: jest.fn().mockResolvedValue({ success: true }),
|
|
2481
|
+
};
|
|
2482
|
+
|
|
2483
|
+
const TestModule = ProviderModule.create({
|
|
2484
|
+
id: 'TestModule',
|
|
2485
|
+
providers: [{ provide: PaymentGateway, useValue: mockPaymentGateway }, PaymentService],
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2488
|
+
const paymentService = TestModule.get(PaymentService);
|
|
2489
|
+
await paymentService.processPayment(100);
|
|
2490
|
+
|
|
2491
|
+
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(100);
|
|
2492
|
+
```
|
|
2493
|
+
|
|
2494
|
+
**Using useFactory for complex mocks:**
|
|
2495
|
+
|
|
2496
|
+
```ts
|
|
2497
|
+
const TestModule = ProviderModule.create({
|
|
2498
|
+
id: 'TestModule',
|
|
2499
|
+
providers: [
|
|
2500
|
+
{
|
|
2501
|
+
provide: 'DATABASE_CONNECTION',
|
|
2502
|
+
useFactory: () => {
|
|
2503
|
+
return {
|
|
2504
|
+
query: jest.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
|
|
2505
|
+
connect: jest.fn().mockResolvedValue(true),
|
|
2506
|
+
disconnect: jest.fn().mockResolvedValue(true),
|
|
2507
|
+
};
|
|
2508
|
+
},
|
|
2509
|
+
},
|
|
2510
|
+
],
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
const db = TestModule.get('DATABASE_CONNECTION');
|
|
2514
|
+
const results = await db.query('SELECT * FROM users');
|
|
2515
|
+
expect(results).toEqual([{ id: 1, name: 'Test' }]);
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
**Complete Testing Example:**
|
|
2519
|
+
|
|
2520
|
+
```ts
|
|
2521
|
+
// Production code
|
|
2522
|
+
@Injectable()
|
|
2523
|
+
class EmailService {
|
|
2524
|
+
async sendEmail(to: string, subject: string, body: string) {
|
|
2525
|
+
// Real email sending logic
|
|
2526
|
+
console.log(`Sending email to ${to}`);
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
@Injectable()
|
|
2531
|
+
class UserNotificationService {
|
|
2532
|
+
constructor(private emailService: EmailService) {}
|
|
2533
|
+
|
|
2534
|
+
async notifyUser(userId: string, message: string) {
|
|
2535
|
+
await this.emailService.sendEmail(`user${userId}@example.com`, 'Notification', message);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Test code
|
|
2540
|
+
describe('UserNotificationService', () => {
|
|
2541
|
+
it('should send email notification', async () => {
|
|
2542
|
+
const mockEmailService = {
|
|
2543
|
+
sendEmail: jest.fn().mockResolvedValue(undefined),
|
|
2544
|
+
};
|
|
2545
|
+
|
|
2546
|
+
const TestModule = ProviderModule.create({
|
|
2547
|
+
id: 'TestModule',
|
|
2548
|
+
providers: [{ provide: EmailService, useValue: mockEmailService }, UserNotificationService],
|
|
2549
|
+
});
|
|
2550
|
+
|
|
2551
|
+
const notificationService = TestModule.get(UserNotificationService);
|
|
2552
|
+
await notificationService.notifyUser('123', 'Test message');
|
|
2553
|
+
|
|
2554
|
+
expect(mockEmailService.sendEmail).toHaveBeenCalledWith('user123@example.com', 'Notification', 'Test message');
|
|
2555
|
+
});
|
|
2556
|
+
});
|
|
2557
|
+
```
|
|
2558
|
+
|
|
2559
|
+
**Testing with Multiple Module Layers:**
|
|
2560
|
+
|
|
2561
|
+
```ts
|
|
2562
|
+
// Create mock blueprint
|
|
2563
|
+
const MockDataModuleBp = ProviderModule.blueprint({
|
|
2564
|
+
id: 'MockDataModule',
|
|
2565
|
+
providers: [
|
|
2566
|
+
{ provide: DatabaseService, useClass: MockDatabaseService },
|
|
2567
|
+
{ provide: CacheService, useClass: MockCacheService },
|
|
2568
|
+
],
|
|
2569
|
+
exports: [DatabaseService, CacheService],
|
|
2570
|
+
});
|
|
2571
|
+
|
|
2572
|
+
// Use mock in feature module tests
|
|
2573
|
+
const FeatureModuleTest = ProviderModule.create({
|
|
2574
|
+
id: 'FeatureModuleTest',
|
|
2575
|
+
imports: [MockDataModuleBp],
|
|
2576
|
+
providers: [FeatureService],
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
const featureService = FeatureModuleTest.get(FeatureService);
|
|
2580
|
+
// FeatureService receives mock dependencies
|
|
2581
|
+
```
|
|
2582
|
+
|
|
2583
|
+
> [!TIP]
|
|
2584
|
+
> Testing strategies:
|
|
2585
|
+
>
|
|
2586
|
+
> - Use `blueprint.clone()` to create test variations without modifying originals
|
|
2587
|
+
> - Use `useValue` for simple mocks with jest.fn()
|
|
2588
|
+
> - Use `useClass` for class-based mocks with behavior
|
|
2589
|
+
> - Use `useFactory` for complex mock setup
|
|
2590
|
+
> - Test module isolation by mocking all external dependencies
|
|
2591
|
+
> - Verify mock calls with jest expectations
|
|
2592
|
+
|
|
2593
|
+
## Advanced Module API
|
|
2594
|
+
|
|
2595
|
+
### Query Methods
|
|
2596
|
+
|
|
2597
|
+
Check module state and relationships.
|
|
2598
|
+
|
|
2599
|
+
```ts
|
|
2600
|
+
const MyModule = ProviderModule.create({
|
|
2601
|
+
id: 'MyModule',
|
|
2602
|
+
imports: [DatabaseModule, ConfigModule],
|
|
2603
|
+
providers: [ServiceA, ServiceB],
|
|
2604
|
+
exports: [ServiceA, DatabaseModule],
|
|
2605
|
+
});
|
|
2606
|
+
|
|
2607
|
+
// Provider queries
|
|
2608
|
+
MyModule.hasProvider(ServiceA); // true
|
|
2609
|
+
MyModule.hasProvider(ServiceC); // false
|
|
2610
|
+
MyModule.hasProvider(DatabaseService); // true (from import)
|
|
2611
|
+
|
|
2612
|
+
// Import queries
|
|
2613
|
+
MyModule.isImportingModule('DatabaseModule'); // true
|
|
2614
|
+
MyModule.isImportingModule(ConfigModule); // true (by reference)
|
|
2615
|
+
MyModule.isImportingModule('NonExistent'); // false
|
|
2616
|
+
|
|
2617
|
+
// Export queries
|
|
2618
|
+
MyModule.isExportingProvider(ServiceA); // true
|
|
2619
|
+
MyModule.isExportingProvider(ServiceB); // false
|
|
2620
|
+
MyModule.isExportingModule('DatabaseModule'); // true
|
|
2621
|
+
MyModule.isExportingModule(ConfigModule); // false
|
|
2622
|
+
|
|
2623
|
+
// State queries
|
|
2624
|
+
MyModule.isDisposed; // false
|
|
2625
|
+
MyModule.id; // 'MyModule'
|
|
2626
|
+
```
|
|
2627
|
+
|
|
2628
|
+
**Using Symbol Identifiers:**
|
|
2629
|
+
|
|
2630
|
+
```ts
|
|
2631
|
+
const MODULE_ID = Symbol('FeatureModule');
|
|
2632
|
+
|
|
2633
|
+
const FeatureModule = ProviderModule.create({
|
|
2634
|
+
id: MODULE_ID,
|
|
2635
|
+
providers: [FeatureService],
|
|
2636
|
+
exports: [FeatureService],
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
const AppModule = ProviderModule.create({
|
|
2640
|
+
id: 'AppModule',
|
|
2641
|
+
imports: [FeatureModule],
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
// Query using Symbol
|
|
2645
|
+
console.log(AppModule.isImportingModule(MODULE_ID)); // true
|
|
2646
|
+
```
|
|
2647
|
+
|
|
2648
|
+
### Multiple Provider Binding
|
|
2649
|
+
|
|
2650
|
+
Bind multiple providers to the same token and retrieve them as a list.
|
|
2651
|
+
|
|
2652
|
+
```ts
|
|
2653
|
+
@Injectable()
|
|
2654
|
+
abstract class Plugin {
|
|
2655
|
+
abstract execute(): void;
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
@Injectable()
|
|
2659
|
+
class PluginA extends Plugin {
|
|
2660
|
+
execute() {
|
|
2661
|
+
console.log('Plugin A executing');
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
@Injectable()
|
|
2666
|
+
class PluginB extends Plugin {
|
|
2667
|
+
execute() {
|
|
2668
|
+
console.log('Plugin B executing');
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
@Injectable()
|
|
2673
|
+
class PluginC extends Plugin {
|
|
2674
|
+
execute() {
|
|
2675
|
+
console.log('Plugin C executing');
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
const PluginModule = ProviderModule.create({
|
|
2680
|
+
id: 'PluginModule',
|
|
2681
|
+
providers: [
|
|
2682
|
+
{ provide: Plugin, useClass: PluginA },
|
|
2683
|
+
{ provide: Plugin, useClass: PluginB },
|
|
2684
|
+
{ provide: Plugin, useClass: PluginC },
|
|
2685
|
+
],
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2688
|
+
// Get all plugins as array (third parameter = asList)
|
|
2689
|
+
const plugins = PluginModule.get(Plugin, false, true);
|
|
2690
|
+
console.log(plugins.length); // 3
|
|
2691
|
+
|
|
2692
|
+
// Execute all plugins
|
|
2693
|
+
plugins.forEach((plugin) => plugin.execute());
|
|
2694
|
+
// Output:
|
|
2695
|
+
// Plugin A executing
|
|
2696
|
+
// Plugin B executing
|
|
2697
|
+
// Plugin C executing
|
|
2698
|
+
```
|
|
2699
|
+
|
|
2700
|
+
**String Token Example:**
|
|
2701
|
+
|
|
2702
|
+
```ts
|
|
2703
|
+
const MyModule = ProviderModule.create({
|
|
2704
|
+
id: 'MyModule',
|
|
2705
|
+
providers: [
|
|
2706
|
+
{ provide: 'Handler', useValue: 'Handler1' },
|
|
2707
|
+
{ provide: 'Handler', useValue: 'Handler2' },
|
|
2708
|
+
{ provide: 'Handler', useValue: 'Handler3' },
|
|
2709
|
+
],
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
const handlers = MyModule.get('Handler', false, true);
|
|
2713
|
+
console.log(handlers); // ['Handler1', 'Handler2', 'Handler3']
|
|
2714
|
+
```
|
|
2715
|
+
|
|
2716
|
+
### Batch Resolution with getMany()
|
|
2717
|
+
|
|
2718
|
+
Resolve multiple providers in a single call.
|
|
2719
|
+
|
|
2720
|
+
```ts
|
|
2721
|
+
@Injectable()
|
|
2722
|
+
class ServiceA {
|
|
2723
|
+
name = 'A';
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
@Injectable()
|
|
2727
|
+
class ServiceB {
|
|
2728
|
+
name = 'B';
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
@Injectable()
|
|
2732
|
+
class ServiceC {
|
|
2733
|
+
name = 'C';
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
const MyModule = ProviderModule.create({
|
|
2737
|
+
id: 'MyModule',
|
|
2738
|
+
providers: [
|
|
2739
|
+
ServiceA,
|
|
2740
|
+
ServiceB,
|
|
2741
|
+
ServiceC,
|
|
2742
|
+
{ provide: 'CONFIG_A', useValue: 'config-a' },
|
|
2743
|
+
{ provide: 'CONFIG_B', useValue: 'config-b' },
|
|
2744
|
+
],
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
// Simple getMany
|
|
2748
|
+
const [serviceA, serviceB, configA] = MyModule.getMany(ServiceA, ServiceB, 'CONFIG_A');
|
|
2749
|
+
|
|
2750
|
+
console.log(serviceA.name); // 'A'
|
|
2751
|
+
console.log(serviceB.name); // 'B'
|
|
2752
|
+
console.log(configA); // 'config-a'
|
|
2753
|
+
```
|
|
2754
|
+
|
|
2755
|
+
**With Options:**
|
|
2756
|
+
|
|
2757
|
+
```ts
|
|
2758
|
+
// Optional providers
|
|
2759
|
+
const [serviceA, missing, serviceC] = MyModule.getMany(
|
|
2760
|
+
ServiceA,
|
|
2761
|
+
{ provider: 'NON_EXISTENT', isOptional: true },
|
|
2762
|
+
ServiceC
|
|
2763
|
+
);
|
|
2764
|
+
|
|
2765
|
+
console.log(serviceA); // ServiceA instance
|
|
2766
|
+
console.log(missing); // undefined (no error)
|
|
2767
|
+
console.log(serviceC); // ServiceC instance
|
|
2768
|
+
|
|
2769
|
+
// Get as list (multiple bindings)
|
|
2770
|
+
const HandlerModule = ProviderModule.create({
|
|
2771
|
+
id: 'HandlerModule',
|
|
2772
|
+
providers: [
|
|
2773
|
+
{ provide: 'Handler', useValue: 'H1' },
|
|
2774
|
+
{ provide: 'Handler', useValue: 'H2' },
|
|
2775
|
+
{ provide: 'Handler', useValue: 'H3' },
|
|
2776
|
+
],
|
|
2777
|
+
});
|
|
2778
|
+
|
|
2779
|
+
const [handlers] = HandlerModule.getMany({
|
|
2780
|
+
provider: 'Handler',
|
|
2781
|
+
asList: true,
|
|
2782
|
+
});
|
|
2783
|
+
|
|
2784
|
+
console.log(handlers); // ['H1', 'H2', 'H3']
|
|
2785
|
+
```
|
|
2786
|
+
|
|
2787
|
+
**Complex Example:**
|
|
2788
|
+
|
|
2789
|
+
```ts
|
|
2790
|
+
const [database, cache, optionalLogger, allPlugins, config] = MyModule.getMany(
|
|
2791
|
+
DatabaseService,
|
|
2792
|
+
CacheService,
|
|
2793
|
+
{ provider: LoggerService, isOptional: true },
|
|
2794
|
+
{ provider: Plugin, asList: true },
|
|
2795
|
+
'APP_CONFIG'
|
|
2796
|
+
);
|
|
2797
|
+
|
|
2798
|
+
// All providers resolved in one call
|
|
2799
|
+
// optionalLogger is undefined if not available
|
|
2800
|
+
// allPlugins is an array of all Plugin bindings
|
|
2801
|
+
```
|
|
2802
|
+
|
|
2803
|
+
> [!IMPORTANT] > `getMany()` parameter types:
|
|
2804
|
+
>
|
|
2805
|
+
> - **Simple**: Just pass the token directly
|
|
2806
|
+
> - **With options**: Use object with `provider`, `isOptional`, and/or `asList`
|
|
2807
|
+
|
|
2808
|
+
## Resources
|
|
2809
|
+
|
|
2810
|
+
📚 **[Full API Documentation](https://adimarianmutu.github.io/x-injection/index.html)** - Complete TypeDoc reference
|
|
2811
|
+
|
|
2812
|
+
⚛️ **[React Integration](https://github.com/AdiMarianMutu/x-injection-reactjs)** - Official React hooks and providers
|
|
2813
|
+
|
|
2814
|
+
💡 **[GitHub Issues](https://github.com/AdiMarianMutu/x-injection/issues)** - Bug reports and feature requests
|
|
2815
|
+
|
|
2816
|
+
🌟 **[GitHub Repository](https://github.com/AdiMarianMutu/x-injection)** - Source code and examples
|
|
2817
|
+
|
|
2818
|
+
## Contributing
|
|
2819
|
+
|
|
2820
|
+
Contributions are welcome! Please ensure code follows the project style guidelines and includes appropriate tests.
|
|
2821
|
+
|
|
2822
|
+
1. Fork the repository
|
|
2823
|
+
2. Create a feature branch
|
|
2824
|
+
3. Make your changes with tests
|
|
2825
|
+
4. Submit a pull request
|
|
2826
|
+
|
|
2827
|
+
## Credits
|
|
2828
|
+
|
|
2829
|
+
**Author:** [Adi-Marian Mutu](https://www.linkedin.com/in/mutu-adi-marian/)
|
|
2830
|
+
|
|
2831
|
+
**Built on:** [InversifyJS](https://github.com/inversify/monorepo)
|
|
2832
|
+
|
|
2833
|
+
**Logo:** [Alexandru Turica](https://www.linkedin.com/in/alexandru-turica-82215522b/)
|
|
2834
|
+
|
|
2835
|
+
## License
|
|
2836
|
+
|
|
2837
|
+
MIT © Adi-Marian Mutu
|