Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add useInfiniteQuery support #122

Merged
merged 7 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 77 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

## Features

- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery` and `useMutation` hooks
- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks
- Generates query keys and functions for query caching
- Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)

Expand Down Expand Up @@ -45,18 +45,20 @@ Options:
-V, --version output the version number
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
-o, --output <value> Output directory (default: "openapi")
-c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
-c, --client <value> HTTP client to generate (choices: "angular", "axios", "fetch", "node", "xhr", default: "fetch")
--request <value> Path to custom request file
--format <value> Process output folder with formatter? ['biome', 'prettier']
--lint <value> Process output folder with linter? ['eslint', 'biome']
--format <value> Process output folder with formatter? (choices: "biome", "prettier")
--lint <value> Process output folder with linter? (choices: "biome", "eslint")
--operationId Use operation ID to generate operation names?
--serviceResponse <value> Define shape of returned value from service calls ['body', 'response'] (default: "body")
--serviceResponse <value> Define shape of returned value from service calls (choices: "body", "response", default: "body")
--base <value> Manually set base in OpenAPI config instead of inferring from server value
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
--enums <value> Generate JavaScript objects from enum definitions? (choices: "javascript", "typescript")
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
--debug Enable debug mode
--noSchemas Disable generating schemas for request and response objects
--schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
--debug Run in debug mode?
--noSchemas Disable generating JSON schemas
--schemaType <value> Type of JSON schema [Default: 'json'] (choices: "form", "json")
--pageParam <value> Name of the query parameter used for pagination (default: "page")
--nextPageParam <value> Name of the response parameter used for next page (default: "nextPage")
-h, --help display help for command
```

