@tachybase/di 1.3.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE +201 -0
  2. package/lib/container-instance.class.d.ts +111 -0
  3. package/lib/container-instance.class.js +343 -0
  4. package/lib/container-registry.class.d.ts +51 -0
  5. package/lib/container-registry.class.js +95 -0
  6. package/lib/decorators/inject-many.decorator.d.ts +8 -0
  7. package/lib/decorators/inject-many.decorator.js +56 -0
  8. package/lib/decorators/inject.decorator.d.ts +9 -0
  9. package/lib/decorators/inject.decorator.js +56 -0
  10. package/lib/decorators/service.decorator.d.ts +6 -0
  11. package/lib/decorators/service.decorator.js +49 -0
  12. package/lib/decorators.d.ts +16 -0
  13. package/lib/decorators.js +86 -0
  14. package/lib/empty.const.d.ts +6 -0
  15. package/lib/empty.const.js +27 -0
  16. package/lib/error/cannot-inject-value.error.d.ts +11 -0
  17. package/lib/error/cannot-inject-value.error.js +40 -0
  18. package/lib/error/cannot-instantiate-value.error.d.ts +11 -0
  19. package/lib/error/cannot-instantiate-value.error.js +49 -0
  20. package/lib/error/service-not-found.error.d.ts +11 -0
  21. package/lib/error/service-not-found.error.js +49 -0
  22. package/lib/index.d.ts +15 -0
  23. package/lib/index.js +50 -0
  24. package/lib/interfaces/container-options.interface.d.ts +45 -0
  25. package/lib/interfaces/container-options.interface.js +15 -0
  26. package/lib/interfaces/handler.interface.d.ts +27 -0
  27. package/lib/interfaces/handler.interface.js +15 -0
  28. package/lib/interfaces/service-metadata.interface.d.ts +53 -0
  29. package/lib/interfaces/service-metadata.interface.js +15 -0
  30. package/lib/interfaces/service-options.interface.d.ts +6 -0
  31. package/lib/interfaces/service-options.interface.js +15 -0
  32. package/lib/token.class.d.ts +11 -0
  33. package/lib/token.class.js +37 -0
  34. package/lib/types/abstract-constructable.type.d.ts +9 -0
  35. package/lib/types/abstract-constructable.type.js +15 -0
  36. package/lib/types/container-identifier.type.d.ts +4 -0
  37. package/lib/types/container-identifier.type.js +15 -0
  38. package/lib/types/container-scope.type.d.ts +1 -0
  39. package/lib/types/container-scope.type.js +15 -0
  40. package/lib/types/service-identifier.type.d.ts +8 -0
  41. package/lib/types/service-identifier.type.js +15 -0
  42. package/lib/utils/resolve-to-type-wrapper.util.d.ts +15 -0
  43. package/lib/utils/resolve-to-type-wrapper.util.js +39 -0
  44. package/package.json +11 -0
  45. package/src/container-instance.class.ts +487 -0
  46. package/src/container-registry.class.ts +92 -0
  47. package/src/decorators/inject-many.decorator.ts +48 -0
  48. package/src/decorators/inject.decorator.ts +46 -0
  49. package/src/decorators/service.decorator.ts +34 -0
  50. package/src/decorators.ts +70 -0
  51. package/src/empty.const.ts +6 -0
  52. package/src/error/cannot-inject-value.error.ts +22 -0
  53. package/src/error/cannot-instantiate-value.error.ts +34 -0
  54. package/src/error/service-not-found.error.ts +33 -0
  55. package/src/index.ts +21 -0
  56. package/src/interfaces/container-options.interface.ts +48 -0
  57. package/src/interfaces/handler.interface.ts +32 -0
  58. package/src/interfaces/service-metadata.interface.ts +62 -0
  59. package/src/interfaces/service-options.interface.ts +10 -0
  60. package/src/token.class.ts +11 -0
  61. package/src/types/abstract-constructable.type.ts +7 -0
  62. package/src/types/container-identifier.type.ts +4 -0
  63. package/src/types/container-scope.type.ts +1 -0
  64. package/src/types/service-identifier.type.ts +15 -0
  65. package/src/utils/resolve-to-type-wrapper.util.ts +44 -0
