|
| 1 | +import fetch from 'cross-fetch'; |
| 2 | +import 'url-search-params-polyfill'; |
| 3 | + |
| 4 | +export interface ErrorResponse { |
| 5 | + error: string; |
| 6 | +} |
| 7 | + |
| 8 | +export type TransportOptions = { |
| 9 | + /** |
| 10 | + * [jwt auth token](security) |
| 11 | + */ |
| 12 | + authorization?: string; |
| 13 | + /** |
| 14 | + * path to `/cubejs-api/v1` |
| 15 | + */ |
| 16 | + apiUrl: string; |
| 17 | + /** |
| 18 | + * custom headers |
| 19 | + */ |
| 20 | + headers: Record<string, string>; |
| 21 | + credentials?: 'omit' | 'same-origin' | 'include'; |
| 22 | + method?: 'GET' | 'PUT' | 'POST' | 'PATCH'; |
| 23 | + /** |
| 24 | + * Fetch timeout in milliseconds. Would be passed as AbortSignal.timeout() |
| 25 | + */ |
| 26 | + fetchTimeout?: number; |
| 27 | +}; |
| 28 | + |
| 29 | +export interface ITransportResponse<R> { |
| 30 | + subscribe: <CBResult>(cb: (result: R | ErrorResponse, resubscribe: () => Promise<CBResult>) => CBResult) => Promise<CBResult>; |
| 31 | + // Optional, supported in WebSocketTransport |
| 32 | + unsubscribe?: () => Promise<void>; |
| 33 | +} |
| 34 | + |
| 35 | +export interface ITransport<R> { |
| 36 | + request(method: string, params: Record<string, unknown>): ITransportResponse<R>; |
| 37 | + authorization: TransportOptions['authorization']; |
| 38 | +} |
| 39 | + |
| 40 | +/** |
| 41 | + * Default transport implementation. |
| 42 | + */ |
| 43 | +class HttpTransport implements ITransport<Response> { |
| 44 | + public authorization: TransportOptions['authorization']; |
| 45 | + |
| 46 | + protected apiUrl: TransportOptions['apiUrl']; |
| 47 | + |
| 48 | + protected method: TransportOptions['method']; |
| 49 | + |
| 50 | + protected headers: TransportOptions['headers']; |
| 51 | + |
| 52 | + protected credentials: TransportOptions['credentials']; |
| 53 | + |
| 54 | + protected fetchTimeout: number | undefined; |
| 55 | + |
| 56 | + public constructor({ authorization, apiUrl, method, headers = {}, credentials, fetchTimeout }: Omit<TransportOptions, 'headers'> & { headers?: TransportOptions['headers'] }) { |
| 57 | + this.authorization = authorization; |
| 58 | + this.apiUrl = apiUrl; |
| 59 | + this.method = method; |
| 60 | + this.headers = headers; |
| 61 | + this.credentials = credentials; |
| 62 | + this.fetchTimeout = fetchTimeout; |
| 63 | + } |
| 64 | + |
| 65 | + public request(method: string, { baseRequestId, ...params }: any): ITransportResponse<Response> { |
| 66 | + let spanCounter = 1; |
| 67 | + const searchParams = new URLSearchParams( |
| 68 | + params && Object.keys(params) |
| 69 | + .map(k => ({ [k]: typeof params[k] === 'object' ? JSON.stringify(params[k]) : params[k] })) |
| 70 | + .reduce((a, b) => ({ ...a, ...b }), {}) |
| 71 | + ); |
| 72 | + |
| 73 | + let url = `${this.apiUrl}/${method}${searchParams.toString().length ? `?${searchParams}` : ''}`; |
| 74 | + |
| 75 | + const requestMethod = this.method || (url.length < 2000 ? 'GET' : 'POST'); |
| 76 | + if (requestMethod === 'POST') { |
| 77 | + url = `${this.apiUrl}/${method}`; |
| 78 | + this.headers['Content-Type'] = 'application/json'; |
| 79 | + } |
| 80 | + |
| 81 | + // Currently, all methods make GET requests. If a method makes a request with a body payload, |
| 82 | + // remember to add {'Content-Type': 'application/json'} to the header. |
| 83 | + const runRequest = () => fetch(url, { |
| 84 | + method: requestMethod, |
| 85 | + headers: { |
| 86 | + Authorization: this.authorization, |
| 87 | + 'x-request-id': baseRequestId && `${baseRequestId}-span-${spanCounter++}`, |
| 88 | + ...this.headers |
| 89 | + } as HeadersInit, |
| 90 | + credentials: this.credentials, |
| 91 | + body: requestMethod === 'POST' ? JSON.stringify(params) : null, |
| 92 | + signal: this.fetchTimeout ? AbortSignal.timeout(this.fetchTimeout) : undefined, |
| 93 | + }); |
| 94 | + |
| 95 | + return { |
| 96 | + /* eslint no-unsafe-finally: off */ |
| 97 | + async subscribe(callback) { |
| 98 | + try { |
| 99 | + const result = await runRequest(); |
| 100 | + return callback(result, () => this.subscribe(callback)); |
| 101 | + } catch (e) { |
| 102 | + const result: ErrorResponse = { error: 'network Error' }; |
| 103 | + return callback(result, () => this.subscribe(callback)); |
| 104 | + } |
| 105 | + } |
| 106 | + }; |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +export default HttpTransport; |
0 commit comments