From 7c8567e5a5df0c1d5db078fcda2bb41f4d174580 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 5 Mar 2024 01:11:28 -0500 Subject: [PATCH] feat: add --ky option Generate a client that uses this library: https://github.com/sindresorhus/ky --- README.md | 3 +- index.js | 7 +- package-lock.json | 1 + package.json | 1 + src/constants.js | 1 + .../base/http-clients/ky-http-client.ejs | 140 ++++++++++++++++++ templates/default/api.ejs | 2 + templates/default/procedure-call.ejs | 3 + templates/modular/api.ejs | 2 + templates/modular/procedure-call.ejs | 3 + 10 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 templates/base/http-clients/ky-http-client.ejs diff --git a/README.md b/README.md index 379ebd7c..af15eb39 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ Options: --disableStrictSSL disabled strict SSL (default: false) --disableProxy disabled proxy (default: false) --axios generate axios http client (default: false) + --ky generate ky http client (default: false) --unwrap-response-data unwrap the data item from the response (default: false) --disable-throw-on-error Do not throw an error when response.ok is not true (default: false) --single-http-client Ability to send HttpClient instance to Api constructor (default: false) @@ -124,7 +125,7 @@ generateApi({ // ... }, templates: path.resolve(process.cwd(), './api-templates'), - httpClientType: "axios", // or "fetch" + httpClientType: "axios", // or "fetch" or "ky" defaultResponseAsSuccess: false, generateClient: true, generateRouteTypes: false, diff --git a/index.js b/index.js index 9302d910..c0e01154 100644 --- a/index.js +++ b/index.js @@ -162,6 +162,11 @@ const program = cli({ description: 'generate axios http client', default: codeGenBaseConfig.httpClientType === HTTP_CLIENT.AXIOS, }, + { + flags: '--ky', + description: 'generate axios http client', + default: codeGenBaseConfig.httpClientType === HTTP_CLIENT.KY, + }, { flags: '--unwrap-response-data', description: 'unwrap the data item from the response', @@ -324,7 +329,7 @@ const main = async () => { url: options.path, generateRouteTypes: options.routeTypes, generateClient: !!(options.axios || options.client), - httpClientType: options.axios ? HTTP_CLIENT.AXIOS : HTTP_CLIENT.FETCH, + httpClientType: options.axios ? HTTP_CLIENT.AXIOS : options.ky ? HTTP_CLIENT.KY : HTTP_CLIENT.FETCH, input: resolve(process.cwd(), options.path), output: resolve(process.cwd(), options.output || '.'), ...customConfig, diff --git a/package-lock.json b/package-lock.json index 33ecee8e..aaa84095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "eslint-plugin-prettier": "^5.0.0", "git-diff": "^2.0.6", "husky": "^8.0.3", + "ky": "^1.2.2", "pretty-quick": "^3.1.3", "rimraf": "^5.0.1" } diff --git a/package.json b/package.json index ab1bffd8..96d1b3cf 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "dotenv": "^16.3.1", "git-diff": "^2.0.6", "husky": "^8.0.3", + "ky": "^1.2.2", "pretty-quick": "^3.1.3", "rimraf": "^5.0.1" }, diff --git a/src/constants.js b/src/constants.js index 61b55e14..8d7c5282 100644 --- a/src/constants.js +++ b/src/constants.js @@ -28,6 +28,7 @@ const SCHEMA_TYPES = { const HTTP_CLIENT = { FETCH: 'fetch', AXIOS: 'axios', + KY: 'ky', }; const PROJECT_VERSION = packageJson.version; diff --git a/templates/base/http-clients/ky-http-client.ejs b/templates/base/http-clients/ky-http-client.ejs new file mode 100644 index 00000000..6b7ddcfe --- /dev/null +++ b/templates/base/http-clients/ky-http-client.ejs @@ -0,0 +1,140 @@ +<% +const { apiConfig, generateResponses, config } = it; +%> + +import type { KyInstance, Options as KyOptions } from "ky"; +import ky from "ky"; + +export type KyResponse = Response & { + json(): Promise; +} + +export type QueryParamsType = Record; +export type ResponseFormat = keyof Omit; + +export interface FullRequestParams extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseFormat; + /** request body */ + body?: unknown; +} + +export type RequestParams = Omit; + +export interface ApiConfig extends Omit { + securityWorker?: (securityData: SecurityDataType | null) => Promise | KyOptions | void; + secure?: boolean; + format?: ResponseType; +} + +export enum ContentType { + Json = "application/json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", + Text = "text/plain", +} + +export class HttpClient { + public instance: KyInstance; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private secure?: boolean; + private format?: ResponseType; + + constructor({ securityWorker, secure, format, ...options }: ApiConfig = {}) { + this.instance = axios.create({ ...options, prefixUrl: options.prefixUrl || "<%~ apiConfig.baseUrl %>" }) + this.secure = secure; + this.format = format; + this.securityWorker = securityWorker; + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data + } + + protected mergeRequestParams(params1: KyOptions, params2?: KyOptions): KyOptions { + return { + ...params1, + ...params2, + headers: { + ...(params1.headers), + ...(params2 && params2.headers), + }, + }; + } + + protected stringifyFormItem(formItem: unknown) { + if (typeof formItem === "object" && formItem !== null) { + return JSON.stringify(formItem); + } else { + return `${formItem}`; + } + } + + protected createFormData(input: Record): FormData { + return Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + const propertyContent: any[] = (property instanceof Array) ? property : [property] + + for (const formItem of propertyContent) { + const isFileType = formItem instanceof Blob || formItem instanceof File; + formData.append( + key, + isFileType ? formItem : this.stringifyFormItem(formItem) + ); + } + + return formData; + }, new FormData()); + } + + public request = async ({ + secure, + path, + type, + query, + format, + body, + ...params +<% if (config.unwrapResponseData) { %> + }: FullRequestParams): Promise => { +<% } else { %> + }: FullRequestParams): KyResponse => { +<% } %> + const secureParams = ((typeof secure === 'boolean' ? secure : this.secure) && this.securityWorker && (await this.securityWorker(this.securityData))) || {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const responseFormat = (format || this.format) || undefined; + + if (type === ContentType.FormData && body && body !== null && typeof body === "object") { + body = this.createFormData(body as Record); + } + + if (type === ContentType.Text && body && body !== null && typeof body !== "string") { + body = JSON.stringify(body); + } + + return this.instance.request({ + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), + }, + params: query, + responseType: responseFormat, + data: body, + url: path, +<% if (config.unwrapResponseData) { %> + }).json(); +<% } else { %> + }); +<% } %> + }; +} diff --git a/templates/default/api.ejs b/templates/default/api.ejs index a393c917..a54340ec 100644 --- a/templates/default/api.ejs +++ b/templates/default/api.ejs @@ -28,6 +28,8 @@ const descriptionLines = _.compact([ <% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %> +<% if (config.httpClientType === config.constants.HTTP_CLIENT.KY) { %> import type { KyResponse } from "ky"; <% } %> + <% if (descriptionLines.length) { %> /** <% descriptionLines.forEach((descriptionLine) => { %> diff --git a/templates/default/procedure-call.ejs b/templates/default/procedure-call.ejs index 465d6327..b2e43226 100644 --- a/templates/default/procedure-call.ejs +++ b/templates/default/procedure-call.ejs @@ -72,6 +72,9 @@ const describeReturnType = () => { case HTTP_CLIENT.AXIOS: { return `Promise>` } + case HTTP_CLIENT.KY: { + return `KyResponse<${type}>` + } default: { return `Promise` } diff --git a/templates/modular/api.ejs b/templates/modular/api.ejs index 25bfa182..ad9c33ac 100644 --- a/templates/modular/api.ejs +++ b/templates/modular/api.ejs @@ -8,6 +8,8 @@ const dataContracts = _.map(modelTypes, "name"); <% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { AxiosRequestConfig, AxiosResponse } from "axios"; <% } %> +<% if (config.httpClientType === config.constants.HTTP_CLIENT.AXIOS) { %> import type { KyResponse } from "ky"; <% } %> + import { HttpClient, RequestParams, ContentType, HttpResponse } from "./<%~ config.fileNames.httpClient %>"; <% if (dataContracts.length) { %> import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>" diff --git a/templates/modular/procedure-call.ejs b/templates/modular/procedure-call.ejs index 6f15500f..11112b74 100644 --- a/templates/modular/procedure-call.ejs +++ b/templates/modular/procedure-call.ejs @@ -72,6 +72,9 @@ const describeReturnType = () => { case HTTP_CLIENT.AXIOS: { return `Promise>` } + case HTTP_CLIENT.KY: { + return `KyResponse<${type}>` + } default: { return `Promise` }