Skip to content

Commit

Permalink
feat: reimplement securityWorker
Browse files Browse the repository at this point in the history
  • Loading branch information
aleclarson committed Mar 6, 2024
1 parent 69a26ec commit bf2adbb
Showing 1 changed file with 154 additions and 76 deletions.
230 changes: 154 additions & 76 deletions templates/base/http-clients/ky-http-client.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
const { apiConfig, generateResponses, config } = it;
%>

import type { KyInstance, Options as KyOptions, SearchParamsOption } from "ky";
import type {
BeforeRequestHook,
Hooks,
KyInstance,
Options as KyOptions,
NormalizedOptions,
SearchParamsOption,
} from "ky";
import ky from "ky";

type KyResponse<Data> = Response & {
Expand All @@ -19,7 +26,10 @@ export type ResponsePromise<Data> = {

export type ResponseFormat = keyof Omit<Body, "body" | "bodyUsed">;

export interface FullRequestParams extends Omit<KyOptions, "json" | "body" | "searchParams"> {
export interface FullRequestParams
extends Omit<KyOptions, "json" | "body" | "searchParams"> {
/** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean;
/** request path */
path: string;
/** content type of request body */
Expand All @@ -32,9 +42,17 @@ export interface FullRequestParams extends Omit<KyOptions, "json" | "body" | "se
body?: unknown;
}

export type RequestParams = Omit<FullRequestParams, "body" | "method" | "query" | "path">;

export interface ApiConfig<SecurityDataType = unknown> extends Omit<KyOptions, "data" | "cancelToken"> {
export type RequestParams = Omit<
FullRequestParams,
"body" | "method" | "query" | "path"
>;

export interface ApiConfig<SecurityDataType = unknown>
extends Omit<KyOptions, "data" | "cancelToken"> {
securityWorker?: (
securityData: SecurityDataType | null,
) => Promise<NormalizedOptions | void> | NormalizedOptions | void;
secure?: boolean;
format?: ResponseType;
}

Expand All @@ -46,92 +64,152 @@ export enum ContentType {
}

export class HttpClient<SecurityDataType = unknown> {
public ky: KyInstance;
private format?: ResponseType;
public ky: KyInstance;
private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
private secure?: boolean;
private format?: ResponseType;

constructor({
securityWorker,
secure,
format,
...options
}: ApiConfig<SecurityDataType> = {}) {
this.ky = ky.create({ ...options, prefixUrl: options.prefixUrl || "" });
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),
},
};
}

constructor({ format, ...options }: ApiConfig<SecurityDataType> = {}) {
this.ky = ky.create({ ...options, prefixUrl: options.prefixUrl || "<%~ apiConfig.baseUrl %>" })
this.format = format;
protected stringifyFormItem(formItem: unknown) {
if (typeof formItem === "object" && formItem !== null) {
return JSON.stringify(formItem);
} else {
return `${formItem}`;
}

protected stringifyFormItem(formItem: unknown) {
if (typeof formItem === "object" && formItem !== null) {
return JSON.stringify(formItem);
} else {
return `${formItem}`;
}

protected createFormData(input: Record<string, unknown>): 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),
);
}
}

protected createFormData(input: Record<string, unknown>): 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 = <T = any, _E = any>({
path,
type,
query,
format,
body,
...options
return formData;
}, new FormData());
}

public request = <T = any, _E = any>({
secure = this.secure,
path,
type,
query,
format,
body,
...options
<% if (config.unwrapResponseData) { %>
}: FullRequestParams): Promise<T> => {
}: FullRequestParams): Promise<T> => {
<% } else { %>
}: FullRequestParams): ResponsePromise<T> => {
}: FullRequestParams): ResponsePromise<T> => {
<% } %>
if (body) {
if (type === ContentType.FormData) {
body = typeof body === "object" ? this.createFormData(body as Record<string, unknown>) : body;
} else if (type === ContentType.Text) {
body = typeof body !== "string" ? JSON.stringify(body) : body;
}
}
if (body) {
if (type === ContentType.FormData) {
body =
typeof body === "object"
? this.createFormData(body as Record<string, unknown>)
: body;
} else if (type === ContentType.Text) {
body = typeof body !== "string" ? JSON.stringify(body) : body;
}
}

let headers: Headers | Record<string, string | undefined> | undefined;
if (options.headers instanceof Headers) {
headers = new Headers(options.headers);
if (type && type !== ContentType.FormData) {
headers.set('Content-Type', type);
}
} else {
headers = { ...options.headers } as Record<string, string | undefined>;
if (type && type !== ContentType.FormData) {
headers['Content-Type'] = type;
let headers: Headers | Record<string, string | undefined> | undefined;
if (options.headers instanceof Headers) {
headers = new Headers(options.headers);
if (type && type !== ContentType.FormData) {
headers.set("Content-Type", type);
}
} else {
headers = { ...options.headers } as Record<string, string | undefined>;
if (type && type !== ContentType.FormData) {
headers["Content-Type"] = type;
}
}

let hooks: Hooks | undefined;
if (secure && this.securityWorker) {
const securityWorker: BeforeRequestHook = async (_request, options) => {
const secureOptions = await this.securityWorker!(this.securityData);
if (secureOptions && typeof secureOptions === "object") {
const headers = new Headers(options.headers);
if (secureOptions && secureOptions.headers) {
const secureHeaders = new Headers(secureOptions.headers);
secureHeaders.forEach((value, key) => {
headers.set(key, value);
});
}
Object.assign(options, secureOptions);
options.headers = headers;
}
};

hooks = {
...options.hooks,
beforeRequest:
options.hooks && options.hooks.beforeRequest
? [securityWorker, ...options.hooks.beforeRequest]
: [securityWorker],
};
}

const request = this.ky(path.replace(/^\//, ''), {
...options,
headers,
searchParams: query,
body: body as any,
});
const request = this.ky(path.replace(/^\//, ""), {
...options,
headers,
searchParams: query,
body: body as any,
hooks,
});

<% if (config.unwrapResponseData) { %>
const responseFormat = (format || this.format) || undefined;
return responseFormat === "json"
? request.json()
: responseFormat === "arrayBuffer"
? request.arrayBuffer()
: responseFormat === "blob"
const responseFormat = format || this.format || undefined;
return (responseFormat === "json"
? request.json()
: responseFormat === "arrayBuffer"
? request.arrayBuffer()
: responseFormat === "blob"
? request.blob()
: responseFormat === "formData"
? request.formData()
: request.text();
? request.formData()
: request.text()) as Promise<T>;
<% } else { %>
return request;
return request;
<% } %>
};
};
}

0 comments on commit bf2adbb

Please sign in to comment.