Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ofetch http client #696

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Options:
--module-name-index <number> determines which path index should be used for routes separation (example: GET:/fruits/getFruit -> index:0 -> moduleName -> fruits) (default: 0)
--module-name-first-tag splits routes based on the first tag (default: false)
--axios generate axios http client (default: false)
--ofetch generate ofetch 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)
Expand All @@ -62,7 +63,7 @@ Commands:
generate-templates Generate ".ejs" templates needed for generate api
-o, --output <string> output path of generated templates
-m, --modular generate templates needed to separate files for http client, data contracts, and routes (default: false)
--http-client <string> http client type (possible values: "fetch", "axios") (default: "fetch")
--http-client <string> http client type (possible values: "fetch", "axios", "ofetch") (default: "fetch")
-c, --clean-output clean output folder before generate template. WARNING: May cause data loss (default: false)
-r, --rewrite rewrite content in existing templates (default: false)
--silent Output only errors to console (default: false)
Expand Down
9 changes: 8 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ const generateCommand = defineCommand({
description: "generate axios http client",
default: false,
},
ofetch: {
type: "boolean",
description: "generate ofetch http client",
default: false,
},
"unwrap-response-data": {
type: "boolean",
description: "unwrap the data item from the response",
Expand Down Expand Up @@ -318,7 +323,9 @@ const generateCommand = defineCommand({
httpClientType:
args["http-client"] || args.axios
? HTTP_CLIENT.AXIOS
: HTTP_CLIENT.FETCH,
: args.ofetch
? HTTP_CLIENT.OFETCH
: HTTP_CLIENT.FETCH,
input: path.resolve(process.cwd(), args.path as string),
modular: args.modular,
moduleNameFirstTag: args["module-name-first-tag"],
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const FILE_PREFIX = `/* eslint-disable */

export const HTTP_CLIENT = {
FETCH: "fetch",
OFETCH: "ofetch",
AXIOS: "axios",
} as const;

Expand Down
123 changes: 123 additions & 0 deletions templates/base/http-clients/ofetch-http-client.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<%
const { apiConfig, generateResponses, config } = it;
%>

import type { $Fetch, FetchOptions } from 'ofetch'
import { $fetch } from 'ofetch'

export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;

export interface CustomFetchOptions extends FetchOptions {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean
}

export type RequestParams = Omit<CustomFetchOptions, 'body' | 'method'>

export interface ApiConfig<SecurityDataType = unknown> {
baseURL?: string;
basePath?: string;
baseApiParams?: Omit<RequestParams, "baseURL" | "cancelToken" | "signal">;
securityWorker?: (securityData: SecurityDataType | null) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: $Fetch;
}

type CancelToken = Symbol | string | number;

export enum ContentType {
Json = "application/json",
FormData = "multipart/form-data",
UrlEncoded = "application/x-www-form-urlencoded",
}

export class HttpClient<SecurityDataType = unknown> {
public baseURL: string = "<%~ apiConfig.baseUrl %>";
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (url: string, fetchParams: FetchOptions) => $fetch(url, fetchParams)

private baseApiParams: RequestParams = {
credentials: 'same-origin',
headers: {},
redirect: 'follow',
referrerPolicy: 'no-referrer',
}

constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig);
}

public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data;
}

protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams {
return {
...this.baseApiParams,
...params1,
...(params2 || {}),
headers: {
...(this.baseApiParams.headers || {}),
...(params1.headers || {}),
...((params2 && params2.headers) || {}),
},
};
}

protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken);
if (abortController) {
return abortController.signal;
}
return void 0;
}

const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController);
return abortController.signal;
}

public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken)

if (abortController) {
abortController.abort();
this.abortControllers.delete(cancelToken);
}
}

public request = async <T = any>(url: string, {
body,
secure,
method,
baseURL,
signal,
params,
...options
<% if (config.unwrapResponseData) { %>
}: CustomFetchOptions): Promise<T> => {
<% } else { %>
}: CustomFetchOptions): Promise<T> => {
<% } %>
const secureParams = ((typeof secure === 'boolean' ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData)) || {};
const requestOptions = this.mergeRequestParams(options, secureParams)

return this.customFetch(
`${baseURL || this.baseURL || ""}${this.basePath ? `${this.basePath}` : ''}${url}`,
{
params,
method,
...requestOptions,
signal,
body,
}
<% if (config.unwrapResponseData) { %>
).then((response: T) => response.data)
<% } else { %>
).then((response: T) => response)
<% } %>
};
}
7 changes: 7 additions & 0 deletions templates/default/procedure-call.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,20 @@ const describeReturnType = () => {

*/
<%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> =>
<% if (config.httpClientType === config.constants.HTTP_CLIENT.OFETCH) { %>
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>>(`<%~ path %>`, {
<% } %>
<% if (config.httpClientType !== config.constants.HTTP_CLIENT.OFETCH) { %>
<%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({
path: `<%~ path %>`,
<% } %>
method: '<%~ _.upperCase(method) %>',
<%~ queryTmpl ? `query: ${queryTmpl},` : '' %>
<%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %>
<%~ securityTmpl ? `secure: ${securityTmpl},` : '' %>
<% if (config.httpClientType !== config.constants.HTTP_CLIENT.OFETCH) { %>
<%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %>
<%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %>
<% } %>
...<%~ _.get(requestConfigParam, "name") %>,
})<%~ route.namespace ? ',' : '' %>
Loading