Skip to content

Commit

Permalink
feat: return observable in node fetch injector
Browse files Browse the repository at this point in the history
Co-authored-by: username1103 <[email protected]>
Co-authored-by: imdudu1 <[email protected]>
  • Loading branch information
3 people committed Jun 19, 2023
1 parent a26a24a commit 308f6c6
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 22 deletions.
4 changes: 2 additions & 2 deletions lib/builders/circuit-breaker.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export class CircuitBreakerBuilder {
constructor(readonly options?: CircuitBreaker.Options) {}

build(
executor: (...args: never[]) => Promise<any>,
): (...args: never[]) => Promise<any> {
executor: (...args: any[]) => Promise<any>,
): (...args: any[]) => Promise<any> {
if (this.options == null) {
return executor;
}
Expand Down
3 changes: 2 additions & 1 deletion lib/decorators/observable-response.decorator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Observable } from 'rxjs';
import { describe, expect, test } from 'vitest';
import { OBSERVABLE_METADATA } from './constants';
import { ObservableResponse } from './observable-response.decorator';
import { imitation } from '../supports';

describe('ObservableResponse', () => {
test('should set observable response metadata', () => {
// given
class TestService {
@ObservableResponse()
request(): Observable<string> {
throw new Error();
return imitation();
}
}

Expand Down
52 changes: 50 additions & 2 deletions lib/supports/node-fetch.injector.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MetadataScanner } from '@nestjs/core';
import { firstValueFrom, Observable } from 'rxjs';
import { beforeEach, describe, test, expect } from 'vitest';
import { Configuration } from './configuration';
import { imitation } from './imitation';
Expand All @@ -19,6 +20,7 @@ import {
RequestParam,
GraphQLExchange,
ResponseBody,
ObservableResponse,
} from '../decorators';
import { StubDiscoveryService } from '../fixtures/stub-discovery.service';
import { StubHttpClient } from '../fixtures/stub-http-client';
Expand Down Expand Up @@ -97,7 +99,7 @@ describe('NodeFetchInjector', () => {
expect(httpClient.requestInfo[0].url).toBe('https://example.com/api');
});

test('should request to path parm replaced url', async () => {
test('should request to path replaced url', async () => {
// given
@HttpInterface('https://example.com')
class SampleClient {
Expand All @@ -119,7 +121,7 @@ describe('NodeFetchInjector', () => {
);
});

test('should request to multiple path parm replaced url', async () => {
test('should request to multiple path replaced url', async () => {
// given
@HttpInterface('https://example.com')
class SampleClient {
Expand Down Expand Up @@ -553,4 +555,50 @@ describe('NodeFetchInjector', () => {
expect(response).toBeInstanceOf(ResponseTest);
expect(response.status).toBeUndefined();
});

test('should response observable text if there is no response body decorator', async () => {
// given
@HttpInterface()
class SampleClient {
@GetExchange('https://example.com/api')
@ObservableResponse()
request(): Observable<string> {
return imitation();
}
}
const instance = discoveryService.addProvider(SampleClient);
httpClient.addResponse({ status: 'ok' });
nodeFetchInjector.onModuleInit();

// when
const result = instance.request();

// then
expect(await firstValueFrom(result)).toBe('{"status":"ok"}');
});

test('should response observable instance provided with response body decorator', async () => {
// given
class ResponseTest {
constructor(readonly value: string) {}
}
@HttpInterface()
class SampleClient {
@GetExchange('https://example.com/api')
@ObservableResponse()
@ResponseBody(ResponseTest)
request(): Observable<ResponseTest> {
return imitation();
}
}
const instance = discoveryService.addProvider(SampleClient);
httpClient.addResponse({ value: 'ok' });
nodeFetchInjector.onModuleInit();

// when
const result = instance.request();

// then
expect(await firstValueFrom(result)).toBeInstanceOf(ResponseTest);
});
});
57 changes: 41 additions & 16 deletions lib/supports/node-fetch.injector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable, type OnModuleInit } from '@nestjs/common';
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
import { type InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { from } from 'rxjs';
import { Configuration } from './configuration';
import { CircuitBreakerBuilder } from '../builders/circuit-breaker.builder';
import { type HttpRequestBuilder } from '../builders/http-request.builder';
Expand All @@ -9,6 +10,7 @@ import {
CIRCUIT_BREAKER_METADATA,
HTTP_EXCHANGE_METADATA,
HTTP_INTERFACE_METADATA,
OBSERVABLE_METADATA,
RESPONSE_BODY_METADATA,
} from '../decorators';

Expand Down Expand Up @@ -49,29 +51,52 @@ export class NodeFetchInjector implements OnModuleInit {
new CircuitBreakerBuilder(this.configuration.circuitBreakerOption);
const responseBodyBuilder: ResponseBodyBuilder<unknown> | undefined =
Reflect.getMetadata(RESPONSE_BODY_METADATA, prototype, methodName);
const isObservable: boolean = Reflect.hasMetadata(
OBSERVABLE_METADATA,
prototype,
methodName,
);

httpRequestBuilder.setBaseUrl(baseUrl);

wrapper.instance[methodName] = circuitBreaker.build(
async (...args: never[]) =>
await this.configuration.httpClient
.request(httpRequestBuilder.build(args), httpRequestBuilder.options)
.then(async (response) => {
if (responseBodyBuilder != null) {
const res = await response.json();

return responseBodyBuilder.build(
res,
this.configuration.transformOption,
);
}
wrapper.instance[methodName] = (...args: any[]) => {
const request = circuitBreaker.build(
async () =>
await this.asyncRequest(
responseBodyBuilder,
httpRequestBuilder,
...args,
),
);

return await response.text();
}),
);
return isObservable
? from(request(...args))
: (request(...args) as any);
};
});
}

private async asyncRequest(
responseBodyBuilder: ResponseBodyBuilder<any> | undefined,
httpRequestBuilder: HttpRequestBuilder,
...args: any[]
): Promise<any> {
return await this.configuration.httpClient
.request(httpRequestBuilder.build(args), httpRequestBuilder.options)
.then(async (response) => {
if (responseBodyBuilder != null) {
const res = await response.json();

return responseBodyBuilder.build(
res,
this.configuration.transformOption,
);
}

return await response.text();
});
}

private getHttpProviders(): InstanceWrapper[] {
return this.discoveryService
.getProviders()
Expand Down
6 changes: 5 additions & 1 deletion lib/types/async-function.ts
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
export type AsyncFunction = (...args: any[]) => Promise<unknown>;
import { type Observable } from 'rxjs';

export type AsyncFunction = (
...args: any[]
) => Promise<unknown> | Observable<unknown>;

0 comments on commit 308f6c6

Please sign in to comment.