diff --git a/.changeset/beige-ligers-tell.md b/.changeset/beige-ligers-tell.md new file mode 100644 index 00000000..42901772 --- /dev/null +++ b/.changeset/beige-ligers-tell.md @@ -0,0 +1,5 @@ +--- +"openapi-fetch": patch +--- + +Fix GET requests requiring 2nd param when it’s not needed diff --git a/.changeset/rich-poems-swim.md b/.changeset/rich-poems-swim.md new file mode 100644 index 00000000..e18d9b0a --- /dev/null +++ b/.changeset/rich-poems-swim.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": minor +--- + +✨ **Feature**: Added debugger that lets you profile performance and see more in-depth messages diff --git a/docs/src/content/docs/openapi-fetch/index.md b/docs/src/content/docs/openapi-fetch/index.md index f03461ad..ddcc696b 100644 --- a/docs/src/content/docs/openapi-fetch/index.md +++ b/docs/src/content/docs/openapi-fetch/index.md @@ -69,8 +69,6 @@ Next, generate TypeScript types from your OpenAPI schema using openapi-typescrip npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts ``` -> ⚠️ Be sure to validate your schemas! openapi-typescript will err on invalid schemas. - Lastly, be sure to **run typechecking** in your project. This can be done by adding `tsc --noEmit` to your npm scripts like so: ```json diff --git a/packages/openapi-fetch/src/index.test.ts b/packages/openapi-fetch/src/index.test.ts index d6849017..afa5e0ec 100644 --- a/packages/openapi-fetch/src/index.test.ts +++ b/packages/openapi-fetch/src/index.test.ts @@ -51,7 +51,7 @@ describe("client", () => { status: 200, body: JSON.stringify(["one", "two", "three"]), }); - const dataRes = await client.GET("/string-array", {}); + const dataRes = await client.GET("/string-array"); // … is initially possibly undefined // @ts-expect-error @@ -73,7 +73,7 @@ describe("client", () => { status: 500, body: JSON.stringify({ code: 500, message: "Something went wrong" }), }); - const errorRes = await client.GET("/string-array", {}); + const errorRes = await client.GET("/string-array"); // … is initially possibly undefined // @ts-expect-error diff --git a/packages/openapi-fetch/src/index.ts b/packages/openapi-fetch/src/index.ts index a11278fa..32db3849 100644 --- a/packages/openapi-fetch/src/index.ts +++ b/packages/openapi-fetch/src/index.ts @@ -13,7 +13,6 @@ import type { const DEFAULT_HEADERS = { "Content-Type": "application/json", }; -const TRAILING_SLASH_RE = /\/*$/; // Note: though "any" is considered bad practice in general, this library relies // on "any" for type inference only it can give. Same goes for the "{}" type. @@ -32,28 +31,46 @@ interface ClientOptions extends Omit { // headers override to make typing friendlier headers?: HeadersOptions; } + export type HeadersOptions = | HeadersInit | Record; + export type QuerySerializer = ( query: T extends { parameters: any } ? NonNullable : Record, ) => string; + export type BodySerializer = (body: OperationRequestBodyContent) => any; + export type ParseAs = "json" | "text" | "blob" | "arrayBuffer" | "stream"; + export interface DefaultParamsOption { params?: { query?: Record }; } + +export interface EmptyParameters { + query?: never; + header?: never; + path?: never; + cookie?: never; +} + export type ParamsOption = T extends { parameters: any } - ? { params: NonNullable } + ? T["parameters"] extends EmptyParameters + ? DefaultParamsOption + : { params: NonNullable } : DefaultParamsOption; + export type RequestBodyOption = OperationRequestBodyContent extends never ? { body?: never } : undefined extends OperationRequestBodyContent ? { body?: OperationRequestBodyContent } : { body: OperationRequestBodyContent }; + export type FetchOptions = RequestOptions & Omit; + export type FetchResponse = | { data: FilterKeys>, MediaType>; @@ -65,6 +82,7 @@ export type FetchResponse = error: FilterKeys>, MediaType>; response: Response; }; + export type RequestOptions = ParamsOption & RequestBodyOption & { querySerializer?: QuerySerializer; @@ -81,6 +99,10 @@ export default function createClient( bodySerializer: globalBodySerializer, ...options } = clientOptions; + let baseUrl = options.baseUrl ?? ""; + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } async function coreFetch

