Skip to content

Commit

Permalink
feat: inject можно использовать с параметрами конструктора
Browse files Browse the repository at this point in the history
  • Loading branch information
gleip committed May 23, 2023
1 parent 94dedda commit aa65b34
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 59 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). Цепочка внедряемых зависимостей.

```
┌---------┐ ┌───────────-┐ ┌───────────---┐ ┌---------┐
Expand Down
13 changes: 12 additions & 1 deletion examples/LogicService/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,4 +24,8 @@ export default class ServiceMathClient extends Client {
public async getUser(payload: GetUserRequest) {
return this.request<GetUserResponse>(`${name}.${methods.GetUser.action}`, payload, methods.GetUser);
}

public async getUserV2(payload: GetUserRequestV2) {
return this.request<GetUserResponseV2>(`${name}.${methods.GetUserV2.action}`, payload, methods.GetUserV2);
}
}
9 changes: 9 additions & 0 deletions examples/LogicService/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ export type GetUserResponse = {
firstName: string;
lastName: string;
};

export type GetUserRequestV2 = {
userId: string;
};

export type GetUserResponseV2 = {
firstName: string;
lastName: string;
};
1 change: 1 addition & 0 deletions examples/LogicService/methods/GetUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GetUserResponse> {
Expand Down
24 changes: 24 additions & 0 deletions examples/LogicService/methods/GetUserV2.ts
Original file line number Diff line number Diff line change
@@ -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<GetUserResponse> {
const result = await this.repository.getUserById(userId);
if (!result) {
throw new Error(`User ${userId} not found!`);
}

return result;
}
}
23 changes: 21 additions & 2 deletions examples/LogicService/service.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"type": "object",
"properties": {
"a": { "type": "number" },
"b": {"type": "number" }
"b": { "type": "number" }
},
"required": ["a", "b"]
},
Expand Down Expand Up @@ -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"]
}
}
}
}
}
3 changes: 2 additions & 1 deletion examples/LogicService/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] }));
Expand All @@ -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;
Expand Down
51 changes: 30 additions & 21 deletions src/Container.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { DependencyType, ClientService } from '.';
import { dependencyStorageMetaKet } from './injector';
import { DependencyType, ClientService, dependencyStorageMetaKey, ConstructorDependencyKey } from '.';

type Constant = Record<string, any>;

type Service<R extends Constant = Constant> = ClientService<R>;
export type Adapter<R extends Constant = Constant> = new () => R;
export type Adapter<R extends Constant = Constant> = new (...args: unknown[]) => R;

type Dependency = Service | Adapter | Constant;

Expand All @@ -17,35 +16,45 @@ type ContainerValue = { type: DependencyType; value: Dependency };
class Container {
private readonly container = new Map<symbol, ContainerValue>();

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<unknown> } {
if (this.isServiceDependency(dependency)) {
return dependency;
return { dependency, constructor: [] };
}

const deepDependencies: Map<string, symbol> | undefined = Reflect.getMetadata(
dependencyStorageMetaKet,
const deepDependencies: Map<string, symbol | symbol[]> | 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 {
Expand Down Expand Up @@ -86,7 +95,7 @@ class Container {
}

public getInstance<R = Constant>(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`);
Expand All @@ -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;
Expand Down
86 changes: 63 additions & 23 deletions src/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<E extends Emitter = Emitter> extends Root {
public emitter = {} as E;
Expand Down Expand Up @@ -110,27 +116,74 @@ export class Service<E extends Emitter = Emitter> 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)
*/
private createObjectWithDependencies(method: Method, tracer: Tracer, baggage?: Baggage) {
const services = ServiceContainer.get(method.settings.action) || new Map<string, Dependency>();
const instances = InstanceContainer.get(method.settings.action) || new Map<string, Instance>();

const dependences: Record<string, unknown> = {};
const dependences: Record<string, any> = { [ConstructorDependencyKey]: [] };

const dependencyStorage: Map<string, symbol> | undefined = Reflect.getMetadata(dependencyStorageMetaKet, method);
const dependencyStorage: Map<string, symbol | symbol[]> | 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) {
Expand All @@ -145,23 +198,9 @@ export class Service<E extends Emitter = Emitter> 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);
});
}
Expand All @@ -178,7 +217,8 @@ export class Service<E extends Emitter = Emitter> extends Root {
* Create Method (business logic) context
*/
private createMethodContext(Method: Method, dependencies: Record<string, unknown>) {
const context = new Method();
const constructor = (dependencies[ConstructorDependencyKey] as Array<unknown>) || [];
const context = new Method(...constructor);
for (const key in dependencies) {
context[key] = dependencies[key];
}
Expand Down
9 changes: 8 additions & 1 deletion src/__tests__/Container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RepositoryPort>(TYPES.Repository);
Expand Down
Loading

0 comments on commit aa65b34

Please sign in to comment.