@@ -0,0 +1,487 @@
1
+ import { ContainerRegistry } from './container-registry.class';
2
+ import { EMPTY_VALUE } from './empty.const';
3
+ import { CannotInstantiateValueError } from './error/cannot-instantiate-value.error';
4
+ import { ServiceNotFoundError } from './error/service-not-found.error';
5
+ import { Handler } from './interfaces/handler.interface';
6
+ import { ServiceMetadata } from './interfaces/service-metadata.interface';
7
+ import { ServiceOptions } from './interfaces/service-options.interface';
8
+ import { Token } from './token.class';
9
+ import { ContainerIdentifier } from './types/container-identifier.type';
10
+ import { ContainerScope } from './types/container-scope.type';
11
+ import { ServiceIdentifier } from './types/service-identifier.type';
12
+
13
+ /**
14
+ * TypeDI can have multiple containers.
15
+ * One container is ContainerInstance.
16
+ */
17
+ export class ContainerInstance {
18
+ /** Container instance id. */
19
+ public readonly id!: ContainerIdentifier;
20
+
21
+ /** Metadata for all registered services in this container. */
22
+ private metadataMap: Map<ServiceIdentifier, ServiceMetadata<unknown>> = new Map();
23
+
24
+ /**
25
+ * Services registered with 'multiple: true' are saved as simple services
26
+ * with a generated token and the mapping between the original ID and the
27
+ * generated one is stored here. This is handled like this to allow simplifying
28
+ * the inner workings of the service instance.
29
+ */
30
+ private multiServiceIds: Map<ServiceIdentifier, { tokens: Token<unknown>[]; scope: ContainerScope }> = new Map();
31
+
32
+ /**
33
+ * All registered handlers. The @Inject() decorator uses handlers internally to mark a property for injection.
34
+ **/
35
+ private readonly handlers: Handler[] = [];
36
+
37
+ /**
38
+ * The default global container. By default services are registered into this
39
+ * container when registered via `Container.set()` or `@Service` decorator.
40
+ */
41
+ private static _default: ContainerInstance;
42
+
43
+ public static get default(): ContainerInstance {
44
+ if (!this._default) {
45
+ this._default = new ContainerInstance('default');
46
+ ContainerRegistry.registerContainer(this._default);
47
+ }
48
+ return this._default;
49
+ }
50
+
51
+ /**
52
+ * Indicates if the container has been disposed or not.
53
+ * Any function call should fail when called after being disposed.
54
+ *
55
+ * NOTE: Currently not in used
56
+ */
57
+ private disposed = false;
58
+
59
+ constructor(id: ContainerIdentifier) {
60
+ this.id = id;
61
+
62
+ if (id !== 'default') {
63
+ ContainerRegistry.registerContainer(this);
64
+
65
+ /**
66
+ * TODO: This is to replicate the old functionality. This should be copied only
67
+ * TODO: if the container decides to inherit registered classes from a parent container.
68
+ */
69
+ this.handlers = ContainerInstance.default.handlers || [];
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Checks if the service with given name or type is registered service container.
75
+ * Optionally, parameters can be passed in case if instance is initialized in the container for the first time.
76
+ */
77
+ public has<T = unknown>(identifier: ServiceIdentifier<T>): boolean {
78
+ this.throwIfDisposed();
79
+
80
+ return !!this.metadataMap.has(identifier) || !!this.multiServiceIds.has(identifier);
81
+ }
82
+
83
+ /**
84
+ * Retrieves the service with given name or type from the service container.
85
+ * Optionally, parameters can be passed in case if instance is initialized in the container for the first time.
86
+ */
87
+ public get<T = unknown>(identifier: ServiceIdentifier<T>): T {
88
+ this.throwIfDisposed();
89
+
90
+ const global = ContainerInstance.default.metadataMap.get(identifier);
91
+ const local = this.metadataMap.get(identifier);
92
+ /** If the service is registered as global we load it from there, otherwise we use the local one. */
93
+ const metadata = global?.scope === 'singleton' ? global : local;
94
+
95
+ /** This should never happen as multi services are masked with custom token in Container.set. */
96
+ if (metadata && metadata.multiple === true) {
97
+ throw new Error(`Cannot resolve multiple values for ${identifier.toString()} service!`);
98
+ }
99
+
100
+ /** Otherwise it's returned from the current container. */
101
+ if (metadata) {
102
+ return this.getServiceValue(metadata);
103
+ }
104
+
105
+ /**
106
+ * If it's the first time requested in the child container we load it from parent and set it.
107
+ * TODO: This will be removed with the container inheritance rework.
108
+ */
109
+ if (global && this !== ContainerInstance.default) {
110
+ const clonedService = { ...global };
111
+ clonedService.value = EMPTY_VALUE;
112
+
113
+ /**
114
+ * We need to immediately set the empty value from the root container
115
+ * to prevent infinite lookup in cyclic dependencies.
116
+ */
117
+ this.set(clonedService);
118
+
119
+ const value = this.getServiceValue(clonedService);
120
+ this.set({ ...clonedService, value });
121
+
122
+ return value;
123
+ }
124
+
125
+ throw new ServiceNotFoundError(identifier);
126
+ }
127
+
128
+ /**
129
+ * Gets all instances registered in the container of the given service identifier.
130
+ * Used when service defined with multiple: true flag.
131
+ */
132
+ public getMany<T = unknown>(identifier: ServiceIdentifier<T>): T[] {
133
+ this.throwIfDisposed();
134
+
135
+ const globalIdMap = ContainerInstance.default.multiServiceIds.get(identifier);
136
+ const localIdMap = this.multiServiceIds.get(identifier);
137
+
138
+ /**
139
+ * If the service is registered as singleton we load it from default
140
+ * container, otherwise we use the local one.
141
+ */
142
+ if (globalIdMap?.scope === 'singleton') {
143
+ return globalIdMap.tokens.map((generatedId) => ContainerInstance.default.get<T>(generatedId));
144
+ }
145
+
146
+ if (localIdMap) {
147
+ return localIdMap.tokens.map((generatedId) => this.get<T>(generatedId));
148
+ }
149
+
150
+ throw new ServiceNotFoundError(identifier);
151
+ }
152
+
153
+ /**
154
+ * Sets a value for the given type or service name in the container.
155
+ */
156
+ public set<T = unknown>(serviceOptions: ServiceOptions<T>): this {
157
+ this.throwIfDisposed();
158
+
159
+ /**
160
+ * If the service is marked as singleton, we set it in the default container.
161
+ * (And avoid an infinite loop via checking if we are in the default container or not.)
162
+ */
163
+ if (serviceOptions.scope === 'singleton' && ContainerInstance.default !== this) {
164
+ ContainerInstance.default.set(serviceOptions);
165
+
166
+ return this;
167
+ }
168
+
169
+ const newMetadata: ServiceMetadata<T> = {
170
+ /**
171
+ * Typescript cannot understand that if ID doesn't exists then type must exists based on the
172
+ * typing so we need to explicitly cast this to a `ServiceIdentifier`
173
+ */
174
+ id: ((serviceOptions as any).id || (serviceOptions as any).type) as ServiceIdentifier,
175
+ type: (serviceOptions as ServiceMetadata<T>).type || null,
176
+ factory: (serviceOptions as ServiceMetadata<T>).factory,
177
+ value: (serviceOptions as ServiceMetadata<T>).value || EMPTY_VALUE,
178
+ multiple: serviceOptions.multiple || false,
179
+ eager: serviceOptions.eager || false,
180
+ scope: serviceOptions.scope || 'container',
181
+ /** We allow overriding the above options via the received config object. */
182
+ ...serviceOptions,
183
+ referencedBy: new Map().set(this.id, this),
184
+ };
185
+
186
+ /** If the incoming metadata is marked as multiple we mask the ID and continue saving as single value. */
187
+ if (serviceOptions.multiple) {
188
+ const maskedToken = new Token(`MultiMaskToken-${newMetadata.id.toString()}`);
189
+ const existingMultiGroup = this.multiServiceIds.get(newMetadata.id);
190
+
191
+ if (existingMultiGroup) {
192
+ existingMultiGroup.tokens.push(maskedToken);
193
+ } else {
194
+ this.multiServiceIds.set(newMetadata.id, { scope: newMetadata.scope, tokens: [maskedToken] });
195
+ }
196
+
197
+ /**
198
+ * We mask the original metadata with this generated ID, mark the service
199
+ * as and continue multiple: false and continue. Marking it as
200
+ * non-multiple is important otherwise Container.get would refuse to
201
+ * resolve the value.
202
+ */
203
+ newMetadata.id = maskedToken;
204
+ newMetadata.multiple = false;
205
+ }
206
+
207
+ const existingMetadata = this.metadataMap.get(newMetadata.id);
208
+
209
+ if (existingMetadata) {
210
+ /** Service already exists, we overwrite it. (This is legacy behavior.) */
211
+ // TODO: Here we should differentiate based on the received set option.
212
+ Object.assign(existingMetadata, newMetadata);
213
+ } else {
214
+ /** This service hasn't been registered yet, so we register it. */
215
+ this.metadataMap.set(newMetadata.id, newMetadata);
216
+ }
217
+
218
+ /**
219
+ * If the service is eager, we need to create an instance immediately except
220
+ * when the service is also marked as transient. In that case we ignore
221
+ * the eager flag to prevent creating a service what cannot be disposed later.
222
+ */
223
+ if (newMetadata.eager && newMetadata.scope !== 'transient') {
224
+ this.get(newMetadata.id);
225
+ }
226
+
227
+ return this;
228
+ }
229
+
230
+ /**
231
+ * Removes services with a given service identifiers.
232
+ */
233
+ public remove(identifierOrIdentifierArray: ServiceIdentifier | ServiceIdentifier[]): this {
234
+ this.throwIfDisposed();
235
+
236
+ if (Array.isArray(identifierOrIdentifierArray)) {
237
+ identifierOrIdentifierArray.forEach((id) => this.remove(id));
238
+ } else {
239
+ const serviceMetadata = this.metadataMap.get(identifierOrIdentifierArray);
240
+
241
+ if (serviceMetadata) {
242
+ this.disposeServiceInstance(serviceMetadata);
243
+ this.metadataMap.delete(identifierOrIdentifierArray);
244
+ }
245
+ }
246
+
247
+ return this;
248
+ }
249
+
250
+ /**
251
+ * Gets a separate container instance for the given instance id.
252
+ */
253
+ public of(containerId: ContainerIdentifier = 'default'): ContainerInstance {
254
+ this.throwIfDisposed();
255
+
256
+ if (containerId === 'default') {
257
+ return ContainerInstance.default;
258
+ }
259
+
260
+ let container: ContainerInstance;
261
+
262
+ if (ContainerRegistry.hasContainer(containerId)) {
263
+ container = ContainerRegistry.getContainer(containerId);
264
+ } else {
265
+ /**
266
+ * This is deprecated functionality, for now we create the container if it's doesn't exists.
267
+ * This will be reworked when container inheritance is reworked.
268
+ */
269
+ container = new ContainerInstance(containerId);
270
+ }
271
+
272
+ return container;
273
+ }
274
+
275
+ /**
276
+ * Registers a new handler.
277
+ */
278
+ public registerHandler(handler: Handler): ContainerInstance {
279
+ this.handlers.push(handler);
280
+ return this;
281
+ }
282
+
283
+ /**
284
+ * Helper method that imports given services.
285
+ */
286
+
287
+ public import(services: Function[]): ContainerInstance {
288
+ this.throwIfDisposed();
289
+
290
+ return this;
291
+ }
292
+
293
+ /**
294
+ * Completely resets the container by removing all previously registered services from it.
295
+ */
296
+ public reset(options: { strategy: 'resetValue' | 'resetServices' } = { strategy: 'resetValue' }): this {
297
+ this.throwIfDisposed();
298
+
299
+ switch (options.strategy) {
300
+ case 'resetValue':
301
+ this.metadataMap.forEach((service) => this.disposeServiceInstance(service));
302
+ break;
303
+ case 'resetServices':
304
+ this.metadataMap.forEach((service) => this.disposeServiceInstance(service));
305
+ this.metadataMap.clear();
306
+ this.multiServiceIds.clear();
307
+ break;
308
+ default:
309
+ throw new Error('Received invalid reset strategy.');
310
+ }
311
+ return this;
312
+ }
313
+
314
+ public async dispose(): Promise<void> {
315
+ this.reset({ strategy: 'resetServices' });
316
+
317
+ /** We mark the container as disposed, forbidding any further interaction with it. */
318
+ this.disposed = true;
319
+
320
+ /**
321
+ * Placeholder, this function returns a promise in preparation to support async services.
322
+ */
323
+ await Promise.resolve();
324
+ }
325
+
326
+ private throwIfDisposed() {
327
+ if (this.disposed) {
328
+ // TODO: Use custom error.
329
+ throw new Error('Cannot use container after it has been disposed.');
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Gets the value belonging to passed in `ServiceMetadata` instance.
335
+ *
336
+ * - if `serviceMetadata.value` is already set it is immediately returned
337
+ * - otherwise the requested type is resolved to the value saved to `serviceMetadata.value` and returned
338
+ */
339
+ private getServiceValue(serviceMetadata: ServiceMetadata<unknown>): any {
340
+ let value: unknown = EMPTY_VALUE;
341
+
342
+ /**
343
+ * If the service value has been set to anything prior to this call we return that value.
344
+ * NOTE: This part builds on the assumption that transient dependencies has no value set ever.
345
+ */
346
+ if (serviceMetadata.value !== EMPTY_VALUE) {
347
+ return serviceMetadata.value;
348
+ }
349
+
350
+ /** If both factory and type is missing, we cannot resolve the requested ID. */
351
+ if (!serviceMetadata.factory && typeof serviceMetadata.type === 'undefined') {
352
+ throw new CannotInstantiateValueError(serviceMetadata.id);
353
+ }
354
+
355
+ /**
356
+ * If a factory is defined it takes priority over creating an instance via `new`.
357
+ * The return value of the factory is not checked, we believe by design that the user knows what he/she is doing.
358
+ */
359
+ if (serviceMetadata.factory) {
360
+ /**
361
+ * If we received the factory in the [Constructable<Factory>, "functionName"] format, we need to create the
362
+ * factory first and then call the specified function on it.
363
+ */
364
+ if (serviceMetadata.factory instanceof Array) {
365
+ let factoryInstance;
366
+
367
+ try {
368
+ /** Try to get the factory from TypeDI first, if failed, fall back to simply initiating the class. */
369
+ factoryInstance = this.get<any>(serviceMetadata.factory[0]);
370
+ } catch (error) {
371
+ if (error instanceof ServiceNotFoundError) {
372
+ factoryInstance = new serviceMetadata.factory[0]();
373
+ } else {
374
+ throw error;
375
+ }
376
+ }
377
+
378
+ value = factoryInstance[serviceMetadata.factory[1]](this, serviceMetadata.id);
379
+ } else {
380
+ /** If only a simple function was provided we simply call it. */
381
+ value = serviceMetadata.factory(this, serviceMetadata.id);
382
+ }
383
+ } else if (typeof serviceMetadata.type === 'function') {
384
+ value = new serviceMetadata.type();
385
+ }
386
+
387
+ /** If this is not a transient service, and we resolved something, then we set it as the value. */
388
+ if (serviceMetadata.scope !== 'transient' && value !== EMPTY_VALUE) {
389
+ serviceMetadata.value = value;
390
+ }
391
+
392
+ if (value === EMPTY_VALUE) {
393
+ /** This branch should never execute, but better to be safe than sorry. */
394
+ throw new CannotInstantiateValueError(serviceMetadata.id);
395
+ }
396
+
397
+ if (serviceMetadata.type) {
398
+ this.applyPropertyHandlers(serviceMetadata.type, value as Record<string, any>);
399
+ }
400
+
401
+ return value;
402
+ }
403
+
404
+ /**
405
+ * Initializes all parameter types for a given target service class.
406
+ */
407
+ private initializeParams(target: Function, paramTypes: any[]): unknown[] {
408
+ return paramTypes.map((paramType, index) => {
409
+ const paramHandler =
410
+ this.handlers.find((handler) => {
411
+ /**
412
+ * @Inject()-ed values are stored as parameter handlers and they reference their target
413
+ * when created. So when a class is extended the @Inject()-ed values are not inherited
414
+ * because the handler still points to the old object only.
415
+ *
416
+ * As a quick fix a single level parent lookup is added via `Object.getPrototypeOf(target)`,
417
+ * however this should be updated to a more robust solution.
418
+ *
419
+ * TODO: Add proper inheritance handling: either copy the handlers when a class is registered what
420
+ * TODO: has it's parent already registered as dependency or make the lookup search up to the base Object.
421
+ */
422
+ return handler.object === target && handler.index === index;
423
+ }) ||
424
+ this.handlers.find((handler) => {
425
+ return handler.object === Object.getPrototypeOf(target) && handler.index === index;
426
+ });
427
+
428
+ if (paramHandler) return paramHandler.value(this);
429
+
430
+ if (paramType && paramType.name && !this.isPrimitiveParamType(paramType.name)) {
431
+ return this.get(paramType);
432
+ }
433
+
434
+ return undefined;
435
+ });
436
+ }
437
+
438
+ /**
439
+ * Checks if given parameter type is primitive type or not.
440
+ */
441
+ private isPrimitiveParamType(paramTypeName: string): boolean {
442
+ return ['string', 'boolean', 'number', 'object'].includes(paramTypeName.toLowerCase());
443
+ }
444
+
445
+ /**
446
+ * Applies all registered handlers on a given target class.
447
+ */
448
+ private applyPropertyHandlers(target: Function, instance: { [key: string]: any }) {
449
+ this.handlers.forEach((handler) => {
450
+ if (typeof handler.index === 'number') return;
451
+ if (handler.object !== target && !(target.prototype instanceof handler.object)) return;
452
+
453
+ if (handler.propertyName) {
454
+ instance[handler.propertyName] = handler.value(this);
455
+ }
456
+ });
457
+ }
458
+
459
+ /**
460
+ * Checks if the given service metadata contains a destroyable service instance and destroys it in place. If the service
461
+ * contains a callable function named `destroy` it is called but not awaited and the return value is ignored..
462
+ *
463
+ * @param serviceMetadata the service metadata containing the instance to destroy
464
+ * @param force when true the service will be always destroyed even if it's cannot be re-created
465
+ */
466
+ private disposeServiceInstance(serviceMetadata: ServiceMetadata, force = false) {
467
+ this.throwIfDisposed();
468
+
469
+ /** We reset value only if we can re-create it (aka type or factory exists). */
470
+ const shouldResetValue = force || !!serviceMetadata.type || !!serviceMetadata.factory;
471
+
472
+ if (shouldResetValue) {
473
+ /** If we wound a function named destroy we call it without any params. */
474
+ if (typeof (serviceMetadata?.value as Record<string, unknown>)['dispose'] === 'function') {
475
+ try {
476
+ (serviceMetadata.value as { dispose: CallableFunction }).dispose();
477
+ } catch (error) {
478
+ /** We simply ignore the errors from the destroy function. */
479
+ }
480
+ }
481
+
482
+ serviceMetadata.value = EMPTY_VALUE;
483
+ }
484
+ }
485
+ }
486
+ /** We export the default container under the Container alias. */
487
+ export const Container = ContainerInstance.default;
@@ -0,0 +1,92 @@
1
+ import { ContainerInstance } from './container-instance.class';
2
+ import { ContainerIdentifier } from './types/container-identifier.type';
3
+
4
+ /**
5
+ * The container registry is responsible for holding the default and every
6
+ * created container instance for later access.
7
+ *
8
+ * _Note: This class is for internal use and it's API may break in minor or
9
+ * patch releases without warning._
10
+ */
11
+ export class ContainerRegistry {
12
+ /**
13
+ * The list of all known container. Created containers are automatically added
14
+ * to this list. Two container cannot be registered with the same ID.
15
+ *
16
+ * This map doesn't contains the default container.
17
+ */
18
+ private static readonly containerMap: Map<ContainerIdentifier, ContainerInstance> = new Map();
19
+
20
+ /**
21
+ * Registers the given container instance or throws an error.
22
+ *
23
+ * _Note: This function is auto-called when a Container instance is created,
24
+ * it doesn't need to be called manually!_
25
+ *
26
+ * @param container the container to add to the registry
27
+ */
28
+ public static registerContainer(container: ContainerInstance): void {
29
+ if (container instanceof ContainerInstance === false) {
30
+ // TODO: Create custom error for this.
31
+ throw new Error('Only ContainerInstance instances can be registered.');
32
+ }
33
+
34
+ if (ContainerRegistry.containerMap.has(container.id)) {
35
+ // TODO: Create custom error for this.
36
+ throw new Error('Cannot register container with same ID.');
37
+ }
38
+
39
+ ContainerRegistry.containerMap.set(container.id, container);
40
+ }
41
+
42
+ /**
43
+ * Returns true if a container exists with the given ID or false otherwise.
44
+ *
45
+ * @param container the ID of the container
46
+ */
47
+ public static hasContainer(id: ContainerIdentifier): boolean {
48
+ return ContainerRegistry.containerMap.has(id);
49
+ }
50
+
51
+ /**
52
+ * Returns the container for requested ID or throws an error if no container
53
+ * is registered with the given ID.
54
+ *
55
+ * @param container the ID of the container
56
+ */
57
+ public static getContainer(id: ContainerIdentifier): ContainerInstance {
58
+ const registeredContainer = this.containerMap.get(id);
59
+
60
+ if (registeredContainer === undefined) {
61
+ // TODO: Create custom error for this.
62
+ throw new Error('No container is registered with the given ID.');
63
+ }
64
+
65
+ return registeredContainer;
66
+ }
67
+
68
+ /**
69
+ * Removes the given container from the registry and disposes all services
70
+ * registered only in this container.
71
+ *
72
+ * This function throws an error if no
73
+ * - container exists with the given ID
74
+ * - any of the registered services threw an error during it's disposal
75
+ *
76
+ * @param container the container to remove from the registry
77
+ */
78
+ public static async removeContainer(container: ContainerInstance): Promise<void> {
79
+ const registeredContainer = ContainerRegistry.containerMap.get(container.id);
80
+
81
+ if (registeredContainer === undefined) {
82
+ // TODO: Create custom error for this.
83
+ throw new Error('No container is registered with the given ID.');
84
+ }
85
+
86
+ /** We remove the container first. */
87
+ ContainerRegistry.containerMap.delete(container.id);
88
+
89
+ /** We dispose all registered classes in the container. */
90
+ await registeredContainer.dispose();
91
+ }
92
+ }
@@ -0,0 +1,48 @@
1
+ import { Constructable } from '@tachybase/utils';
2
+
3
+ import { ContainerInstance } from '../container-instance.class';
4
+ import { CannotInjectValueError } from '../error/cannot-inject-value.error';
5
+ import { Token } from '../token.class';
6
+ import { ServiceIdentifier } from '../types/service-identifier.type';
7
+ import { resolveToTypeWrapper } from '../utils/resolve-to-type-wrapper.util';
8
+
9
+ /**
10
+ * Injects a list of services into a class property or constructor parameter.
11
+ */
12
+ export function InjectMany(): Function;
13
+ export function InjectMany(type?: (type?: any) => Function): Function;
14
+ export function InjectMany(serviceName?: string): Function;
15
+ export function InjectMany(token: Token<any>): Function;
16
+ export function InjectMany(
17
+ typeOrIdentifier?: ((type?: never) => Constructable<unknown>) | ServiceIdentifier<unknown>,
18
+ ): Function {
19
+ return function (_: any, context: ClassFieldDecoratorContext) {
20
+ if (!context.metadata.injects) {
21
+ context.metadata.injects = [];
22
+ }
23
+ (context.metadata.injects as any[]).push((target: Constructable<unknown>) => {
24
+ const propertyName = context.name;
25
+ const typeWrapper = resolveToTypeWrapper(typeOrIdentifier, target, propertyName);
26
+
27
+ /** If no type was inferred, or the general Object type was inferred we throw an error. */
28
+ if (typeWrapper === undefined || typeWrapper.eagerType === undefined || typeWrapper.eagerType === Object) {
29
+ throw new CannotInjectValueError(target as Constructable<unknown>, propertyName as string);
30
+ }
31
+
32
+ ContainerInstance.default.registerHandler({
33
+ object: target as Constructable<unknown>,
34
+ propertyName: propertyName as string,
35
+ value: (containerInstance) => {
36
+ const evaluatedLazyType = typeWrapper.lazyType();
37
+
38
+ /** If no type was inferred lazily, or the general Object type was inferred we throw an error. */
39
+ if (evaluatedLazyType === undefined || evaluatedLazyType === Object) {
40
+ throw new CannotInjectValueError(target as Constructable<unknown>, propertyName as string);
41
+ }
42
+
43
+ return containerInstance.getMany<unknown>(evaluatedLazyType);
44
+ },
45
+ });
46
+ });
47
+ };
48
+ }
@@ -0,0 +1,46 @@
1
+ import { Constructable } from '@tachybase/utils';
2
+
3
+ import { ContainerInstance } from '../container-instance.class';
4
+ import { CannotInjectValueError } from '../error/cannot-inject-value.error';
5
+ import { Token } from '../token.class';
6
+ import { ServiceIdentifier } from '../types/service-identifier.type';
7
+ import { resolveToTypeWrapper } from '../utils/resolve-to-type-wrapper.util';
8
+
9
+ /**
10
+ * Injects a service into a class property or constructor parameter.
11
+ */
12
+ export function Inject(): Function;
13
+ export function Inject(typeFn: (type?: never) => Constructable<unknown>): Function;
14
+ export function Inject(serviceName?: string): Function;
15
+ export function Inject(token: Token<unknown>): Function;
16
+ export function Inject(typeOrIdentifier?: ((type?: never) => Constructable<unknown>) | ServiceIdentifier<unknown>) {
17
+ return function (_: any, context: ClassFieldDecoratorContext) {
18
+ if (!context.metadata.injects) {
19
+ context.metadata.injects = [];
20
+ }
21
+ (context.metadata.injects as any[]).push((target: Constructable<unknown>) => {
22
+ const propertyName = context.name;
23
+ const typeWrapper = resolveToTypeWrapper(typeOrIdentifier, target, propertyName);
24
+
25
+ /** If no type was inferred, or the general Object type was inferred we throw an error. */
26
+ if (typeWrapper === undefined || typeWrapper.eagerType === undefined || typeWrapper.eagerType === Object) {
27
+ throw new CannotInjectValueError(target as Constructable<unknown>, propertyName as string);
28
+ }
29
+
30
+ ContainerInstance.default.registerHandler({
31
+ object: target as Constructable<unknown>,
32
+ propertyName: propertyName as string,
33
+ value: (containerInstance) => {
34
+ const evaluatedLazyType = typeWrapper.lazyType();
35
+
36
+ /** If no type was inferred lazily, or the general Object type was inferred we throw an error. */
37
+ if (evaluatedLazyType === undefined || evaluatedLazyType === Object) {
38
+ throw new CannotInjectValueError(target as Constructable<unknown>, propertyName as string);
39
+ }
40
+
41
+ return containerInstance.get<unknown>(evaluatedLazyType);
42
+ },
43
+ });
44
+ });
45
+ };
46
+ }