Expand Down Expand Up @@ -234,6 +236,72 @@ function App() {
export default App;
```

##### Using Infinite Query hooks

This feature will generate a function in infiniteQueries.ts when the name specified by the `pageParam` option exists in the query parameters and the name specified by the `nextPageParam` option exists in the response.

Example Schema:

```yml
paths:
/paginated-pets:
get:
description: |
Returns paginated pets from the system that the user has access to
operationId: findPaginatedPets
parameters:
- name: page
in: query
description: page number
required: false
schema:
type: integer
format: int32
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: object
properties:
pets:
type: array
items:
$ref: '#/components/schemas/Pet'
nextPage:
type: integer
format: int32
minimum: 1
```

Usage of Generated Hooks:

```ts
import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";

const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
limit: 10,
tags: [],
});
```

##### Runtime Configuration

You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
Expand Down
37 changes: 37 additions & 0 deletions examples/nextjs-app/app/components/PaginatedPets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import React from "react";

export default function PaginatedPets() {
const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
limit: 10,
tags: [],
});

return (
<>
<h1>Pet List with Pagination</h1>
<ul>
{data?.pages.map((group, i) => (
<React.Fragment key={group.pets?.at(0)?.id}>
{group.pets?.map((pet) => (
<li key={pet.id}>{pet.name}</li>
))}
</React.Fragment>
))}
</ul>
{data?.pages.at(-1)?.nextPage && (
<button
type="button"
onClick={() => fetchNextPage()}
className="bg-blue-500 px-4 py-2 text-white mt-4"
>
Load More
</button>
)}
<ReactQueryDevtools initialIsOpen={false} />
</>
);
}
9 changes: 9 additions & 0 deletions examples/nextjs-app/app/infinite-loader/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PaginatedPets from "../components/PaginatedPets";

export default async function InfiniteLoaderPage() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<PaginatedPets />
</main>
);
}
6 changes: 5 additions & 1 deletion examples/nextjs-app/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
QueryClient,
dehydrate,
} from "@tanstack/react-query";
import Pets from "./pets";
import Link from "next/link";
import Pets from "./components/Pets";

export default async function Home() {
const queryClient = new QueryClient();
Expand All @@ -19,6 +20,9 @@ export default async function Home() {
<HydrationBoundary state={dehydrate(queryClient)}>
<Pets />
</HydrationBoundary>
<Link href="/infinite-loader" className="underline">
Go to Infinite Loader &rarr;
</Link>
</main>
);
}
5 changes: 4 additions & 1 deletion examples/nextjs-app/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export const request = <T>(
url: options.url,
data: options.body,
method: options.method,
params: options.path,
params: {
...options.query,
...options.path,
},
headers: formattedHeaders,
cancelToken: source.token,
})
Expand Down
46 changes: 46 additions & 0 deletions examples/petstore.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,52 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'
/paginated-pets:
get:
description: |
Returns paginated pets from the system that the user has access to
operationId: findPaginatedPets
parameters:
- name: page
in: query
description: page number
required: false
schema:
type: integer
format: int32
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: object
properties:
pets:
type: array
items:
$ref: '#/components/schemas/Pet'
nextPage:
type: integer
format: int32
minimum: 1

components:
schemas:
Pet:
Expand Down
5 changes: 4 additions & 1 deletion examples/react-app/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ export const request = <T>(
url: options.url,
data: options.body,
method: options.method,
params: options.path,
params: {
...options.query,
...options.path,
},
headers: formattedHeaders,
cancelToken: source.token,
})
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/cli.mts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type LimitedUserConfig = {
debug?: boolean;
noSchemas?: boolean;
schemaType?: "form" | "json";
pageParam: string;
nextPageParam: string;
};

async function setupProgram() {
Expand Down Expand Up @@ -90,6 +92,16 @@ async function setupProgram() {
"Type of JSON schema [Default: 'json']",
).choices(["form", "json"]),
)
.option(
"--pageParam <value>",
"Name of the query parameter used for pagination",
"page",
)
.option(
"--nextPageParam <value>",
"Name of the response parameter used for next page",
"nextPage",
)
.parse();

const options = program.opts<LimitedUserConfig>();
Expand Down
1 change: 1 addition & 0 deletions src/constants.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const modalsFileName = "types.gen";

export const OpenApiRqFiles = {
queries: "queries",
infiniteQueries: "infiniteQueries",
common: "common",
suspense: "suspense",
index: "index",
Expand Down
19 changes: 17 additions & 2 deletions src/createExports.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type ts from "typescript";
import { createPrefetch } from "./createPrefetch.mjs";
import { createUseMutation } from "./createUseMutation.mjs";
import { createUseQuery } from "./createUseQuery.mjs";
import type { Service } from "./service.mjs";

export const createExports = (service: Service) => {
export const createExports = (
service: Service,
pageParam: string,
nextPageParam: string,
) => {
const { klasses } = service;
const methods = klasses.flatMap((k) => k.methods);

Expand All @@ -23,7 +28,9 @@ export const createExports = (service: Service) => {
m.httpMethodName.toUpperCase().includes("DELETE"),
);

const allGetQueries = allGet.map((m) => createUseQuery(m));
const allGetQueries = allGet.map((m) =>
createUseQuery(m, pageParam, nextPageParam),
);
const allPrefetchQueries = allGet.map((m) => createPrefetch(m));

const allPostMutations = allPost.map((m) => createUseMutation(m));
Expand Down Expand Up @@ -60,6 +67,10 @@ export const createExports = (service: Service) => {

const mainExports = [...mainQueries, ...mainMutations];

const infiniteQueriesExports = allQueries
.flatMap(({ infiniteQueryHook }) => [infiniteQueryHook])
.filter(Boolean) as ts.VariableStatement[];

const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [
suspenseQueryHook,
]);
Expand All @@ -81,6 +92,10 @@ export const createExports = (service: Service) => {
* Main exports are the hooks that are used in the components
*/
mainExports,
/**
* Infinite queries exports are the hooks that are used in the infinite scroll components
*/
infiniteQueriesExports,
/**
* Suspense exports are the hooks that are used in the suspense components
*/
Expand Down
Loading