diff --git a/packages/openapi-pinia-colada/.npmignore b/packages/openapi-pinia-colada/.npmignore
new file mode 100644
index 000000000..20c0b3b59
--- /dev/null
+++ b/packages/openapi-pinia-colada/.npmignore
@@ -0,0 +1,5 @@
+.turbo
+test
+vitest.config.ts
+tsconfig*.json
+biome.json
diff --git a/packages/openapi-pinia-colada/CHANGELOG.md b/packages/openapi-pinia-colada/CHANGELOG.md
new file mode 100644
index 000000000..c32f4da71
--- /dev/null
+++ b/packages/openapi-pinia-colada/CHANGELOG.md
@@ -0,0 +1,7 @@
+# openapi-pinia-colada
+
+## 0.0.1
+
+### Patch Changes
+
+- [#2060](https://github.com/openapi-ts/openapi-typescript/pull/2060) [`d4ae6c8`](https://github.com/openapi-ts/openapi-typescript/pull/2060/commits/d4ae6c8ec5549e317f09ee32c6a06f9e9c60d97e) Thanks [@mettekou](https://github.com/mettekou)! - Initial release
\ No newline at end of file
diff --git a/packages/openapi-pinia-colada/CONTRIBUTING.md b/packages/openapi-pinia-colada/CONTRIBUTING.md
new file mode 100644
index 000000000..e64717c75
--- /dev/null
+++ b/packages/openapi-pinia-colada/CONTRIBUTING.md
@@ -0,0 +1,91 @@
+# Contributing
+
+Thanks for being willing to contribute! 🙏
+
+**Working on your first Pull Request (PR)?** You can learn how from this free series [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
+
+## Open issues
+
+Please check out the [the open issues](https://github.com/openapi-ts/openapi-typescript/issues). Issues labelled [**Good First Issue**](https://github.com/openapi-ts/openapi-typescript/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are especially good to start with.
+
+Contributing doesn’t have to be in code. Simply answering questions in open issues or providing workarounds is as important as making pull requests.
+
+## Writing code
+
+### Setup
+
+1. Install [pnpm](https://pnpm.io/)
+2. [Fork this repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and clone your copy locally
+3. Run `pnpm i` to install dependencies
+
+### Testing
+
+This library uses [Vitest](https://vitest.dev/) for testing. There’s a great [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer) you can optionally use if you’d like in-editor debugging tools.
+
+To run the entire test suite, run:
+
+```bash
+pnpm test
+```
+
+To run an individual test:
+
+```bash
+pnpm test -- [partial filename]
+```
+
+To start the entire test suite in watch mode:
+
+```bash
+npx vitest
+```
+
+#### TypeScript tests
+
+**Don’t neglect writing TS tests!** In the test suite, you’ll see `// @ts-expect-error` comments. These are critical tests in and of themselves—they are asserting that TypeScript throws an error when it should be throwing an error (the test suite will actually fail in places if a TS error is _not_ raised).
+
+As this is just a minimal fetch wrapper meant to provide deep type inference for API schemas, **testing TS types** is arguably more important than testing the runtime. So please make liberal use of `// @ts-expect-error`, and as a general rule of thumb, write more **unwanted** output tests than _wanted_ output tests.
+
+### Running linting
+
+Linting is handled via [Biome](https://biomejs.dev), a faster ESLint replacement. It was installed with `pnpm i` and can be run with:
+
+```bash
+pnpm run lint
+```
+
+### Changelogs
+
+The changelog is generated via [changesets](https://github.com/changesets/changesets), and is separate from Git commit messages and pull request titles. To write a human-readable changelog for your changes, run:
+
+```
+npx changeset
+```
+
+This will ask if it’s a `patch`, `minor`, or `major` change ([semver](https://semver.org/)), along with a plain description of what you did. Commit this new file along with the rest of your PR, and during the next release this will go into the official changelog!
+
+## Opening a Pull Request
+
+Pull requests are **welcome** for this repo!
+
+Bugfixes will always be accepted, though in some cases some small changes may be requested.
+
+However, if adding a feature or breaking change, please **open an issue first to discuss.** This ensures no time or work is wasted writing code that won’t be accepted to the project (see [Project Goals](https://openapi-ts.dev/openapi-fetch/about/#project-goals)). Undiscussed feature work may be rejected at the discretion of the maintainers.
+
+### Writing the commit
+
+Create a new branch for your PR with `git checkout -b your-branch-name`. Add the relevant code as well as docs and tests. When you push everything up (`git push`), navigate back to your repo in GitHub and you should see a prompt to open a new PR.
+
+While best practices for commit messages are encouraged (e.g. start with an imperative verb, keep it short, use the body if needed), this repo doesn’t follow any specific guidelines. Clarity is favored over strict rules. Changelogs are generated separately from git (see [the Changelogs section](#changelogs)).
+
+### Writing the PR notes
+
+**Please fill out the template!** It’s a very lightweight template 🙂.
+
+### Adding docs
+
+If you added a feature, or changed how something worked, please [update the docs](../../docs/)!
+
+### Passing CI
+
+All PRs must fix lint errors, and all tests must pass. PRs will not be merged until all CI checks are “green” (✅).
diff --git a/packages/openapi-pinia-colada/LICENSE b/packages/openapi-pinia-colada/LICENSE
new file mode 100644
index 000000000..11c04a265
--- /dev/null
+++ b/packages/openapi-pinia-colada/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Dylan Meysmans
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/openapi-pinia-colada/README.md b/packages/openapi-pinia-colada/README.md
new file mode 100644
index 000000000..f8edfab39
--- /dev/null
+++ b/packages/openapi-pinia-colada/README.md
@@ -0,0 +1,65 @@
+# openapi-pinia-colada
+
+openapi-pinia-colada is a type-safe tiny wrapper (1 kb) around [`@pinia/colada`](https://pinia-colada.esm.dev/) to work with OpenAPI schema.
+
+It works by using [`openapi-fetch`](../openapi-fetch) and [`openapi-typescript`](../openapi-typescript) so you get all the following features:
+
+- âś… No typos in URLs or params.
+- âś… All parameters, request bodies, and responses are type-checked and 100% match your schema
+- âś… No manual typing of your API
+- âś… Eliminates `any` types that hide bugs
+- âś… Eliminates `as` type overrides that can also hide bugs
+
+## Setup
+
+Install this library along with [`openapi-fetch`](../openapi-fetch) and [`openapi-typescript`](../openapi-typescript):
+
+```bash
+npm i openapi-pinia-colada openapi-fetch
+npm i -D openapi-typescript typescript
+```
+
+Next, generate TypeScript types from your OpenAPI schema using openapi-typescript:
+
+```bash
+npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts
+```
+
+## Usage
+
+Once your types have been generated from your schema, you can create a [fetch client](../openapi-fetch), a Pinia Colada client and start querying your API.
+
+```vue
+
+
+
+ Loading...
+ {{`An error occurred: ${error.message}`}}
+ {{data.title}}
+
+```
+
+> You can find more information about `createFetchClient` in the [openapi-fetch documentation](../openapi-fetch).
+
+## đź““ Docs
+
+[View Docs](https://openapi-ts.dev/openapi-pinia-colada/)
diff --git a/packages/openapi-pinia-colada/biome.json b/packages/openapi-pinia-colada/biome.json
new file mode 100644
index 000000000..d5bf28ca0
--- /dev/null
+++ b/packages/openapi-pinia-colada/biome.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
+ "extends": ["../../biome.json"],
+ "files": {
+ "ignore": ["./test/fixtures/"]
+ },
+ "linter": {
+ "rules": {
+ "complexity": {
+ "noBannedTypes": "off"
+ },
+ "suspicious": {
+ "noConfusingVoidType": "off"
+ }
+ }
+ }
+}
diff --git a/packages/openapi-pinia-colada/package.json b/packages/openapi-pinia-colada/package.json
new file mode 100644
index 000000000..b66c1e8bd
--- /dev/null
+++ b/packages/openapi-pinia-colada/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "openapi-pinia-colada",
+ "description": "Fast, type-safe @pinia/colada client to work with your OpenAPI schema.",
+ "version": "0.0.1",
+ "author": {
+ "name": "Dylan Meysmans",
+ "email": "contact@mettekou.com"
+ },
+ "license": "MIT",
+ "type": "module",
+ "main": "./dist/index.js",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ }
+ },
+ "./*": "./*"
+ },
+ "homepage": "https://openapi-ts.dev",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/openapi-ts/openapi-typescript",
+ "directory": "packages/openapi-pinia-colada"
+ },
+ "bugs": {
+ "url": "https://github.com/openapi-ts/openapi-typescript/issues"
+ },
+ "keywords": [
+ "openapi",
+ "swagger",
+ "rest",
+ "api",
+ "oapi_3",
+ "oapi_3_1",
+ "typescript",
+ "fetch",
+ "vue",
+ "pinia-colada",
+ "pinia"
+ ],
+ "scripts": {
+ "build": "pnpm run build:clean && pnpm run build:esm && pnpm run build:cjs",
+ "build:clean": "del-cli dist",
+ "build:esm": "tsc -p tsconfig.build.json",
+ "build:cjs": "esbuild --bundle --platform=node --target=es2019 --outfile=dist/index.cjs --external:typescript src/index.ts",
+ "dev": "tsc -p tsconfig.build.json --watch",
+ "format": "biome format . --write",
+ "lint": "biome check .",
+ "generate-types": "openapi-typescript test/fixtures/api.yaml -o test/fixtures/api.d.ts",
+ "pretest": "pnpm run generate-types",
+ "test": "pnpm run \"/^test:/\"",
+ "test:js": "vitest run",
+ "test:ts": "tsc --noEmit",
+ "version": "pnpm run prepare && pnpm run build"
+ },
+ "dependencies": {
+ "openapi-typescript-helpers": "workspace:^"
+ },
+ "devDependencies": {
+ "@pinia/colada": "^0.13.0",
+ "@testing-library/vue": "^8.1.0",
+ "@vitejs/plugin-vue": "^5.2.1",
+ "del-cli": "^5.1.0",
+ "esbuild": "^0.24.0",
+ "execa": "^8.0.1",
+ "msw": "^2.7.0",
+ "openapi-fetch": "workspace:^",
+ "openapi-typescript": "workspace:^",
+ "vue": "^3.5.12"
+ },
+ "peerDependencies": {
+ "@pinia/colada": "^0.13.0",
+ "openapi-fetch": "workspace:^"
+ }
+}
diff --git a/packages/openapi-pinia-colada/src/index.ts b/packages/openapi-pinia-colada/src/index.ts
new file mode 100644
index 000000000..cd2cfb0cd
--- /dev/null
+++ b/packages/openapi-pinia-colada/src/index.ts
@@ -0,0 +1,87 @@
+import { type MaybeRef, unref } from "vue"
+import {
+ type UseMutationOptions,
+ type UseMutationReturn,
+ type UseQueryOptions,
+ type UseQueryReturn,
+ useMutation,
+ useQuery as piniaColadaUseQuery,
+} from "@pinia/colada";
+import type { ClientMethod, FetchResponse, MaybeOptionalInit, Client as FetchClient } from "openapi-fetch";
+import type { HttpMethod, MediaType, PathsWithMethod, RequiredKeysOf } from "openapi-typescript-helpers";
+
+type InitWithUnknowns = Init & { [key: string]: unknown };
+
+export type QueryKey<
+ Paths extends Record>,
+ Method extends HttpMethod,
+ Path extends PathsWithMethod,
+> = readonly [Method, Path, MaybeOptionalInit];
+
+export type QueryOptionsFunction>, Media extends MediaType> = <
+ Method extends HttpMethod,
+ Path extends PathsWithMethod,
+ Init extends MaybeOptionalInit,
+ Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types
+ Options extends Omit<
+ UseQueryOptions,
+ "key" | "query"
+ >,
+>(
+ method: Method,
+ path: Path,
+ ...[init, options]: RequiredKeysOf extends never
+ ? [InitWithUnknowns?, Options?]
+ : [InitWithUnknowns, Options?]
+) => NoInfer<
+ Omit<
+ UseQueryOptions,
+ "query"
+ > & {
+ query: UseQueryOptions["query"];
+ }
+>;
+
+
+export type UseMutationMethod>, Media extends MediaType> = <
+ Method extends HttpMethod,
+ Path extends PathsWithMethod,
+ Init extends MaybeOptionalInit,
+ Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types
+ Options extends Omit, "mutationKey" | "mutation">,
+>(
+ method: Method,
+ url: Path,
+ options?: Options,
+) => UseMutationReturn;
+
+export interface UseQuery>, Media extends MediaType> {<
+ Method extends HttpMethod,
+ Path extends PathsWithMethod,
+ Init extends MaybeOptionalInit,
+ Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types
+ Options extends Omit<
+ UseQueryOptions,
+ "key" | "query"
+ >,
+>(
+ fetchClient: MaybeRef,
+ method: MaybeRef,
+ url: MaybeRef,
+ ...[init, options]: RequiredKeysOf extends never
+ ? [InitWithUnknowns?, Options?]
+ : [InitWithUnknowns, Options?],
+): UseQueryReturn
+}
+
+export const useQuery = >, Path, Media extends MediaType, Method extends HttpMethod, Init extends MaybeOptionalInit,
+ Response extends Required>, // note: Required is used to avoid repeating NonNullable in UseQuery types
+ Options extends Omit<
+ UseQueryOptions,
+ "key" | "query"
+ >>(fetchClient: MaybeRef, method: MaybeRef, url: MaybeRef, , Method>>, init, options) => {
+ const methodValue = unref(method)
+ const urlValue = unref(url)
+
+ return {}
+}
\ No newline at end of file
diff --git a/packages/openapi-pinia-colada/test/fixtures/api.d.ts b/packages/openapi-pinia-colada/test/fixtures/api.d.ts
new file mode 100644
index 000000000..cad5160d4
--- /dev/null
+++ b/packages/openapi-pinia-colada/test/fixtures/api.d.ts
@@ -0,0 +1,1055 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/comment": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: components["requestBodies"]["CreateReply"];
+ responses: {
+ 201: components["responses"]["CreateReply"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/blogposts": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: {
+ tags?: string[];
+ published?: boolean;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["AllPostsGet"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: components["requestBodies"]["CreatePost"];
+ responses: {
+ 201: components["responses"]["CreatePost"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: components["requestBodies"]["PatchPost"];
+ responses: {
+ 201: components["responses"]["PatchPost"];
+ };
+ };
+ trace?: never;
+ };
+ "/blogposts/{post_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ post_id: string;
+ };
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: {
+ version?: number;
+ format?: string;
+ };
+ header?: never;
+ path: {
+ post_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["PostGet"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ post_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["PostDelete"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ options?: never;
+ head?: never;
+ patch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ post_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: components["requestBodies"]["PatchPost"];
+ responses: {
+ 200: components["responses"]["PatchPost"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ trace?: never;
+ };
+ "/blogposts-optional": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: components["requestBodies"]["CreatePostOptional"];
+ responses: {
+ 201: components["responses"]["CreatePost"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/blogposts-optional-inline": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: {
+ content: {
+ "application/json": components["schemas"]["Post"];
+ };
+ };
+ responses: {
+ 201: components["responses"]["CreatePost"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/header-params": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["getHeaderParams"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/media": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "application/json": {
+ /** Format: blob */
+ media: string;
+ name: string;
+ };
+ };
+ };
+ responses: {
+ "2XX": {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ "4XX": components["responses"]["Error"];
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/mismatched-data": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 201: components["responses"]["PostGet"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/mismatched-errors": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 401: components["responses"]["EmptyError"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/self": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/string-array": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["StringArray"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/tag/{name}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ };
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["Tag"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ };
+ cookie?: never;
+ };
+ requestBody: components["requestBodies"]["CreateTag"];
+ responses: {
+ 201: components["responses"]["CreateTag"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ post?: never;
+ delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ name: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description No Content */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ 500: components["responses"]["Error"];
+ };
+ };
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/query-params": {
+ parameters: {
+ query?: {
+ string?: string;
+ number?: number;
+ boolean?: boolean;
+ array?: string[];
+ object?: {
+ foo: string;
+ bar: string;
+ };
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: {
+ string?: string;
+ number?: number;
+ boolean?: boolean;
+ array?: string[];
+ object?: {
+ foo: string;
+ bar: string;
+ };
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ default: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/path-params/{simple_primitive}/{simple_obj_flat}/{simple_arr_flat}/{simple_obj_explode*}/{simple_arr_explode*}/{.label_primitive}/{.label_obj_flat}/{.label_arr_flat}/{.label_obj_explode*}/{.label_arr_explode*}/{;matrix_primitive}/{;matrix_obj_flat}/{;matrix_arr_flat}/{;matrix_obj_explode*}/{;matrix_arr_explode*}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ simple_primitive: string;
+ simple_obj_flat: {
+ a: string;
+ c: string;
+ };
+ simple_arr_flat: number[];
+ simple_obj_explode: {
+ e: string;
+ g: string;
+ };
+ simple_arr_explode: number[];
+ label_primitive: string;
+ label_obj_flat: {
+ a: string;
+ c: string;
+ };
+ label_arr_flat: number[];
+ label_obj_explode: {
+ e: string;
+ g: string;
+ };
+ label_arr_explode: number[];
+ matrix_primitive: string;
+ matrix_obj_flat: {
+ a: string;
+ c: string;
+ };
+ matrix_arr_flat: number[];
+ matrix_obj_explode: {
+ e: string;
+ g: string;
+ };
+ matrix_arr_explode: number[];
+ };
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ simple_primitive: string;
+ simple_obj_flat: {
+ a: string;
+ c: string;
+ };
+ simple_arr_flat: number[];
+ simple_obj_explode: {
+ e: string;
+ g: string;
+ };
+ simple_arr_explode: number[];
+ label_primitive: string;
+ label_obj_flat: {
+ a: string;
+ c: string;
+ };
+ label_arr_flat: number[];
+ label_obj_explode: {
+ e: string;
+ g: string;
+ };
+ label_arr_explode: number[];
+ matrix_primitive: string;
+ matrix_obj_flat: {
+ a: string;
+ c: string;
+ };
+ matrix_arr_flat: number[];
+ matrix_obj_explode: {
+ e: string;
+ g: string;
+ };
+ matrix_arr_explode: number[];
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ default: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/default-as-error": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ default: components["responses"]["Error"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/anyMethod": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ options: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ head: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ patch: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ trace: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["User"];
+ 404: components["responses"]["Error"];
+ 500: components["responses"]["Error"];
+ };
+ };
+ };
+ "/contact": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody: components["requestBodies"]["Contact"];
+ responses: {
+ 200: components["responses"]["Contact"];
+ };
+ };
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/multiple-response-content": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: components["responses"]["MultipleResponse"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ Post: {
+ title: string;
+ body: string;
+ publish_date?: number;
+ };
+ StringArray: string[];
+ User: {
+ email: string;
+ age?: number;
+ avatar?: string;
+ /** Format: date */
+ created_at: number;
+ /** Format: date */
+ updated_at: number;
+ };
+ };
+ responses: {
+ AllPostsGet: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Post"][];
+ };
+ };
+ CreatePost: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ CreateTag: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ CreateReply: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json;charset=utf-8": {
+ message: string;
+ };
+ };
+ };
+ Contact: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "text/html": string;
+ };
+ };
+ EmptyError: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ Error: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ code: number;
+ message: string;
+ };
+ };
+ };
+ PatchPost: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ PostDelete: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ PostGet: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["Post"];
+ };
+ };
+ StringArray: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["StringArray"];
+ };
+ };
+ Tag: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": string;
+ };
+ };
+ User: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["User"];
+ };
+ };
+ MultipleResponse: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ id: string;
+ email: string;
+ name?: string;
+ };
+ "application/ld+json": {
+ "@id": string;
+ email: string;
+ name?: string;
+ };
+ };
+ };
+ };
+ parameters: never;
+ requestBodies: {
+ CreatePost: {
+ content: {
+ "application/json": {
+ title: string;
+ body: string;
+ publish_date: number;
+ };
+ };
+ };
+ CreatePostOptional: {
+ content: {
+ "application/json": {
+ title: string;
+ body: string;
+ publish_date: number;
+ };
+ };
+ };
+ CreateTag: {
+ content: {
+ "application/json": {
+ description?: string;
+ };
+ };
+ };
+ CreateReply: {
+ content: {
+ "application/json;charset=utf-8": {
+ message: string;
+ replied_at: number;
+ };
+ };
+ };
+ Contact: {
+ content: {
+ "multipart/form-data": {
+ name: string;
+ email: string;
+ subject: string;
+ message: string;
+ };
+ };
+ };
+ PatchPost: {
+ content: {
+ "application/json": {
+ title?: string;
+ body?: string;
+ publish_date?: number;
+ };
+ };
+ };
+ };
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export interface operations {
+ getHeaderParams: {
+ parameters: {
+ query?: never;
+ header: {
+ "x-required-header": string;
+ };
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ status: string;
+ };
+ };
+ };
+ 500: components["responses"]["Error"];
+ };
+ };
+}
diff --git a/packages/openapi-pinia-colada/test/fixtures/api.yaml b/packages/openapi-pinia-colada/test/fixtures/api.yaml
new file mode 100644
index 000000000..8994e1ba8
--- /dev/null
+++ b/packages/openapi-pinia-colada/test/fixtures/api.yaml
@@ -0,0 +1,726 @@
+openapi: 3.1.0
+info:
+ title: Test Specification
+ version: "1.0"
+paths:
+ /comment:
+ put:
+ requestBody:
+ $ref: "#/components/requestBodies/CreateReply"
+ responses:
+ 201:
+ $ref: "#/components/responses/CreateReply"
+ 500:
+ $ref: "#/components/responses/Error"
+ /blogposts:
+ get:
+ parameters:
+ - in: query
+ name: tags
+ schema:
+ type: array
+ items:
+ type: string
+ - in: query
+ name: published
+ schema:
+ type: boolean
+ responses:
+ 200:
+ $ref: "#/components/responses/AllPostsGet"
+ 500:
+ $ref: "#/components/responses/Error"
+ put:
+ requestBody:
+ $ref: "#/components/requestBodies/CreatePost"
+ responses:
+ 201:
+ $ref: "#/components/responses/CreatePost"
+ 500:
+ $ref: "#/components/responses/Error"
+ patch:
+ requestBody:
+ $ref: "#/components/requestBodies/PatchPost"
+ responses:
+ 201:
+ $ref: "#/components/responses/PatchPost"
+ /blogposts/{post_id}:
+ parameters:
+ - in: path
+ name: post_id
+ schema:
+ type: string
+ required: true
+ get:
+ parameters:
+ - in: query
+ name: version
+ schema:
+ type: number
+ - in: query
+ name: format
+ schema:
+ type: string
+ responses:
+ 200:
+ $ref: "#/components/responses/PostGet"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ patch:
+ requestBody:
+ $ref: "#/components/requestBodies/PatchPost"
+ responses:
+ 200:
+ $ref: "#/components/responses/PatchPost"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ delete:
+ responses:
+ 200:
+ $ref: "#/components/responses/PostDelete"
+ 500:
+ $ref: "#/components/responses/Error"
+ /blogposts-optional:
+ put:
+ requestBody:
+ $ref: "#/components/requestBodies/CreatePostOptional"
+ responses:
+ 201:
+ $ref: "#/components/responses/CreatePost"
+ 500:
+ $ref: "#/components/responses/Error"
+ /blogposts-optional-inline:
+ put:
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Post"
+ responses:
+ 201:
+ $ref: "#/components/responses/CreatePost"
+ 500:
+ $ref: "#/components/responses/Error"
+ /header-params:
+ get:
+ operationId: getHeaderParams
+ parameters:
+ - name: x-required-header
+ in: header
+ required: true
+ schema:
+ type: string
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ 500:
+ $ref: "#/components/responses/Error"
+ /media:
+ put:
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ media:
+ type: string
+ format: blob
+ name:
+ type: string
+ required:
+ - media
+ - name
+ responses:
+ 2XX:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ 4XX:
+ $ref: "#/components/responses/Error"
+ /mismatched-data:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 201:
+ $ref: "#/components/responses/PostGet"
+ 500:
+ $ref: "#/components/responses/Error"
+ /mismatched-errors:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 401:
+ $ref: "#/components/responses/EmptyError"
+ 500:
+ $ref: "#/components/responses/Error"
+ /self:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ /string-array:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/StringArray"
+ 500:
+ $ref: "#/components/responses/Error"
+ /tag/{name}:
+ parameters:
+ - in: path
+ name: name
+ schema:
+ type: string
+ required: true
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/Tag"
+ 500:
+ $ref: "#/components/responses/Error"
+ put:
+ requestBody:
+ $ref: "#/components/requestBodies/CreateTag"
+ responses:
+ 201:
+ $ref: "#/components/responses/CreateTag"
+ 500:
+ $ref: "#/components/responses/Error"
+ delete:
+ responses:
+ 204:
+ description: No Content
+ 500:
+ $ref: "#/components/responses/Error"
+ /query-params:
+ parameters:
+ - in: query
+ name: string
+ schema:
+ type: string
+ - in: query
+ name: number
+ schema:
+ type: number
+ - in: query
+ name: boolean
+ schema:
+ type: boolean
+ - in: query
+ name: array
+ schema:
+ type: array
+ items:
+ type: string
+ - in: query
+ name: object
+ schema:
+ type: object
+ required:
+ - foo
+ - bar
+ properties:
+ foo:
+ type: string
+ bar:
+ type: string
+ get:
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ default:
+ $ref: "#/components/responses/Error"
+ /path-params/{simple_primitive}/{simple_obj_flat}/{simple_arr_flat}/{simple_obj_explode*}/{simple_arr_explode*}/{.label_primitive}/{.label_obj_flat}/{.label_arr_flat}/{.label_obj_explode*}/{.label_arr_explode*}/{;matrix_primitive}/{;matrix_obj_flat}/{;matrix_arr_flat}/{;matrix_obj_explode*}/{;matrix_arr_explode*}:
+ parameters:
+ - in: path
+ name: simple_primitive
+ schema:
+ type: string
+ - in: path
+ name: simple_obj_flat
+ schema:
+ type: object
+ required: [a, c]
+ properties:
+ a:
+ type: string
+ c:
+ type: string
+ - in: path
+ name: simple_arr_flat
+ schema:
+ type: array
+ items:
+ type: number
+ - in: path
+ name: simple_obj_explode
+ schema:
+ type: object
+ required: [e, g]
+ properties:
+ e:
+ type: string
+ g:
+ type: string
+ - in: path
+ name: simple_arr_explode
+ schema:
+ type: array
+ items:
+ type: number
+ - in: path
+ name: label_primitive
+ schema:
+ type: string
+ - in: path
+ name: label_obj_flat
+ schema:
+ type: object
+ required: [a, c]
+ properties:
+ a:
+ type: string
+ c:
+ type: string
+ - in: path
+ name: label_arr_flat
+ schema:
+ type: array
+ items:
+ type: number
+ - in: path
+ name: label_obj_explode
+ schema:
+ type: object
+ required: [e, g]
+ properties:
+ e:
+ type: string
+ g:
+ type: string
+ - in: path
+ name: label_arr_explode
+ schema:
+ type: array
+ items:
+ type: number
+ - in: path
+ name: matrix_primitive
+ schema:
+ type: string
+ - in: path
+ name: matrix_obj_flat
+ schema:
+ type: object
+ required: [a, c]
+ properties:
+ a:
+ type: string
+ c:
+ type: string
+ - in: path
+ name: matrix_arr_flat
+ schema:
+ type: array
+ items:
+ type: number
+ - in: path
+ name: matrix_obj_explode
+ schema:
+ type: object
+ required: [e, g]
+ properties:
+ e:
+ type: string
+ g:
+ type: string
+ - in: path
+ name: matrix_arr_explode
+ schema:
+ type: array
+ items:
+ type: number
+ get:
+ responses:
+ 200:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ default:
+ $ref: "#/components/responses/Error"
+ /default-as-error:
+ get:
+ responses:
+ default:
+ $ref: "#/components/responses/Error"
+ /anyMethod:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ put:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ post:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ delete:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ options:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ head:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ patch:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ trace:
+ responses:
+ 200:
+ $ref: "#/components/responses/User"
+ 404:
+ $ref: "#/components/responses/Error"
+ 500:
+ $ref: "#/components/responses/Error"
+ /contact:
+ put:
+ requestBody:
+ $ref: "#/components/requestBodies/Contact"
+ responses:
+ 200:
+ $ref: "#/components/responses/Contact"
+ /multiple-response-content:
+ get:
+ responses:
+ 200:
+ $ref: "#/components/responses/MultipleResponse"
+components:
+ schemas:
+ Post:
+ type: object
+ properties:
+ title:
+ type: string
+ body:
+ type: string
+ publish_date:
+ type: number
+ required:
+ - title
+ - body
+ StringArray:
+ type: array
+ items:
+ type: string
+ User:
+ type: object
+ properties:
+ email:
+ type: string
+ age:
+ type: number
+ avatar:
+ type: string
+ created_at:
+ type: number
+ format: date
+ updated_at:
+ type: number
+ format: date
+ required:
+ - email
+ - created_at
+ - updated_at
+ requestBodies:
+ CreatePost:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ body:
+ type: string
+ publish_date:
+ type: number
+ required:
+ - title
+ - body
+ - publish_date
+ CreatePostOptional:
+ required: false
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ body:
+ type: string
+ publish_date:
+ type: number
+ required:
+ - title
+ - body
+ - publish_date
+ CreateTag:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ description:
+ type: string
+ CreateReply:
+ required: true
+ content:
+ "application/json;charset=utf-8":
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ replied_at:
+ type: number
+ required:
+ - message
+ - replied_at
+ Contact:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ name:
+ type: string
+ email:
+ type: string
+ subject:
+ type: string
+ message:
+ type: string
+ required:
+ - name
+ - email
+ - subject
+ - message
+ PatchPost:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ body:
+ type: string
+ publish_date:
+ type: number
+ responses:
+ AllPostsGet:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Post"
+ CreatePost:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ CreateTag:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ CreateReply:
+ content:
+ "application/json;charset=utf-8":
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ required:
+ - message
+ Contact:
+ content:
+ text/html:
+ schema:
+ type: string
+ EmptyError:
+ content: {}
+ Error:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ code:
+ type: number
+ message:
+ type: string
+ required:
+ - code
+ - message
+ PatchPost:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ PostDelete:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ type: string
+ required:
+ - status
+ PostGet:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Post"
+ StringArray:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/StringArray"
+ Tag:
+ content:
+ application/json:
+ schema:
+ type: string
+ User:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ MultipleResponse:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ email:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - email
+ application/ld+json:
+ schema:
+ type: object
+ properties:
+ "@id":
+ type: string
+ email:
+ type: string
+ name:
+ type: string
+ required:
+ - "@id"
+ - email
diff --git a/packages/openapi-pinia-colada/test/fixtures/mock-server.ts b/packages/openapi-pinia-colada/test/fixtures/mock-server.ts
new file mode 100644
index 000000000..4c7571427
--- /dev/null
+++ b/packages/openapi-pinia-colada/test/fixtures/mock-server.ts
@@ -0,0 +1,121 @@
+import {
+ http,
+ HttpResponse,
+ type JsonBodyType,
+ type StrictRequest,
+ type DefaultBodyType,
+ type HttpResponseResolver,
+ type PathParams,
+ type AsyncResponseResolverReturnType,
+} from "msw";
+import { setupServer } from "msw/node";
+
+/**
+ * Mock server instance
+ */
+export const server = setupServer();
+
+/**
+ * Default baseUrl for tests
+ */
+export const baseUrl = "https://api.example.com" as const;
+
+/**
+ * Test path helper, returns an absolute URL based on
+ * the given path and base
+ */
+export function toAbsoluteURL(path: string, base: string = baseUrl) {
+ // If we have absolute path
+ // if (URL.canParse(path)) {
+ // return new URL(path).toString();
+ // }
+
+ // Otherwise we want to support relative paths
+ // where base may also contain some part of the path
+ // e.g.
+ // base = https://api.foo.bar/v1/
+ // path = /self
+ // should result in https://api.foo.bar/v1/self
+
+ // Construct base URL
+ const baseUrlInstance = new URL(base);
+
+ // prepend base url url pathname to path and ensure only one slash between the URL parts
+ const newPath = `${baseUrlInstance.pathname}/${path}`.replace(/\/+/g, "/");
+
+ return new URL(newPath, baseUrlInstance).toString();
+}
+
+export type MswHttpMethod = keyof typeof http;
+
+export interface MockRequestHandlerOptions<
+ // Recreate the generic signature of the HTTP resolver
+ // so the arguments passed to http handlers propagate here.
+ Params extends PathParams = PathParams,
+ RequestBodyType extends DefaultBodyType = DefaultBodyType,
+ ResponseBodyType extends DefaultBodyType = undefined,
+> {
+ baseUrl?: string;
+ method: MswHttpMethod;
+ /**
+ * Relative or absolute path to match.
+ * When relative, baseUrl will be used as base.
+ */
+ path: string;
+ body?: JsonBodyType;
+ headers?: Record;
+ status?: number;
+
+ /**
+ * Optional handler which will be called instead of using the body, headers and status
+ */
+ handler?: HttpResponseResolver;
+}
+
+/**
+ * Configures a msw request handler using the provided options.
+ */
+export function useMockRequestHandler<
+ // Recreate the generic signature of the HTTP resolver
+ // so the arguments passed to http handlers propagate here.
+ Params extends PathParams = PathParams,
+ RequestBodyType extends DefaultBodyType = DefaultBodyType,
+ ResponseBodyType extends DefaultBodyType = undefined,
+>({
+ baseUrl: requestBaseUrl,
+ method,
+ path,
+ body,
+ headers,
+ status,
+ handler,
+}: MockRequestHandlerOptions) {
+ let requestUrl = "";
+ let receivedRequest: StrictRequest;
+ let receivedCookies: Record = {};
+
+ const resolvedPath = toAbsoluteURL(path, requestBaseUrl);
+
+ server.use(
+ http[method](resolvedPath, (args) => {
+ requestUrl = args.request.url;
+ receivedRequest = args.request.clone();
+ receivedCookies = { ...args.cookies };
+
+ if (handler) {
+ return handler(args);
+ }
+
+ return HttpResponse.json(body as any, {
+ status: status ?? 200,
+ headers,
+ }) as AsyncResponseResolverReturnType;
+ }),
+ );
+
+ return {
+ getRequestCookies: () => receivedCookies,
+ getRequest: () => receivedRequest,
+ getRequestUrl: () => new URL(requestUrl),
+ };
+}
diff --git a/packages/openapi-pinia-colada/test/index.test.tsx b/packages/openapi-pinia-colada/test/index.test.tsx
new file mode 100644
index 000000000..294175df9
--- /dev/null
+++ b/packages/openapi-pinia-colada/test/index.test.tsx
@@ -0,0 +1,792 @@
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { server, baseUrl, useMockRequestHandler } from "./fixtures/mock-server.js";
+import type { paths } from "./fixtures/api.js";
+import createClient from "../src/index.js";
+import createFetchClient from "openapi-fetch";
+import { fireEvent, render, renderHook, screen, waitFor, act } from "@testing-library/react";
+import {
+ QueryClient,
+ QueryClientProvider,
+ useQueries,
+ useQuery,
+ useSuspenseQuery,
+ skipToken,
+} from "@tanstack/react-query";
+import { Suspense, type ReactNode } from "react";
+import { ErrorBoundary } from "react-error-boundary";
+
+type minimalGetPaths = {
+ // Without parameters.
+ "/foo": {
+ get: {
+ responses: {
+ 200: { content: { "application/json": true } };
+ 500: { content: { "application/json": false } };
+ };
+ };
+ };
+ // With some parameters (makes init required) and different responses.
+ "/bar": {
+ get: {
+ parameters: { query: {} };
+ responses: {
+ 200: { content: { "application/json": "bar 200" } };
+ 500: { content: { "application/json": "bar 500" } };
+ };
+ };
+ };
+};
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+});
+
+const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+);
+
+const fetchInfinite = async () => {
+ await new Promise(() => {});
+ return Response.error();
+};
+
+beforeAll(() => {
+ server.listen({
+ onUnhandledRequest: "error",
+ });
+});
+
+afterEach(() => {
+ server.resetHandlers();
+ queryClient.removeQueries();
+});
+
+afterAll(() => server.close());
+
+describe("client", () => {
+ it("generates all proper functions", () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+ expect(client).toHaveProperty("queryOptions");
+ expect(client).toHaveProperty("useQuery");
+ expect(client).toHaveProperty("useSuspenseQuery");
+ expect(client).toHaveProperty("useMutation");
+ });
+
+ describe("queryOptions", () => {
+ it("has correct parameter types", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ client.queryOptions("get", "/string-array");
+ // @ts-expect-error: Wrong method.
+ client.queryOptions("put", "/string-array");
+ // @ts-expect-error: Wrong path.
+ client.queryOptions("get", "/string-arrayX");
+ // @ts-expect-error: Missing 'post_id' param.
+ client.queryOptions("get", "/blogposts/{post_id}", {});
+ });
+
+ it("returns query options that can resolve data correctly with fetchQuery", async () => {
+ const response = { title: "title", body: "body" };
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/blogposts/1",
+ status: 200,
+ body: response,
+ });
+
+ const data = await queryClient.fetchQuery(
+ client.queryOptions("get", "/blogposts/{post_id}", {
+ params: {
+ path: {
+ post_id: "1",
+ },
+ },
+ }),
+ );
+
+ expectTypeOf(data).toEqualTypeOf<{
+ title: string;
+ body: string;
+ publish_date?: number;
+ }>();
+
+ expect(data).toEqual(response);
+ });
+
+ it("returns query options that can be passed to useQueries", async () => {
+ const fetchClient = createFetchClient({ baseUrl, fetch: fetchInfinite });
+ const client = createClient(fetchClient);
+
+ const { result } = renderHook(
+ () =>
+ useQueries(
+ {
+ queries: [
+ client.queryOptions("get", "/string-array"),
+ client.queryOptions("get", "/string-array", {}),
+ client.queryOptions("get", "/blogposts/{post_id}", {
+ params: {
+ path: {
+ post_id: "1",
+ },
+ },
+ }),
+ client.queryOptions("get", "/blogposts/{post_id}", {
+ params: {
+ path: {
+ post_id: "2",
+ },
+ },
+ }),
+ ],
+ },
+ queryClient,
+ ),
+ {
+ wrapper,
+ },
+ );
+
+ expectTypeOf(result.current[0].data).toEqualTypeOf();
+ expectTypeOf(result.current[0].error).toEqualTypeOf<{ code: number; message: string } | null>();
+
+ expectTypeOf(result.current[1]).toEqualTypeOf<(typeof result.current)[0]>();
+
+ expectTypeOf(result.current[2].data).toEqualTypeOf<
+ | {
+ title: string;
+ body: string;
+ publish_date?: number;
+ }
+ | undefined
+ >();
+ expectTypeOf(result.current[2].error).toEqualTypeOf<{ code: number; message: string } | null>();
+
+ expectTypeOf(result.current[3]).toEqualTypeOf<(typeof result.current)[2]>();
+
+ // Generated different queryKey for each query.
+ expect(queryClient.isFetching()).toBe(4);
+ });
+
+ it("returns query options that can be passed to useQuery", async () => {
+ const SKIP = { queryKey: [] as any, queryFn: skipToken } as const;
+
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ const { result } = renderHook(
+ () =>
+ useQuery(
+ // biome-ignore lint/correctness/noConstantCondition: it's just here to test types
+ false
+ ? {
+ ...client.queryOptions("get", "/foo"),
+ select: (data) => {
+ expectTypeOf(data).toEqualTypeOf();
+
+ return "select(true)" as const;
+ },
+ }
+ : SKIP,
+ ),
+ { wrapper },
+ );
+
+ expectTypeOf(result.current.data).toEqualTypeOf<"select(true)" | undefined>();
+ expectTypeOf(result.current.error).toEqualTypeOf();
+ });
+
+ it("returns query options that can be passed to useSuspenseQuery", async () => {
+ const fetchClient = createFetchClient({
+ baseUrl,
+ fetch: () => Promise.resolve(Response.json(true)),
+ });
+ const client = createClient(fetchClient);
+
+ const { result } = renderHook(
+ () =>
+ useSuspenseQuery({
+ ...client.queryOptions("get", "/foo"),
+ select: (data) => {
+ expectTypeOf(data).toEqualTypeOf();
+
+ return "select(true)" as const;
+ },
+ }),
+ { wrapper },
+ );
+
+ await waitFor(() => expect(result.current).not.toBeNull());
+
+ expectTypeOf(result.current.data).toEqualTypeOf<"select(true)">();
+ expectTypeOf(result.current.error).toEqualTypeOf();
+ });
+ });
+
+ describe("useQuery", () => {
+ it("should resolve data properly and have error as null when successfull request", async () => {
+ const response = ["one", "two", "three"];
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/string-array",
+ status: 200,
+ body: response,
+ });
+
+ const { result } = renderHook(() => client.useQuery("get", "/string-array"), {
+ wrapper,
+ });
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(data).toEqual(response);
+ expect(error).toBeNull();
+ });
+
+ it("should resolve error properly and have undefined data when failed request", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/string-array",
+ status: 500,
+ body: { code: 500, message: "Something went wrong" },
+ });
+
+ const { result } = renderHook(() => client.useQuery("get", "/string-array"), {
+ wrapper,
+ });
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(error?.message).toBe("Something went wrong");
+ expect(data).toBeUndefined();
+ });
+
+ it("should resolve data properly and have error as null when queryFn returns null", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/string-array",
+ status: 200,
+ body: null,
+ });
+
+ const { result } = renderHook(() => client.useQuery("get", "/string-array"), { wrapper });
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(data).toBeNull();
+ expect(error).toBeNull();
+ });
+
+ it("should resolve error properly and have undefined data when queryFn returns undefined", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/string-array",
+ status: 200,
+ body: undefined,
+ });
+
+ const { result } = renderHook(() => client.useQuery("get", "/string-array"), { wrapper });
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(error).toBeInstanceOf(Error);
+ expect(data).toBeUndefined();
+ });
+
+ it("should infer correct data and error type", async () => {
+ const fetchClient = createFetchClient({ baseUrl, fetch: fetchInfinite });
+ const client = createClient(fetchClient);
+
+ const { result } = renderHook(() => client.useQuery("get", "/string-array"), {
+ wrapper,
+ });
+
+ const { data, error } = result.current;
+
+ expectTypeOf(data).toEqualTypeOf();
+ expectTypeOf(error).toEqualTypeOf<{ code: number; message: string } | null>();
+ });
+
+ it("passes abort signal to fetch", async () => {
+ let signalPassedToFetch: AbortSignal | undefined;
+
+ const fetchClient = createFetchClient({
+ baseUrl,
+ fetch: async ({ signal }) => {
+ signalPassedToFetch = signal;
+ return await fetchInfinite();
+ },
+ });
+ const client = createClient(fetchClient);
+
+ const { unmount } = renderHook(() => client.useQuery("get", "/string-array"), { wrapper });
+
+ unmount();
+
+ expect(signalPassedToFetch?.aborted).toBeTruthy();
+ });
+
+ describe("params", () => {
+ it("should be required if OpenAPI schema requires params", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/blogposts/:post_id",
+ status: 200,
+ body: { message: "OK" },
+ });
+
+ // expect error on missing 'params'
+ // @ts-expect-error
+ const { result } = renderHook(() => client.useQuery("get", "/blogposts/{post_id}"), {
+ wrapper,
+ });
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ });
+ });
+
+ it("should use provided custom queryClient", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+ const customQueryClient = new QueryClient({});
+
+ function Page() {
+ const { data } = client.useQuery(
+ "get",
+ "/blogposts/{post_id}",
+ {
+ params: {
+ path: {
+ post_id: "1",
+ },
+ },
+ },
+ {},
+ customQueryClient,
+ );
+ return data: {data?.title}
;
+ }
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/blogposts/:post_id",
+ status: 200,
+ body: { title: "hello" },
+ });
+
+ const rendered = render( );
+
+ await waitFor(() => expect(rendered.getByText("data: hello")));
+ });
+
+ it("uses provided options", async () => {
+ const initialData = ["initial", "data"];
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ const { result } = renderHook(
+ () => client.useQuery("get", "/string-array", {}, { enabled: false, initialData }),
+ { wrapper },
+ );
+
+ const { data, error } = result.current;
+
+ expect(data).toBe(initialData);
+ expect(error).toBeNull();
+ });
+ });
+
+ describe("useSuspenseQuery", () => {
+ it("should resolve data properly and have error as null when successfull request", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/string-array",
+ status: 200,
+ body: ["one", "two", "three"],
+ });
+
+ const { result } = renderHook(() => client.useSuspenseQuery("get", "/string-array"), {
+ wrapper,
+ });
+
+ await waitFor(() => expect(result.current.isFetching).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(data[0]).toBe("one");
+ expect(error).toBeNull();
+ });
+
+ it("should properly propagate error to suspense with a failed http request", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); // to avoid sending errors to console
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/string-array",
+ status: 500,
+ body: { code: 500, message: "Something went wrong" },
+ });
+
+ const TestComponent = () => {
+ client.useSuspenseQuery("get", "/string-array");
+ return
;
+ };
+
+ render(
+
+ {error.message}
}>
+ loading
}>
+
+
+
+ ,
+ );
+
+ expect(await screen.findByText("Something went wrong")).toBeDefined();
+ errorSpy.mockRestore();
+ });
+
+ it("should use provided custom queryClient", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+ const customQueryClient = new QueryClient({});
+
+ function Page() {
+ const { data } = client.useSuspenseQuery(
+ "get",
+ "/blogposts/{post_id}",
+ {
+ params: {
+ path: {
+ post_id: "1",
+ },
+ },
+ },
+ {},
+ customQueryClient,
+ );
+ return data: {data?.title}
;
+ }
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "get",
+ path: "/blogposts/:post_id",
+ status: 200,
+ body: { title: "Hello" },
+ });
+
+ const rendered = render( );
+
+ await waitFor(() => rendered.findByText("data: Hello"));
+ });
+
+ it("passes abort signal to fetch", async () => {
+ let signalPassedToFetch: AbortSignal | undefined;
+
+ const fetchClient = createFetchClient({
+ baseUrl,
+ fetch: async ({ signal }) => {
+ signalPassedToFetch = signal;
+ await new Promise(() => {});
+ return Response.error();
+ },
+ });
+ const client = createClient(fetchClient);
+ const queryClient = new QueryClient({});
+
+ const { unmount } = renderHook(() => client.useSuspenseQuery("get", "/string-array", {}, {}, queryClient));
+
+ unmount();
+
+ await act(() => queryClient.cancelQueries());
+
+ expect(signalPassedToFetch?.aborted).toBeTruthy();
+ });
+ });
+
+ describe("useMutation", () => {
+ describe("mutate", () => {
+ it("should resolve data properly and have error as null when successfull request", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 200,
+ body: { message: "Hello" },
+ });
+
+ const { result } = renderHook(() => client.useMutation("put", "/comment"), {
+ wrapper,
+ });
+
+ result.current.mutate({ body: { message: "Hello", replied_at: 0 } });
+
+ await waitFor(() => expect(result.current.isPending).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(data?.message).toBe("Hello");
+ expect(error).toBeNull();
+ });
+
+ it("should resolve error properly and have undefined data when failed request", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 500,
+ body: { code: 500, message: "Something went wrong" },
+ });
+
+ const { result } = renderHook(() => client.useMutation("put", "/comment"), {
+ wrapper,
+ });
+
+ result.current.mutate({ body: { message: "Hello", replied_at: 0 } });
+
+ await waitFor(() => expect(result.current.isPending).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(data).toBeUndefined();
+ expect(error?.message).toBe("Something went wrong");
+ });
+
+ it("should resolve data properly and have error as null when mutationFn returns null", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 200,
+ body: null,
+ });
+
+ const { result } = renderHook(() => client.useMutation("put", "/comment"), { wrapper });
+
+ result.current.mutate({ body: { message: "Hello", replied_at: 0 } });
+
+ await waitFor(() => expect(result.current.isPending).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(data).toBeNull();
+ expect(error).toBeNull();
+ });
+
+ it("should resolve data properly and have error as null when mutationFn returns undefined", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 200,
+ body: undefined,
+ });
+
+ const { result } = renderHook(() => client.useMutation("put", "/comment"), { wrapper });
+
+ result.current.mutate({ body: { message: "Hello", replied_at: 0 } });
+
+ await waitFor(() => expect(result.current.isPending).toBe(false));
+
+ const { data, error } = result.current;
+
+ expect(error).toBeNull();
+ expect(data).toBeUndefined();
+ });
+
+ it("should use provided custom queryClient", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+ const customQueryClient = new QueryClient({});
+
+ function Page() {
+ const mutation = client.useMutation("put", "/comment", {}, customQueryClient);
+
+ return (
+
+
+ mutation.mutate({
+ body: {
+ message: "Hello",
+ replied_at: 0,
+ },
+ })
+ }
+ >
+ mutate
+
+
+ data: {mutation.data?.message ?? "null"}, status: {mutation.status}
+
+
+ );
+ }
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 200,
+ body: { message: "Hello" },
+ });
+
+ const rendered = render( );
+
+ await rendered.findByText("data: null, status: idle");
+
+ fireEvent.click(rendered.getByRole("button", { name: /mutate/i }));
+
+ await waitFor(() => rendered.findByText("data: Hello, status: success"));
+ });
+ });
+
+ describe("mutateAsync", () => {
+ it("should resolve data properly", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 200,
+ body: { message: "Hello" },
+ });
+
+ const { result } = renderHook(() => client.useMutation("put", "/comment"), {
+ wrapper,
+ });
+
+ const data = await result.current.mutateAsync({ body: { message: "Hello", replied_at: 0 } });
+
+ expect(data.message).toBe("Hello");
+ });
+
+ it("should throw an error when failed request", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 500,
+ body: { code: 500, message: "Something went wrong" },
+ });
+
+ const { result } = renderHook(() => client.useMutation("put", "/comment"), {
+ wrapper,
+ });
+
+ expect(result.current.mutateAsync({ body: { message: "Hello", replied_at: 0 } })).rejects.toThrow();
+ });
+
+ it("should use provided custom queryClient", async () => {
+ const fetchClient = createFetchClient({ baseUrl });
+ const client = createClient(fetchClient);
+ const customQueryClient = new QueryClient({});
+
+ function Page() {
+ const mutation = client.useMutation("put", "/comment", {}, customQueryClient);
+
+ return (
+
+
+ mutation.mutateAsync({
+ body: {
+ message: "Hello",
+ replied_at: 0,
+ },
+ })
+ }
+ >
+ mutate
+
+
+ data: {mutation.data?.message ?? "null"}, status: {mutation.status}
+
+
+ );
+ }
+
+ useMockRequestHandler({
+ baseUrl,
+ method: "put",
+ path: "/comment",
+ status: 200,
+ body: { message: "Hello" },
+ });
+
+ const rendered = render( );
+
+ await rendered.findByText("data: null, status: idle");
+
+ fireEvent.click(rendered.getByRole("button", { name: /mutate/i }));
+
+ await waitFor(() => rendered.findByText("data: Hello, status: success"));
+ });
+ });
+ });
+});
diff --git a/packages/openapi-pinia-colada/tsconfig.build.json b/packages/openapi-pinia-colada/tsconfig.build.json
new file mode 100644
index 000000000..b90fc83e0
--- /dev/null
+++ b/packages/openapi-pinia-colada/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src"]
+}
diff --git a/packages/openapi-pinia-colada/tsconfig.json b/packages/openapi-pinia-colada/tsconfig.json
new file mode 100644
index 000000000..e103674b1
--- /dev/null
+++ b/packages/openapi-pinia-colada/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "lib": ["ESNext", "DOM"],
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "outDir": "dist",
+ "skipLibCheck": true,
+ "sourceRoot": ".",
+ "jsx": "preserve",
+ "target": "ES2022",
+ "types": ["vitest/globals"]
+ },
+ "include": ["src", "test", "*.ts"],
+ "exclude": ["node_modules"]
+}
\ No newline at end of file
diff --git a/packages/openapi-pinia-colada/vitest.config.ts b/packages/openapi-pinia-colada/vitest.config.ts
new file mode 100644
index 000000000..a1f5e5ffa
--- /dev/null
+++ b/packages/openapi-pinia-colada/vitest.config.ts
@@ -0,0 +1,10 @@
+import vue from "@vitejs/plugin-vue";
+import { defineConfig, type Plugin } from "vitest/config";
+
+export default defineConfig({
+ plugins: [vue() as unknown as Plugin],
+ test: {
+ environment: "jsdom",
+ globals: true,
+ },
+});