From 94deddabd798c7cde21208ceb745b350223d4d6d Mon Sep 17 00:00:00 2001 From: Sergey Kuchin <3on.gleip@gmail.com> Date: Thu, 18 May 2023 23:54:21 +0300 Subject: [PATCH 1/2] feat: implement DI container and inject decorator --- README.md | 51 ++++++--- examples/HttpGate/index.ts | 8 +- .../LogicService/adapters/Configurator.ts | 15 +++ examples/LogicService/adapters/Repository.ts | 17 +++ examples/LogicService/adapters/index.ts | 2 + .../LogicService/domain/ports/Configurator.ts | 4 + examples/LogicService/domain/ports/Math.ts | 7 ++ .../LogicService/domain/ports/Repository.ts | 3 + examples/LogicService/domain/ports/Storage.ts | 1 + examples/LogicService/domain/ports/index.ts | 4 + examples/LogicService/index.ts | 6 +- examples/LogicService/interfaces.ts | 9 ++ examples/LogicService/inversion.types.ts | 6 + examples/LogicService/methods/GetUser.ts | 21 ++++ examples/LogicService/methods/WeirdSum.ts | 8 +- examples/LogicService/service.schema.json | 19 ++++ examples/LogicService/service.ts | 24 +++- package.json | 2 +- src/Client.ts | 8 +- src/Container.ts | 107 ++++++++++++++++++ src/Service.ts | 66 ++++++++--- src/__tests__/Client.ts | 4 +- src/__tests__/Container.ts | 57 ++++++++++ src/index.ts | 1 + src/injector.ts | 16 ++- src/interfaces.ts | 7 ++ 26 files changed, 424 insertions(+), 49 deletions(-) create mode 100644 examples/LogicService/adapters/Configurator.ts create mode 100644 examples/LogicService/adapters/Repository.ts create mode 100644 examples/LogicService/adapters/index.ts create mode 100644 examples/LogicService/domain/ports/Configurator.ts create mode 100644 examples/LogicService/domain/ports/Math.ts create mode 100644 examples/LogicService/domain/ports/Repository.ts create mode 100644 examples/LogicService/domain/ports/Storage.ts create mode 100644 examples/LogicService/domain/ports/index.ts create mode 100644 examples/LogicService/inversion.types.ts create mode 100644 examples/LogicService/methods/GetUser.ts create mode 100644 src/Container.ts create mode 100644 src/__tests__/Container.ts diff --git a/README.md b/README.md index 22ddef2..aaefa3d 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,18 @@ # nsc-toolkit Содержание -1. [О библиотеке](#о-библиотеке) -2. [Возможности](#возможности) -3. [Установка](#установка) -4. [Быстрый старт](#быстрый-старт) -5. [Основные компоненты библиотеки](#основные-компоненты-библиотеки) -6. [Рекомендации](#рекомендации) -7. [Пример использования](#пример-использования) -8. [Сворачивание сервисов в монолитное приложение](#сворачивание-сервисов-в-монолитное-приложение) +- [nsc-toolkit](#nsc-toolkit) + - [О библиотеке](#о-библиотеке) + - [Возможности](#возможности) + - [Установка](#установка) + - [Быстрый старт](#быстрый-старт) + - [Переменные окружения](#переменные-окружения) + - [Основные компоненты библиотеки](#основные-компоненты-библиотеки) + - [Рекомендации](#рекомендации) + - [Пример использования](#пример-использования) + - [Описание каталога `examples`](#описание-каталога-examples) + - [Сворачивание сервисов в монолитное приложение](#сворачивание-сервисов-в-монолитное-приложение) + - [Инверсия зависимостей и DI-контейнер](#инверсия-зависимостей-и-di-контейнер) ## О библиотеке @@ -269,7 +273,20 @@ npm i - `getListener` 一 метод получения объекта `EventEmitter` для подписки на события сервиса. -4. Декораторы. Применение декораторов: +4. Класс `Container`. Реализует DI-контейнре. Сам класс не доступен для импорта, а доступен только его экземпляр, что позволяет реализовать шаблон Singlton. В микросервисном варианте контейнер один на сервис. В монолитном варианте использования контейнер один на все приложение. За счет использользования объектов Symbol в качестве ключей для привязки зависимостей исключены коллизии при привязки зависимостей в разных частях приложения через один контейнре. Для привязки зависимости к ключу необходимо указать тип зависимости. Это нужно чтобы библиотека могла корректно создать ээкземпляр зависимости. Всего существуют 3 вида зависимостей: + +- `service` 一 сервис как зависимости. +- `adapter` 一 класс с набором асинхронных методов. Например репозиторий или фасад от стороннего API. +- `constant`一 обычный объект. Например объект с конфигурацией. + +Публичные методы: + +- `bind` 一 привязать реализацию к ключу. +- `unbind` 一 отвязать реализацию от ключа. +- `get` 一 получить реализацию по ключу. +- `getInstance` 一 получить экземпляр реализации. Для зависимости с типом `service` нельзя получить экземпляр через этот метод поскольку для создания экземпляра сервиса требуется контекст в рамках которого он будет работать. Для зависимости с типом `constant` вернется привязанный объект. + +5. Декораторы. Применение декораторов: * `@service`: - Если в методе сервиса вызывается метод другого сервиса, то клиент сервиса зависимости следует подключить через декоратор `@service`, а к самому классу метода применить декоратор `@related`. Тогда при вызове метода сервиса не пропадет контекст запроса и можно будет использовать распределенные трассировки. - Инъекция через декоратор `@service` позволяет использовать функциональность сборки приложения в монолит. @@ -325,8 +342,7 @@ Service/ ├── interfaces.ts ├── service.ts ├── start.ts -├── inversify.config.ts -├── inversify.types.ts +├── inversion.types ├── service.schema.json ├── package.json ├── package-lock.json @@ -355,8 +371,7 @@ Service/ - `interfaces.ts` 一 интерфейсы сервиса (генерируется автоматически); - `service.ts` 一 реализация сервиса (генерируется автоматически); - `start.ts` 一 точка входа для запуска сервиса (генерируется автоматически); - - `inversify.config.ts` 一 DI-контейнер (файл необязательный, однако рекомендуется реализовывать DI-контейнеры через библиотеку `inversify`, настройки которой хранятся в этом файле); - - `inversify.types.ts` 一 DI-контейнер (файл необязательный, однако рекомендуется реализовывать DI-контейнеры через библиотеку `inversify`, настройки которой хранятся в этом файле); + - `inversion.types.ts` 一 Типы зависимостей используемые в логике сервиса. Типы используются для получения зависимости. Реализация требуемой зависимости привязывается к контейнеру через тип в файле сервиса. [Описание встроенных возможностей инверсии зависимостей](#инверсия-зависимостей-и-di-контейнер). - `service.schema.json` 一 описание сервиса. Вся бизнес-логика сконцентрирована в двух местах структуры: @@ -430,4 +445,14 @@ Service/ ┌──┴───┐ ┌─────┴─────┐ ┌─────┴─────┐ │ Math │ │ Service_2 │ │ Service_3 │ └──────┘ └───────────┘ └───────────┘ +``` + +## Инверсия зависимостей и DI-контейнер + +Библиотека реализует возможности по инверсии зависимостей через DI-контейнер. Экземпляр контейнера можно получить импортировав его из библиотеки. Для описания существующих ключей зависимостей рекомендуется использовать отдеьный файл `inversion.types.ts` в корне сервиса. [Пример файла](./examples/LogicService/inversion.types.ts). Для внедрения зависимостей используются свойства класса метода. Для описания зависимости используется декоратор `inject`, который можно испортировать из библиотеки. В декоратор необходимо передать символьный ключ из файла `inversion.types.ts`. Саму привязку реализаций к DI-контейнеру рекомендуется осуществлять в основном файле сервиса `service.ts`. Пример с глубоковложенными зависимостями разных типов можно [посмотреть в методе](./examples/LogicService/methods/GetUser.ts). Цепочка внедряемых зависимостей. + +``` +┌---------┐ ┌───────────-┐ ┌───────────---┐ ┌---------┐ +| GetUser |--->│ Repository |--->| Configurator |--->| Storage | +└---------┘ └─────-─────-┘ └─────-─────---┘ └---------┘ ``` \ No newline at end of file diff --git a/examples/HttpGate/index.ts b/examples/HttpGate/index.ts index 3bf81ef..534d5c6 100644 --- a/examples/HttpGate/index.ts +++ b/examples/HttpGate/index.ts @@ -1,5 +1,5 @@ import { NatsConnection } from 'nats'; -import LogicService, { WeirdSumRequest } from '../LogicService'; +import LogicService, { WeirdSumRequest, GetUserRequest } from '../LogicService'; import MathService from '../MathService'; import { Service } from '../../src/Service'; import { SimpleCache } from '../SimpleCache'; @@ -29,7 +29,7 @@ const upHttpGate = async (service: Service) => { mathEmmiter.on('Notify', message => { logger.info('Get new event "Notify": ', message.data); - }) + }); const fastify = Fastify(); @@ -50,6 +50,10 @@ const upHttpGate = async (service: Service) => { return await service.buildService(LogicService, request.baggage).weirdSum(request.body); }); + fastify.get<{ Params: GetUserRequest }>('/logic/user/:userId', async request => { + return await service.buildService(LogicService, request.baggage).getUser(request.params); + }); + await fastify.listen({ port: HTTP_SERVICE_PORT }); }; diff --git a/examples/LogicService/adapters/Configurator.ts b/examples/LogicService/adapters/Configurator.ts new file mode 100644 index 0000000..d74b6ab --- /dev/null +++ b/examples/LogicService/adapters/Configurator.ts @@ -0,0 +1,15 @@ +import { inject } from '../../../src'; +import { TYPES } from '../inversion.types'; +import { StoragePort } from '../domain/ports'; + +export class Configurator { + @inject(TYPES.Storage) private storage: StoragePort; + + public async setUserId(userId: string) { + this.storage[userId] = true; + } + + public async userIdExist(userId: string) { + return !!this.storage[userId]; + } +} diff --git a/examples/LogicService/adapters/Repository.ts b/examples/LogicService/adapters/Repository.ts new file mode 100644 index 0000000..b77f866 --- /dev/null +++ b/examples/LogicService/adapters/Repository.ts @@ -0,0 +1,17 @@ +import { inject } from '../../../src'; +import { TYPES } from '../inversion.types'; +import { ConfiguratorPort } from '../domain/ports'; + +export class Repository { + @inject(TYPES.Configurator) private configurator: ConfiguratorPort; + + public async getUserById(userId: string) { + const exist = await this.configurator.userIdExist(userId); + + if (exist) { + return { firstName: 'Jon', lastName: 'Dow' }; + } + + return null; + } +} diff --git a/examples/LogicService/adapters/index.ts b/examples/LogicService/adapters/index.ts new file mode 100644 index 0000000..b0cd7d2 --- /dev/null +++ b/examples/LogicService/adapters/index.ts @@ -0,0 +1,2 @@ +export * from './Configurator'; +export * from './Repository'; \ No newline at end of file diff --git a/examples/LogicService/domain/ports/Configurator.ts b/examples/LogicService/domain/ports/Configurator.ts new file mode 100644 index 0000000..1c441bf --- /dev/null +++ b/examples/LogicService/domain/ports/Configurator.ts @@ -0,0 +1,4 @@ +export interface ConfiguratorPort { + setUserId(userId: string): Promise; + userIdExist(userId: string): Promise; +} diff --git a/examples/LogicService/domain/ports/Math.ts b/examples/LogicService/domain/ports/Math.ts new file mode 100644 index 0000000..c25efeb --- /dev/null +++ b/examples/LogicService/domain/ports/Math.ts @@ -0,0 +1,7 @@ +import { Readable } from 'stream'; + +export interface MathPort { + sum(params: { a: number; b: number }): Promise<{ result: number }>; + sumStream(params: Readable): Promise<{ result: number }>; + fibonacci(params: { length: number }): Promise; +} diff --git a/examples/LogicService/domain/ports/Repository.ts b/examples/LogicService/domain/ports/Repository.ts new file mode 100644 index 0000000..cf9e76f --- /dev/null +++ b/examples/LogicService/domain/ports/Repository.ts @@ -0,0 +1,3 @@ +export interface RepositoryPort { + getUserById(userId: string): Promise<{ firstName: string; lastName: string } | null>; +} diff --git a/examples/LogicService/domain/ports/Storage.ts b/examples/LogicService/domain/ports/Storage.ts new file mode 100644 index 0000000..fce2fac --- /dev/null +++ b/examples/LogicService/domain/ports/Storage.ts @@ -0,0 +1 @@ +export type StoragePort = Record; diff --git a/examples/LogicService/domain/ports/index.ts b/examples/LogicService/domain/ports/index.ts new file mode 100644 index 0000000..1971397 --- /dev/null +++ b/examples/LogicService/domain/ports/index.ts @@ -0,0 +1,4 @@ +export * from './Configurator'; +export * from './Math'; +export * from './Repository'; +export * from './Storage'; diff --git a/examples/LogicService/index.ts b/examples/LogicService/index.ts index 30b558c..39cef51 100644 --- a/examples/LogicService/index.ts +++ b/examples/LogicService/index.ts @@ -1,6 +1,6 @@ import { Client } from '../../src/Client'; import { NatsConnection } from 'nats'; -import { WeirdSumRequest, WeirdSumResponse } from './interfaces'; +import { WeirdSumRequest, WeirdSumResponse, GetUserRequest, GetUserResponse } from './interfaces'; import { Baggage, CacheSettings } from '../../src/interfaces'; import { name, methods } from './service.schema.json'; export * from './interfaces'; @@ -13,4 +13,8 @@ export default class ServiceMathClient extends Client { public async weirdSum(payload: WeirdSumRequest) { return this.request(`${name}.${methods.WeirdSum.action}`, payload, methods.WeirdSum); } + + public async getUser(payload: GetUserRequest) { + return this.request(`${name}.${methods.GetUser.action}`, payload, methods.GetUser); + } } diff --git a/examples/LogicService/interfaces.ts b/examples/LogicService/interfaces.ts index 3c77e54..add37e2 100644 --- a/examples/LogicService/interfaces.ts +++ b/examples/LogicService/interfaces.ts @@ -8,3 +8,12 @@ export type WeirdSumRequest = { export type WeirdSumResponse = { result: number; }; + +export type GetUserRequest = { + userId: string; +}; + +export type GetUserResponse = { + firstName: string; + lastName: string; +}; diff --git a/examples/LogicService/inversion.types.ts b/examples/LogicService/inversion.types.ts new file mode 100644 index 0000000..7ac2552 --- /dev/null +++ b/examples/LogicService/inversion.types.ts @@ -0,0 +1,6 @@ +export const TYPES = { + Math: Symbol.for('Math'), + Repository: Symbol('Repository'), + Configurator: Symbol('Configurator'), + Storage: Symbol('Storage'), +}; diff --git a/examples/LogicService/methods/GetUser.ts b/examples/LogicService/methods/GetUser.ts new file mode 100644 index 0000000..7f68585 --- /dev/null +++ b/examples/LogicService/methods/GetUser.ts @@ -0,0 +1,21 @@ +import { GetUserRequest, GetUserResponse } from '../interfaces'; +import { inject } from '../../../src/injector'; +import { methods } from '../service.schema.json'; +import { TYPES } from '../inversion.types'; +import { RepositoryPort } from '../domain/ports'; + +import { BaseMethod } from '../../../src/Method'; + +export class GetUser extends BaseMethod { + static settings = methods.GetUser; + @inject(TYPES.Repository) private repository: RepositoryPort; + + public async handler({ userId }: GetUserRequest): Promise { + const result = await this.repository.getUserById(userId); + if (!result) { + throw new Error(`User ${userId} not found!`); + } + + return result; + } +} diff --git a/examples/LogicService/methods/WeirdSum.ts b/examples/LogicService/methods/WeirdSum.ts index d4a5466..c09f9ef 100644 --- a/examples/LogicService/methods/WeirdSum.ts +++ b/examples/LogicService/methods/WeirdSum.ts @@ -1,14 +1,14 @@ import { WeirdSumRequest, WeirdSumResponse } from '../interfaces'; -import { related, service } from '../../../src/injector'; +import { inject } from '../../../src/injector'; import { methods } from '../service.schema.json'; +import { TYPES } from '../inversion.types'; +import { MathPort } from '../domain/ports/Math'; -import Math from '../../MathService/index'; import { BaseMethod } from '../../../src/Method'; -@related export class WeirdSum extends BaseMethod { static settings = methods.WeirdSum; - @service(Math) private math: Math; + @inject(TYPES.Math) private math: MathPort; public async handler(request: WeirdSumRequest): Promise { this.logger.info('sum started: ', request); diff --git a/examples/LogicService/service.schema.json b/examples/LogicService/service.schema.json index 1950212..97046c9 100644 --- a/examples/LogicService/service.schema.json +++ b/examples/LogicService/service.schema.json @@ -26,6 +26,25 @@ }, "required": ["result"] } + }, + "GetUser": { + "action": "getuser", + "description": "Get user object", + "request": { + "type": "object", + "properties": { + "userId": { "type": "string" } + }, + "required": ["a", "b"] + }, + "response": { + "type": "object", + "properties": { + "firstName": { "type": "string" }, + "lastName": { "type": "string" } + }, + "required": ["result"] + } } } } \ No newline at end of file diff --git a/examples/LogicService/service.ts b/examples/LogicService/service.ts index 5ea9eba..298938b 100644 --- a/examples/LogicService/service.ts +++ b/examples/LogicService/service.ts @@ -1,15 +1,35 @@ -import { Service } from '../../src/Service'; +import { Service, DependencyType, container } from '../../src'; import { connect, NatsConnection } from 'nats'; import { name } from './service.schema.json'; +import { TYPES } from './inversion.types'; +// Ports +import { MathPort, RepositoryPort, ConfiguratorPort, StoragePort } from './domain/ports'; + +// Adapters +import { Configurator, Repository } from './adapters'; + +// Services +import Math from '../MathService/index'; + +// Methods import { WeirdSum } from './methods/WeirdSum'; +import { GetUser } from './methods/GetUser'; export const service = async (broker?: NatsConnection) => { const brokerConnection = broker || (await connect({ servers: ['localhost:4222'] })); + + const storage = { test: true }; + + container.bind(TYPES.Math, DependencyType.SERVICE, Math); + container.bind(TYPES.Repository, DependencyType.ADAPTER, Repository); + container.bind(TYPES.Configurator, DependencyType.ADAPTER, Configurator); + container.bind(TYPES.Storage, DependencyType.CONSTANT, storage); + const service = new Service({ name, brokerConnection, - methods: [WeirdSum], + methods: [WeirdSum, GetUser], }); await service.start(); return service; diff --git a/package.json b/package.json index 5fe1a6f..78d47b2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "./dist/types/index.d.ts", "scripts": { "build": "rm -rf ./dist && tsc", - "test": "rm -rf ./dist && NODE_PATH=../ npx jest --maxWorkers=50% --coverage --forceExit" + "test": "rm -rf ./dist && NODE_PATH=../ npx jest --coverage --forceExit" }, "keywords": [], "author": "DevHive crew", diff --git a/src/Client.ts b/src/Client.ts index b8e6219..bb5fdb4 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -1,10 +1,10 @@ import * as opentelemetry from '@opentelemetry/api'; import Ajv from 'ajv'; -import { createHash } from 'crypto'; -import * as http from 'http'; +import { createHash } from 'node:crypto'; +import * as http from 'node:http'; import { JetStreamSubscription, JsMsg, Msg, JSONCodec, Subscription } from 'nats'; -import { EventEmitter, Readable } from 'stream'; -import { setTimeout } from 'timers/promises'; +import { EventEmitter, Readable } from 'node:stream'; +import { setTimeout } from 'node:timers/promises'; import { CacheSettings } from '.'; import { Baggage, diff --git a/src/Container.ts b/src/Container.ts new file mode 100644 index 0000000..fbff2fd --- /dev/null +++ b/src/Container.ts @@ -0,0 +1,107 @@ +import { DependencyType, ClientService } from '.'; +import { dependencyStorageMetaKet } from './injector'; + +type Constant = Record; + +type Service = ClientService; +export type Adapter = new () => R; + +type Dependency = Service | Adapter | Constant; + +type ServiceDependency = { type: typeof DependencyType.SERVICE; value: Service }; +type AdapterDependency = { type: typeof DependencyType.ADAPTER; value: Adapter }; +type ConstantDependency = { type: typeof DependencyType.CONSTANT; value: Constant }; + +type ContainerValue = { type: DependencyType; value: Dependency }; + +class Container { + private readonly container = new Map(); + + private inject(dependency: ContainerValue): ContainerValue { + if (this.isServiceDependency(dependency)) { + return dependency; + } + + const deepDependencies: Map | undefined = Reflect.getMetadata( + dependencyStorageMetaKet, + dependency.value, + ); + + if (deepDependencies && deepDependencies.size) { + deepDependencies.forEach((key, propertyName) => { + const deepDependency = this.get(key); + + const dependencyProto = dependency.value.prototype; + + if (this.isAdapterDependency(deepDependency)) { + dependencyProto[propertyName] = new deepDependency.value(); + } + + if (this.isConstantDependency(deepDependency)) { + dependencyProto[propertyName] = deepDependency.value; + } + }); + + return dependency; + } + + return dependency; + } + + private isServiceDependency(dependency: ContainerValue): dependency is ServiceDependency { + return dependency.type === DependencyType.SERVICE; + } + + private isAdapterDependency(dependency: ContainerValue): dependency is AdapterDependency { + return dependency.type === DependencyType.ADAPTER; + } + + private isConstantDependency(dependency: ContainerValue): dependency is ConstantDependency { + return dependency.type === DependencyType.CONSTANT; + } + + bind>(key: symbol, type: typeof DependencyType.SERVICE, value: ClientService): void; + bind>(key: symbol, type: typeof DependencyType.ADAPTER, value: Adapter): void; + bind>(key: symbol, type: typeof DependencyType.CONSTANT, value: R): void; + public bind>( + key: symbol, + type: DependencyType, + value: ClientService | Adapter | R, + ) { + this.container.set(key, { type, value }); + } + + public unbind(key: symbol) { + this.container.delete(key); + } + + public get(key: symbol) { + const dependency = this.container.get(key); + + if (!dependency) { + throw new Error(`Dependency ${key.toString()} is not bound to the container`); + } + + return this.inject(dependency); + } + + public getInstance(key: symbol): R | null { + const dependency = this.get(key); + + if (this.isServiceDependency(dependency)) { + throw new Error(`Unable to get service instance`); + } + + if (this.isConstantDependency(dependency)) { + return dependency.value as R; + } + + if (this.isAdapterDependency(dependency)) { + return new dependency.value() as R; + } + + return null; + } +} + +export const container = new Container(); diff --git a/src/Service.ts b/src/Service.ts index 524dcc6..7980c66 100644 --- a/src/Service.ts +++ b/src/Service.ts @@ -1,21 +1,31 @@ import { Root } from './Root'; import { JSONCodec, Subscription } from 'nats'; -import { Message, Emitter, Method, ServiceOptions, Baggage, ExternalBaggage, ClientService } from './interfaces'; +import { + Message, + Emitter, + Method, + ServiceOptions, + Baggage, + ExternalBaggage, + ClientService, + DependencyType, +} from './interfaces'; import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { Tracer, Context, Span, trace } from '@opentelemetry/api'; -import { InstanceContainer, ServiceContainer } from './injector'; +import { dependencyStorageMetaKet, InstanceContainer, ServiceContainer, Dependency, Instance } from './injector'; import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; -import { IncomingHttpHeaders, ServerResponse } from 'http'; -import { Readable, Transform } from 'stream'; -import { pipeline } from 'stream/promises'; +import { IncomingHttpHeaders, ServerResponse } from 'node:http'; +import { Readable, Transform } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; import { Logs } from '@lad-tech/toolbelt'; -import * as http from 'http'; -import * as os from 'os'; -import { setTimeout } from 'timers/promises'; -import { promisify } from 'util'; +import * as http from 'node:http'; +import * as os from 'node:os'; +import { setTimeout } from 'node:timers/promises'; +import { promisify } from 'node:util'; import { StreamManager } from './StreamManager'; +import { Adapter, container } from './Container'; export class Service extends Root { public emitter = {} as E; @@ -103,18 +113,42 @@ export class Service extends Root { /** * Creating an object to inject into Method (business logic) */ - private createObjectWithDependencies(action: string, tracer: Tracer, baggage?: Baggage) { - const services = ServiceContainer.get(action); + private createObjectWithDependencies(method: Method, tracer: Tracer, baggage?: Baggage) { + const services = ServiceContainer.get(method.settings.action) || new Map(); + const instances = InstanceContainer.get(method.settings.action) || new Map(); + const dependences: Record = {}; - if (services?.size) { + + const dependencyStorage: Map | undefined = Reflect.getMetadata(dependencyStorageMetaKet, method); + + if (dependencyStorage && dependencyStorage.size) { + dependencyStorage.forEach((dependencyKey, propertyName) => { + const dependency = container.get(dependencyKey); + + if (dependency.type === DependencyType.SERVICE) { + services.set(propertyName, dependency.value as Dependency); + } + + if (dependency.type === DependencyType.ADAPTER) { + instances.set(propertyName, new (dependency.value as Adapter)() as Instance); + } + + if (dependency.type === DependencyType.CONSTANT) { + dependences[propertyName] = dependency; + } + }); + } + + if (services.size) { services.forEach((Dependence, key) => { dependences[key] = new Dependence(this.broker, baggage, this.options.cache); }); } + const perform = this.perform; const context = this.getContext(baggage); - const instances = InstanceContainer.get(action); - if (instances?.size) { + + if (instances.size) { instances.forEach((instance, key) => { const trap = { get(target: any, propKey: string, receiver: any) { @@ -133,7 +167,7 @@ export class Service extends Root { } dependences['logger'] = new Logs.Logger({ - location: `${this.serviceName}.${action}`, + location: `${this.serviceName}.${method.settings.action}`, metadata: baggage, outputFormatter: this.options.loggerOutputFormatter, }); @@ -311,7 +345,7 @@ export class Service extends Root { try { const requestedDependencies = this.createObjectWithDependencies( - Method.settings.action, + Method, tracer, this.getNextBaggage(span, baggage), ); diff --git a/src/__tests__/Client.ts b/src/__tests__/Client.ts index aec65f9..f76be19 100644 --- a/src/__tests__/Client.ts +++ b/src/__tests__/Client.ts @@ -1,7 +1,7 @@ import { JSONCodec } from 'nats'; -import { PassThrough, EventEmitter, Readable } from 'stream'; -import * as http from 'http'; +import { PassThrough, EventEmitter, Readable } from 'node:stream'; +import * as http from 'node:http'; import { MathClient } from './fixtures/MathService'; beforeEach(() => { diff --git a/src/__tests__/Container.ts b/src/__tests__/Container.ts new file mode 100644 index 0000000..771eee9 --- /dev/null +++ b/src/__tests__/Container.ts @@ -0,0 +1,57 @@ +import { container, Service } from '..'; +import Logic from '../../examples/LogicService/index'; +import Math from '../../examples/MathService/index'; +import { service as logicService } from '../../examples/LogicService/service'; +import { DependencyType } from '..'; +import { RepositoryPort, ConfiguratorPort, StoragePort, MathPort } from '../../examples/LogicService/domain/ports'; +import { TYPES } from '../../examples/LogicService/inversion.types'; + +import { Configurator, Repository } from '../../examples/LogicService/adapters'; + +describe('Successful injection of multi-level dependencies of different types', () => { + const storage = { test: true }; + + container.bind(TYPES.Repository, DependencyType.ADAPTER, Repository); + container.bind(TYPES.Configurator, DependencyType.ADAPTER, Configurator); + container.bind(TYPES.Storage, DependencyType.CONSTANT, storage); + + process.env.DEFAULT_REPONSE_TIMEOUT = '50000'; + + const service = new Service({ + name: 'ServiceForTest', + methods: [], + }); + + logicService(service.broker); + + test('The required dependencies are successfully injected into the method', async () => { + const logicClient = service.buildService(Logic); + const result = await logicClient.getUser({ userId: 'test' }); + + expect(result).toEqual({ firstName: 'Jon', lastName: 'Dow' }); + }); + + describe('Getting an instance of a dependency', () => { + test('Из контейнера можно получить экземпляр зависимости', async () => { + const repository = container.getInstance(TYPES.Repository); + + const result = await repository?.getUserById('test'); + + expect(result).toEqual({ firstName: 'Jon', lastName: 'Dow' }); + }); + + test('If the dependency is not bound to a container, an error is returned', async () => { + container.unbind(TYPES.Repository); + expect(() => container.getInstance(TYPES.Repository)).toThrow(); + }); + + test('Can`t get service instance', async () => { + container.bind(TYPES.Math, DependencyType.SERVICE, Math); + expect(() => container.getInstance(TYPES.Math)).toThrow(); + }); + + test('If an instance of a constant is requested, a constant is returned', async () => { + expect(container.getInstance(TYPES.Storage)).toEqual(storage); + }); + }); +}); diff --git a/src/index.ts b/src/index.ts index 84419af..c46e5fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,5 @@ export * from './Service'; export * from './Client'; export * from './Method'; export * from './injector'; +export * from './Container'; export * from './interfaces'; diff --git a/src/injector.ts b/src/injector.ts index 122c6e1..5d726a5 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -2,13 +2,15 @@ import 'reflect-metadata'; import { Method, ClientService } from './interfaces'; export type Instance = Record Promise>; -export type Dependence = ClientService; -export type DependenceStorage = Map; +export type Dependency = ClientService; +export type DependenceStorage = Map; export type InstanceStorage = Map; const serviceMetaKey = Symbol('services'); const instanceMetaKey = Symbol('instance'); +export const dependencyStorageMetaKet = Symbol('dependency'); + export const ServiceContainer: Map = new Map(); export const InstanceContainer: Map = new Map(); @@ -19,7 +21,7 @@ export function related(target: T) { InstanceContainer.set(target.settings.action, instances); } -function setMetaData(item: Dependence | Instance, itemName: string, metaKey: symbol, target: any) { +function setMetaData(item: Dependency | Instance | symbol, itemName: string, metaKey: symbol, target: any) { let storage: Map; if (Reflect.hasMetadata(metaKey, target)) { storage = Reflect.getMetadata(metaKey, target); @@ -30,7 +32,7 @@ function setMetaData(item: Dependence | Instance, itemName: string, metaKey: sym storage.set(itemName, item); } -export function service(dependence: Dependence) { +export function service(dependence: Dependency) { return function (target: any, dependenceName: string): void { setMetaData(dependence, dependenceName, serviceMetaKey, target); }; @@ -41,3 +43,9 @@ export function instance(instance: Instance) { setMetaData(instance, instanceName, instanceMetaKey, target); }; } + +export function inject(key: symbol) { + return function (target: any, property: string): void { + setMetaData(key, property, dependencyStorageMetaKet, target.constructor); + }; +} diff --git a/src/interfaces.ts b/src/interfaces.ts index fdcc8c2..adeef73 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -162,3 +162,10 @@ export interface CacheService { get: (key: string) => Promise; delete: (key: string) => Promise; } + +export type DependencyType = typeof DependencyType[keyof typeof DependencyType]; +export const DependencyType = { + SERVICE: 'service', // External service + ADAPTER: 'adapter', // A class with asynchronous methods such as a repository + CONSTANT: 'constant', // Just an object +} as const; \ No newline at end of file From aa65b341ca4a2f2fc85b05cafe9533300e212645 Mon Sep 17 00:00:00 2001 From: Sergey Kuchin <3on.gleip@gmail.com> Date: Tue, 23 May 2023 17:31:00 +0300 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20inject=20=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=81=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=B0=D0=BC=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=81?= =?UTF-8?q?=D1=82=D1=80=D1=83=D0=BA=D1=82=D0=BE=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- examples/LogicService/index.ts | 13 +++- examples/LogicService/interfaces.ts | 9 +++ examples/LogicService/methods/GetUser.ts | 1 + examples/LogicService/methods/GetUserV2.ts | 24 ++++++ examples/LogicService/service.schema.json | 23 +++++- examples/LogicService/service.ts | 3 +- src/Container.ts | 51 +++++++------ src/Service.ts | 86 ++++++++++++++++------ src/__tests__/Container.ts | 9 ++- src/injector.ts | 46 ++++++++++-- src/interfaces.ts | 4 +- 12 files changed, 212 insertions(+), 59 deletions(-) create mode 100644 examples/LogicService/methods/GetUserV2.ts diff --git a/README.md b/README.md index aaefa3d..5747ede 100644 --- a/README.md +++ b/README.md @@ -449,7 +449,7 @@ Service/ ## Инверсия зависимостей и DI-контейнер -Библиотека реализует возможности по инверсии зависимостей через DI-контейнер. Экземпляр контейнера можно получить импортировав его из библиотеки. Для описания существующих ключей зависимостей рекомендуется использовать отдеьный файл `inversion.types.ts` в корне сервиса. [Пример файла](./examples/LogicService/inversion.types.ts). Для внедрения зависимостей используются свойства класса метода. Для описания зависимости используется декоратор `inject`, который можно испортировать из библиотеки. В декоратор необходимо передать символьный ключ из файла `inversion.types.ts`. Саму привязку реализаций к DI-контейнеру рекомендуется осуществлять в основном файле сервиса `service.ts`. Пример с глубоковложенными зависимостями разных типов можно [посмотреть в методе](./examples/LogicService/methods/GetUser.ts). Цепочка внедряемых зависимостей. +Библиотека реализует возможности по инверсии зависимостей через DI-контейнер. Экземпляр контейнера можно получить импортировав его из библиотеки. Для описания существующих ключей зависимостей рекомендуется использовать отдеьный файл `inversion.types.ts` в корне сервиса. [Пример файла](./examples/LogicService/inversion.types.ts). Для внедрения зависимостей используются свойства класса метода или параметры конструктора. Для описания зависимости используется декоратор `inject`, который можно испортировать из библиотеки. В декоратор необходимо передать символьный ключ из файла `inversion.types.ts`. Саму привязку реализаций к DI-контейнеру рекомендуется осуществлять в основном файле сервиса `service.ts`. Пример с глубоковложенными зависимостями разных типов можно [посмотреть в методе](./examples/LogicService/methods/GetUser.ts). Цепочка внедряемых зависимостей. ``` ┌---------┐ ┌───────────-┐ ┌───────────---┐ ┌---------┐ diff --git a/examples/LogicService/index.ts b/examples/LogicService/index.ts index 39cef51..f209880 100644 --- a/examples/LogicService/index.ts +++ b/examples/LogicService/index.ts @@ -1,6 +1,13 @@ import { Client } from '../../src/Client'; import { NatsConnection } from 'nats'; -import { WeirdSumRequest, WeirdSumResponse, GetUserRequest, GetUserResponse } from './interfaces'; +import { + WeirdSumRequest, + WeirdSumResponse, + GetUserRequest, + GetUserResponse, + GetUserRequestV2, + GetUserResponseV2, +} from './interfaces'; import { Baggage, CacheSettings } from '../../src/interfaces'; import { name, methods } from './service.schema.json'; export * from './interfaces'; @@ -17,4 +24,8 @@ export default class ServiceMathClient extends Client { public async getUser(payload: GetUserRequest) { return this.request(`${name}.${methods.GetUser.action}`, payload, methods.GetUser); } + + public async getUserV2(payload: GetUserRequestV2) { + return this.request(`${name}.${methods.GetUserV2.action}`, payload, methods.GetUserV2); + } } diff --git a/examples/LogicService/interfaces.ts b/examples/LogicService/interfaces.ts index add37e2..9f52a22 100644 --- a/examples/LogicService/interfaces.ts +++ b/examples/LogicService/interfaces.ts @@ -17,3 +17,12 @@ export type GetUserResponse = { firstName: string; lastName: string; }; + +export type GetUserRequestV2 = { + userId: string; +}; + +export type GetUserResponseV2 = { + firstName: string; + lastName: string; +}; diff --git a/examples/LogicService/methods/GetUser.ts b/examples/LogicService/methods/GetUser.ts index 7f68585..396676a 100644 --- a/examples/LogicService/methods/GetUser.ts +++ b/examples/LogicService/methods/GetUser.ts @@ -8,6 +8,7 @@ import { BaseMethod } from '../../../src/Method'; export class GetUser extends BaseMethod { static settings = methods.GetUser; + @inject(TYPES.Repository) private repository: RepositoryPort; public async handler({ userId }: GetUserRequest): Promise { diff --git a/examples/LogicService/methods/GetUserV2.ts b/examples/LogicService/methods/GetUserV2.ts new file mode 100644 index 0000000..5a51c02 --- /dev/null +++ b/examples/LogicService/methods/GetUserV2.ts @@ -0,0 +1,24 @@ +import { GetUserRequest, GetUserResponse } from '../interfaces'; +import { inject } from '../../../src/injector'; +import { methods } from '../service.schema.json'; +import { TYPES } from '../inversion.types'; +import { RepositoryPort } from '../domain/ports'; + +import { BaseMethod } from '../../../src/Method'; + +export class GetUserV2 extends BaseMethod { + static settings = methods.GetUserV2; + + constructor(@inject(TYPES.Repository) private repository: RepositoryPort) { + super(); + } + + public async handler({ userId }: GetUserRequest): Promise { + const result = await this.repository.getUserById(userId); + if (!result) { + throw new Error(`User ${userId} not found!`); + } + + return result; + } +} diff --git a/examples/LogicService/service.schema.json b/examples/LogicService/service.schema.json index 97046c9..07d6c1a 100644 --- a/examples/LogicService/service.schema.json +++ b/examples/LogicService/service.schema.json @@ -15,7 +15,7 @@ "type": "object", "properties": { "a": { "type": "number" }, - "b": {"type": "number" } + "b": { "type": "number" } }, "required": ["a", "b"] }, @@ -45,6 +45,25 @@ }, "required": ["result"] } + }, + "GetUserV2": { + "action": "getuserv2", + "description": "Get user object", + "request": { + "type": "object", + "properties": { + "userId": { "type": "string" } + }, + "required": ["a", "b"] + }, + "response": { + "type": "object", + "properties": { + "firstName": { "type": "string" }, + "lastName": { "type": "string" } + }, + "required": ["result"] + } } } -} \ No newline at end of file +} diff --git a/examples/LogicService/service.ts b/examples/LogicService/service.ts index 298938b..084baff 100644 --- a/examples/LogicService/service.ts +++ b/examples/LogicService/service.ts @@ -15,6 +15,7 @@ import Math from '../MathService/index'; // Methods import { WeirdSum } from './methods/WeirdSum'; import { GetUser } from './methods/GetUser'; +import { GetUserV2 } from './methods/GetUserV2'; export const service = async (broker?: NatsConnection) => { const brokerConnection = broker || (await connect({ servers: ['localhost:4222'] })); @@ -29,7 +30,7 @@ export const service = async (broker?: NatsConnection) => { const service = new Service({ name, brokerConnection, - methods: [WeirdSum, GetUser], + methods: [WeirdSum, GetUser, GetUserV2], }); await service.start(); return service; diff --git a/src/Container.ts b/src/Container.ts index fbff2fd..e1b4a29 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -1,10 +1,9 @@ -import { DependencyType, ClientService } from '.'; -import { dependencyStorageMetaKet } from './injector'; +import { DependencyType, ClientService, dependencyStorageMetaKey, ConstructorDependencyKey } from '.'; type Constant = Record; type Service = ClientService; -export type Adapter = new () => R; +export type Adapter = new (...args: unknown[]) => R; type Dependency = Service | Adapter | Constant; @@ -17,35 +16,45 @@ type ContainerValue = { type: DependencyType; value: Dependency }; class Container { private readonly container = new Map(); - private inject(dependency: ContainerValue): ContainerValue { + private buildDependency(key: symbol) { + const deepDependency = this.get(key); + + if (this.isAdapterDependency(deepDependency.dependency)) { + return new deepDependency.dependency.value(...deepDependency.constructor); + } + + if (this.isConstantDependency(deepDependency.dependency)) { + return deepDependency.dependency.value; + } + } + + private inject(dependency: ContainerValue): { dependency: ContainerValue; constructor: Array } { if (this.isServiceDependency(dependency)) { - return dependency; + return { dependency, constructor: [] }; } - const deepDependencies: Map | undefined = Reflect.getMetadata( - dependencyStorageMetaKet, + const deepDependencies: Map | undefined = Reflect.getMetadata( + dependencyStorageMetaKey, dependency.value, ); if (deepDependencies && deepDependencies.size) { - deepDependencies.forEach((key, propertyName) => { - const deepDependency = this.get(key); - - const dependencyProto = dependency.value.prototype; + const constructor: unknown[] = []; - if (this.isAdapterDependency(deepDependency)) { - dependencyProto[propertyName] = new deepDependency.value(); - } - - if (this.isConstantDependency(deepDependency)) { - dependencyProto[propertyName] = deepDependency.value; + deepDependencies.forEach((key, propertyName) => { + if (Array.isArray(key)) { + key.forEach((item, index) => { + constructor[index] = this.buildDependency(item); + }); + } else { + dependency.value.prototype[propertyName] = this.buildDependency(key); } }); - return dependency; + return { dependency, constructor }; } - return dependency; + return { dependency, constructor: [] }; } private isServiceDependency(dependency: ContainerValue): dependency is ServiceDependency { @@ -86,7 +95,7 @@ class Container { } public getInstance(key: symbol): R | null { - const dependency = this.get(key); + const { dependency, constructor } = this.get(key); if (this.isServiceDependency(dependency)) { throw new Error(`Unable to get service instance`); @@ -97,7 +106,7 @@ class Container { } if (this.isAdapterDependency(dependency)) { - return new dependency.value() as R; + return new dependency.value(...constructor) as R; } return null; diff --git a/src/Service.ts b/src/Service.ts index 7980c66..acf58c3 100644 --- a/src/Service.ts +++ b/src/Service.ts @@ -9,12 +9,19 @@ import { ExternalBaggage, ClientService, DependencyType, -} from './interfaces'; + Adapter, + container, + InstanceContainer, + ServiceContainer, + Dependency, + Instance, + dependencyStorageMetaKey, + ConstructorDependencyKey, +} from '.'; import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; import { Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import { Tracer, Context, Span, trace } from '@opentelemetry/api'; -import { dependencyStorageMetaKet, InstanceContainer, ServiceContainer, Dependency, Instance } from './injector'; import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; import { IncomingHttpHeaders, ServerResponse } from 'node:http'; import { Readable, Transform } from 'node:stream'; @@ -25,7 +32,6 @@ import * as os from 'node:os'; import { setTimeout } from 'node:timers/promises'; import { promisify } from 'node:util'; import { StreamManager } from './StreamManager'; -import { Adapter, container } from './Container'; export class Service extends Root { public emitter = {} as E; @@ -110,6 +116,26 @@ export class Service extends Root { return query; } + /** + * Build trap for object with async methods + */ + private getTrap(instance: Instance, tracer: Tracer, baggage?: Baggage) { + const perform = this.perform; + const context = this.getContext(baggage); + return { + get(target: any, propKey: string, receiver: any) { + const method = Reflect.get(target, propKey, receiver); + if (typeof method === 'function') { + return function (...args: unknown[]) { + return perform(method, instance, args, tracer, context); + }; + } else { + return method; + } + }, + }; + } + /** * Creating an object to inject into Method (business logic) */ @@ -117,20 +143,47 @@ export class Service extends Root { const services = ServiceContainer.get(method.settings.action) || new Map(); const instances = InstanceContainer.get(method.settings.action) || new Map(); - const dependences: Record = {}; + const dependences: Record = { [ConstructorDependencyKey]: [] }; - const dependencyStorage: Map | undefined = Reflect.getMetadata(dependencyStorageMetaKet, method); + const dependencyStorage: Map | undefined = Reflect.getMetadata( + dependencyStorageMetaKey, + method, + ); if (dependencyStorage && dependencyStorage.size) { dependencyStorage.forEach((dependencyKey, propertyName) => { - const dependency = container.get(dependencyKey); + if (Array.isArray(dependencyKey)) { + if (propertyName === ConstructorDependencyKey) { + dependencyKey.forEach((item, index) => { + const { dependency, constructor } = container.get(item); + if (dependency.type === DependencyType.SERVICE) { + dependences[ConstructorDependencyKey][index] = new (dependency.value as Dependency)( + this.broker, + baggage, + this.options.cache, + ); + } + if (dependency.type === DependencyType.ADAPTER) { + const instance = new (dependency.value as Adapter)(...constructor); + const trap = this.getTrap(instance, tracer, baggage); + dependences[ConstructorDependencyKey][index] = new Proxy(instance, trap); + } + if (dependency.type === DependencyType.CONSTANT) { + dependences[ConstructorDependencyKey][index] = dependency.value; + } + }); + } + return; + } + + const { dependency, constructor } = container.get(dependencyKey); if (dependency.type === DependencyType.SERVICE) { services.set(propertyName, dependency.value as Dependency); } if (dependency.type === DependencyType.ADAPTER) { - instances.set(propertyName, new (dependency.value as Adapter)() as Instance); + instances.set(propertyName, new (dependency.value as Adapter)(...constructor) as Instance); } if (dependency.type === DependencyType.CONSTANT) { @@ -145,23 +198,9 @@ export class Service extends Root { }); } - const perform = this.perform; - const context = this.getContext(baggage); - if (instances.size) { instances.forEach((instance, key) => { - const trap = { - get(target: any, propKey: string, receiver: any) { - const method = Reflect.get(target, propKey, receiver); - if (typeof method === 'function') { - return function (...args: unknown[]) { - return perform(method, instance, args, tracer, context); - }; - } else { - return method; - } - }, - }; + const trap = this.getTrap(instance, tracer, baggage); dependences[key] = new Proxy(instance, trap); }); } @@ -178,7 +217,8 @@ export class Service extends Root { * Create Method (business logic) context */ private createMethodContext(Method: Method, dependencies: Record) { - const context = new Method(); + const constructor = (dependencies[ConstructorDependencyKey] as Array) || []; + const context = new Method(...constructor); for (const key in dependencies) { context[key] = dependencies[key]; } diff --git a/src/__tests__/Container.ts b/src/__tests__/Container.ts index 771eee9..412718a 100644 --- a/src/__tests__/Container.ts +++ b/src/__tests__/Container.ts @@ -24,13 +24,20 @@ describe('Successful injection of multi-level dependencies of different types', logicService(service.broker); - test('The required dependencies are successfully injected into the method', async () => { + test('Required dependencies successfully injected into method property', async () => { const logicClient = service.buildService(Logic); const result = await logicClient.getUser({ userId: 'test' }); expect(result).toEqual({ firstName: 'Jon', lastName: 'Dow' }); }); + test('Required dependencies successfully injected into method constructor parameter', async () => { + const logicClient = service.buildService(Logic); + const result = await logicClient.getUserV2({ userId: 'test' }); + + expect(result).toEqual({ firstName: 'Jon', lastName: 'Dow' }); + }); + describe('Getting an instance of a dependency', () => { test('Из контейнера можно получить экземпляр зависимости', async () => { const repository = container.getInstance(TYPES.Repository); diff --git a/src/injector.ts b/src/injector.ts index 5d726a5..4405c7b 100644 --- a/src/injector.ts +++ b/src/injector.ts @@ -1,7 +1,7 @@ import 'reflect-metadata'; import { Method, ClientService } from './interfaces'; -export type Instance = Record Promise>; +export type Instance = Record Promise>; export type Dependency = ClientService; export type DependenceStorage = Map; export type InstanceStorage = Map; @@ -9,11 +9,21 @@ export type InstanceStorage = Map; const serviceMetaKey = Symbol('services'); const instanceMetaKey = Symbol('instance'); -export const dependencyStorageMetaKet = Symbol('dependency'); +export const dependencyStorageMetaKey = Symbol('dependency'); export const ServiceContainer: Map = new Map(); export const InstanceContainer: Map = new Map(); +export const ConstructorDependencyKey = 'constructor'; + +interface MetaDataParam { + item: Dependency | Instance | symbol; + itemName: string; + metaKey: symbol; + target: any; + index?: number; +} + export function related(target: T) { const dependencies: DependenceStorage = Reflect.getMetadata(serviceMetaKey, target.prototype); const instances: InstanceStorage = Reflect.getMetadata(instanceMetaKey, target.prototype); @@ -21,31 +31,53 @@ export function related(target: T) { InstanceContainer.set(target.settings.action, instances); } -function setMetaData(item: Dependency | Instance | symbol, itemName: string, metaKey: symbol, target: any) { +function setMetaData({ item, itemName, metaKey, target, index }: MetaDataParam) { let storage: Map; + if (Reflect.hasMetadata(metaKey, target)) { storage = Reflect.getMetadata(metaKey, target); } else { storage = new Map(); Reflect.defineMetadata(metaKey, storage, target); } + + if (typeof index === 'number') { + let constructor: Array; + + if (storage.has(ConstructorDependencyKey)) { + constructor = storage.get(ConstructorDependencyKey) as Array; + } else { + constructor = []; + storage.set(ConstructorDependencyKey, constructor); + } + + constructor[index] = item; + return; + } + storage.set(itemName, item); } export function service(dependence: Dependency) { return function (target: any, dependenceName: string): void { - setMetaData(dependence, dependenceName, serviceMetaKey, target); + setMetaData({ item: dependence, itemName: dependenceName, metaKey: serviceMetaKey, target }); }; } export function instance(instance: Instance) { return function (target: any, instanceName: string): void { - setMetaData(instance, instanceName, instanceMetaKey, target); + setMetaData({ item: instance, itemName: instanceName, metaKey: instanceMetaKey, target }); }; } export function inject(key: symbol) { - return function (target: any, property: string): void { - setMetaData(key, property, dependencyStorageMetaKet, target.constructor); + return function (target: any, property: string, index?: number): void { + setMetaData({ + item: key, + itemName: property, + metaKey: dependencyStorageMetaKey, + target: typeof index === 'number' ? target : target.constructor, + index, + }); }; } diff --git a/src/interfaces.ts b/src/interfaces.ts index adeef73..738c840 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -26,7 +26,7 @@ export interface MethodSettings { export interface Method { settings: MethodSettings; - new (): { handler: (params: any) => Promise }; + new (...args: unknown[]): { handler: (params: any) => Promise }; } export type ClientService = new ( @@ -168,4 +168,4 @@ export const DependencyType = { SERVICE: 'service', // External service ADAPTER: 'adapter', // A class with asynchronous methods such as a repository CONSTANT: 'constant', // Just an object -} as const; \ No newline at end of file +} as const;