diff --git a/packages/request/LICENSE.txt b/packages/request/LICENSE.txt new file mode 100644 index 0000000..357ce95 --- /dev/null +++ b/packages/request/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Lido + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/request/README.md b/packages/request/README.md new file mode 100644 index 0000000..ddde8ef --- /dev/null +++ b/packages/request/README.md @@ -0,0 +1,127 @@ +# Request + +NestJS Fetch for Lido Finance projects. +Part of [Lido NestJS Modules](https://github.com/lidofinance/lido-nestjs-modules/#readme) + +The module is based on the [node-fetch](https://www.npmjs.com/package/node-fetch) package. + +## Install + +```bash +yarn add @lido-nestjs/request +``` + +## Usage + +### Basic usage + +```ts +// Import +import { Module } from '@nestjs/common'; +import { RequestModule } from '@lido-nestjs/request'; +import { MyService } from './my.service'; + +@Module({ + imports: [RequestModule.forFeature()], + providers: [MyService], + exports: [MyService], +}) +export class MyModule {} + +// Usage +import { RequestService } from '@lido-nestjs/request'; + +export class MyService { + constructor(private requestService: RequestService) {} + + async myFetch() { + return await this.requestService.json({ url: '/url' }); + } +} +``` + +The `requestService` provides 2 methods: `json` and `text`, which are based on a call to the `fetch` function followed by a call to `.json()` or `.text()`. Method arguments are compatible with the `fetch`. + +### Global usage + +```ts +import { Module } from '@nestjs/common'; +import { RequestModule } from '@lido-nestjs/request'; + +@Module({ + imports: [RequestModule.forRoot()], +}) +export class MyModule {} +``` + +### Async usage + +```ts +import { Module } from '@nestjs/common'; +import { RequestModule } from '@lido-nestjs/request'; +import { ConfigModule, ConfigService } from './my.service'; + +@Module({ + imports: [ + ConfigModule, + RequestModule.forRootAsync({ + async useFactory(configService: ConfigService) { + return { url: configService.url }; + }, + inject: [ConfigService], + }), + ], +}) +export class MyModule {} +``` + +### Middlewares + +Middlewares - is a main difference between request module and fetch module + +#### Global middlewares + +You can add global middlewares into the Module instance. These middlewares will be used for each request. +For example this _standard_ middlewares will be repeated request on each network error 4 times and change the base url on each failure. + +Let's setup our module + +```ts +import { Module } from '@nestjs/common'; +import { + RequestModule, + repeat, + rotate, + notOkError, +} from '@lido-nestjs/request'; +import { ConfigModule, ConfigService } from './my.service'; + +@Module({ + imports: [ + ConfigModule, + RequestModule.forRootAsync({ + async useFactory(configService: ConfigService) { + return { + middlewares: [repeat(5), rotate(configService.baseUrls), notOkError], + }; + }, + inject: [ConfigService], + }), + ], +}) +export class MyModule {} +``` + +And use it + +```ts +import { RequestService } from '@lido-nestjs/request'; + +export class MyService { + constructor(private requestService: RequestService) {} + + async myFetch() { + return await this.requestService.json({ url: '/url' }); + } +} +``` diff --git a/packages/request/package.json b/packages/request/package.json new file mode 100644 index 0000000..7da4a90 --- /dev/null +++ b/packages/request/package.json @@ -0,0 +1,51 @@ +{ + "name": "@lido-nestjs/request", + "version": "0.0.0-semantic-release", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "license": "MIT", + "homepage": "https://github.com/lidofinance/lido-nestjs-modules", + "repository": { + "type": "git", + "url": "https://github.com/lidofinance/lido-nestjs-modules.git", + "directory": "packages/request" + }, + "bugs": { + "url": "https://github.com/lidofinance/lido-nestjs-modules/issues" + }, + "sideEffects": false, + "keywords": [ + "lido", + "lido-nestjs", + "lido-nestjs-modules", + "lidofinance" + ], + "files": [ + "dist/*" + ], + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, + "dependencies": { + "@lido-nestjs/middleware": "workspace:*", + "@types/node-fetch": "^2.5.12", + "node-fetch": "^2.6.7", + "prom-client": "^14.1.0" + }, + "peerDependencies": { + "@nestjs/common": "^8.2.5", + "@nestjs/core": "^8.2.5", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.5.2", + "tslib": "^2.3.1" + }, + "devDependencies": { + "@nestjs/common": "^8.2.5", + "@nestjs/core": "^8.2.5", + "@nestjs/testing": "^8.2.5", + "nock": "^13.2.9", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.5.2" + } +} diff --git a/packages/request/src/common/clone.ts b/packages/request/src/common/clone.ts new file mode 100644 index 0000000..02a1f98 --- /dev/null +++ b/packages/request/src/common/clone.ts @@ -0,0 +1,26 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const deepClone = (obj: any) => { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + if (obj instanceof Date) { + return new Date(obj.getTime()); + } + + if (obj instanceof Array) { + return obj.reduce((arr, item, i) => { + arr[i] = deepClone(item); + return arr; + }, []); + } + + if (obj instanceof Object) { + return Object.keys(obj).reduce((newObj, key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + newObj[key] = deepClone(obj[key]); + return newObj; + }, {}); + } +}; diff --git a/packages/request/src/common/http.ts b/packages/request/src/common/http.ts new file mode 100644 index 0000000..ae9b71b --- /dev/null +++ b/packages/request/src/common/http.ts @@ -0,0 +1,13 @@ +import { Response, FetchError } from 'node-fetch'; +import { HttpException } from '@nestjs/common'; + +export const extractErrorBody = async (response: Response) => { + try { + return await response.json(); + } catch (error) { + return response.statusText; + } +}; + +export const isNotServerError = (error: Error) => + !(error instanceof HttpException) && !(error instanceof FetchError); diff --git a/packages/request/src/common/index.ts b/packages/request/src/common/index.ts new file mode 100644 index 0000000..bffc88c --- /dev/null +++ b/packages/request/src/common/index.ts @@ -0,0 +1,3 @@ +export * from './clone'; +export * from './http'; +export * from './url'; diff --git a/packages/request/src/common/url.ts b/packages/request/src/common/url.ts new file mode 100644 index 0000000..922cbab --- /dev/null +++ b/packages/request/src/common/url.ts @@ -0,0 +1,17 @@ +import { RequestInfo } from 'node-fetch'; + +export const getUrl = ( + baseUrl: RequestInfo | undefined, + url: RequestInfo, +): RequestInfo => { + if (typeof url !== 'string') return url; + if (baseUrl == null) return url; + if (isAbsoluteUrl(url)) return url; + + return `${baseUrl}${url}`; +}; + +export const isAbsoluteUrl = (url: RequestInfo): boolean => { + const regexp = new RegExp('^(?:[a-z]+:)?//', 'i'); + return regexp.test(url.toString()); +}; diff --git a/packages/request/src/compose/compose.ts b/packages/request/src/compose/compose.ts new file mode 100644 index 0000000..2103754 --- /dev/null +++ b/packages/request/src/compose/compose.ts @@ -0,0 +1,26 @@ +import { InternalConfig, Middleware, RequestConfig } from '../interfaces'; +import fetch, { Response } from 'node-fetch'; +import { deepClone, getUrl } from '../common'; + +const fetchCall = ({ url, baseUrl, ...rest }: RequestConfig) => + fetch(getUrl(baseUrl, url), rest); + +export function compose(middleware: Middleware[]) { + return (requestConfig: RequestConfig) => { + // copy object bcs we can mutate it + const internalConfig = deepClone(requestConfig) as InternalConfig; + internalConfig.attempt = 0; + + async function chain( + config: InternalConfig, + middleware: Middleware[], + ): Promise { + if (middleware.length === 0) return fetchCall(config); + return middleware[0](config, (config) => + chain(config, middleware.slice(1)), + ); + } + + return chain(internalConfig, middleware); + }; +} diff --git a/packages/request/src/compose/index.ts b/packages/request/src/compose/index.ts new file mode 100644 index 0000000..ce4e4b8 --- /dev/null +++ b/packages/request/src/compose/index.ts @@ -0,0 +1 @@ +export * from './compose'; diff --git a/packages/request/src/index.ts b/packages/request/src/index.ts new file mode 100644 index 0000000..dcf2e22 --- /dev/null +++ b/packages/request/src/index.ts @@ -0,0 +1,4 @@ +export * from './interfaces'; + +export * from './middlewares'; +export * from './request'; diff --git a/packages/request/src/interfaces/compose.interface.ts b/packages/request/src/interfaces/compose.interface.ts new file mode 100644 index 0000000..c2555cc --- /dev/null +++ b/packages/request/src/interfaces/compose.interface.ts @@ -0,0 +1,15 @@ +import { RequestInfo, RequestInit, Response } from 'node-fetch'; + +export interface RequestConfig extends RequestInit { + url: RequestInfo; + baseUrl?: RequestInfo; +} + +export interface InternalConfig extends RequestConfig { + attempt: number; +} + +export type Middleware = ( + config: InternalConfig, + next: (config: InternalConfig) => Promise, +) => Promise; diff --git a/packages/request/src/interfaces/index.ts b/packages/request/src/interfaces/index.ts new file mode 100644 index 0000000..476d6e6 --- /dev/null +++ b/packages/request/src/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from './compose.interface'; +export * from './request.interface'; diff --git a/packages/request/src/interfaces/request.interface.ts b/packages/request/src/interfaces/request.interface.ts new file mode 100644 index 0000000..08130c6 --- /dev/null +++ b/packages/request/src/interfaces/request.interface.ts @@ -0,0 +1,14 @@ +import { ModuleMetadata } from '@nestjs/common'; +import { Middleware, RequestConfig } from './index'; + +export interface RequestModuleOptions { + readonly middlewares?: Middleware[]; + readonly globalConfig?: RequestConfig; +} +export interface RequestModuleAsyncOptions + extends Pick { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useFactory: (...args: any[]) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + inject?: any[]; +} diff --git a/packages/request/src/middlewares/index.ts b/packages/request/src/middlewares/index.ts new file mode 100644 index 0000000..46459ea --- /dev/null +++ b/packages/request/src/middlewares/index.ts @@ -0,0 +1,4 @@ +export * from './logger'; +export * from './not-ok-error'; +export * from './retry'; +export * from './rotate'; diff --git a/packages/request/src/middlewares/logger.ts b/packages/request/src/middlewares/logger.ts new file mode 100644 index 0000000..ebb5f64 --- /dev/null +++ b/packages/request/src/middlewares/logger.ts @@ -0,0 +1,14 @@ +import { Response } from 'node-fetch'; +import { LoggerService } from '@nestjs/common'; +import { Middleware } from '../interfaces'; + +export const logger = + (logger: LoggerService): Middleware => + async (config, next) => { + let response!: Response; + logger.log('Start request', config); + // eslint-disable-next-line prefer-const + response = await next(config); + logger.log('End request', config); + return response; + }; diff --git a/packages/request/src/middlewares/not-ok-error.ts b/packages/request/src/middlewares/not-ok-error.ts new file mode 100644 index 0000000..72c99d9 --- /dev/null +++ b/packages/request/src/middlewares/not-ok-error.ts @@ -0,0 +1,12 @@ +import { HttpException } from '@nestjs/common'; +import { Middleware } from '../interfaces'; +import { extractErrorBody } from '../common'; + +export const notOkError = (): Middleware => async (config, next) => { + const response = await next(config); + if (!response?.ok) { + const errorBody = await extractErrorBody(response); + throw new HttpException(errorBody, response.status); + } + return response; +}; diff --git a/packages/request/src/middlewares/prom.ts b/packages/request/src/middlewares/prom.ts new file mode 100644 index 0000000..5545bcb --- /dev/null +++ b/packages/request/src/middlewares/prom.ts @@ -0,0 +1,72 @@ +import { Response } from 'node-fetch'; +import { InternalConfig, Middleware } from '../interfaces'; +import { Histogram } from 'prom-client'; +import { getUrl } from '../common'; +import { HttpException } from '@nestjs/common'; + +type PromReturnTypeSerializer = + | Partial> + | undefined; + +const defaultSerializer = ( + config: InternalConfig, + response: Response, + error?: Error, +): PromReturnTypeSerializer => { + const url = getUrl(config.baseUrl, config.url); + const status = error instanceof HttpException ? error.getStatus() : 'unknown'; + return { + result: error ? 'error' : 'success', + status: error ? status : response.status, + url: url.toString(), + }; +}; + +/** + * Simple middleware for prom-client + * + * ```ts + * const h = new Histogram({ + * name: 'name', + * help: 'help', + * buckets: [0.1, 0.2, 0.5, 1, 2, 5, 10, 15, 20], + * labelNames: ['result', 'status'], + * }); + * // With default serializer + * prom(h) + * // With custom serializer + * prom(h, (config, res, error) => ({ + * result: error ? 'error' : 'result', + * status: 200, + * url: config.url.toString(), + * })); + * ``` + * @param prom Histogram instance + * @param serialize callback fn for pick values from iteration + * @returns Response + */ +export const prom = + ( + prom: Histogram, + serialize: ( + conf: InternalConfig, + response: Response, + error?: Error, + ) => PromReturnTypeSerializer = defaultSerializer, + ): Middleware => + async (config, next) => { + let response!: Response; + const timer = prom.startTimer(); + try { + response = await next(config); + timer(serialize(config, response)); + } catch (error) { + if (!(error instanceof Error)) { + timer(serialize(config, response)); + throw error; + } + timer(serialize(config, response, error)); + throw error; + } + return response; + }; diff --git a/packages/request/src/middlewares/retry.ts b/packages/request/src/middlewares/retry.ts new file mode 100644 index 0000000..b8aad08 --- /dev/null +++ b/packages/request/src/middlewares/retry.ts @@ -0,0 +1,25 @@ +import { Middleware } from '../interfaces'; +import { Response } from 'node-fetch'; +import { isNotServerError } from '../common'; + +export const retry = + (maxTries = 3, retryConfig: { ignoreAbort: boolean }): Middleware => + async (config, next) => { + let response!: Response; + while (maxTries > config.attempt) { + config.attempt++; + try { + response = await next(config); + } catch (error) { + // ts I love you! + if (!(error instanceof Error)) throw error; + if (error.name === 'AbortError' && !retryConfig.ignoreAbort) + throw error; + if (isNotServerError(error)) throw error; + if (maxTries <= config.attempt) throw error; + // TODO: delay from Kirill method + response = await next(config); + } + } + return response; + }; diff --git a/packages/request/src/middlewares/rotate.ts b/packages/request/src/middlewares/rotate.ts new file mode 100644 index 0000000..3703686 --- /dev/null +++ b/packages/request/src/middlewares/rotate.ts @@ -0,0 +1,17 @@ +import { Middleware } from '../interfaces'; +import { RequestInfo } from 'node-fetch'; + +const pickUrl = (baseUrls: [RequestInfo], attempt: number) => + baseUrls[attempt % baseUrls.length]; + +export const rotate = + (baseUrls: [RequestInfo]): Middleware => + async (config, next) => { + if (!config.baseUrl && baseUrls.length) + config.baseUrl = pickUrl(baseUrls, config.attempt); + const response = await next(config); + if (!baseUrls.length) return response; + if (isNaN(config.attempt) || config.attempt <= 0) return response; + config.baseUrl = pickUrl(baseUrls, config.attempt); + return response; + }; diff --git a/packages/request/src/request/index.ts b/packages/request/src/request/index.ts new file mode 100644 index 0000000..1838fa8 --- /dev/null +++ b/packages/request/src/request/index.ts @@ -0,0 +1,3 @@ +export * from './request.constants'; +export * from './request.module'; +export * from './request.service'; diff --git a/packages/request/src/request/request.constants.ts b/packages/request/src/request/request.constants.ts new file mode 100644 index 0000000..eb3fdc4 --- /dev/null +++ b/packages/request/src/request/request.constants.ts @@ -0,0 +1 @@ +export const REQUEST_GLOBAL_OPTIONS_TOKEN = Symbol('requestGlobalOptions'); diff --git a/packages/request/src/request/request.module.ts b/packages/request/src/request/request.module.ts new file mode 100644 index 0000000..5379a82 --- /dev/null +++ b/packages/request/src/request/request.module.ts @@ -0,0 +1,58 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { + RequestModuleAsyncOptions, + RequestModuleOptions, +} from '../interfaces/request.interface'; +import { REQUEST_GLOBAL_OPTIONS_TOKEN } from './request.constants'; +import { RequestService } from './request.service'; + +@Module({ + imports: [], + providers: [RequestService], + exports: [RequestService], +}) +export class RequestModule { + static forRoot(options?: RequestModuleOptions): DynamicModule { + return { + global: true, + ...this.forFeature(options), + }; + } + + public static forRootAsync( + options: RequestModuleAsyncOptions, + ): DynamicModule { + return { + global: true, + ...this.forFeatureAsync(options), + }; + } + + static forFeature(options?: RequestModuleOptions): DynamicModule { + return { + module: RequestModule, + providers: [ + { + provide: REQUEST_GLOBAL_OPTIONS_TOKEN, + useValue: options, + }, + ], + }; + } + + public static forFeatureAsync( + options: RequestModuleAsyncOptions, + ): DynamicModule { + return { + module: RequestModule, + imports: options.imports, + providers: [ + { + provide: REQUEST_GLOBAL_OPTIONS_TOKEN, + useFactory: options.useFactory, + inject: options.inject, + }, + ], + }; + } +} diff --git a/packages/request/src/request/request.service.ts b/packages/request/src/request/request.service.ts new file mode 100644 index 0000000..eaf9799 --- /dev/null +++ b/packages/request/src/request/request.service.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable, Optional } from '@nestjs/common'; +import { Middleware, RequestConfig } from '../interfaces'; +import { compose } from '../compose'; +import { REQUEST_GLOBAL_OPTIONS_TOKEN } from '.'; +import { RequestModuleOptions } from '../interfaces/request.interface'; + +@Injectable() +export class RequestService { + constructor( + @Optional() + @Inject(REQUEST_GLOBAL_OPTIONS_TOKEN) + private readonly options: RequestModuleOptions, + ) {} + + public async json( + config: RequestConfig, + middlewares: Middleware[] = [], + ): Promise { + const response = await compose([...this.globalMiddlewares, ...middlewares])( + this.resolveConfig(config), + ); + return await response.json(); + } + + public async text( + config: RequestConfig, + middlewares: Middleware[] = [], + ): Promise { + const response = await compose([...this.globalMiddlewares, ...middlewares])( + this.resolveConfig(config), + ); + return await response.text(); + } + + get globalMiddlewares() { + return this.options?.middlewares ?? []; + } + + private resolveConfig(config: RequestConfig) { + return { ...this.options?.globalConfig, ...config }; + } +} diff --git a/packages/request/test/async.spec.ts b/packages/request/test/async.spec.ts new file mode 100644 index 0000000..f2b6eb3 --- /dev/null +++ b/packages/request/test/async.spec.ts @@ -0,0 +1,96 @@ +jest.mock('node-fetch'); + +import fetch from 'node-fetch'; +import { + DynamicModule, + Injectable, + Module, + ModuleMetadata, +} from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { + RequestModule, + RequestModuleAsyncOptions, + RequestService, +} from '../src'; + +const { Response } = jest.requireActual('node-fetch'); +const mockFetch = fetch as jest.MockedFunction; + +@Injectable() +class TestService { + public foo = jest.fn(); +} +@Module({ + providers: [TestService], + exports: [TestService], +}) +class TestModule { + static forRoot(): DynamicModule { + return { + module: TestModule, + global: true, + }; + } +} + +describe('Async module initializing', () => { + const expected = { foo: 'bar' }; + + const testModules = async (imports: ModuleMetadata['imports']) => { + const moduleRef = await Test.createTestingModule({ imports }).compile(); + + mockFetch.mockImplementation(() => + Promise.resolve(new Response(JSON.stringify(expected))), + ); + + const fetchService = await moduleRef.resolve(RequestService); + const testService = await moduleRef.resolve(TestService); + + expect(testService.foo).toBeCalledTimes(0); + fetchService.json({ url: '/foo' }); + expect(testService.foo).toBeCalledTimes(1); + }; + + const factory: RequestModuleAsyncOptions = { + async useFactory(testService: TestService) { + return { + middlewares: [ + (config, next) => { + testService.foo(); + return next(config); + }, + ], + }; + }, + inject: [TestService], + }; + + test('forRootAsync', async () => { + await testModules([ + TestModule.forRoot(), + RequestModule.forRootAsync(factory), + ]); + + await testModules([ + RequestModule.forRootAsync({ + imports: [TestModule], + ...factory, + }), + ]); + }); + + test('forFeatureAsync', async () => { + await testModules([ + TestModule.forRoot(), + RequestModule.forFeatureAsync(factory), + ]); + + await testModules([ + RequestModule.forFeatureAsync({ + imports: [TestModule], + ...factory, + }), + ]); + }); +}); diff --git a/packages/request/test/middlewares/prom.spec.ts b/packages/request/test/middlewares/prom.spec.ts new file mode 100644 index 0000000..b6e7893 --- /dev/null +++ b/packages/request/test/middlewares/prom.spec.ts @@ -0,0 +1,55 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { prom } from '../../src/middlewares/prom'; +import { compose } from '../../src/compose'; +import { register, Histogram, Registry } from 'prom-client'; +import nock from 'nock'; +type Awaited = T extends PromiseLike ? Awaited : T; + +const createH = (registryInstance: Registry) => + new Histogram({ + name: 'test', + help: 'help', + buckets: [1], + labelNames: ['result', 'status', 'url'], + registers: [registryInstance], + }); + +const checkValues = ( + results: Awaited>, + expected: { + result: string; + status: number | string; + url: string; + }, +) => { + results.map((result) => { + // @ts-ignore + for (const value of result.values) { + expect(value.labels).toEqual(expect.objectContaining(expected)); + } + }); +}; + +describe('Prom middleware', () => { + let registryInstance = new Registry(); + let instance = createH(registryInstance); + + afterEach(() => { + register.clear(); + }); + + beforeEach(() => { + registryInstance = new Registry(); + instance = createH(registryInstance); + }); + test('based', async () => { + nock('http://yolo').get('/foo').reply(200, 'ok'); + await compose([prom(instance)])({ url: 'http://yolo/foo' }); + + checkValues(await registryInstance.getMetricsAsJSON(), { + result: 'success', + status: 200, + url: 'http://yolo/foo', + }); + }); +}); diff --git a/packages/request/test/sync.spec.ts b/packages/request/test/sync.spec.ts new file mode 100644 index 0000000..97369b4 --- /dev/null +++ b/packages/request/test/sync.spec.ts @@ -0,0 +1,25 @@ +import { ModuleMetadata } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { RequestModule, RequestService } from '../src'; + +describe('Sync module initializing', () => { + const testModules = async (imports: ModuleMetadata['imports']) => { + const moduleRef = await Test.createTestingModule({ imports }).compile(); + const fetchService = moduleRef.get(RequestService); + + expect(fetchService.json).toBeDefined(); + expect(fetchService.text).toBeDefined(); + }; + + test('Module', async () => { + await testModules([RequestModule]); + }); + + test('forRoot', async () => { + await testModules([RequestModule.forRoot()]); + }); + + test('forFeature', async () => { + await testModules([RequestModule.forFeature()]); + }); +}); diff --git a/packages/request/tsconfig.json b/packages/request/tsconfig.json new file mode 100644 index 0000000..4082f16 --- /dev/null +++ b/packages/request/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/yarn.lock b/yarn.lock index 5665d75..6d01456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2625,6 +2625,29 @@ __metadata: languageName: unknown linkType: soft +"@lido-nestjs/request@workspace:packages/request": + version: 0.0.0-use.local + resolution: "@lido-nestjs/request@workspace:packages/request" + dependencies: + "@lido-nestjs/middleware": "workspace:*" + "@nestjs/common": ^8.2.5 + "@nestjs/core": ^8.2.5 + "@nestjs/testing": ^8.2.5 + "@types/node-fetch": ^2.5.12 + nock: ^13.2.9 + node-fetch: ^2.6.7 + prom-client: ^14.1.0 + reflect-metadata: ^0.1.13 + rxjs: ^7.5.2 + peerDependencies: + "@nestjs/common": ^8.2.5 + "@nestjs/core": ^8.2.5 + reflect-metadata: ^0.1.13 + rxjs: ^7.5.2 + tslib: ^2.3.1 + languageName: unknown + linkType: soft + "@lido-nestjs/utils@workspace:*, @lido-nestjs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@lido-nestjs/utils@workspace:packages/utils" @@ -4886,6 +4909,13 @@ __metadata: languageName: node linkType: hard +"bintrees@npm:1.0.2": + version: 1.0.2 + resolution: "bintrees@npm:1.0.2" + checksum: 56a52b7d3634e30002b1eda740d2517a22fa8e9e2eb088e919f37c030a0ed86e364ab59e472fc770fc8751308054bb1c892979d150e11d9e11ac33bcc1b5d16e + languageName: node + linkType: hard + "blork@npm:^9.3.0": version: 9.3.0 resolution: "blork@npm:9.3.0" @@ -11887,6 +11917,18 @@ __metadata: languageName: node linkType: hard +"nock@npm:^13.2.9": + version: 13.2.9 + resolution: "nock@npm:13.2.9" + dependencies: + debug: ^4.1.0 + json-stringify-safe: ^5.0.1 + lodash: ^4.17.21 + propagate: ^2.0.0 + checksum: 04a2dc60b4b55fd1240f28fe34865bbc744088a4570db3781fcf66021644cc3cc9178fd86a0cb0c1f28ea77b83e8f1c9288535f6b39a6d07100059f156ccc23b + languageName: node + linkType: hard + "node-abort-controller@npm:^3.0.1": version: 3.0.1 resolution: "node-abort-controller@npm:3.0.1" @@ -13301,6 +13343,15 @@ __metadata: languageName: node linkType: hard +"prom-client@npm:^14.1.0": + version: 14.1.0 + resolution: "prom-client@npm:14.1.0" + dependencies: + tdigest: ^0.1.1 + checksum: 88f8e67020c0af07d09ab8f15caaa2a3d0810b01d903d0977d82947d3b0100a539db77bcccfde0bf2449a85060475f7fb05b64e7c34951c8a54b84240ae7ae81 + languageName: node + linkType: hard + "promise-all-reject-late@npm:^1.0.0": version: 1.0.1 resolution: "promise-all-reject-late@npm:1.0.1" @@ -13368,6 +13419,13 @@ __metadata: languageName: node linkType: hard +"propagate@npm:^2.0.0": + version: 2.0.1 + resolution: "propagate@npm:2.0.1" + checksum: c4febaee2be0979e82fb6b3727878fd122a98d64a7fa3c9d09b0576751b88514a9e9275b1b92e76b364d488f508e223bd7e1dcdc616be4cdda876072fbc2a96c + languageName: node + linkType: hard + "proto-list@npm:~1.2.1": version: 1.2.4 resolution: "proto-list@npm:1.2.4" @@ -15205,6 +15263,15 @@ __metadata: languageName: node linkType: hard +"tdigest@npm:^0.1.1": + version: 0.1.2 + resolution: "tdigest@npm:0.1.2" + dependencies: + bintrees: 1.0.2 + checksum: 44de8246752b6f8c2924685f969fd3d94c36949f22b0907e99bef2b2220726dd8467f4730ea96b06040b9aa2587c0866049640039d1b956952dfa962bc2075a3 + languageName: node + linkType: hard + "temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0"