From 04b800bee93cb05781fa114ce717abb86d5e81d7 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 5 Mar 2024 20:00:30 -0500 Subject: [PATCH] feat: reimplement securityWorker --- .../base/http-clients/ky-http-client.ejs | 234 ++++++++++++------ 1 file changed, 158 insertions(+), 76 deletions(-) diff --git a/templates/base/http-clients/ky-http-client.ejs b/templates/base/http-clients/ky-http-client.ejs index 490fc0ae..2cd901de 100644 --- a/templates/base/http-clients/ky-http-client.ejs +++ b/templates/base/http-clients/ky-http-client.ejs @@ -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 = Response & { @@ -19,7 +26,10 @@ export type ResponsePromise = { export type ResponseFormat = keyof Omit; -export interface FullRequestParams extends 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 */ @@ -32,9 +42,17 @@ export interface FullRequestParams extends Omit; - -export interface ApiConfig extends Omit { +export type RequestParams = Omit< + FullRequestParams, + "body" | "method" | "query" | "path" +>; + +export interface ApiConfig + extends Omit { + securityWorker?: ( + securityData: SecurityDataType | null, + ) => Promise | NormalizedOptions | void; + secure?: boolean; format?: ResponseType; } @@ -46,92 +64,156 @@ export enum ContentType { } export class HttpClient { - public ky: KyInstance; - private format?: ResponseType; + public ky: KyInstance; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private secure?: boolean; + private format?: ResponseType; + + constructor({ + securityWorker, + secure, + format, + ...options + }: ApiConfig = {}) { + 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 = {}) { - 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): 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): 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 = ({ - path, - type, - query, - format, - body, - ...options + return formData; + }, new FormData()); + } + + public request = ({ + secure = this.secure, + path, + type, + query, + format, + body, + ...options <% if (config.unwrapResponseData) { %> - }: FullRequestParams): Promise => { + }: FullRequestParams): Promise => { <% } else { %> - }: FullRequestParams): ResponsePromise => { + }: FullRequestParams): ResponsePromise => { <% } %> - if (body) { - if (type === ContentType.FormData) { - body = typeof body === "object" ? this.createFormData(body as Record) : 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) + : body; + } else if (type === ContentType.Text) { + body = typeof body !== "string" ? JSON.stringify(body) : body; + } + } - let headers: Headers | Record | 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; - if (type && type !== ContentType.FormData) { - headers['Content-Type'] = type; + let headers: Headers | Record | 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; + 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") { + let { headers } = options; + if (secureOptions.headers) { + const secureHeaders = new Headers(secureOptions.headers); + headers = new Headers(headers); + secureHeaders.forEach((value, key) => { + headers.set(key, value); + }); } + return new Request(request.url, { + ...options, + ...secureOptions, + 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; <% } else { %> - return request; + return request; <% } %> - }; + }; }