decorator-dependency-injection 1.0.1 → 1.0.3

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.
@@ -1,4 +1,4 @@
1
- import {Factory, Inject, Singleton} from '../index.js'
1
+ import {Factory, Inject, resetMocks, Singleton} from '../index.js'
2
2
 
3
3
  describe('Injection via fields', () => {
4
4
  @Singleton()
@@ -46,6 +46,12 @@ describe('Injection via fields', () => {
46
46
  }
47
47
  }
48
48
 
49
+ afterEach(() => {
50
+ TestFactory.calls = 0
51
+ TestSingleton.calls = 0
52
+ resetMocks()
53
+ })
54
+
49
55
  it('should inject factory', () => {
50
56
  class TestInjectionFactory {
51
57
  @Inject(TestFactory) testFactory
@@ -83,7 +89,53 @@ describe('Injection via fields', () => {
83
89
  new TestInjectionFactoryParams()
84
90
  })
85
91
 
86
- @Singleton("named")
92
+ it('should cache factory instance on repeated accesses', () => {
93
+ class TestRepeatedFactoryAccess {
94
+ @Inject(TestFactory) testFactory
95
+
96
+ constructor() {
97
+ const instance1 = this.testFactory
98
+ const instance2 = this.testFactory
99
+ expect(instance1).toBe(instance2)
100
+ }
101
+ }
102
+
103
+ new TestRepeatedFactoryAccess()
104
+ })
105
+
106
+ it('should create distinct factory instances for different fields in the same object', () => {
107
+ class TestMultipleFactoryInjection {
108
+ @Inject(TestFactory) testFactory1
109
+ @Inject(TestFactory) testFactory2
110
+
111
+ constructor() {
112
+ // Access both properties to trigger initialization.
113
+ const one = this.testFactory1
114
+ const two = this.testFactory2
115
+ expect(one).not.toBe(two)
116
+ }
117
+ }
118
+
119
+ new TestMultipleFactoryInjection()
120
+ })
121
+
122
+ it('should inject the same singleton instance for different fields in the same object', () => {
123
+ class TestMultipleSingletonInjection {
124
+ @Inject(TestSingleton) testSingleton1
125
+ @Inject(TestSingleton) testSingleton2
126
+
127
+ constructor() {
128
+ // Access both properties to trigger initialization.
129
+ const one = this.testSingleton1
130
+ const two = this.testSingleton2
131
+ expect(one).toBe(two)
132
+ }
133
+ }
134
+
135
+ new TestMultipleSingletonInjection()
136
+ })
137
+
138
+ @Singleton('named')
87
139
  class NamedSingleton {
88
140
  static calls = 0
89
141
 
@@ -94,7 +146,7 @@ describe('Injection via fields', () => {
94
146
 
95
147
  it('should inject named singleton', () => {
96
148
  class TestInjectionNamedSingleton {
97
- @Inject("named") namedSingleton
149
+ @Inject('named') namedSingleton
98
150
 
99
151
  constructor() {
100
152
  expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
@@ -103,7 +155,7 @@ describe('Injection via fields', () => {
103
155
  }
104
156
 
105
157
  class TestInjectionNamedSingleton2 {
106
- @Inject("named") namedSingleton
158
+ @Inject('named') namedSingleton
107
159
 
108
160
  constructor() {
109
161
  expect(this.namedSingleton).toBeInstanceOf(NamedSingleton)
@@ -115,7 +167,7 @@ describe('Injection via fields', () => {
115
167
  new TestInjectionNamedSingleton2()
116
168
  })
117
169
 
118
- @Factory("named2")
170
+ @Factory('named2')
119
171
  class NamedFactory {
120
172
  static calls = 0
121
173
  params
@@ -128,7 +180,7 @@ describe('Injection via fields', () => {
128
180
 
129
181
  it('should inject named factory', () => {
130
182
  class TestInjectionNamedFactory {
131
- @Inject("named2") namedFactory
183
+ @Inject('named2') namedFactory
132
184
 
133
185
  constructor() {
134
186
  expect(this.namedFactory).toBeInstanceOf(NamedFactory)
@@ -137,7 +189,7 @@ describe('Injection via fields', () => {
137
189
  }
138
190
 
139
191
  class TestInjectionNamedFactory2 {
140
- @Inject("named2") namedFactory
192
+ @Inject('named2') namedFactory
141
193
 
142
194
  constructor() {
143
195
  expect(this.namedFactory).toBeInstanceOf(NamedFactory)
@@ -149,4 +201,109 @@ describe('Injection via fields', () => {
149
201
  new TestInjectionNamedFactory2()
150
202
  expect(result.namedFactory.params).toEqual([])
151
203
  })
204
+
205
+ it('should cache named factory instance on repeated accesses', () => {
206
+ class TestRepeatedNamedFactoryAccess {
207
+ @Inject('named2') namedFactory
208
+
209
+ constructor() {
210
+ const instance1 = this.namedFactory
211
+ const instance2 = this.namedFactory
212
+ expect(instance1).toBe(instance2)
213
+ }
214
+ }
215
+
216
+ new TestRepeatedNamedFactoryAccess()
217
+ })
218
+
219
+ it('should throw if @Inject is applied to a method', () => {
220
+ expect(() => {
221
+ // noinspection JSUnusedLocalSymbols
222
+ class BadInjection {
223
+ @Inject('something')
224
+ someMethod() {
225
+ }
226
+ }
227
+ }).toThrow()
228
+ })
229
+
230
+ it('should handle circular dependencies gracefully', () => {
231
+ @Singleton()
232
+ class A {
233
+ @Inject('B') b
234
+ }
235
+
236
+ @Singleton('B')
237
+ class B {
238
+ @Inject(A) a
239
+ }
240
+
241
+ expect(() => new A()).toThrow(/Circular dependency detected.*@InjectLazy/)
242
+ })
243
+
244
+ it('should throw if decorator is used on non-class object', () => {
245
+ expect(() => {
246
+ const obj = {}
247
+ Inject('something')(obj, 'field')
248
+ }).toThrow()
249
+ })
250
+
251
+ it('should throw a helpful error for eager circular dependencies', () => {
252
+ @Factory()
253
+ class A2 {
254
+ @Inject('B2') b
255
+ }
256
+
257
+ @Singleton('B2')
258
+ class B2 {
259
+ @Inject(A2) a
260
+ }
261
+
262
+ expect(() => new A2()).toThrow(/Circular dependency detected.*@InjectLazy/)
263
+ })
264
+
265
+ it('should inject into symbol-named fields', () => {
266
+ const sym = Symbol('sym')
267
+
268
+ @Singleton()
269
+ class S {
270
+ }
271
+
272
+ class Test {
273
+ @Inject(S) [sym]
274
+ }
275
+
276
+ const t = new Test()
277
+ expect(t[sym]).toBeInstanceOf(S)
278
+ })
279
+
280
+ it('should not leak injected properties to prototype', () => {
281
+ @Singleton()
282
+ class S {
283
+ }
284
+
285
+ class Test {
286
+ @Inject(S) dep
287
+ }
288
+
289
+ // noinspection JSUnusedLocalSymbols
290
+ const t = new Test()
291
+ expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
292
+ })
293
+
294
+ it('should handle undefined/null/complex params in factory', () => {
295
+ @Factory()
296
+ class F {
297
+ constructor(...params) {
298
+ this.params = params
299
+ }
300
+ }
301
+
302
+ class Test {
303
+ @Inject(F, undefined, null, {a: 1}) dep
304
+ }
305
+
306
+ const t = new Test()
307
+ expect(t.dep.params).toEqual([undefined, null, {a: 1}])
308
+ })
152
309
  })
@@ -0,0 +1,249 @@
1
+ import {Factory, InjectLazy, resetMocks, Singleton} from '../index.js'
2
+
3
+ describe('Lazy Injection via fields', () => {
4
+ @Singleton()
5
+ class TestLazySingleton {
6
+ static calls = 0
7
+
8
+ constructor() {
9
+ TestLazySingleton.calls++
10
+ }
11
+ }
12
+
13
+ it('should lazily inject singleton', () => {
14
+ class TestLazySingletonInjection {
15
+ @InjectLazy(TestLazySingleton) testSingleton
16
+
17
+ constructor() {
18
+ }
19
+ }
20
+
21
+ const instance = new TestLazySingletonInjection()
22
+ expect(TestLazySingleton.calls).toBe(0) // Not constructed until access
23
+ const first = instance.testSingleton
24
+ expect(first).toBeInstanceOf(TestLazySingleton)
25
+ expect(TestLazySingleton.calls).toBe(1)
26
+ // Repeated access returns the same instance.
27
+ const second = instance.testSingleton
28
+ expect(first).toBe(second)
29
+ })
30
+
31
+ @Factory()
32
+ class TestLazyFactory {
33
+ static calls = 0
34
+ params
35
+
36
+ constructor(...params) {
37
+ TestLazyFactory.calls++
38
+ this.params = params
39
+ }
40
+ }
41
+
42
+ afterEach(() => {
43
+ TestLazyFactory.calls = 0
44
+ TestLazySingleton.calls = 0
45
+ resetMocks()
46
+ })
47
+
48
+ it('should lazily inject factory with caching on first access per field', () => {
49
+ class TestLazyFactoryInjection {
50
+ @InjectLazy(TestLazyFactory) testFactory
51
+
52
+ constructor() {
53
+ }
54
+ }
55
+
56
+ const inst = new TestLazyFactoryInjection()
57
+ expect(TestLazyFactory.calls).toBe(0)
58
+ const first = inst.testFactory
59
+ expect(first).toBeInstanceOf(TestLazyFactory)
60
+ expect(TestLazyFactory.calls).toBe(1)
61
+ expect(inst.testFactory).toBe(first)
62
+ })
63
+
64
+ it('should create distinct factory instances for different fields', () => {
65
+ class TestMultipleLazyFactoryInjection {
66
+ @InjectLazy(TestLazyFactory) testFactory1
67
+ @InjectLazy(TestLazyFactory) testFactory2
68
+
69
+ constructor() {
70
+ }
71
+ }
72
+
73
+ const instance = new TestMultipleLazyFactoryInjection()
74
+ expect(TestLazyFactory.calls).toBe(0)
75
+ const one = instance.testFactory1
76
+ const two = instance.testFactory2
77
+ expect(one).toBeInstanceOf(TestLazyFactory)
78
+ expect(two).toBeInstanceOf(TestLazyFactory)
79
+ expect(one).not.toBe(two)
80
+ })
81
+
82
+ it('should pass constructor parameters when using lazy injection', () => {
83
+ class TestLazyFactoryParamsInjection {
84
+ @InjectLazy(TestLazyFactory, 'param1', 'param2') testFactory
85
+
86
+ constructor() {
87
+ }
88
+ }
89
+
90
+ TestLazyFactory.calls = 0
91
+ const instance = new TestLazyFactoryParamsInjection()
92
+ expect(TestLazyFactory.calls).toBe(0)
93
+ const factoryInst = instance.testFactory
94
+ expect(factoryInst).toBeInstanceOf(TestLazyFactory)
95
+ expect(factoryInst.params).toEqual(['param1', 'param2'])
96
+ })
97
+
98
+ it('should not initialize dependency if property is never accessed', () => {
99
+ class TestNeverAccess {
100
+ @InjectLazy(TestLazySingleton) testSingleton
101
+
102
+ constructor() {
103
+ }
104
+ }
105
+
106
+ TestLazySingleton.calls = 0
107
+ new TestNeverAccess() // Do not access testSingleton.
108
+ expect(TestLazySingleton.calls).toBe(0)
109
+ })
110
+
111
+ @Singleton('lazyNamedSingleton')
112
+ class NamedLazySingleton {
113
+ static calls = 0
114
+
115
+ constructor() {
116
+ NamedLazySingleton.calls++
117
+ }
118
+ }
119
+
120
+ it('should lazily inject named singleton', () => {
121
+ class TestLazyNamedSingletonInjection {
122
+ @InjectLazy('lazyNamedSingleton') namedSingleton
123
+
124
+ constructor() {
125
+ }
126
+ }
127
+
128
+ const instance = new TestLazyNamedSingletonInjection()
129
+ expect(NamedLazySingleton.calls).toBe(0)
130
+ const first = instance.namedSingleton
131
+ expect(first).toBeInstanceOf(NamedLazySingleton)
132
+ expect(NamedLazySingleton.calls).toBe(1)
133
+ expect(instance.namedSingleton).toBe(first)
134
+ })
135
+
136
+ @Factory('lazyNamedFactory')
137
+ class NamedLazyFactory {
138
+ static calls = 0
139
+ params
140
+
141
+ constructor(...params) {
142
+ NamedLazyFactory.calls++
143
+ this.params = params
144
+ }
145
+ }
146
+
147
+ it('should lazily inject named factory', () => {
148
+ class TestLazyNamedFactoryInjection {
149
+ @InjectLazy('lazyNamedFactory') namedFactory
150
+
151
+ constructor() {
152
+ }
153
+ }
154
+
155
+ NamedLazyFactory.calls = 0
156
+ const instance = new TestLazyNamedFactoryInjection()
157
+ expect(NamedLazyFactory.calls).toBe(0)
158
+ const first = instance.namedFactory
159
+ expect(first).toBeInstanceOf(NamedLazyFactory)
160
+ expect(NamedLazyFactory.calls).toBe(1)
161
+ expect(instance.namedFactory).toBe(first)
162
+ })
163
+
164
+ it('should not expose any internal caching artifacts', () => {
165
+ class TestLazyEnum {
166
+ @InjectLazy(TestLazySingleton) lazyProp
167
+
168
+ constructor() {
169
+ }
170
+ }
171
+
172
+ const inst = new TestLazyEnum()
173
+ // noinspection JSUnusedLocalSymbols: Force lazy initialization.
174
+ const _ = inst.lazyProp
175
+ const keys = Object.keys(inst)
176
+ expect(keys).toContain('lazyProp')
177
+ expect(keys.length).toBe(1)
178
+ const symbols = Object.getOwnPropertySymbols(inst)
179
+ expect(symbols.length).toBe(0)
180
+ })
181
+
182
+ it('should throw if @InjectLazy is applied to a method', () => {
183
+ expect(() => {
184
+ // noinspection JSUnusedLocalSymbols
185
+ class BadLazy {
186
+ @InjectLazy('something')
187
+ someMethod() {
188
+ }
189
+ }
190
+ }).toThrow()
191
+ })
192
+
193
+ it('should handle circular dependencies (lazy)', () => {
194
+ @Singleton()
195
+ class A {
196
+ @InjectLazy('B') b
197
+ }
198
+
199
+ @Singleton('B')
200
+ class B {
201
+ @InjectLazy(A) a
202
+ }
203
+
204
+ expect(() => new A()).not.toThrow()
205
+ })
206
+
207
+ it('should inject into symbol-named fields (lazy)', () => {
208
+ const sym = Symbol('sym')
209
+
210
+ @Singleton()
211
+ class S {
212
+ }
213
+
214
+ class Test {
215
+ @InjectLazy(S) [sym]
216
+ }
217
+
218
+ const t = new Test()
219
+ expect(t[sym]).toBeInstanceOf(S)
220
+ })
221
+
222
+ it('should not leak lazy injected properties to prototype', () => {
223
+ @Singleton()
224
+ class S {
225
+ }
226
+
227
+ class Test {
228
+ @InjectLazy(S) dep
229
+ }
230
+
231
+ // noinspection JSUnusedLocalSymbols
232
+ const t = new Test()
233
+ expect(Object.prototype.hasOwnProperty.call(Test.prototype, 'dep')).toBe(false)
234
+ })
235
+
236
+ it('should allow circular dependencies with lazy injection', () => {
237
+ @Singleton()
238
+ class A1 {
239
+ @InjectLazy('B1') b
240
+ }
241
+
242
+ @Factory('B1')
243
+ class B1 {
244
+ @InjectLazy(A1) a
245
+ }
246
+
247
+ expect(() => new A1()).not.toThrow()
248
+ })
249
+ })