You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.ts 5.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import querystring from 'querystring'
  2. import {
  3. IncomingHttpHeaders,
  4. OutgoingHttpHeaders,
  5. } from 'http'
  6. import defaultsDeep from 'lodash.defaultsdeep'
  7. import { RequestOptions as HttpsRequestOptions } from 'https'
  8. interface BaseAuth {
  9. type: 'basic' | 'bearer' | 'none',
  10. }
  11. export interface BasicAuth extends BaseAuth {
  12. type: 'basic',
  13. username: string,
  14. password: string,
  15. }
  16. export interface BearerAuth extends BaseAuth {
  17. type: 'bearer',
  18. authToken: string,
  19. }
  20. export interface NoAuth extends BaseAuth {
  21. type: 'none',
  22. }
  23. export type Authentication = NoAuth | BasicAuth | BearerAuth
  24. interface AuthSpec {
  25. useHeader: boolean,
  26. usePayload: boolean,
  27. useQueryString: boolean,
  28. key: string,
  29. value: string,
  30. }
  31. export type Seconds = number
  32. export interface RawResponse {
  33. body: any,
  34. statusCode?: number,
  35. statusMessage?: string,
  36. headers: IncomingHttpHeaders,
  37. }
  38. export const rawResponseSymbol = Symbol.for('portal:symbols:raw-response')
  39. export interface Response {
  40. status: {
  41. code?: number,
  42. word?: string,
  43. },
  44. body: any,
  45. headers: IncomingHttpHeaders,
  46. [rawResponseSymbol]: RawResponse,
  47. }
  48. export type RequestBodyObject = { [k: string]: any }
  49. export type RequestBody = string | RequestBodyObject // | Array<RequestBodyObject> // TODO: TS is made when i add the array :(
  50. function isObject (body: RequestBody): body is RequestBodyObject {
  51. return typeof body === 'object'
  52. }
  53. // Add compatibility with Got type
  54. export interface RequestOptions extends HttpsRequestOptions {
  55. baseUrl?: string,
  56. url?: string,
  57. json?: boolean,
  58. body?: RequestBody,
  59. retries?: number,
  60. throwHttpErrors?: boolean,
  61. headers?: OutgoingHttpHeaders,
  62. }
  63. export interface Config {
  64. baseUrl?: string, // TODO: this shouldn't be optional but i can construct RequestConfig unless i set it as optional
  65. // protocol?: 'http' | 'https',
  66. // port?: number,
  67. headers?: OutgoingHttpHeaders,
  68. authentication?: Authentication,
  69. retries?: number, // available from RequestOptions
  70. timeout?: Seconds, // available from HttpsRequestOptions
  71. onHttpErrors?: 'reject' | 'resolve',
  72. }
  73. export type ClientFn = (options: RequestOptions) => Promise<RawResponse>
  74. export interface RequestConfig extends RequestOptions, Config {}
  75. // export type RequestConfig = Config
  76. export interface Client {
  77. request (method: string, path: string, payload: RequestBody, options: any): Promise<Response>
  78. }
  79. export class PortalClient implements Client {
  80. config: Config
  81. client: ClientFn
  82. defaultRequestOptions: RequestConfig
  83. constructor (client: ClientFn, config: Config) {
  84. // TODO: this shouldn't exists but i had to set baseUrl as otpional to construc the RequestConfig type
  85. if (typeof config.baseUrl === 'undefined') throw Error('TODO: givem em a meaningful message')
  86. this.client = client
  87. this.config = config
  88. this.defaultRequestOptions = {
  89. // port: 443,
  90. retries: 0,
  91. headers: {},
  92. timeout: 30,
  93. onHttpErrors: 'reject',
  94. // protocol: 'https',
  95. }
  96. }
  97. async request (method: string, path: string, payload: RequestBody, options: RequestConfig): Promise<Response> {
  98. const requestOptions = this.constructRequestOptions(method, path, payload, options)
  99. const response = await this.client(requestOptions)
  100. return this.transformResponse(response)
  101. }
  102. constructRequestOptions (method: string, path: string, payload: RequestBody, options: RequestConfig): RequestOptions {
  103. const {
  104. baseUrl,
  105. headers,
  106. retries,
  107. timeout,
  108. onHttpErrors,
  109. authentication,
  110. } = defaultsDeep({}, options, this.config, this.defaultRequestOptions)
  111. if (typeof authentication !== 'undefined') {
  112. const {
  113. useHeader,
  114. usePayload,
  115. useQueryString,
  116. key,
  117. value,
  118. } = this.setupAuthentication(authentication)
  119. if (useHeader) {
  120. headers[key] = value
  121. } else if (usePayload && isObject(payload)) {
  122. payload[key] = value
  123. } else if (useQueryString) {
  124. const [url, queryString = ''] = path.split('?', 2)
  125. const query = querystring.parse(queryString)
  126. query[key] = value
  127. path = `${url}?${querystring.stringify(query)}`
  128. }
  129. }
  130. const isJson = headers['Content-Type'] === 'application/json'
  131. return {
  132. method,
  133. baseUrl,
  134. url: path,
  135. headers,
  136. retries,
  137. json: isJson,
  138. timeout: timeout * 1000,
  139. throwHttpErrors: onHttpErrors !== 'resolve',
  140. // TODO: what if the payload is undefined ?
  141. body: payload,
  142. }
  143. }
  144. setupAuthentication (auth: Authentication): AuthSpec {
  145. switch (auth.type) {
  146. case 'none': {
  147. return {
  148. useHeader: false,
  149. usePayload: false,
  150. useQueryString: false,
  151. key: '',
  152. value: '',
  153. }
  154. }
  155. case 'basic': {
  156. const token = Buffer.from(`${auth.username}:${auth.password}`).toString('base64')
  157. return {
  158. useHeader: true,
  159. usePayload: false,
  160. useQueryString: false,
  161. key: 'Authorization',
  162. value: `Basic ${token}`,
  163. }
  164. }
  165. case 'bearer': {
  166. return {
  167. useHeader: true,
  168. usePayload: false,
  169. useQueryString: false,
  170. key: 'Authorization',
  171. value: `Bearer ${auth.authToken}`,
  172. }
  173. }
  174. }
  175. throw new Error('TODO: givem me a meangingful error')
  176. }
  177. transformResponse(response: RawResponse): Response {
  178. return {
  179. status: {
  180. code: response.statusCode,
  181. word: response.statusMessage,
  182. },
  183. body: response.body, // TODO: should it be parsed if needed ?
  184. headers: response.headers,
  185. [rawResponseSymbol]: response,
  186. }
  187. }
  188. }
  189. export default PortalClient