diff --git a/.gitignore b/.gitignore
index b0a76c1..534545f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,3 +130,6 @@ dist
.pnp.*
lib/
+
+# AI Assistant Configuration
+CLAUDE.md
diff --git a/.prettierignore b/.prettierignore
index e738f12..56eb95b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,3 +2,5 @@ coverage
node_modules
lib
+
+README.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d62cb81
--- /dev/null
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index d16892a..1198188 100644
--- a/README.md
+++ b/README.md
@@ -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 (
+
+
+ {/* Your app components */}
+
+
+ );
+ }
+ ```
+
+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
Loading...
;
+ return {data?.map(widget =>
{widget.name}
)}
;
+ }
+ ```
+
+### 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 {/* render widgets */}
;
+}
+```
+
+### 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 ;
+}
+```
+
+### 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 (
+
+ {data?.pages.map(page =>
+ page.edges.map(({ node }) => (
+
+ ))
+ )}
+ fetchNextPage()} disabled={!hasNextPage}>
+ Load More
+
+
+ );
+}
+```
+
+## 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
---
@@ -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
diff --git a/package-lock.json b/package-lock.json
index a6e008a..5671f22 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@basketry/react-query",
- "version": "0.0.0",
+ "version": "0.2.0-alpha.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@basketry/react-query",
- "version": "0.0.0",
+ "version": "0.2.0-alpha.3",
"license": "MIT",
"dependencies": {
"@basketry/typescript": "^0.1.2",
diff --git a/package.json b/package.json
index c8e904c..625192b 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/context-file.ts b/src/context-file.ts
index 6fb1b8a..6af4bd9 100644
--- a/src/context-file.ts
+++ b/src/context-file.ts
@@ -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');
@@ -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()}( 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()}> = ({ 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 {children} ;`;
+ 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);`;
diff --git a/src/hook-file.ts b/src/hook-file.ts
index ad199a3..af0f3a3 100644
--- a/src/hook-file.ts
+++ b/src/hook-file.ts
@@ -17,11 +17,18 @@ import {
} from '@basketry/typescript';
import { from } from '@basketry/typescript/lib/utils';
-import { camel } from 'case';
+import { camel, pascal } from 'case';
import { NamespacedReactQueryOptions } from './types';
import { ModuleBuilder } from './module-builder';
import { ImportBuilder } from './import-builder';
-import { getQueryOptionsName } from './name-factory';
+import {
+ getQueryOptionsName,
+ buildServiceGetterName,
+ buildQueryOptionsName,
+ buildMutationOptionsName,
+ buildInfiniteQueryOptionsName,
+ buildServiceHookName,
+} from './name-helpers';
export class HookFile extends ModuleBuilder {
constructor(
@@ -46,6 +53,7 @@ export class HookFile extends ModuleBuilder {
];
*body(): Iterable {
+ // === LEGACY HOOKS (v0.1.0) ===
const useMutation = () => this.tanstack.fn('useMutation');
const useQuery = () => this.tanstack.fn('useQuery');
const useQueryClient = () => this.tanstack.fn('useQueryClient');
@@ -74,7 +82,6 @@ export class HookFile extends ModuleBuilder {
)) {
const name = this.getHookName(method);
const suspenseName = this.getHookName(method, { suspense: true });
- const infiniteName = this.getHookName(method, { infinite: true });
const paramsType = from(buildParamsType(method));
const httpMethod = getHttpMethodByName(this.service, method.name.value);
const httpPath = this.getHttpPath(httpMethod);
@@ -88,10 +95,6 @@ export class HookFile extends ModuleBuilder {
const isGet = httpMethod?.verb.value === 'get' && !!httpPath;
- if (isGet) {
- yield* this.generateQueryOptions(method, httpPath);
- }
-
if (isGet) {
const queryOptionsName = getQueryOptionsName(method);
const paramsCallsite = method.parameters.length ? 'params' : '';
@@ -134,11 +137,7 @@ export class HookFile extends ModuleBuilder {
dataTypeName,
)} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`;
- yield* buildDescription(
- method.description,
- undefined,
- method.deprecated?.value,
- );
+ yield* buildDescription(method.description, undefined, true);
yield `export function ${name}(${[
paramsExpression,
optionsExpression,
@@ -147,11 +146,7 @@ export class HookFile extends ModuleBuilder {
yield ` return ${useQuery()}({...defaultOptions, ...options});`;
yield `}`;
yield '';
- yield* buildDescription(
- method.description,
- undefined,
- method.deprecated?.value,
- );
+ yield* buildDescription(method.description, undefined, true);
yield `export function ${suspenseName}(${[
paramsExpression,
optionsExpression,
@@ -178,11 +173,7 @@ export class HookFile extends ModuleBuilder {
typeName,
)}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`;
- yield* buildDescription(
- method.description,
- undefined,
- method.deprecated?.value,
- );
+ yield* buildDescription(method.description, undefined, true);
yield `export function ${name}(${optionsExpression}) {`;
yield ` const queryClient = ${useQueryClient()}();`;
yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}()`;
@@ -194,15 +185,9 @@ export class HookFile extends ModuleBuilder {
yield ` if (res.errors.length) { throw new ${CompositeError()}(res.errors); }`;
yield ` else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); }`;
- const queryKeys = new Set();
- queryKeys.add(this.buildResourceKey(httpPath, method)); // Invalidate this resource
- queryKeys.add(
- this.buildResourceKey(httpPath, method, { skipTerminalParams: true }), // Invalidate the parent resource group
- );
-
- for (const queryKey of Array.from(queryKeys)) {
- yield ` queryClient.invalidateQueries({ queryKey: [${queryKey}] });`;
- }
+ // Invalidate all queries for this interface
+ const interfaceName = camel(this.int.name.value);
+ yield ` queryClient.invalidateQueries({ queryKey: ['${interfaceName}'] });`;
yield ` return res.data;`;
yield ` },`;
yield ` ...options,`;
@@ -223,7 +208,7 @@ export class HookFile extends ModuleBuilder {
yield `function ${infiniteOptionsHook}(${paramsExpression}) {`;
yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}();`;
yield ` return {`;
- yield ` queryKey: ${this.buildQueryKey(httpPath, method, {
+ yield ` queryKey: ${this.buildQueryKey(method, {
includeRelayParams: false,
infinite: true,
})},`;
@@ -241,11 +226,7 @@ export class HookFile extends ModuleBuilder {
yield ` };`;
yield `}`;
- yield* buildDescription(
- method.description,
- undefined,
- method.deprecated?.value,
- );
+ yield* buildDescription(method.description, undefined, true);
yield `export const ${this.getHookName(method, {
suspense: false,
infinite: true,
@@ -254,11 +235,7 @@ export class HookFile extends ModuleBuilder {
yield ` return ${useInfiniteQuery()}(options);`;
yield `}`;
- yield* buildDescription(
- method.description,
- undefined,
- method.deprecated?.value,
- );
+ yield* buildDescription(method.description, undefined, true);
yield `export const ${this.getHookName(method, {
suspense: true,
infinite: true,
@@ -270,6 +247,35 @@ export class HookFile extends ModuleBuilder {
yield '';
}
+ yield '';
+
+ // === NEW QUERY OPTIONS EXPORTS (v0.2.0) ===
+ yield '';
+ for (const method of this.int.methods) {
+ const httpMethod = getHttpMethodByName(this.service, method.name.value);
+ const httpPath = this.getHttpPath(httpMethod);
+ yield* this.generateAllQueryOptions(method, httpMethod, httpPath);
+ }
+ }
+
+ private *generateAllQueryOptions(
+ method: Method,
+ httpMethod: HttpMethod | undefined,
+ httpPath: HttpPath | undefined,
+ ): Iterable {
+ if (!httpPath) return;
+
+ const isGet = httpMethod?.verb.value === 'get';
+
+ if (isGet) {
+ yield* this.generateQueryOptions(method, httpPath);
+
+ if (this.isRelayPaginated(method)) {
+ yield* this.generateInfiniteQueryOptions(method, httpPath);
+ }
+ } else {
+ yield* this.generateMutationOptions(method);
+ }
}
private *buildInfiniteSelectFn(method: Method): Iterable {
@@ -294,6 +300,109 @@ export class HookFile extends ModuleBuilder {
}),`;
}
+ private *generateMutationOptions(method: Method): Iterable {
+ const mutationOptions = () => this.tanstack.fn('mutationOptions');
+ const CompositeError = () => this.runtime.fn('CompositeError');
+ const type = (t: string) => this.types.type(t);
+
+ const serviceName = camel(`${this.int.name.value}_service`);
+ const serviceGetterName = buildServiceGetterName(this.int);
+ const mutationOptionsName = buildMutationOptionsName(method);
+
+ const paramsType = from(buildParamsType(method));
+ const paramsExpression = method.parameters.length
+ ? `params: ${type(paramsType)}`
+ : '';
+ const paramsCallsite = method.parameters.length ? 'params' : '';
+
+ const returnType = getTypeByName(
+ this.service,
+ method.returnType?.typeName.value,
+ );
+ const dataType = getTypeByName(
+ this.service,
+ returnType?.properties.find((p) => p.name.value === 'data')?.typeName
+ .value,
+ );
+
+ const typeName = dataType ? buildTypeName(dataType) : 'void';
+
+ yield* buildDescription(
+ method.description,
+ undefined,
+ method.deprecated?.value,
+ );
+ yield `export const ${mutationOptionsName} = () => {`;
+ yield ` const ${serviceName} = ${this.context.fn(serviceGetterName)}()`;
+ yield ` return ${mutationOptions()}({`;
+ yield ` mutationFn: async (${paramsExpression}) => {`;
+ yield ` const res = await ${serviceName}.${camel(
+ method.name.value,
+ )}(${paramsCallsite});`;
+ yield ` if (res.errors.length) { throw new ${CompositeError()}(res.errors); }`;
+ yield ` else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); }`;
+ yield ` return res.data;`;
+ yield ` },`;
+ yield ` });`;
+ yield `};`;
+ }
+
+ private *generateInfiniteQueryOptions(
+ method: Method,
+ httpPath: HttpPath,
+ ): Iterable {
+ const infiniteQueryOptions = () => this.tanstack.fn('infiniteQueryOptions');
+ const CompositeError = () => this.runtime.fn('CompositeError');
+ const type = (t: string) => this.types.type(t);
+ const applyPageParam = () => this.runtime.fn('applyPageParam');
+ const getInitialPageParam = () => this.runtime.fn('getInitialPageParam');
+ const getNextPageParam = () => this.runtime.fn('getNextPageParam');
+ const getPreviousPageParam = () => this.runtime.fn('getPreviousPageParam');
+ const PageParam = () => this.runtime.type('PageParam');
+
+ const serviceName = camel(`${this.int.name.value}_service`);
+ const serviceGetterName = buildServiceGetterName(this.int);
+ const infiniteOptionsName = buildInfiniteQueryOptionsName(method);
+
+ const paramsType = from(buildParamsType(method));
+ const q = method.parameters.every((param) => !isRequired(param)) ? '?' : '';
+ const paramsExpression = method.parameters.length
+ ? `params${q}: ${type(paramsType)}`
+ : '';
+
+ const methodExpression = `${serviceName}.${camel(method.name.value)}`;
+ const paramsCallsite = method.parameters.length
+ ? `${applyPageParam()}(params${q ? '?? {}' : ''}, pageParam)`
+ : '';
+
+ yield '';
+ yield* buildDescription(
+ method.description,
+ undefined,
+ method.deprecated?.value,
+ );
+ yield `export const ${infiniteOptionsName} = (${paramsExpression}) => {`;
+ yield ` const ${serviceName} = ${this.context.fn(serviceGetterName)}();`;
+ yield ` return ${infiniteQueryOptions()}({`;
+ yield ` queryKey: ${this.buildQueryKey(method, {
+ includeRelayParams: false,
+ infinite: true,
+ })},`;
+ yield ` queryFn: async ({ pageParam }: ${PageParam()}) => {`;
+ yield ` const res = await ${methodExpression}(${paramsCallsite});`;
+ yield ` if (res.errors.length) { throw new ${CompositeError()}(res.errors); }`;
+ yield ` return res;`;
+ yield ` },`;
+ yield* this.buildInfiniteSelectFn(method);
+ yield ` initialPageParam: ${getInitialPageParam()}(params${
+ q ? '?? {}' : ''
+ }),`;
+ yield ` ${getNextPageParam()},`;
+ yield ` ${getPreviousPageParam()},`;
+ yield ` });`;
+ yield `};`;
+ }
+
private *generateQueryOptions(
method: Method,
httpPath: HttpPath,
@@ -303,8 +412,13 @@ export class HookFile extends ModuleBuilder {
const type = (t: string) => this.types.type(t);
const serviceName = camel(`${this.int.name.value}_service`);
- const serviceHookName = camel(`use_${this.int.name.value}_service`);
- const name = getQueryOptionsName(method);
+ const serviceGetterName = buildServiceGetterName(this.int);
+
+ // Keep the internal function for backward compatibility
+ const internalName = getQueryOptionsName(method);
+ // New exported function name
+ const exportedName = buildQueryOptionsName(method);
+
const paramsType = from(buildParamsType(method));
const q = method.parameters.every((param) => !isRequired(param)) ? '?' : '';
const paramsExpression = method.parameters.length
@@ -323,10 +437,40 @@ export class HookFile extends ModuleBuilder {
(prop) => prop.name.value !== 'data' && prop.name.value !== 'errors',
);
- yield `const ${name} = (${paramsExpression}) => {`;
- yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}()`;
+ // Internal function for backward compatibility with hooks
+ yield `const ${internalName} = (${paramsExpression}) => {`;
+ yield ` const ${serviceName} = ${this.context.fn(
+ buildServiceHookName(this.int),
+ )}()`;
+ yield ` return ${queryOptions()}({`;
+ yield ` queryKey: ${this.buildQueryKey(method, {
+ includeRelayParams: true,
+ })},`;
+ yield ` queryFn: async () => {`;
+ yield ` const res = await ${serviceName}.${camel(
+ method.name.value,
+ )}(${paramsCallsite});`;
+ yield ` if (res.errors.length) { throw new ${CompositeError()}(res.errors); }`;
+ yield ` else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); }`;
+ yield ` return res;`;
+ yield ` },`;
+ if (!skipSelect) {
+ yield ` select: (data) => data.data,`;
+ }
+ yield ` });`;
+ yield `};`;
+
+ // New exported query options function (v0.2.0)
+ yield '';
+ yield* buildDescription(
+ method.description,
+ undefined,
+ method.deprecated?.value,
+ );
+ yield `export const ${exportedName} = (${paramsExpression}) => {`;
+ yield ` const ${serviceName} = ${this.context.fn(serviceGetterName)}()`;
yield ` return ${queryOptions()}({`;
- yield ` queryKey: ${this.buildQueryKey(httpPath, method, {
+ yield ` queryKey: ${this.buildQueryKey(method, {
includeRelayParams: true,
})},`;
yield ` queryFn: async () => {`;
@@ -362,7 +506,11 @@ export class HookFile extends ModuleBuilder {
);
}
- return camel(`use_${name}`);
+ return camel(
+ `use_${options?.suspense ? 'suspense_' : ''}${
+ options?.infinite ? 'infinite_' : ''
+ }${name}`,
+ );
}
private getHttpPath(
@@ -384,71 +532,25 @@ export class HookFile extends ModuleBuilder {
}
private buildQueryKey(
- httpPath: HttpPath,
method: Method,
options?: { includeRelayParams?: boolean; infinite?: boolean },
): string {
- const compact = () => this.runtime.fn('compact');
-
- const resourceKey = this.buildResourceKey(httpPath, method);
- const q = method.parameters.every((param) => !isRequired(param)) ? '?' : '';
-
- const httpMethod = getHttpMethodByName(this.service, method.name.value);
- const queryParams = httpMethod?.parameters.filter((p) =>
- isCacheParam(p, options?.includeRelayParams ?? false),
- );
+ const interfaceName = camel(this.int.name.value);
+ const methodName = camel(method.name.value);
- const queryKey = [resourceKey];
+ const queryKey = [`'${interfaceName}'`, `'${methodName}'`];
- let couldHaveNullQueryParams = false;
- if (queryParams?.length) {
- couldHaveNullQueryParams = queryParams.every((hp) => {
- const param = method.parameters.find(
- (p) => camel(p.name.value) === camel(hp.name.value),
- );
- return param ? !isRequired(param) : true;
- });
- queryKey.push(
- `${compact()}({${queryParams
- .map((p) => `${p.name.value}: params${q}.${p.name.value}`)
- .join(',')}})`,
- );
+ if (method.parameters.length) {
+ queryKey.push(`params || {}`);
+ } else {
+ queryKey.push('{}');
}
if (options?.infinite) {
- queryKey.push('{inifinite: true}');
- }
-
- return `[${queryKey.join(', ')}]${
- couldHaveNullQueryParams ? '.filter(Boolean)' : ''
- }`;
- }
-
- private buildResourceKey(
- httpPath: HttpPath,
- method: Method,
- options?: { skipTerminalParams: boolean },
- ): string {
- const q = method.parameters.every((param) => !isRequired(param)) ? '?' : '';
-
- const parts = httpPath.path.value.split('/');
-
- if (options?.skipTerminalParams) {
- while (isPathParam(parts[parts.length - 1])) {
- parts.pop();
- }
+ queryKey.push('{infinite: true}');
}
- const path = parts.filter(Boolean).map((part) => {
- if (part.startsWith('{') && part.endsWith('}')) {
- const param = part.slice(1, -1);
- return `\${params${q}.${camel(param)}}`;
- }
-
- return part;
- });
-
- return `\`/${path.join('/')}\``;
+ return `[${queryKey.join(', ')}]`;
}
private isRelayPaginated(method: Method): boolean {
diff --git a/src/hook-generator.test.ts b/src/hook-generator.test.ts
index 1c518bb..9e0bf31 100644
--- a/src/hook-generator.test.ts
+++ b/src/hook-generator.test.ts
@@ -2,7 +2,7 @@ import { readFileSync } from 'fs';
import { join } from 'path';
import { generateFiles } from './snapshot/test-utils';
-describe.skip('HookGenerator', () => {
+describe('HookGenerator', () => {
it('recreates a valid snapshot using the Engine', async () => {
for await (const file of generateFiles()) {
const snapshot = readFileSync(join(...file.path)).toString();
diff --git a/src/hook-generator.ts b/src/hook-generator.ts
index 189ba58..70059d8 100644
--- a/src/hook-generator.ts
+++ b/src/hook-generator.ts
@@ -1,15 +1,13 @@
import { File, Generator, Service } from 'basketry';
import { plural } from 'pluralize';
-
import { buildFilePath } from '@basketry/typescript';
import { format, from } from '@basketry/typescript/lib/utils';
-import { header } from '@basketry/typescript/lib/warning';
-
import { kebab } from 'case';
import { NamespacedReactQueryOptions } from './types';
import { HookFile } from './hook-file';
import { ContextFile } from './context-file';
import { RuntimeFile } from './runtime-file';
+import { QueryKeyBuilder } from './query-key-builder';
export const generateHooks: Generator = (service, options) => {
return new HookGenerator(service, options).generate();
@@ -40,6 +38,18 @@ class HookGenerator {
),
});
+ files.push({
+ path: buildFilePath(
+ ['hooks', 'query-key-builder.ts'],
+ this.service,
+ this.options,
+ ),
+ contents: format(
+ from(new QueryKeyBuilder(this.service, this.options).build()),
+ this.options,
+ ),
+ });
+
for (const int of this.service.interfaces) {
const contents = format(
from(new HookFile(this.service, this.options, int).build()),
diff --git a/src/name-factory.ts b/src/name-factory.ts
deleted file mode 100644
index 465525d..0000000
--- a/src/name-factory.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Method } from 'basketry';
-import { camel } from 'case';
-
-export function getQueryOptionsName(method: Method): string {
- return camel(`use_${method.name.value}_query_options`);
-}
diff --git a/src/name-helpers.ts b/src/name-helpers.ts
new file mode 100644
index 0000000..40900a4
--- /dev/null
+++ b/src/name-helpers.ts
@@ -0,0 +1,38 @@
+import { Interface, Method, Service } from 'basketry';
+import { camel, pascal } from 'case';
+
+export function getQueryOptionsName(method: Method): string {
+ return camel(`use_${method.name.value}_query_options`);
+}
+
+export function buildServiceGetterName(int: Interface): string {
+ return camel(`get_${int.name.value}_service`);
+}
+
+export function buildQueryOptionsName(method: Method): string {
+ return camel(`${method.name.value}_query_options`);
+}
+
+export function buildMutationOptionsName(method: Method): string {
+ return camel(`${method.name.value}_mutation_options`);
+}
+
+export function buildInfiniteQueryOptionsName(method: Method): string {
+ return camel(`${method.name.value}_infinite_query_options`);
+}
+
+export function buildServiceHookName(int: Interface): string {
+ return camel(`use_${int.name.value}_service`);
+}
+
+export function buildContextName(service: Service): string {
+ return pascal(`${service.title.value}_context`);
+}
+
+export function buildProviderName(service: Service): string {
+ return pascal(`${service.title.value}_provider`);
+}
+
+export function buildServiceName(int: Interface): string {
+ return camel(`${int.name.value}_service`);
+}
diff --git a/src/query-key-builder.ts b/src/query-key-builder.ts
new file mode 100644
index 0000000..0c2bb5f
--- /dev/null
+++ b/src/query-key-builder.ts
@@ -0,0 +1,165 @@
+import { isRequired, Method, Service } from 'basketry';
+
+import { buildParamsType } from '@basketry/typescript';
+import { from } from '@basketry/typescript/lib/utils';
+
+import { camel } from 'case';
+import { NamespacedReactQueryOptions } from './types';
+import { ModuleBuilder } from './module-builder';
+import { ImportBuilder } from './import-builder';
+
+export class QueryKeyBuilder extends ModuleBuilder {
+ constructor(
+ service: Service,
+ options: NamespacedReactQueryOptions | undefined,
+ ) {
+ super(service, options);
+ }
+
+ private readonly types = new ImportBuilder(
+ this.options?.reactQuery?.typesModule ?? '../types',
+ );
+
+ protected readonly importBuilders = [this.types];
+
+ *body(): Iterable {
+ // Generate QueryKeyMap interface
+ yield* this.generateQueryKeyMap();
+ yield '';
+
+ // Generate type extraction helpers
+ yield* this.generateTypeHelpers();
+ yield '';
+
+ // Generate matchQueryKey function
+ yield* this.generateMatchQueryKeyFunction();
+ }
+
+ private *generateQueryKeyMap(): Iterable {
+ yield '/**';
+ yield ' * Type mapping for all available query keys in the service';
+ yield ' */';
+ yield 'export interface QueryKeyMap {';
+
+ for (const int of this.service.interfaces) {
+ const serviceName = camel(int.name.value);
+ yield ` ${serviceName}: {`;
+
+ for (const method of int.methods) {
+ const methodName = camel(method.name.value);
+ const paramsType = this.buildMethodParamsType(method);
+
+ yield ` ${methodName}: ${paramsType};`;
+ }
+
+ yield ' };';
+ }
+
+ yield '}';
+ }
+
+ private *generateTypeHelpers(): Iterable {
+ // ServiceKeys type
+ yield '/**';
+ yield ' * Extract all service names from QueryKeyMap';
+ yield ' */';
+ yield 'export type ServiceKeys = keyof QueryKeyMap;';
+ yield '';
+
+ // OperationKeys type
+ yield '/**';
+ yield ' * Extract operation names for a given service';
+ yield ' */';
+ yield 'export type OperationKeys = keyof QueryKeyMap[S];';
+ yield '';
+
+ // OperationParams type
+ yield '/**';
+ yield ' * Extract parameter type for a given service and operation';
+ yield ' */';
+ yield 'export type OperationParams<';
+ yield ' S extends ServiceKeys,';
+ yield ' O extends OperationKeys';
+ yield '> = QueryKeyMap[S][O];';
+ }
+
+ private *generateMatchQueryKeyFunction(): Iterable {
+ yield '/**';
+ yield ' * Build type-safe query keys for React Query cache operations';
+ yield ' * ';
+ yield ' * @example';
+ yield ' * // Match all queries for a service';
+ yield ' * matchQueryKey("widget")';
+ yield ' * // Returns: ["widget"]';
+ yield ' * ';
+ yield ' * @example';
+ yield ' * // Match all queries for a specific operation';
+ yield ' * matchQueryKey("widget", "getWidgets")';
+ yield ' * // Returns: ["widget", "getWidgets"]';
+ yield ' * ';
+ yield ' * @example';
+ yield ' * // Match specific query with parameters';
+ yield ' * matchQueryKey("widget", "getWidgets", { status: "active" })';
+ yield ' * // Returns: ["widget", "getWidgets", { status: "active" }]';
+ yield ' */';
+
+ // Function overloads
+ yield 'export function matchQueryKey(';
+ yield ' service: S';
+ yield '): readonly [S];';
+ yield '';
+
+ yield 'export function matchQueryKey<';
+ yield ' S extends ServiceKeys,';
+ yield ' O extends OperationKeys';
+ yield '>(';
+ yield ' service: S,';
+ yield ' operation: O';
+ yield '): readonly [S, O];';
+ yield '';
+
+ yield 'export function matchQueryKey<';
+ yield ' S extends ServiceKeys,';
+ yield ' O extends OperationKeys';
+ yield '>(';
+ yield ' service: S,';
+ yield ' operation: O,';
+ yield ' params: OperationParams extends undefined ? undefined : OperationParams';
+ yield '): readonly [S, O, OperationParams extends undefined ? {} : OperationParams];';
+ yield '';
+
+ // Implementation
+ yield 'export function matchQueryKey<';
+ yield ' S extends ServiceKeys,';
+ yield ' O extends OperationKeys';
+ yield '>(';
+ yield ' service: S,';
+ yield ' operation?: O,';
+ yield ' params?: OperationParams';
+ yield ') {';
+ yield ' if (arguments.length === 3 && operation !== undefined) {';
+ yield ' // When called with 3 arguments, always include params (use {} if undefined)';
+ yield ' const finalParams = params === undefined ? {} : params;';
+ yield ' return [service, operation, finalParams] as const;';
+ yield ' }';
+ yield ' if (operation !== undefined) {';
+ yield ' return [service, operation] as const;';
+ yield ' }';
+ yield ' return [service] as const;';
+ yield '}';
+ }
+
+ private buildMethodParamsType(method: Method): string {
+ const paramsType = from(buildParamsType(method));
+
+ if (!paramsType) {
+ return 'undefined';
+ }
+
+ // Register the type with the import builder
+ this.types.type(paramsType);
+
+ const hasRequired = method.parameters.some((p) => isRequired(p));
+ return hasRequired ? paramsType : `${paramsType} | undefined`;
+ }
+}
diff --git a/src/snapshot/v1/hooks/auth-permutations.ts b/src/snapshot/v1/hooks/auth-permutations.ts
new file mode 100644
index 0000000..00d4d29
--- /dev/null
+++ b/src/snapshot/v1/hooks/auth-permutations.ts
@@ -0,0 +1,130 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import {
+ mutationOptions,
+ queryOptions,
+ type UndefinedInitialDataOptions,
+ useMutation,
+ type UseMutationOptions,
+ useQuery,
+ useQueryClient,
+ useSuspenseQuery,
+} from '@tanstack/react-query';
+import type { ComboAuthSchemesParams } from '../types';
+import {
+ getAuthPermutationService,
+ useAuthPermutationService,
+} from './context';
+import { CompositeError } from './runtime';
+
+/**
+ * @deprecated
+ */
+export function useAllAuthSchemes(
+ options?: Omit<
+ UndefinedInitialDataOptions,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useAllAuthSchemesQueryOptions();
+ return useQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useSuspenseAllAuthSchemes(
+ options?: Omit<
+ UndefinedInitialDataOptions,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useAllAuthSchemesQueryOptions();
+ return useSuspenseQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useComboAuthSchemes(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const authPermutationService = useAuthPermutationService();
+ return useMutation({
+ mutationFn: async () => {
+ const res = await authPermutationService.comboAuthSchemes();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['authPermutation'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+const useAllAuthSchemesQueryOptions = () => {
+ const authPermutationService = useAuthPermutationService();
+ return queryOptions({
+ queryKey: ['authPermutation', 'allAuthSchemes', {}],
+ queryFn: async () => {
+ const res = await authPermutationService.allAuthSchemes();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+
+export const allAuthSchemesQueryOptions = () => {
+ const authPermutationService = getAuthPermutationService();
+ return queryOptions({
+ queryKey: ['authPermutation', 'allAuthSchemes', {}],
+ queryFn: async () => {
+ const res = await authPermutationService.allAuthSchemes();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+export const comboAuthSchemesMutationOptions = () => {
+ const authPermutationService = getAuthPermutationService();
+ return mutationOptions({
+ mutationFn: async () => {
+ const res = await authPermutationService.comboAuthSchemes();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
diff --git a/src/snapshot/v1/hooks/context.tsx b/src/snapshot/v1/hooks/context.tsx
new file mode 100644
index 0000000..3251d35
--- /dev/null
+++ b/src/snapshot/v1/hooks/context.tsx
@@ -0,0 +1,172 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import {
+ createContext,
+ type FC,
+ type PropsWithChildren,
+ useContext,
+ useMemo,
+} from 'react';
+import {
+ type BasketryExampleOptions,
+ type FetchLike,
+ HttpAuthPermutationService,
+ HttpExhaustiveService,
+ HttpGizmoService,
+ HttpWidgetService,
+} from '../http-client';
+import type {
+ AuthPermutationService,
+ ExhaustiveService,
+ GizmoService,
+ WidgetService,
+} from '../types';
+
+export interface BasketryExampleContextProps {
+ fetch: FetchLike;
+ options: BasketryExampleOptions;
+}
+const BasketryExampleContext = createContext<
+ BasketryExampleContextProps | undefined
+>(undefined);
+
+let currentContext: BasketryExampleContextProps | undefined;
+
+export const BasketryExampleProvider: FC<
+ PropsWithChildren
+> = ({ children, fetch, options }) => {
+ const value = useMemo(
+ () => ({ fetch, options }),
+ [
+ fetch,
+ options.mapUnhandledException,
+ options.mapValidationError,
+ options.root,
+ ],
+ );
+ currentContext = value;
+ return (
+
+ {children}
+
+ );
+};
+
+export const getGizmoService = () => {
+ if (!currentContext) {
+ throw new Error(
+ 'getGizmoService called outside of BasketryExampleProvider',
+ );
+ }
+ const gizmoService: GizmoService = new HttpGizmoService(
+ currentContext.fetch,
+ currentContext.options,
+ );
+ return gizmoService;
+};
+
+export const useGizmoService = () => {
+ const context = useContext(BasketryExampleContext);
+ if (!context) {
+ throw new Error(
+ 'useGizmoService must be used within a BasketryExampleProvider',
+ );
+ }
+ const gizmoService: GizmoService = new HttpGizmoService(
+ context.fetch,
+ context.options,
+ );
+ return gizmoService;
+};
+
+export const getWidgetService = () => {
+ if (!currentContext) {
+ throw new Error(
+ 'getWidgetService called outside of BasketryExampleProvider',
+ );
+ }
+ const widgetService: WidgetService = new HttpWidgetService(
+ currentContext.fetch,
+ currentContext.options,
+ );
+ return widgetService;
+};
+
+export const useWidgetService = () => {
+ const context = useContext(BasketryExampleContext);
+ if (!context) {
+ throw new Error(
+ 'useWidgetService must be used within a BasketryExampleProvider',
+ );
+ }
+ const widgetService: WidgetService = new HttpWidgetService(
+ context.fetch,
+ context.options,
+ );
+ return widgetService;
+};
+
+export const getExhaustiveService = () => {
+ if (!currentContext) {
+ throw new Error(
+ 'getExhaustiveService called outside of BasketryExampleProvider',
+ );
+ }
+ const exhaustiveService: ExhaustiveService = new HttpExhaustiveService(
+ currentContext.fetch,
+ currentContext.options,
+ );
+ return exhaustiveService;
+};
+
+export const useExhaustiveService = () => {
+ const context = useContext(BasketryExampleContext);
+ if (!context) {
+ throw new Error(
+ 'useExhaustiveService must be used within a BasketryExampleProvider',
+ );
+ }
+ const exhaustiveService: ExhaustiveService = new HttpExhaustiveService(
+ context.fetch,
+ context.options,
+ );
+ return exhaustiveService;
+};
+
+export const getAuthPermutationService = () => {
+ if (!currentContext) {
+ throw new Error(
+ 'getAuthPermutationService called outside of BasketryExampleProvider',
+ );
+ }
+ const authPermutationService: AuthPermutationService =
+ new HttpAuthPermutationService(
+ currentContext.fetch,
+ currentContext.options,
+ );
+ return authPermutationService;
+};
+
+export const useAuthPermutationService = () => {
+ const context = useContext(BasketryExampleContext);
+ if (!context) {
+ throw new Error(
+ 'useAuthPermutationService must be used within a BasketryExampleProvider',
+ );
+ }
+ const authPermutationService: AuthPermutationService =
+ new HttpAuthPermutationService(context.fetch, context.options);
+ return authPermutationService;
+};
diff --git a/src/snapshot/v1/hooks/exhaustives.ts b/src/snapshot/v1/hooks/exhaustives.ts
new file mode 100644
index 0000000..bfd9a78
--- /dev/null
+++ b/src/snapshot/v1/hooks/exhaustives.ts
@@ -0,0 +1,170 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import {
+ queryOptions,
+ type UndefinedInitialDataOptions,
+ useQuery,
+ useSuspenseQuery,
+} from '@tanstack/react-query';
+import type { ExhaustiveFormatsParams, ExhaustiveParamsParams } from '../types';
+import { getExhaustiveService, useExhaustiveService } from './context';
+import { CompositeError } from './runtime';
+
+/**
+ * @deprecated
+ */
+export function useExhaustiveFormats(
+ params?: ExhaustiveFormatsParams,
+ options?: Omit<
+ UndefinedInitialDataOptions<
+ void,
+ Error,
+ void | undefined,
+ (string | Record)[]
+ >,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useExhaustiveFormatsQueryOptions(params);
+ return useQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useSuspenseExhaustiveFormats(
+ params?: ExhaustiveFormatsParams,
+ options?: Omit<
+ UndefinedInitialDataOptions<
+ void,
+ Error,
+ void | undefined,
+ (string | Record)[]
+ >,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useExhaustiveFormatsQueryOptions(params);
+ return useSuspenseQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useExhaustiveParams(
+ params: ExhaustiveParamsParams,
+ options?: Omit<
+ UndefinedInitialDataOptions<
+ void,
+ Error,
+ void | undefined,
+ (string | Record)[]
+ >,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useExhaustiveParamsQueryOptions(params);
+ return useQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useSuspenseExhaustiveParams(
+ params: ExhaustiveParamsParams,
+ options?: Omit<
+ UndefinedInitialDataOptions<
+ void,
+ Error,
+ void | undefined,
+ (string | Record)[]
+ >,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useExhaustiveParamsQueryOptions(params);
+ return useSuspenseQuery({ ...defaultOptions, ...options });
+}
+
+const useExhaustiveFormatsQueryOptions = (params?: ExhaustiveFormatsParams) => {
+ const exhaustiveService = useExhaustiveService();
+ return queryOptions({
+ queryKey: ['exhaustive', 'exhaustiveFormats', params || {}],
+ queryFn: async () => {
+ const res = await exhaustiveService.exhaustiveFormats(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+
+export const exhaustiveFormatsQueryOptions = (
+ params?: ExhaustiveFormatsParams,
+) => {
+ const exhaustiveService = getExhaustiveService();
+ return queryOptions({
+ queryKey: ['exhaustive', 'exhaustiveFormats', params || {}],
+ queryFn: async () => {
+ const res = await exhaustiveService.exhaustiveFormats(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+const useExhaustiveParamsQueryOptions = (params: ExhaustiveParamsParams) => {
+ const exhaustiveService = useExhaustiveService();
+ return queryOptions({
+ queryKey: ['exhaustive', 'exhaustiveParams', params || {}],
+ queryFn: async () => {
+ const res = await exhaustiveService.exhaustiveParams(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+
+export const exhaustiveParamsQueryOptions = (
+ params: ExhaustiveParamsParams,
+) => {
+ const exhaustiveService = getExhaustiveService();
+ return queryOptions({
+ queryKey: ['exhaustive', 'exhaustiveParams', params || {}],
+ queryFn: async () => {
+ const res = await exhaustiveService.exhaustiveParams(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
diff --git a/src/snapshot/v1/hooks/gizmos.ts b/src/snapshot/v1/hooks/gizmos.ts
new file mode 100644
index 0000000..b556fb0
--- /dev/null
+++ b/src/snapshot/v1/hooks/gizmos.ts
@@ -0,0 +1,239 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import {
+ mutationOptions,
+ queryOptions,
+ type UndefinedInitialDataOptions,
+ useMutation,
+ type UseMutationOptions,
+ useQuery,
+ useQueryClient,
+ useSuspenseQuery,
+} from '@tanstack/react-query';
+import type {
+ CreateGizmoParams,
+ GetGizmosParams,
+ Gizmo,
+ GizmosResponse,
+ UpdateGizmoParams,
+ UploadGizmoParams,
+} from '../types';
+import { getGizmoService, useGizmoService } from './context';
+import { CompositeError } from './runtime';
+
+/**
+ * Has a summary in addition to a description
+ * Has a description in addition to a summary
+ * @deprecated
+ */
+export function useCreateGizmo(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const gizmoService = useGizmoService();
+ return useMutation({
+ mutationFn: async (params?: CreateGizmoParams) => {
+ const res = await gizmoService.createGizmo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['gizmo'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+/**
+ * Only has a summary
+ * @deprecated
+ */
+export function useGizmos(
+ params?: GetGizmosParams,
+ options?: Omit<
+ UndefinedInitialDataOptions<
+ GizmosResponse,
+ Error,
+ Gizmo | undefined,
+ (string | Record)[]
+ >,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useGetGizmosQueryOptions(params);
+ return useQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * Only has a summary
+ * @deprecated
+ */
+export function useSuspenseGizmos(
+ params?: GetGizmosParams,
+ options?: Omit<
+ UndefinedInitialDataOptions<
+ GizmosResponse,
+ Error,
+ Gizmo | undefined,
+ (string | Record)[]
+ >,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useGetGizmosQueryOptions(params);
+ return useSuspenseQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useUpdateGizmo(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const gizmoService = useGizmoService();
+ return useMutation({
+ mutationFn: async (params?: UpdateGizmoParams) => {
+ const res = await gizmoService.updateGizmo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['gizmo'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+/**
+ * @deprecated
+ */
+export function useUploadGizmo(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const gizmoService = useGizmoService();
+ return useMutation({
+ mutationFn: async (params: UploadGizmoParams) => {
+ const res = await gizmoService.uploadGizmo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['gizmo'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+const useGetGizmosQueryOptions = (params?: GetGizmosParams) => {
+ const gizmoService = useGizmoService();
+ return queryOptions({
+ queryKey: ['gizmo', 'getGizmos', params || {}],
+ queryFn: async () => {
+ const res = await gizmoService.getGizmos(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+
+/**
+ * Only has a summary
+ * @deprecated
+ */
+export const getGizmosQueryOptions = (params?: GetGizmosParams) => {
+ const gizmoService = getGizmoService();
+ return queryOptions({
+ queryKey: ['gizmo', 'getGizmos', params || {}],
+ queryFn: async () => {
+ const res = await gizmoService.getGizmos(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ select: (data) => data.data,
+ });
+};
+
+/**
+ * Has a summary in addition to a description
+ * Has a description in addition to a summary
+ */
+export const createGizmoMutationOptions = () => {
+ const gizmoService = getGizmoService();
+ return mutationOptions({
+ mutationFn: async (params: CreateGizmoParams) => {
+ const res = await gizmoService.createGizmo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
+export const updateGizmoMutationOptions = () => {
+ const gizmoService = getGizmoService();
+ return mutationOptions({
+ mutationFn: async (params: UpdateGizmoParams) => {
+ const res = await gizmoService.updateGizmo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
+export const uploadGizmoMutationOptions = () => {
+ const gizmoService = getGizmoService();
+ return mutationOptions({
+ mutationFn: async (params: UploadGizmoParams) => {
+ const res = await gizmoService.uploadGizmo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
diff --git a/src/snapshot/v1/hooks/query-key-builder.ts b/src/snapshot/v1/hooks/query-key-builder.ts
new file mode 100644
index 0000000..9ab4250
--- /dev/null
+++ b/src/snapshot/v1/hooks/query-key-builder.ts
@@ -0,0 +1,129 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import type {
+ AllAuthSchemesParams,
+ ComboAuthSchemesParams,
+ CreateGizmoParams,
+ CreateWidgetParams,
+ DeleteWidgetFooParams,
+ ExhaustiveFormatsParams,
+ ExhaustiveParamsParams,
+ GetGizmosParams,
+ GetWidgetFooParams,
+ GetWidgetsParams,
+ PutWidgetParams,
+ UpdateGizmoParams,
+ UploadGizmoParams,
+} from '../types';
+
+/**
+ * Type mapping for all available query keys in the service
+ */
+export interface QueryKeyMap {
+ gizmo: {
+ getGizmos: GetGizmosParams | undefined;
+ createGizmo: CreateGizmoParams | undefined;
+ updateGizmo: UpdateGizmoParams | undefined;
+ uploadGizmo: UploadGizmoParams;
+ };
+ widget: {
+ getWidgets: GetWidgetsParams | undefined;
+ createWidget: CreateWidgetParams | undefined;
+ putWidget: PutWidgetParams | undefined;
+ getWidgetFoo: GetWidgetFooParams;
+ deleteWidgetFoo: DeleteWidgetFooParams;
+ };
+ exhaustive: {
+ exhaustiveFormats: ExhaustiveFormatsParams | undefined;
+ exhaustiveParams: ExhaustiveParamsParams;
+ };
+ authPermutation: {
+ allAuthSchemes: AllAuthSchemesParams | undefined;
+ comboAuthSchemes: ComboAuthSchemesParams | undefined;
+ };
+}
+
+/**
+ * Extract all service names from QueryKeyMap
+ */
+export type ServiceKeys = keyof QueryKeyMap;
+
+/**
+ * Extract operation names for a given service
+ */
+export type OperationKeys = keyof QueryKeyMap[S];
+
+/**
+ * Extract parameter type for a given service and operation
+ */
+export type OperationParams<
+ S extends ServiceKeys,
+ O extends OperationKeys,
+> = QueryKeyMap[S][O];
+
+/**
+ * Build type-safe query keys for React Query cache operations
+ *
+ * @example
+ * // Match all queries for a service
+ * matchQueryKey("widget")
+ * // Returns: ["widget"]
+ *
+ * @example
+ * // Match all queries for a specific operation
+ * matchQueryKey("widget", "getWidgets")
+ * // Returns: ["widget", "getWidgets"]
+ *
+ * @example
+ * // Match specific query with parameters
+ * matchQueryKey("widget", "getWidgets", { status: "active" })
+ * // Returns: ["widget", "getWidgets", { status: "active" }]
+ */
+export function matchQueryKey(service: S): readonly [S];
+
+export function matchQueryKey<
+ S extends ServiceKeys,
+ O extends OperationKeys,
+>(service: S, operation: O): readonly [S, O];
+
+export function matchQueryKey<
+ S extends ServiceKeys,
+ O extends OperationKeys,
+>(
+ service: S,
+ operation: O,
+ params: OperationParams extends undefined
+ ? undefined
+ : OperationParams,
+): readonly [
+ S,
+ O,
+ OperationParams extends undefined ? {} : OperationParams,
+];
+
+export function matchQueryKey<
+ S extends ServiceKeys,
+ O extends OperationKeys,
+>(service: S, operation?: O, params?: OperationParams) {
+ if (arguments.length === 3 && operation !== undefined) {
+ // When called with 3 arguments, always include params (use {} if undefined)
+ const finalParams = params === undefined ? {} : params;
+ return [service, operation, finalParams] as const;
+ }
+ if (operation !== undefined) {
+ return [service, operation] as const;
+ }
+ return [service] as const;
+}
diff --git a/src/snapshot/v1/hooks/runtime.ts b/src/snapshot/v1/hooks/runtime.ts
new file mode 100644
index 0000000..7bc72bc
--- /dev/null
+++ b/src/snapshot/v1/hooks/runtime.ts
@@ -0,0 +1,109 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import type {
+ GetNextPageParamFunction,
+ GetPreviousPageParamFunction,
+} from '@tanstack/react-query';
+
+export type PageParam = { pageParam?: string };
+
+export class CompositeError extends Error {
+ constructor(readonly errors: { title: string }[]) {
+ super(errors.map((e) => e.title).join(', '));
+ if (Error.captureStackTrace) Error.captureStackTrace(this, CompositeError);
+ }
+}
+
+export type RelayParams = {
+ first?: number;
+ after?: string;
+ last?: number;
+ before?: string;
+};
+
+export type Response = {
+ pageInfo?: {
+ startCursor?: string;
+ hasPreviousPage: boolean;
+ hasNextPage: boolean;
+ endCursor?: string;
+ };
+};
+
+export const getNextPageParam: GetNextPageParamFunction<
+ string | undefined,
+ Response
+> = (lastPage) => {
+ return lastPage.pageInfo?.hasNextPage
+ ? `after:${lastPage.pageInfo.endCursor}`
+ : undefined;
+};
+
+export const getPreviousPageParam: GetPreviousPageParamFunction<
+ string | undefined,
+ Response
+> = (lastPage) => {
+ return lastPage.pageInfo?.hasPreviousPage
+ ? `before:${lastPage.pageInfo.startCursor}`
+ : undefined;
+};
+
+export function applyPageParam(
+ params: T,
+ pageParam: string | undefined,
+): T {
+ const { first, after, last, before, ...rest } = params;
+ const syntheticParams: T = rest as T;
+
+ if (pageParam) {
+ const [key, value] = pageParam.split(':');
+
+ if (key === 'after') {
+ syntheticParams.first = first ?? last;
+ syntheticParams.after = value;
+ } else if (key === 'before') {
+ syntheticParams.last = last ?? first;
+ syntheticParams.before = value;
+ }
+ } else {
+ if (first) syntheticParams.first = first;
+ if (after) syntheticParams.after = after;
+ if (last) syntheticParams.last = last;
+ if (before) syntheticParams.before = before;
+ }
+
+ return syntheticParams;
+}
+
+export function getInitialPageParam(params: {
+ after?: string;
+ before?: string;
+}): string | undefined {
+ if (params.after) return `after:${params.after}`;
+ if (params.before) return `before:${params.before}`;
+ return;
+}
+
+export function compact(
+ params: Record,
+): Record | undefined {
+ const result: Record = Object.fromEntries(
+ Object.entries(params).filter(
+ ([, value]) => value !== null && value !== undefined,
+ ),
+ ) as any;
+
+ return Object.keys(result).length ? result : undefined;
+}
diff --git a/src/snapshot/v1/hooks/widgets.ts b/src/snapshot/v1/hooks/widgets.ts
new file mode 100644
index 0000000..b5a116a
--- /dev/null
+++ b/src/snapshot/v1/hooks/widgets.ts
@@ -0,0 +1,270 @@
+/**
+ * This code was generated by @basketry/react-query@{{version}}
+ *
+ * Changes to this file may cause incorrect behavior and will be lost if
+ * the code is regenerated.
+ *
+ * To make changes to the contents of this file:
+ * 1. Edit source/path.ext
+ * 2. Run the Basketry CLI
+ *
+ * About Basketry: https://github.com/basketry/basketry/wiki
+ * About @basketry/react-query: https://github.com/basketry/react-query#readme
+ */
+
+import {
+ mutationOptions,
+ queryOptions,
+ type UndefinedInitialDataOptions,
+ useMutation,
+ type UseMutationOptions,
+ useQuery,
+ useQueryClient,
+ useSuspenseQuery,
+} from '@tanstack/react-query';
+import type {
+ CreateWidgetParams,
+ DeleteWidgetFooParams,
+ GetWidgetFooParams,
+ PutWidgetParams,
+ Widget,
+} from '../types';
+import { getWidgetService, useWidgetService } from './context';
+import { CompositeError } from './runtime';
+
+/**
+ * @deprecated
+ */
+export function useCreateWidget(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const widgetService = useWidgetService();
+ return useMutation({
+ mutationFn: async (params?: CreateWidgetParams) => {
+ const res = await widgetService.createWidget(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['widget'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+/**
+ * @deprecated
+ */
+export function useDeleteWidgetFoo(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const widgetService = useWidgetService();
+ return useMutation({
+ mutationFn: async (params: DeleteWidgetFooParams) => {
+ const res = await widgetService.deleteWidgetFoo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['widget'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+/**
+ * @deprecated
+ */
+export function usePutWidget(
+ options?: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+ >,
+) {
+ const queryClient = useQueryClient();
+ const widgetService = useWidgetService();
+ return useMutation({
+ mutationFn: async () => {
+ const res = await widgetService.putWidget();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ queryClient.invalidateQueries({ queryKey: ['widget'] });
+ return res.data;
+ },
+ ...options,
+ });
+}
+
+/**
+ * @deprecated
+ */
+export function useWidgetFoo(
+ params: GetWidgetFooParams,
+ options?: Omit<
+ UndefinedInitialDataOptions,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useGetWidgetFooQueryOptions(params);
+ return useQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useSuspenseWidgetFoo(
+ params: GetWidgetFooParams,
+ options?: Omit<
+ UndefinedInitialDataOptions,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useGetWidgetFooQueryOptions(params);
+ return useSuspenseQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useWidgets(
+ options?: Omit<
+ UndefinedInitialDataOptions,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useGetWidgetsQueryOptions();
+ return useQuery({ ...defaultOptions, ...options });
+}
+
+/**
+ * @deprecated
+ */
+export function useSuspenseWidgets(
+ options?: Omit<
+ UndefinedInitialDataOptions,
+ 'queryKey' | 'queryFn' | 'select'
+ >,
+) {
+ const defaultOptions = useGetWidgetsQueryOptions();
+ return useSuspenseQuery({ ...defaultOptions, ...options });
+}
+
+const useGetWidgetsQueryOptions = () => {
+ const widgetService = useWidgetService();
+ return queryOptions({
+ queryKey: ['widget', 'getWidgets', {}],
+ queryFn: async () => {
+ const res = await widgetService.getWidgets();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ });
+};
+
+export const getWidgetsQueryOptions = () => {
+ const widgetService = getWidgetService();
+ return queryOptions({
+ queryKey: ['widget', 'getWidgets', {}],
+ queryFn: async () => {
+ const res = await widgetService.getWidgets();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ });
+};
+export const createWidgetMutationOptions = () => {
+ const widgetService = getWidgetService();
+ return mutationOptions({
+ mutationFn: async (params: CreateWidgetParams) => {
+ const res = await widgetService.createWidget(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
+export const putWidgetMutationOptions = () => {
+ const widgetService = getWidgetService();
+ return mutationOptions({
+ mutationFn: async () => {
+ const res = await widgetService.putWidget();
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
+const useGetWidgetFooQueryOptions = (params: GetWidgetFooParams) => {
+ const widgetService = useWidgetService();
+ return queryOptions({
+ queryKey: ['widget', 'getWidgetFoo', params || {}],
+ queryFn: async () => {
+ const res = await widgetService.getWidgetFoo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ });
+};
+
+export const getWidgetFooQueryOptions = (params: GetWidgetFooParams) => {
+ const widgetService = getWidgetService();
+ return queryOptions({
+ queryKey: ['widget', 'getWidgetFoo', params || {}],
+ queryFn: async () => {
+ const res = await widgetService.getWidgetFoo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res;
+ },
+ });
+};
+export const deleteWidgetFooMutationOptions = () => {
+ const widgetService = getWidgetService();
+ return mutationOptions({
+ mutationFn: async (params: DeleteWidgetFooParams) => {
+ const res = await widgetService.deleteWidgetFoo(params);
+ if (res.errors.length) {
+ throw new CompositeError(res.errors);
+ } else if (!res.data) {
+ throw new Error('Unexpected data error: Failed to get example');
+ }
+ return res.data;
+ },
+ });
+};
diff --git a/tsconfig.json b/tsconfig.json
index d8f8bc7..b6462f8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,5 +10,5 @@
"strictNullChecks": true
},
"include": ["src"],
- "exclude": ["**/*.test?.*"]
+ "exclude": ["**/*.test?.*", "src/snapshot/**/*"]
}