From 80a5886031f104b5216cde848669ebb20cba2acb Mon Sep 17 00:00:00 2001 From: Jordan Shatford Date: Sat, 30 Mar 2024 14:29:54 +1100 Subject: [PATCH] feat(client): unify fetch and node clients --- .changeset/mean-coats-fetch.md | 5 + src/templates/core/fetch/request.hbs | 5 + src/templates/core/fetch/sendRequest.hbs | 2 + src/templates/core/node/getHeaders.hbs | 42 -------- src/templates/core/node/getRequestBody.hbs | 12 --- src/templates/core/node/getResponseBody.hbs | 23 ---- src/templates/core/node/getResponseHeader.hbs | 9 -- src/templates/core/node/request.hbs | 101 ------------------ src/templates/core/node/sendRequest.hbs | 26 ----- src/templates/core/request.hbs | 2 +- src/utils/handlebars.ts | 14 --- .../v3_node/core/request.ts.snap | 6 +- 12 files changed, 16 insertions(+), 231 deletions(-) create mode 100644 .changeset/mean-coats-fetch.md delete mode 100644 src/templates/core/node/getHeaders.hbs delete mode 100644 src/templates/core/node/getRequestBody.hbs delete mode 100644 src/templates/core/node/getResponseBody.hbs delete mode 100644 src/templates/core/node/getResponseHeader.hbs delete mode 100644 src/templates/core/node/request.hbs delete mode 100644 src/templates/core/node/sendRequest.hbs diff --git a/.changeset/mean-coats-fetch.md b/.changeset/mean-coats-fetch.md new file mode 100644 index 0000000000..01e820492f --- /dev/null +++ b/.changeset/mean-coats-fetch.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +Attempt to use body type as content type when sending Blob in node client diff --git a/src/templates/core/fetch/request.hbs b/src/templates/core/fetch/request.hbs index 00a1eed4cf..d8578a8e96 100644 --- a/src/templates/core/fetch/request.hbs +++ b/src/templates/core/fetch/request.hbs @@ -1,3 +1,8 @@ +{{#equals @root.$config.client 'node'}} +import fetch, { FormData, Headers } from 'node-fetch'; +import type { RequestInit, Response } from 'node-fetch'; + +{{/equals}} import { ApiError } from './ApiError'; import type { ApiRequestOptions } from './ApiRequestOptions'; import type { ApiResult } from './ApiResult'; diff --git a/src/templates/core/fetch/sendRequest.hbs b/src/templates/core/fetch/sendRequest.hbs index cac8380b10..0f97798717 100644 --- a/src/templates/core/fetch/sendRequest.hbs +++ b/src/templates/core/fetch/sendRequest.hbs @@ -16,9 +16,11 @@ export const sendRequest = async ( signal: controller.signal, }; + {{#equals @root.$config.client 'fetch'}} if (config.WITH_CREDENTIALS) { request.credentials = config.CREDENTIALS; } + {{/equals}} for (const fn of config.interceptors.request._fns) { request = await fn(request) diff --git a/src/templates/core/node/getHeaders.hbs b/src/templates/core/node/getHeaders.hbs deleted file mode 100644 index 2a0e7f2eba..0000000000 --- a/src/templates/core/node/getHeaders.hbs +++ /dev/null @@ -1,42 +0,0 @@ -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]); - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - }) - .filter(([, value]) => value !== undefined && value !== null) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } - - return new Headers(headers); -}; diff --git a/src/templates/core/node/getRequestBody.hbs b/src/templates/core/node/getRequestBody.hbs deleted file mode 100644 index 71a352a8f7..0000000000 --- a/src/templates/core/node/getRequestBody.hbs +++ /dev/null @@ -1,12 +0,0 @@ -export const getRequestBody = (options: ApiRequestOptions): unknown => { - if (options.body !== undefined) { - if (options.mediaType?.includes('/json')) { - return JSON.stringify(options.body) - } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) { - return options.body as unknown; - } else { - return JSON.stringify(options.body); - } - } - return undefined; -}; diff --git a/src/templates/core/node/getResponseBody.hbs b/src/templates/core/node/getResponseBody.hbs deleted file mode 100644 index e0279c72dd..0000000000 --- a/src/templates/core/node/getResponseBody.hbs +++ /dev/null @@ -1,23 +0,0 @@ -export const getResponseBody = async (response: Response): Promise => { - if (response.status !== 204) { - try { - const contentType = response.headers.get('Content-Type'); - if (contentType) { - const jsonTypes = ['application/json', 'application/problem+json']; - const binaryTypes = ['audio/', 'image/', 'video/']; - const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type)); - const isBinary = binaryTypes.some(type => contentType.toLowerCase().startsWith(type)); - if (isJSON) { - return await response.json(); - } else if (isBinary) { - return await response.blob(); - } else { - return await response.text(); - } - } - } catch (error) { - console.error(error); - } - } - return undefined; -}; diff --git a/src/templates/core/node/getResponseHeader.hbs b/src/templates/core/node/getResponseHeader.hbs deleted file mode 100644 index cc415c0a78..0000000000 --- a/src/templates/core/node/getResponseHeader.hbs +++ /dev/null @@ -1,9 +0,0 @@ -export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers.get(responseHeader); - if (isString(content)) { - return content; - } - } - return undefined; -}; diff --git a/src/templates/core/node/request.hbs b/src/templates/core/node/request.hbs deleted file mode 100644 index aa5a50ae6a..0000000000 --- a/src/templates/core/node/request.hbs +++ /dev/null @@ -1,101 +0,0 @@ -import fetch, { FormData, Headers } from 'node-fetch'; -import type { RequestInit, Response } from 'node-fetch'; - -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -{{>functions/isString}} - - -{{>functions/isStringWithValue}} - - -{{>functions/isBlob}} - - -{{>functions/isFormData}} - - -{{>functions/base64}} - - -{{>functions/getQueryString}} - - -{{>functions/getUrl}} - - -{{>functions/getFormData}} - - -{{>functions/resolve}} - - -{{>node/getHeaders}} - - -{{>node/getRequestBody}} - - -{{>node/sendRequest}} - - -{{>node/getResponseHeader}} - - -{{>node/getResponseBody}} - - -{{>functions/catchErrorCodes}} - - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @returns CancelablePromise<{{#equals @root.$config.serviceResponse 'response'}}ApiResult{{else}}T{{/equals}}> - * @throws ApiError - */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<{{#equals @root.$config.serviceResponse 'response'}}ApiResult{{else}}T{{/equals}}> => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options); - - if (!onCancel.isCancelled) { - let response = await sendRequest(config, options, url, body, formData, headers, onCancel); - - for (const fn of config.interceptors.response._fns) { - response = await fn(response) - } - - const responseBody = await getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: response.ok, - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrorCodes(options, result); - - {{#equals @root.$config.serviceResponse 'generics'}} - resolve(config.RESULT === 'raw' ? result : result.body); - {{else}} - resolve({{#equals @root.$config.serviceResponse 'body'}}result.body{{else}}result{{/equals}}); - {{/equals}} - } - } catch (error) { - reject(error); - } - }); -}; diff --git a/src/templates/core/node/sendRequest.hbs b/src/templates/core/node/sendRequest.hbs deleted file mode 100644 index 319ef552ff..0000000000 --- a/src/templates/core/node/sendRequest.hbs +++ /dev/null @@ -1,26 +0,0 @@ -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: any, - formData: FormData | undefined, - headers: Headers, - onCancel: OnCancel -): Promise => { - const controller = new AbortController(); - - let request: RequestInit = { - headers, - method: options.method, - body: body ?? formData, - signal: controller.signal, - }; - - for (const fn of config.interceptors.request._fns) { - request = await fn(request) - } - - onCancel(() => controller.abort()); - - return await fetch(url, request); -}; diff --git a/src/templates/core/request.hbs b/src/templates/core/request.hbs index 5937980759..2dfd9bb559 100644 --- a/src/templates/core/request.hbs +++ b/src/templates/core/request.hbs @@ -1,5 +1,5 @@ {{~#equals @root.$config.client 'angular'}}{{>angular/request}}{{/equals~}} {{~#equals @root.$config.client 'axios'}}{{>axios/request}}{{/equals~}} {{~#equals @root.$config.client 'fetch'}}{{>fetch/request}}{{/equals~}} -{{~#equals @root.$config.client 'node'}}{{>node/request}}{{/equals~}} +{{~#equals @root.$config.client 'node'}}{{>fetch/request}}{{/equals~}} {{~#equals @root.$config.client 'xhr'}}{{>xhr/request}}{{/equals~}} diff --git a/src/utils/handlebars.ts b/src/utils/handlebars.ts index 965b3b3103..c70a6b237f 100644 --- a/src/utils/handlebars.ts +++ b/src/utils/handlebars.ts @@ -39,12 +39,6 @@ import functionIsStringWithValue from '../templates/core/functions/isStringWithV import functionIsSuccess from '../templates/core/functions/isSuccess.hbs'; import functionResolve from '../templates/core/functions/resolve.hbs'; import templateCoreHttpRequest from '../templates/core/HttpRequest.hbs'; -import nodeGetHeaders from '../templates/core/node/getHeaders.hbs'; -import nodeGetRequestBody from '../templates/core/node/getRequestBody.hbs'; -import nodeGetResponseBody from '../templates/core/node/getResponseBody.hbs'; -import nodeGetResponseHeader from '../templates/core/node/getResponseHeader.hbs'; -import nodeRequest from '../templates/core/node/request.hbs'; -import nodeSendRequest from '../templates/core/node/sendRequest.hbs'; import templateCoreSettings from '../templates/core/OpenAPI.hbs'; import templateCoreRequest from '../templates/core/request.hbs'; import templateCoreTypes from '../templates/core/types.hbs'; @@ -361,14 +355,6 @@ export const registerHandlebarTemplates = (config: Config, client: Client): Temp Handlebars.registerPartial('xhr/request', Handlebars.template(xhrRequest)); Handlebars.registerPartial('xhr/sendRequest', Handlebars.template(xhrSendRequest)); - // Specific files for the node client implementation - Handlebars.registerPartial('node/getHeaders', Handlebars.template(nodeGetHeaders)); - Handlebars.registerPartial('node/getRequestBody', Handlebars.template(nodeGetRequestBody)); - Handlebars.registerPartial('node/getResponseBody', Handlebars.template(nodeGetResponseBody)); - Handlebars.registerPartial('node/getResponseHeader', Handlebars.template(nodeGetResponseHeader)); - Handlebars.registerPartial('node/request', Handlebars.template(nodeRequest)); - Handlebars.registerPartial('node/sendRequest', Handlebars.template(nodeSendRequest)); - // Specific files for the axios client implementation Handlebars.registerPartial('axios/getHeaders', Handlebars.template(axiosGetHeaders)); Handlebars.registerPartial('axios/getRequestBody', Handlebars.template(axiosGetRequestBody)); diff --git a/test/__snapshots__/v3_node/core/request.ts.snap b/test/__snapshots__/v3_node/core/request.ts.snap index 0b5069cd5f..af13375581 100644 --- a/test/__snapshots__/v3_node/core/request.ts.snap +++ b/test/__snapshots__/v3_node/core/request.ts.snap @@ -156,7 +156,7 @@ export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptio if (options.mediaType) { headers['Content-Type'] = options.mediaType; } else if (isBlob(options.body)) { - headers['Content-Type'] = 'application/octet-stream'; + headers['Content-Type'] = options.body.type || 'application/octet-stream'; } else if (isString(options.body)) { headers['Content-Type'] = 'text/plain'; } else if (!isFormData(options.body)) { @@ -172,7 +172,7 @@ export const getRequestBody = (options: ApiRequestOptions): unknown => { if (options.mediaType?.includes('/json')) { return JSON.stringify(options.body); } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) { - return options.body as unknown; + return options.body; } else { return JSON.stringify(options.body); } @@ -193,8 +193,8 @@ export const sendRequest = async ( let request: RequestInit = { headers, - method: options.method, body: body ?? formData, + method: options.method, signal: controller.signal, };