/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import fetch, { Response, RequestInit } from 'node-fetch' import { debug, Debugger } from 'debug' import { Injectable } from '@nestjs/common' import { nanoid } from 'nanoid' export type RequestOptions = { headers?: Record name?: string timeout?: number } & ErrorArgs export type ErrorArgs = { defaultErrorCode?: number errorMessage?: string } export type FetchArgs = { returnHeaders?: boolean returnStatusCode?: boolean } & ErrorArgs export type ProcessArgs = { response: Response debug: Debugger id?: string } & FetchArgs export const DEFAULT_ERROR_CODE = 422 @Injectable() export class HttpService { protected debug: Debugger = debug('http') async get(url: string, options?: RequestOptions & FetchArgs): Promise { return this.fetch({ ...options, url, method: 'GET', body: null, }) } async post(url: string, body?: any, options?: RequestOptions & FetchArgs): Promise { return this.fetch({ ...options, url, method: 'POST', body }) } async put(url: string, body?: any, options?: RequestOptions & FetchArgs): Promise { return this.fetch({ ...options, url, method: 'PUT', body }) } async patch(url: string, body?: any, options?: RequestOptions & FetchArgs): Promise { return this.fetch({ ...options, url, method: 'PATCH', body }) } async delete(url: string, options?: RequestOptions & FetchArgs): Promise { return this.fetch({ ...options, url, method: 'DELETE', body: null, }) } async fetch({ url, method, headers, body, errorMessage, returnHeaders, returnStatusCode, defaultErrorCode, name, timeout, }: { url: string method: string body?: any } & RequestOptions & FetchArgs): Promise { const options: RequestInit = { headers, method, timeout, } const id = nanoid() const debug = name ? this.debug.extend(name) : this.debug if (body) options.body = JSON.stringify(body) debug.extend('request')(JSON.stringify({ id, url, options: { ...options, body } })) return fetch(url, options).then(response => this.processResult({ response, debug, defaultErrorCode, errorMessage, returnHeaders, id, returnStatusCode }), ) } protected async processResult({ response, debug, defaultErrorCode, errorMessage, returnHeaders = false, returnStatusCode = false, id, }: ProcessArgs): Promise { const headers = this.getResponseHeaders(response) let body: any let text: string try { text = await response.text() body = JSON.parse(text) if (!response.ok) return this.processError({ defaultErrorMessage: errorMessage, body, statusCode: response.status }) if (returnHeaders) body.headers = headers if (returnStatusCode) body.statusCode = response.status return body } catch (error) { if (!body) body = { error } return this.processError({ defaultErrorMessage: errorMessage, body: body || text || error, statusCode: response.status, defaultErrorCode, }) } finally { this.logResponse({ response, body: body || text, debug, headers, id }) } } protected logResponse({ response, body, debug, headers, id, }: { response: Response body: any debug: Debugger headers?: Record id?: string }): void { debug.extend('response')( JSON.stringify({ id, status: response.status, headers: headers ?? this.getResponseHeaders(response), body, }), ) } protected processError({ defaultErrorMessage, statusCode, defaultErrorCode, body, }: { defaultErrorMessage: string body?: any statusCode?: number defaultErrorCode?: number }): Promise { const message = defaultErrorMessage || body?.errors || body?.error || body?.message || '' return Promise.reject({ message, statusCode: statusCode ?? defaultErrorCode ?? DEFAULT_ERROR_CODE, body, }) } private getResponseHeaders(response: Response): Record { const headers = {} response.headers.forEach((value: string, name: string) => (headers[name] = value)) return headers } }