( url: P, @@ -98,7 +120,7 @@ export default function createClient( // URL const finalURL = createFinalURL(url as string, { - baseUrl: options.baseUrl, + baseUrl, params, querySerializer, }); @@ -159,13 +181,20 @@ export default function createClient( return { error, response: response as any }; } + type GetPaths = PathsWithMethod; + type GetFetchOptions

= FetchOptions< + FilterKeys + >; + return { /** Call a GET endpoint */ - async GET

>( + async GET

( url: P, - init: FetchOptions>, + ...init: GetFetchOptions

extends DefaultParamsOption // little hack to allow the 2nd param to be omitted if nothing is required (only for GET) + ? [GetFetchOptions

?] + : [GetFetchOptions

] ) { - return coreFetch(url, { ...init, method: "GET" } as any); + return coreFetch(url, { ...init[0], method: "GET" } as any); }, /** Call a PUT endpoint */ async PUT

>( @@ -245,26 +274,20 @@ export function defaultBodySerializer(body: T): string { /** Construct URL string from baseUrl and handle path and query params */ export function createFinalURL( - url: string, + pathname: string, options: { - baseUrl?: string; + baseUrl: string; params: { query?: Record; path?: Record }; querySerializer: QuerySerializer; }, ): string { - let finalURL = `${ - options.baseUrl ? options.baseUrl.replace(TRAILING_SLASH_RE, "") : "" - }${url as string}`; - if (options.params.path) { - for (const [k, v] of Object.entries(options.params.path)) { - finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(String(v))); - } + let finalURL = `${options.baseUrl}${pathname}`; + for (const [k, v] of Object.entries(options.params.path ?? {})) { + finalURL = finalURL.replace(`{${k}}`, encodeURIComponent(String(v))); } - if (options.params.query) { - const search = options.querySerializer(options.params.query as any); - if (search) { - finalURL += `?${search}`; - } + const search = options.querySerializer((options.params.query as any) ?? {}); + if (search) { + finalURL += `?${search}`; } return finalURL; } diff --git a/packages/openapi-typescript-helpers/index.d.ts b/packages/openapi-typescript-helpers/index.d.ts index ce57c429..9db50a06 100644 --- a/packages/openapi-typescript-helpers/index.d.ts +++ b/packages/openapi-typescript-helpers/index.d.ts @@ -2,7 +2,15 @@ // HTTP types -export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"; +export type HttpMethod = + | "get" + | "put" + | "post" + | "delete" + | "options" + | "head" + | "patch" + | "trace"; /** 2XX statuses */ export type OkStatus = 200 | 201 | 202 | 203 | 204 | 206 | 207 | "2XX"; // prettier-ignore @@ -12,8 +20,15 @@ export type ErrorStatus = 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | // OpenAPI type helpers /** Given an OpenAPI **Paths Object**, find all paths that have the given method */ -export type PathsWithMethod, PathnameMethod extends HttpMethod> = { - [Pathname in keyof Paths]: Paths[Pathname] extends { [K in PathnameMethod]: any } ? Pathname : never; +export type PathsWithMethod< + Paths extends Record, + PathnameMethod extends HttpMethod, +> = { + [Pathname in keyof Paths]: Paths[Pathname] extends { + [K in PathnameMethod]: any; + } + ? Pathname + : never; }[keyof Paths]; /** DO NOT USE! Only used only for OperationObject type inference */ export interface OperationObject { @@ -23,27 +38,48 @@ export interface OperationObject { responses: any; } /** Internal helper used in PathsWithMethod */ -export type PathItemObject = { [M in HttpMethod]: OperationObject } & { parameters?: any }; +export type PathItemObject = { + [M in HttpMethod]: OperationObject; +} & { parameters?: any }; /** Return `responses` for an Operation Object */ -export type ResponseObjectMap = T extends { responses: any } ? T["responses"] : unknown; +export type ResponseObjectMap = T extends { responses: any } + ? T["responses"] + : unknown; /** Return `content` for a Response Object */ -export type ResponseContent = T extends { content: any } ? T["content"] : unknown; +export type ResponseContent = T extends { content: any } + ? T["content"] + : unknown; /** Return `requestBody` for an Operation Object */ -export type OperationRequestBody = T extends { requestBody?: any } ? T["requestBody"] : never; +export type OperationRequestBody = T extends { requestBody?: any } + ? T["requestBody"] + : never; /** Internal helper used in OperationRequestBodyContent */ -export type OperationRequestBodyMediaContent = undefined extends OperationRequestBody ? FilterKeys>, "content"> | undefined : FilterKeys, "content">; +export type OperationRequestBodyMediaContent = + undefined extends OperationRequestBody + ? FilterKeys>, "content"> | undefined + : FilterKeys, "content">; /** Return first `content` from a Request Object Mapping, allowing any media type */ -export type OperationRequestBodyContent = FilterKeys, MediaType> extends never - ? FilterKeys>, MediaType> | undefined +export type OperationRequestBodyContent = FilterKeys< + OperationRequestBodyMediaContent, + MediaType +> extends never + ? + | FilterKeys>, MediaType> + | undefined : FilterKeys, MediaType>; /** Return first 2XX response from a Response Object Map */ export type SuccessResponse = FilterKeys, "content">; /** Return first 5XX or 4XX response (in that order) from a Response Object Map */ -export type ErrorResponse = FilterKeys, "content">; +export type ErrorResponse = FilterKeys< + FilterKeys, + "content" +>; // Generic TS utils /** Find first match of multiple keys */ -export type FilterKeys = { [K in keyof Obj]: K extends Matchers ? Obj[K] : never }[keyof Obj]; +export type FilterKeys = { + [K in keyof Obj]: K extends Matchers ? Obj[K] : never; +}[keyof Obj]; /** Return any `[string]/[string]` media type (important because openapi-fetch allows any content response, not just JSON-like) */ export type MediaType = `${string}/${string}`;