Skip to content

Commit 639ec45

Browse files
authored
Add swr-openapi package (#1932)
* Add unmodified source * Update package.json * Add minor changeset * Fix funding * Fix path for windows * Replace ESLint and Prettier with Biome * Add documentation * Enable deep on page outlines * Update lockfile * Replace README docs with link to Vitepress docs * Update contributors.json * Fix del cli
1 parent ff57082 commit 639ec45

35 files changed

+3911
-3
lines changed

.changeset/chilly-scissors-join.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"swr-openapi": minor
3+
---
4+
5+
Modify package.json to point to new repository

docs/.vitepress/en.ts

+13
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ export default defineConfig({
8080
{ text: "About", link: "/openapi-react-query/about" },
8181
],
8282
},
83+
{
84+
text: "swr-openapi",
85+
base: "/swr-openapi",
86+
items: [
87+
{ text: "Getting Started", link: "/" },
88+
{ text: "Hook Builders", link: "/hook-builders" },
89+
{ text: "useQuery", link: "/use-query" },
90+
{ text: "useImmutable", link: "/use-immutable" },
91+
{ text: "useInfinite", link: "/use-infinite" },
92+
{ text: "useMutate", link: "/use-mutate" },
93+
{ text: "About", link: "/about" },
94+
],
95+
},
8396
],
8497
},
8598
search: {

docs/.vitepress/shared.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { UserConfig } from "vitepress";
1+
import type { UserConfig, DefaultTheme } from "vitepress";
22
import { zhSearch } from "./zh";
33
import { jaSearch } from "./ja";
44

@@ -26,6 +26,7 @@ const shared: UserConfig = {
2626
themeConfig: {
2727
siteTitle: false,
2828
logo: "/assets/openapi-ts.svg",
29+
outline: 'deep',
2930
search: {
3031
provider: "algolia",
3132
options: {
@@ -44,7 +45,7 @@ const shared: UserConfig = {
4445
},
4546
{ icon: "github", link: "https://github.com/openapi-ts/openapi-typescript" },
4647
],
47-
},
48+
} satisfies DefaultTheme.Config,
4849
transformPageData({ relativePath, frontmatter }) {
4950
frontmatter.head ??= [];
5051
frontmatter.head.push([

docs/data/contributors.json

+1-1
Large diffs are not rendered by default.

docs/scripts/update-contributors.js

+2
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,15 @@ const CONTRIBUTORS = {
165165
"illright",
166166
]),
167167
"openapi-react-query": new Set(["drwpow", "kerwanp", "yoshi2no"]),
168+
"swr-openapi": new Set(["htunnicliff"])
168169
};
169170

170171
async function main() {
171172
let i = 0;
172173
const total = Object.values(CONTRIBUTORS).reduce((total, next) => total + next.size, 0);
173174
await Promise.all(
174175
Object.entries(CONTRIBUTORS).map(async ([repo, contributors]) => {
176+
data[repo] ??= [];
175177
for (const username of [...contributors]) {
176178
i++;
177179
// skip profiles that have been updated within the past week

docs/swr-openapi/about.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
title: About swr-openapi
3+
description: swr-openapi Project Goals and contributors
4+
---
5+
<script setup>
6+
import { VPTeamMembers } from 'vitepress/theme';
7+
import contributors from '../data/contributors.json';
8+
</script>
9+
10+
# {{ $frontmatter.title }}
11+
12+
## Project Goals
13+
14+
1. Types should be strict and inferred automatically from OpenAPI schemas with the absolute minimum number of generics needed.
15+
2. Respect the original `swr` APIs while reducing boilerplate.
16+
3. Be as light and performant as possible.
17+
18+
## Contributors
19+
20+
This library wouldn’t be possible without all these amazing contributors:
21+
22+
<VPTeamMembers size="small" :members="contributors['swr-openapi']" />

docs/swr-openapi/hook-builders.md

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
---
2+
title: Hook Builders
3+
---
4+
5+
# {{ $frontmatter.title }}
6+
7+
Hook builders initialize `useQuery`, `useImmutate`, `useInfinite`, and `useMutate`.
8+
9+
Each builder function accepts an instance of a [fetch client](../openapi-fetch/api.md) and a prefix unique to that client.
10+
11+
12+
::: tip
13+
14+
Prefixes ensure that `swr` will avoid caching requests from different APIs when requests happen to match (e.g. `GET /health` for "API A" and `GET /health` for "API B").
15+
16+
:::
17+
18+
```ts
19+
import createClient from "openapi-fetch";
20+
import { isMatch } from "lodash-es";
21+
22+
import {
23+
createQueryHook,
24+
createImmutableHook,
25+
createInfiniteHook,
26+
createMutateHook,
27+
} from "swr-openapi";
28+
29+
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
30+
31+
const client = createClient<paths>(/* ... */);
32+
const prefix = "my-api";
33+
34+
export const useQuery = createQueryHook(client, prefix);
35+
export const useImmutable = createImmutableHook(client, prefix);
36+
export const useInfinite = createInfiniteHook(client, prefix);
37+
export const useMutate = createMutateHook(
38+
client,
39+
prefix,
40+
isMatch, // Or any comparision function
41+
);
42+
```
43+
44+
## API
45+
46+
### Parameters
47+
48+
Each builder hook accepts the same initial parameters:
49+
50+
- `client`: A [fetch client](../openapi-fetch/api.md).
51+
- `prefix`: A prefix unique to the fetch client.
52+
53+
`createMutateHook` also accepts a third parameter:
54+
55+
- [`compare`](#compare): A function to compare fetch options).
56+
57+
### Returns
58+
59+
- `createQueryHook` &rarr; [`useQuery`](./use-query.md)
60+
- `createImmutableHook` &rarr; [`useImmutable`](./use-immutable.md)
61+
- `createInfiniteHook` &rarr; [`useInfinite`](./use-infinite.md)
62+
- `createMutateHook` &rarr; [`useMutate`](./use-mutate.md)
63+
64+
## `compare`
65+
66+
When calling `createMutateHook`, a function must be provided with the following contract:
67+
68+
```ts
69+
type Compare = (init: any, partialInit: object) => boolean;
70+
```
71+
72+
This function is used to determine whether or not a cached request should be updated when `mutate` is called with fetch options.
73+
74+
My personal recommendation is to use lodash's [`isMatch`][lodash-is-match]:
75+
76+
> Performs a partial deep comparison between object and source to determine if object contains equivalent property values.
77+
78+
```ts
79+
const useMutate = createMutateHook(client, "<unique-key>", isMatch);
80+
81+
const mutate = useMutate();
82+
83+
await mutate([
84+
"/path",
85+
{
86+
params: {
87+
query: {
88+
version: "beta",
89+
},
90+
},
91+
},
92+
]);
93+
94+
// ✅ Would be updated
95+
useQuery("/path", {
96+
params: {
97+
query: {
98+
version: "beta",
99+
},
100+
},
101+
});
102+
103+
// ✅ Would be updated
104+
useQuery("/path", {
105+
params: {
106+
query: {
107+
version: "beta",
108+
other: true,
109+
example: [1, 2, 3],
110+
},
111+
},
112+
});
113+
114+
// ❌ Would not be updated
115+
useQuery("/path", {
116+
params: {
117+
query: {},
118+
},
119+
});
120+
121+
// ❌ Would not be updated
122+
useQuery("/path");
123+
124+
// ❌ Would not be updated
125+
useQuery("/path", {
126+
params: {
127+
query: {
128+
version: "alpha",
129+
},
130+
},
131+
});
132+
133+
// ❌ Would not be updated
134+
useQuery("/path", {
135+
params: {
136+
query: {
137+
different: "items",
138+
},
139+
},
140+
});
141+
```
142+
143+
[lodash-is-match]: https://lodash.com/docs/4.17.15#isMatch

docs/swr-openapi/index.md

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
title: swr-openapi
3+
---
4+
5+
# Introduction
6+
7+
swr-openapi is a type-safe wrapper around [`swr`](https://swr.vercel.app).
8+
9+
It works by using [openapi-fetch](../openapi-fetch/) and [openapi-typescript](../introduction) so you get all the following features:
10+
11+
- ✅ No typos in URLs or params.
12+
- ✅ All parameters, request bodies, and responses are type-checked and 100% match your schema
13+
- ✅ No manual typing of your API
14+
- ✅ Eliminates `any` types that hide bugs
15+
- ✅ Also eliminates `as` type overrides that can also hide bugs
16+
17+
::: code-group
18+
19+
```tsx [src/my-component.ts]
20+
import createClient from "openapi-fetch";
21+
import { createQueryHook } from "swr-openapi";
22+
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
23+
24+
const client = createClient<paths>({
25+
baseUrl: "https://myapi.dev/v1/",
26+
});
27+
28+
const useQuery = createQueryHook(client, "my-api");
29+
30+
function MyComponent() {
31+
const { data, error, isLoading, isValidating, mutate } = useQuery(
32+
"/blogposts/{post_id}",
33+
{
34+
params: {
35+
path: { post_id: "123" },
36+
},
37+
},
38+
);
39+
40+
if (isLoading || !data) return "Loading...";
41+
42+
if (error) return `An error occured: ${error.message}`;
43+
44+
return <div>{data.title}</div>;
45+
}
46+
47+
```
48+
49+
:::
50+
51+
## Setup
52+
53+
Install this library along with [openapi-fetch](../openapi-fetch/) and [openapi-typescript](../introduction):
54+
55+
```bash
56+
npm i swr-openapi openapi-fetch
57+
npm i -D openapi-typescript typescript
58+
```
59+
60+
::: tip Highly recommended
61+
62+
Enable [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) in your `tsconfig.json` ([docs](../advanced#enable-nouncheckedindexedaccess-in-tsconfig))
63+
64+
:::
65+
66+
Next, generate TypeScript types from your OpenAPI schema using openapi-typescript:
67+
68+
```bash
69+
npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts
70+
```
71+
72+
## Basic usage
73+
74+
Once types have been generated from your schema, you can create a [fetch client](../introduction.md) and export wrapped `swr` hooks.
75+
76+
77+
Wrapper hooks are provided 1:1 for each hook exported by SWR. Check out the other sections of this documentation to learn more about each one.
78+
79+
::: code-group
80+
81+
```ts [src/my-api.ts]
82+
import createClient from "openapi-fetch";
83+
import {
84+
createQueryHook,
85+
createImmutableHook,
86+
createInfiniteHook,
87+
createMutateHook,
88+
} from "swr-openapi";
89+
import { isMatch } from "lodash-es";
90+
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
91+
92+
const client = createClient<paths>({
93+
baseUrl: "https://myapi.dev/v1/",
94+
});
95+
const prefix = "my-api";
96+
97+
export const useQuery = createQueryHook(client, prefix);
98+
export const useImmutable = createImmutableHook(client, prefix);
99+
export const useInfinite = createInfiniteHook(client, prefix);
100+
export const useMutate = createMutateHook(
101+
client,
102+
prefix,
103+
isMatch, // Or any comparision function
104+
);
105+
```
106+
:::
107+
108+
::: tip
109+
You can find more information about `createClient` on the [openapi-fetch documentation](../openapi-fetch/index.md).
110+
:::
111+
112+
113+
Then, import these hooks in your components:
114+
115+
::: code-group
116+
```tsx [src/my-component.tsx]
117+
import { useQuery } from "./my-api";
118+
119+
function MyComponent() {
120+
const { data, error, isLoading, isValidating, mutate } = useQuery(
121+
"/blogposts/{post_id}",
122+
{
123+
params: {
124+
path: { post_id: "123" },
125+
},
126+
},
127+
);
128+
129+
if (isLoading || !data) return "Loading...";
130+
131+
if (error) return `An error occured: ${error.message}`;
132+
133+
return <div>{data.title}</div>;
134+
}
135+
```
136+
:::

0 commit comments

Comments
 (0)