Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bb864fd
feat: add NameFactory class for consistent naming
kyleamazza-fq Jul 20, 2025
faba0c0
refactor: extract legacy hook generation into separate methods
kyleamazza-fq Jul 20, 2025
328cff7
feat: add new query options exports
kyleamazza-fq Jul 20, 2025
3cccee9
feat: add service getter functions to context
kyleamazza-fq Jul 20, 2025
ebf7985
feat: add deprecation messages to legacy hooks
kyleamazza-fq Jul 20, 2025
131c32f
feat: add query key builder for type-safe cache operations
kyleamazza-fq Jul 20, 2025
03ccb96
fix: apply prettier formatting
kyleamazza-fq Jul 20, 2025
6bd1481
WIP: Replace NameFactory class with helper functions for v0.2.0 code
kyleamazza-fq Jul 21, 2025
c738879
Use standard @deprecated JSDoc tag instead of custom deprecation comm…
kyleamazza-fq Jul 21, 2025
5def116
Add CHANGELOG.md and fix package.json description
kyleamazza-fq Jul 21, 2025
36b904c
Fix parameter names with special characters in query keys
kyleamazza-fq Jul 21, 2025
d7fcb31
Update CHANGELOG with parameter fix
kyleamazza-fq Jul 21, 2025
927c6c3
Fix duplicate function declarations for non-get methods
kyleamazza-fq Jul 21, 2025
c82e7cb
Update CHANGELOG with duplicate function fix
kyleamazza-fq Jul 21, 2025
9db6cd0
docs: Update README with queryOptions usage and improve documentation
kyleamazza-fq Jul 21, 2025
a660258
refactor: Replace NameFactory class with helper functions
kyleamazza-fq Jul 21, 2025
dae3074
Update query keys to use simpler static structure
kyleamazza-fq Jul 21, 2025
d3a52d6
Fix query key parameter syntax and build configuration
kyleamazza-fq Jul 21, 2025
9c7ba98
Remove unused variable in buildQueryKey method
kyleamazza-fq Jul 21, 2025
9acd696
Update CHANGELOG with recent changes
kyleamazza-fq Jul 21, 2025
10bf9a4
0.2.0-alpha.3
kyleamazza Jul 21, 2025
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,6 @@ dist
.pnp.*

lib/

# AI Assistant Configuration
CLAUDE.md
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ coverage
node_modules

lib

README.md
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- New query options exports for better React Query v5 compatibility
- `{methodName}QueryOptions` functions for regular queries
- `{methodName}MutationOptions` functions for mutations
- `{methodName}InfiniteQueryOptions` functions for infinite queries
- Service getter functions (`get{ServiceName}Service`) for use in non-React contexts
- Query key builder utility for type-safe cache invalidation and queries

### Changed

- Generated hooks now use simplified `@deprecated` JSDoc tags instead of custom deprecation blocks
- Query keys now use a simpler static structure based on interface and method names
- Changed from URL-based resource keys to pattern: `['interface', 'method', params || {}]`
- Interface names in query keys now use camelCase for consistency with JavaScript conventions
- Removed complex URL path parsing logic for cleaner, more predictable keys
- Refactored internal code generation to use helper functions instead of NameFactory class

### Fixed

- Parameter names with special characters (e.g., hyphens) are now properly handled in query keys
- All parameter access now uses bracket notation for consistency
- Object keys in query key generation are properly quoted
- Fixed duplicate function declarations for methods not starting with "get"
- Suspense hooks now correctly generate with `useSuspense` prefix for all method types
- Prevents TypeScript errors from duplicate function names
- Fixed invalid TypeScript syntax in query keys where optional parameter syntax (`params?`) was incorrectly used in runtime expressions
- Fixed infinite query key typo (`inifinite` → `infinite`)
- Build configuration now properly excludes snapshot directory from TypeScript compilation
- Added README.md to .prettierignore to prevent formatter hanging

### Deprecated

- Legacy hook exports (`use{MethodName}`, `useSuspense{MethodName}`, etc.) are now deprecated
- These hooks will be removed in a future major version
- Users should migrate to the new query options pattern with React Query's built-in hooks
168 changes: 165 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,173 @@

