Skip to content

Commit

Permalink
Add swr-openapi package (#1932)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
htunnicliff authored Nov 5, 2024
1 parent ff57082 commit 639ec45
Show file tree
Hide file tree
Showing 35 changed files with 3,911 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilly-scissors-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"swr-openapi": minor
---

Modify package.json to point to new repository
13 changes: 13 additions & 0 deletions docs/.vitepress/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ export default defineConfig({
{ text: "About", link: "/openapi-react-query/about" },
],
},
{
text: "swr-openapi",
base: "/swr-openapi",
items: [
{ text: "Getting Started", link: "/" },
{ text: "Hook Builders", link: "/hook-builders" },
{ text: "useQuery", link: "/use-query" },
{ text: "useImmutable", link: "/use-immutable" },
{ text: "useInfinite", link: "/use-infinite" },
{ text: "useMutate", link: "/use-mutate" },
{ text: "About", link: "/about" },
],
},
],
},
search: {
Expand Down
5 changes: 3 additions & 2 deletions docs/.vitepress/shared.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { UserConfig } from "vitepress";
import type { UserConfig, DefaultTheme } from "vitepress";
import { zhSearch } from "./zh";
import { jaSearch } from "./ja";

Expand Down Expand Up @@ -26,6 +26,7 @@ const shared: UserConfig = {
themeConfig: {
siteTitle: false,
logo: "/assets/openapi-ts.svg",
outline: 'deep',
search: {
provider: "algolia",
options: {
Expand All @@ -44,7 +45,7 @@ const shared: UserConfig = {
},
{ icon: "github", link: "https://github.com/openapi-ts/openapi-typescript" },
],
},
} satisfies DefaultTheme.Config,
transformPageData({ relativePath, frontmatter }) {
frontmatter.head ??= [];
frontmatter.head.push([
Expand Down
2 changes: 1 addition & 1 deletion docs/data/contributors.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/scripts/update-contributors.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,15 @@ const CONTRIBUTORS = {
"illright",
]),
"openapi-react-query": new Set(["drwpow", "kerwanp", "yoshi2no"]),
"swr-openapi": new Set(["htunnicliff"])
};

async function main() {
let i = 0;
const total = Object.values(CONTRIBUTORS).reduce((total, next) => total + next.size, 0);
await Promise.all(
Object.entries(CONTRIBUTORS).map(async ([repo, contributors]) => {
data[repo] ??= [];
for (const username of [...contributors]) {
i++;
// skip profiles that have been updated within the past week
Expand Down
22 changes: 22 additions & 0 deletions docs/swr-openapi/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: About swr-openapi
description: swr-openapi Project Goals and contributors
---
<script setup>
import { VPTeamMembers } from 'vitepress/theme';
import contributors from '../data/contributors.json';
</script>

# {{ $frontmatter.title }}

## Project Goals

1. Types should be strict and inferred automatically from OpenAPI schemas with the absolute minimum number of generics needed.
2. Respect the original `swr` APIs while reducing boilerplate.
3. Be as light and performant as possible.

## Contributors

This library wouldn’t be possible without all these amazing contributors:

<VPTeamMembers size="small" :members="contributors['swr-openapi']" />
143 changes: 143 additions & 0 deletions docs/swr-openapi/hook-builders.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
title: Hook Builders
---

# {{ $frontmatter.title }}

Hook builders initialize `useQuery`, `useImmutate`, `useInfinite`, and `useMutate`.

Each builder function accepts an instance of a [fetch client](../openapi-fetch/api.md) and a prefix unique to that client.


::: tip

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").

:::

```ts
import createClient from "openapi-fetch";
import { isMatch } from "lodash-es";

import {
createQueryHook,
createImmutableHook,
createInfiniteHook,
createMutateHook,
} from "swr-openapi";

import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

const client = createClient<paths>(/* ... */);
const prefix = "my-api";

export const useQuery = createQueryHook(client, prefix);
export const useImmutable = createImmutableHook(client, prefix);
export const useInfinite = createInfiniteHook(client, prefix);
export const useMutate = createMutateHook(
client,
prefix,
isMatch, // Or any comparision function
);
```

## API

### Parameters

Each builder hook accepts the same initial parameters:

- `client`: A [fetch client](../openapi-fetch/api.md).
- `prefix`: A prefix unique to the fetch client.

`createMutateHook` also accepts a third parameter:

- [`compare`](#compare): A function to compare fetch options).

### Returns

- `createQueryHook` &rarr; [`useQuery`](./use-query.md)
- `createImmutableHook` &rarr; [`useImmutable`](./use-immutable.md)
- `createInfiniteHook` &rarr; [`useInfinite`](./use-infinite.md)
- `createMutateHook` &rarr; [`useMutate`](./use-mutate.md)

## `compare`

When calling `createMutateHook`, a function must be provided with the following contract:

```ts
type Compare = (init: any, partialInit: object) => boolean;
```

This function is used to determine whether or not a cached request should be updated when `mutate` is called with fetch options.

My personal recommendation is to use lodash's [`isMatch`][lodash-is-match]:

> Performs a partial deep comparison between object and source to determine if object contains equivalent property values.
```ts
const useMutate = createMutateHook(client, "<unique-key>", isMatch);

const mutate = useMutate();

await mutate([
"/path",
{
params: {
query: {
version: "beta",
},
},
},
]);

// ✅ Would be updated
useQuery("/path", {
params: {
query: {
version: "beta",
},
},
});

// ✅ Would be updated
useQuery("/path", {
params: {
query: {
version: "beta",
other: true,
example: [1, 2, 3],
},
},
});

// ❌ Would not be updated
useQuery("/path", {
params: {
query: {},
},
});

// ❌ Would not be updated
useQuery("/path");

// ❌ Would not be updated
useQuery("/path", {
params: {
query: {
version: "alpha",
},
},
});

// ❌ Would not be updated
useQuery("/path", {
params: {
query: {
different: "items",
},
},
});
```

[lodash-is-match]: https://lodash.com/docs/4.17.15#isMatch
136 changes: 136 additions & 0 deletions docs/swr-openapi/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: swr-openapi
---

# Introduction

swr-openapi is a type-safe wrapper around [`swr`](https://swr.vercel.app).

It works by using [openapi-fetch](../openapi-fetch/) and [openapi-typescript](../introduction) 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
- ✅ Also eliminates `as` type overrides that can also hide bugs

::: code-group

```tsx [src/my-component.ts]
import createClient from "openapi-fetch";
import { createQueryHook } from "swr-openapi";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

const client = createClient<paths>({
baseUrl: "https://myapi.dev/v1/",
});

const useQuery = createQueryHook(client, "my-api");

function MyComponent() {
const { data, error, isLoading, isValidating, mutate } = useQuery(
"/blogposts/{post_id}",
{
params: {
path: { post_id: "123" },
},
},
);

if (isLoading || !data) return "Loading...";

if (error) return `An error occured: ${error.message}`;

return <div>{data.title}</div>;
}

```

:::

## Setup

Install this library along with [openapi-fetch](../openapi-fetch/) and [openapi-typescript](../introduction):

```bash
npm i swr-openapi openapi-fetch
npm i -D openapi-typescript typescript
```

::: tip Highly recommended

Enable [noUncheckedIndexedAccess](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess) in your `tsconfig.json` ([docs](../advanced#enable-nouncheckedindexedaccess-in-tsconfig))

:::

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
```

## Basic usage

Once types have been generated from your schema, you can create a [fetch client](../introduction.md) and export wrapped `swr` hooks.


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.

::: code-group

```ts [src/my-api.ts]
import createClient from "openapi-fetch";
import {
createQueryHook,
createImmutableHook,
createInfiniteHook,
createMutateHook,
} from "swr-openapi";
import { isMatch } from "lodash-es";
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript

const client = createClient<paths>({
baseUrl: "https://myapi.dev/v1/",
});
const prefix = "my-api";

export const useQuery = createQueryHook(client, prefix);
export const useImmutable = createImmutableHook(client, prefix);
export const useInfinite = createInfiniteHook(client, prefix);
export const useMutate = createMutateHook(
client,
prefix,
isMatch, // Or any comparision function
);
```
:::

::: tip
You can find more information about `createClient` on the [openapi-fetch documentation](../openapi-fetch/index.md).
:::


Then, import these hooks in your components:

::: code-group
```tsx [src/my-component.tsx]
import { useQuery } from "./my-api";

function MyComponent() {
const { data, error, isLoading, isValidating, mutate } = useQuery(
"/blogposts/{post_id}",
{
params: {
path: { post_id: "123" },
},
},
);

if (isLoading || !data) return "Loading...";

if (error) return `An error occured: ${error.message}`;

return <div>{data.title}</div>;
}
```
:::
Loading

0 comments on commit 639ec45

Please sign in to comment.