openapi-hooks
Version:
Magical fetch inference for OpenAPI with React Query support
1 lines • 12.2 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["type Paths = {\n [key: string]: {\n [key: string]: any;\n };\n}\n\ntype HTTPMethod =\n | 'get'\n | 'put'\n | 'post'\n | 'delete'\n | 'options'\n | 'head'\n | 'patch'\n | 'trace';\n\nexport type PathMethods<paths extends Paths, TPath extends keyof paths> = {\n [TMethod in HTTPMethod]: paths[TPath][TMethod] extends undefined\n ? never\n : TMethod;\n}[HTTPMethod];\n\ntype AnyRequestBody = {\n content: Record<string, any>;\n};\n\ntype AnyResponses = Record<\n number,\n { content?: Record<string, any>; headers?: Record<string, any> }\n>;\n\ntype AnyParameters = {\n query?: Record<string, any>;\n header?: Record<string, any>;\n path?: Record<string, any>;\n cookie?: Record<string, any>;\n};\n\ntype AnyRoute = {\n responses: AnyResponses;\n requestBody?: AnyRequestBody;\n parameters: AnyParameters;\n};\n\n\ntype Prettify<T> = {\n [K in keyof T]: T[K];\n} & {};\n\n\nexport type ApiResponse<TResponses extends AnyResponses> = {\n [TStatus in keyof TResponses]: TStatus extends number\n ? TResponses[TStatus]['content'] extends undefined\n ? {\n status: TStatus;\n contentType: never;\n data: never;\n headers: TResponses[TStatus]['headers'] extends Record<\n string,\n unknown\n >\n ? Map<\n keyof TResponses[TStatus]['headers'],\n TResponses[TStatus]['headers'][keyof TResponses[TStatus]['headers']]\n >\n : TResponses[TStatus]['headers'];\n }\n : {\n [K in keyof TResponses[TStatus]['content']]: {\n status: TStatus;\n contentType: K;\n data: TResponses[TStatus]['content'][K];\n headers: TResponses[TStatus]['headers'] extends Record<\n string,\n unknown\n >\n ? Map<\n keyof TResponses[TStatus]['headers'],\n TResponses[TStatus]['headers'][keyof TResponses[TStatus]['headers']]\n >\n : TResponses[TStatus]['headers'];\n };\n }[keyof TResponses[TStatus]['content']]\n : never;\n}[keyof TResponses];\n\nexport type ApiRequestBody<TBody extends AnyRequestBody | undefined> =\n TBody extends AnyRequestBody\n ? {\n [K in keyof TBody['content']]: {\n contentType: K;\n data: TBody['content'][K];\n };\n }[keyof TBody['content']]\n : {\n contentType?: undefined;\n data?: undefined;\n };\n\nexport type HeaderObject = Record<string, string>;\nexport type HeaderPredicate = () => PromiseLike<HeaderObject>;\n\nexport type OpenApiHookOptions = {\n baseUrl: URL | string;\n headers?: HeaderObject | HeaderPredicate;\n onError?: (error: ApiError) => void;\n};\n\nexport class ApiError extends Error {\n // eslint-disable-next-line unused-imports/no-unused-vars\n constructor(message: string, public status: number) {\n super(message);\n }\n\n static fromResponse(response: Response) {\n return new ApiError(response.statusText, response.status);\n }\n}\n\nconst convertBody = (\n data: any,\n contentType: string | undefined\n): BodyInit | undefined => {\n if (contentType === undefined) {\n return;\n }\n\n switch (contentType) {\n case 'application/json':\n case 'application/json; charset=utf-8':\n return JSON.stringify(data);\n default:\n throw new Error('Unsupported content type: ' + contentType);\n }\n};\n\nconst decodeResponse = async (response, responseContentType) => {\n switch (responseContentType) {\n // eslint-disable-next-line unicorn/no-null\n case null:\n return {\n status: response.status,\n headers: response.headers,\n } as any;\n\n case 'text/plain; charset=utf-8':\n return {\n status: response.status,\n contentType: responseContentType,\n data: await response.text(),\n } as any;\n\n case 'application/json; charset=utf-8':\n return {\n status: response.status,\n contentType: responseContentType,\n data: await response.json(),\n headers: response.headers,\n } as any;\n default:\n throw new Error('Unsupported content type: ' + responseContentType);\n }\n};\n\nexport const createFetch = <paths extends Paths>(options?: OpenApiHookOptions) => {\n const { baseUrl = window.location.toString(), headers: defaultHeaders, onError } = options ?? {};\n\n /**\n * Makes a type-safe OpenAPI request using fetch\n *\n * @description\n * This function provides a strongly-typed interface for making API requests based on your OpenAPI schema.\n * It handles authentication, request/response serialization, and error handling automatically.\n *\n * @example\n * Basic GET request:\n * ```ts\n * const response = await fetching('/items', 'get', {\n * query: { limit: 10, offset: 0 }\n * });\n * // response.data is fully typed based on your API schema! 🎉\n * console.log(response.data.items);\n * ```\n *\n * @example\n * POST request with JSON body:\n * ```ts\n * const response = await fetching('/items', 'post', {\n * contentType: 'application/json',\n * data: {\n * name: 'Cool Item',\n * description: 'A very cool item indeed'\n * }\n * });\n * ```\n *\n * @example\n * Using path parameters:\n * ```ts\n * const response = await fetching('/items/{itemId}', 'get', {\n * path: { itemId: '123' }\n * });\n * ```\n *\n * @example\n * Adding custom headers:\n * ```ts\n * const response = await fetching('/items', 'get', {\n * header: {\n * 'X-Custom-Header': 'value'\n * }\n * });\n * ```\n *\n * @example\n * Handling errors:\n * ```ts\n * try {\n * const response = await fetching('/items', 'get', {});\n * } catch (error) {\n * if (error instanceof ApiError) {\n * console.error(`API Error ${error.status}: ${error.message}`);\n * }\n * }\n * ```\n *\n * @template TPath - The API endpoint path (must exist in your OpenAPI schema)\n * @template TMethod - The HTTP method to use (must be valid for the given path)\n * @template TOptions - Request options including query params, headers, and body\n * @template TRoute - Internal type representing the full route definition\n *\n * @param path - The API endpoint path (e.g., '/items')\n * @param method - The HTTP method to use (e.g., 'get', 'post')\n * @param options - Request configuration object containing:\n * - query?: Record<string, any> - Query parameters\n * - header?: Record<string, any> - Custom headers\n * - path?: Record<string, any> - Path parameters\n * - contentType?: string - Request content type\n * - data?: any - Request body\n * - fetchOptions?: RequestInit - Additional fetch options\n *\n * @returns A Promise resolving to a typed response object containing:\n * - status: HTTP status code\n * - contentType: Response content type (if applicable)\n * - data: Response body (if applicable)\n * - headers: Response headers\n *\n * @throws {ApiError} When the server returns a non-2xx status code\n * @throws {Error} When an unsupported content type is encountered\n */\n return async <\n TPath extends keyof paths,\n TMethod extends PathMethods<paths, TPath>,\n TOptions extends TRoute['parameters'] & ApiRequestBody<TRoute['requestBody']> & {\n fetchOptions?: RequestInit;\n },\n TRoute extends AnyRoute = paths[TPath][TMethod] extends AnyRoute\n ? paths[TPath][TMethod]\n : never,\n >(\n path: TPath,\n method: TMethod,\n options: TOptions\n ): Promise<Prettify<ApiResponse<TRoute['responses']>>> => {\n const {\n query,\n header,\n path: pathParameters,\n contentType,\n data,\n fetchOptions,\n } = options;\n\n if (pathParameters) {\n for (const [key, value] of Object.entries(pathParameters)) {\n path = path.toString().replace(`{${key}}`, value) as TPath;\n }\n }\n\n const url = new URL(`.${path.toString()}`, baseUrl);\n\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (!value) continue;\n\n url.searchParams.set(key, value.toString());\n }\n }\n\n const headers = new Headers();\n\n if (defaultHeaders) {\n let addDefaultHeaders = typeof defaultHeaders === 'function' ? await defaultHeaders() : defaultHeaders;\n\n for (const [key, value] of Object.entries(addDefaultHeaders)) {\n headers.set(key, value);\n }\n }\n\n if (contentType) {\n headers.set('Content-Type', contentType);\n }\n\n if (header) {\n for (const [key, value] of Object.entries(header)) {\n headers.set(key, value.toString());\n }\n }\n\n const response = await fetch(url, {\n method: method.toUpperCase(),\n headers,\n body: convertBody(data, contentType),\n ...fetchOptions,\n });\n\n if (!response.ok) {\n onError?.(ApiError.fromResponse(response));\n throw ApiError.fromResponse(response);\n }\n\n const responseContentType = response.headers.get('content-type');\n\n return await decodeResponse(response, responseContentType);\n };\n};\n"],"mappings":"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","names":["options"]}