# React Query

[Basketry generator](https://github.com/basketry/basketry) for generating React Query hooks. This parser can be coupled with any Basketry parser.
[Basketry generator](https://basketry.io) for generating React Query queryOptions and hooks. This generator can be coupled with any Basketry parser.

## Quick Start

// TODO
### Installation

```bash
npm install @basketry/react-query
```

### Getting Started

1. **Create a Basketry configuration file** (`basketry.config.json`):
```json
{
"source": "openapi.json",
"parser": "@basketry/openapi-3",
"generators": ["@basketry/react-query"],
"output": "./src/generated/react-query",
"options": {
"basketry": {
"command": "npx basketry"
},
"typescript": {
"includeVersion": false
},
"reactQuery": {
"typesModule": "@your-api/types", // Path to generated TypeScript types
"clientModule": "@your-api/http-client-sdk" // Path to generated HTTP client
}
}
}
```

2. **Run Basketry** to generate the React Query hooks:
```bash
npx basketry
```

3. **Set up your React Query provider** in your app:
```typescript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Name of provider will depend on the name of the API service in your OpenAPI spec.
import { BasketryExampleProvider } from './src/generated/context';

const queryClient = new QueryClient();
const httpClient = fetch; // or your custom fetch implementation

function App() {
return (
<QueryClientProvider client={queryClient}>
<BasketryExampleProvider httpClient={httpClient}>
{/* Your app components */}
</BasketryExampleProvider>
</QueryClientProvider>
);
}
```

4. **Use the generated hooks** in your components:
```typescript
import { useQuery } from '@tanstack/react-query';
import { getWidgetsQueryOptions } from './src/generated';

function WidgetList() {
const { data, isLoading } = useQuery(getWidgetsQueryOptions());

if (isLoading) return <div>Loading...</div>;
return <div>{data?.map(widget => <div key={widget.id}>{widget.name}</div>)}</div>;
}
```

### Basic Usage

This generator produces React Query compatible code with queryOptions functions that provide maximum flexibility:

```typescript
// Using query options with React Query hooks
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { getWidgetsQueryOptions } from './petstore'; // generated code

function WidgetList() {
// Basic usage
const { data } = useQuery(getWidgetsQueryOptions());

// With parameters
const { data: filtered } = useQuery(
getWidgetsQueryOptions({ status: 'active' })
);

// With custom options
const { data: cached } = useQuery({
...getWidgetsQueryOptions(),
staleTime: 5 * 60 * 1000, // 5 minutes
});

return <div>{/* render widgets */}</div>;
}
```

### Mutations

```typescript
import { useMutation } from '@tanstack/react-query';
import { createWidgetMutationOptions } from './petstore'; // generated code

function CreateWidget() {
const mutation = useMutation(createWidgetMutationOptions());

const handleSubmit = (data: CreateWidgetInput) => {
mutation.mutate(data, {
onSuccess: (widget) => {
console.log('Created widget:', widget);
},
});
};

return <form>{/* form fields */}</form>;
}
```

### Infinite Queries (Pagination)

For services with Relay-style pagination:

```typescript
import { useInfiniteQuery } from '@tanstack/react-query';
import { getWidgetsInfiniteQueryOptions } from './petstore'; // generated code

function InfiniteWidgetList() {
const {
data,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery(getWidgetsInfiniteQueryOptions());

return (
<div>
{data?.pages.map(page =>
page.edges.map(({ node }) => (
<Widget key={node.id} data={node} />
))
)}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage}>
Load More
</button>
</div>
);
}
```

## Configuration

Add to your `basketry.config.json`:

```json
```

## Features

- **React Query Compatible**: Generates queryOptions and mutationOptions functions
- **Type-Safe**: Full TypeScript support with proper type inference
- **Flexible**: Use with any React Query hook (useQuery, useSuspenseQuery, etc.)
- **SSR Ready**: Service getters work outside React components
- **Backward Compatible**: Legacy hooks are deprecated but still available
- **Relay Pagination**: Built-in support for cursor-based pagination
- **Error Handling**: Automatic error aggregation with CompositeError

---

Expand All @@ -24,7 +186,7 @@ Note that the `lint` script is run prior to `build`. Auto-fixable linting or for
### Create and run tests

1. Add tests by creating files with the `.test.ts` suffix
1. Run the tests: `npm t`
1. Run the tests: `npm test`
1. Test coverage can be viewed at `/coverage/lcov-report/index.html`

### Publish a new package version
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@basketry/react-query",
"version": "0.0.0",
"description": "Basketry generator for generating Typescript interfaces",
"version": "0.2.0-alpha.3",
"description": "Basketry generator for generating React Query hooks",
"main": "./lib/index.js",
"scripts": {
"test": "jest",
Expand Down
49 changes: 41 additions & 8 deletions src/context-file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { camel, pascal } from 'case';
import { ModuleBuilder } from './module-builder';
import { ImportBuilder } from './import-builder';
import {
buildContextName,
buildProviderName,
buildServiceHookName,
buildServiceGetterName,
buildServiceName,
} from './name-helpers';

export class ContextFile extends ModuleBuilder {
private readonly react = new ImportBuilder('react');
Expand All @@ -23,23 +30,49 @@ export class ContextFile extends ModuleBuilder {
const FetchLike = () => this.client.type('FetchLike');
const OptionsType = () => this.client.type(optionsName);

yield `export interface ClientContextProps { fetch: ${FetchLike()}; options: ${OptionsType()}; }`;
yield `const ClientContext = ${createContext()}<ClientContextProps | undefined>( undefined );`;
// Use consistent naming from helper functions
const contextName = buildContextName(this.service);
const contextPropsName = pascal(`${contextName}_props`);
const providerName = buildProviderName(this.service);

yield `export interface ${contextPropsName} { fetch: ${FetchLike()}; options: ${OptionsType()}; }`;
yield `const ${contextName} = ${createContext()}<${contextPropsName} | undefined>( undefined );`;
yield ``;

// Store context for non-hook access
yield `let currentContext: ${contextPropsName} | undefined;`;
yield ``;
yield `export const ClientProvider: ${FC()}<${PropsWithChildren()}<ClientContextProps>> = ({ children, fetch, options }) => {`;

yield `export const ${providerName}: ${FC()}<${PropsWithChildren()}<${contextPropsName}>> = ({ children, fetch, options }) => {`;
yield ` const value = ${useMemo()}(() => ({ fetch, options }), [fetch, options.mapUnhandledException, options.mapValidationError, options.root]);`;
yield ` return <ClientContext.Provider value={value}>{children}</ClientContext.Provider>;`;
yield ` currentContext = value;`;
yield ` return <${contextName}.Provider value={value}>{children}</${contextName}.Provider>;`;
yield `};`;

for (const int of this.service.interfaces) {
const hookName = camel(`use_${int.name.value}_service`);
const localName = camel(`${int.name.value}_service`);
const hookName = buildServiceHookName(int);
const getterName = buildServiceGetterName(int);
const localName = buildServiceName(int);
const interfaceName = pascal(`${int.name.value}_service`);
const className = pascal(`http_${int.name.value}_service`);

// Add service getter function (v0.2.0)
yield ``;
yield `export const ${getterName} = () => {`;
yield ` if (!currentContext) { throw new Error('${getterName} called outside of ${providerName}'); }`;
yield ` const ${localName}: ${this.types.type(
interfaceName,
)} = new ${this.client.fn(
className,
)}(currentContext.fetch, currentContext.options);`;
yield ` return ${localName};`;
yield `};`;

// Keep legacy hook for backward compatibility (v0.1.0)
yield ``;
yield `export const ${hookName} = () => {`;
yield ` const context = ${useContext()}(ClientContext);`;
yield ` if (!context) { throw new Error('${hookName} must be used within a ClientProvider'); }`;
yield ` const context = ${useContext()}(${contextName});`;
yield ` if (!context) { throw new Error('${hookName} must be used within a ${providerName}'); }`;
yield ` const ${localName}: ${this.types.type(
interfaceName,
)} = new ${this.client.fn(className)}(context.fetch, context.options);`;
Expand Down
Loading