Skip to content

Commit 308f6c6

Browse files
jbl428username1103imdudu1
committed
feat: return observable in node fetch injector
Co-authored-by: username1103 <[email protected]> Co-authored-by: imdudu1 <[email protected]>
1 parent a26a24a commit 308f6c6

File tree

5 files changed

+100
-22
lines changed

5 files changed

+100
-22
lines changed

lib/builders/circuit-breaker.builder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export class CircuitBreakerBuilder {
44
constructor(readonly options?: CircuitBreaker.Options) {}
55

66
build(
7-
executor: (...args: never[]) => Promise<any>,
8-
): (...args: never[]) => Promise<any> {
7+
executor: (...args: any[]) => Promise<any>,
8+
): (...args: any[]) => Promise<any> {
99
if (this.options == null) {
1010
return executor;
1111
}

lib/decorators/observable-response.decorator.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import { Observable } from 'rxjs';
22
import { describe, expect, test } from 'vitest';
33
import { OBSERVABLE_METADATA } from './constants';
44
import { ObservableResponse } from './observable-response.decorator';
5+
import { imitation } from '../supports';
56

67
describe('ObservableResponse', () => {
78
test('should set observable response metadata', () => {
89
// given
910
class TestService {
1011
@ObservableResponse()
1112
request(): Observable<string> {
12-
throw new Error();
13+
return imitation();
1314
}
1415
}
1516

lib/supports/node-fetch.injector.spec.ts

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MetadataScanner } from '@nestjs/core';
2+
import { firstValueFrom, Observable } from 'rxjs';
23
import { beforeEach, describe, test, expect } from 'vitest';
34
import { Configuration } from './configuration';
45
import { imitation } from './imitation';
@@ -19,6 +20,7 @@ import {
1920
RequestParam,
2021
GraphQLExchange,
2122
ResponseBody,
23+
ObservableResponse,
2224
} from '../decorators';
2325
import { StubDiscoveryService } from '../fixtures/stub-discovery.service';
2426
import { StubHttpClient } from '../fixtures/stub-http-client';
@@ -97,7 +99,7 @@ describe('NodeFetchInjector', () => {
9799
expect(httpClient.requestInfo[0].url).toBe('https://example.com/api');
98100
});
99101

100-
test('should request to path parm replaced url', async () => {
102+
test('should request to path replaced url', async () => {
101103
// given
102104
@HttpInterface('https://example.com')
103105
class SampleClient {
@@ -119,7 +121,7 @@ describe('NodeFetchInjector', () => {
119121
);
120122
});
121123

122-
test('should request to multiple path parm replaced url', async () => {
124+
test('should request to multiple path replaced url', async () => {
123125
// given
124126
@HttpInterface('https://example.com')
125127
class SampleClient {
@@ -553,4 +555,50 @@ describe('NodeFetchInjector', () => {
553555
expect(response).toBeInstanceOf(ResponseTest);
554556
expect(response.status).toBeUndefined();
555557
});
558+
559+
test('should response observable text if there is no response body decorator', async () => {
560+
// given
561+
@HttpInterface()
562+
class SampleClient {
563+
@GetExchange('https://example.com/api')
564+
@ObservableResponse()
565+
request(): Observable<string> {
566+
return imitation();
567+
}
568+
}
569+
const instance = discoveryService.addProvider(SampleClient);
570+
httpClient.addResponse({ status: 'ok' });
571+
nodeFetchInjector.onModuleInit();
572+
573+
// when
574+
const result = instance.request();
575+
576+
// then
577+
expect(await firstValueFrom(result)).toBe('{"status":"ok"}');
578+
});
579+
580+
test('should response observable instance provided with response body decorator', async () => {
581+
// given
582+
class ResponseTest {
583+
constructor(readonly value: string) {}
584+
}
585+
@HttpInterface()
586+
class SampleClient {
587+
@GetExchange('https://example.com/api')
588+
@ObservableResponse()
589+
@ResponseBody(ResponseTest)
590+
request(): Observable<ResponseTest> {
591+
return imitation();
592+
}
593+
}
594+
const instance = discoveryService.addProvider(SampleClient);
595+
httpClient.addResponse({ value: 'ok' });
596+
nodeFetchInjector.onModuleInit();
597+
598+
// when
599+
const result = instance.request();
600+
601+
// then
602+
expect(await firstValueFrom(result)).toBeInstanceOf(ResponseTest);
603+
});
556604
});

lib/supports/node-fetch.injector.ts

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable, type OnModuleInit } from '@nestjs/common';
22
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
33
import { type InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
4+
import { from } from 'rxjs';
45
import { Configuration } from './configuration';
56
import { CircuitBreakerBuilder } from '../builders/circuit-breaker.builder';
67
import { type HttpRequestBuilder } from '../builders/http-request.builder';
@@ -9,6 +10,7 @@ import {
910
CIRCUIT_BREAKER_METADATA,
1011
HTTP_EXCHANGE_METADATA,
1112
HTTP_INTERFACE_METADATA,
13+
OBSERVABLE_METADATA,
1214
RESPONSE_BODY_METADATA,
1315
} from '../decorators';
1416

@@ -49,29 +51,52 @@ export class NodeFetchInjector implements OnModuleInit {
4951
new CircuitBreakerBuilder(this.configuration.circuitBreakerOption);
5052
const responseBodyBuilder: ResponseBodyBuilder<unknown> | undefined =
5153
Reflect.getMetadata(RESPONSE_BODY_METADATA, prototype, methodName);
54+
const isObservable: boolean = Reflect.hasMetadata(
55+
OBSERVABLE_METADATA,
56+
prototype,
57+
methodName,
58+
);
5259

5360
httpRequestBuilder.setBaseUrl(baseUrl);
5461

55-
wrapper.instance[methodName] = circuitBreaker.build(
56-
async (...args: never[]) =>
57-
await this.configuration.httpClient
58-
.request(httpRequestBuilder.build(args), httpRequestBuilder.options)
59-
.then(async (response) => {
60-
if (responseBodyBuilder != null) {
61-
const res = await response.json();
62-
63-
return responseBodyBuilder.build(
64-
res,
65-
this.configuration.transformOption,
66-
);
67-
}
62+
wrapper.instance[methodName] = (...args: any[]) => {
63+
const request = circuitBreaker.build(
64+
async () =>
65+
await this.asyncRequest(
66+
responseBodyBuilder,
67+
httpRequestBuilder,
68+
...args,
69+
),
70+
);
6871

69-
return await response.text();
70-
}),
71-
);
72+
return isObservable
73+
? from(request(...args))
74+
: (request(...args) as any);
75+
};
7276
});
7377
}
7478

79+
private async asyncRequest(
80+
responseBodyBuilder: ResponseBodyBuilder<any> | undefined,
81+
httpRequestBuilder: HttpRequestBuilder,
82+
...args: any[]
83+
): Promise<any> {
84+
return await this.configuration.httpClient
85+
.request(httpRequestBuilder.build(args), httpRequestBuilder.options)
86+
.then(async (response) => {
87+
if (responseBodyBuilder != null) {
88+
const res = await response.json();
89+
90+
return responseBodyBuilder.build(
91+
res,
92+
this.configuration.transformOption,
93+
);
94+
}
95+
96+
return await response.text();
97+
});
98+
}
99+
75100
private getHttpProviders(): InstanceWrapper[] {
76101
return this.discoveryService
77102
.getProviders()

lib/types/async-function.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
export type AsyncFunction = (...args: any[]) => Promise<unknown>;
1+
import { type Observable } from 'rxjs';
2+
3+
export type AsyncFunction = (
4+
...args: any[]
5+
) => Promise<unknown> | Observable<unknown>;

0 commit comments

Comments
 (0)