From bb864fdb9035cd1275418e077dac0af68eb26452 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:12:14 -0700 Subject: [PATCH 01/21] feat: add NameFactory class for consistent naming - Provides centralized naming logic for all generated names - Includes methods for query options, mutation options, service names - Maintains backward compatibility with existing hook names --- src/name-factory.ts | 64 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/src/name-factory.ts b/src/name-factory.ts index 465525d..c8d3222 100644 --- a/src/name-factory.ts +++ b/src/name-factory.ts @@ -1,6 +1,64 @@ -import { Method } from 'basketry'; -import { camel } from 'case'; +import { Interface, Method, Service, getHttpMethodByName } from 'basketry'; +import { camel, pascal } from 'case'; +import { NamespacedReactQueryOptions } from './types'; + +export class NameFactory { + constructor( + private readonly service: Service, + private readonly options?: NamespacedReactQueryOptions, + ) {} + + buildContextName(): string { + return pascal(`${this.service.title.value}_context`); + } + + buildProviderName(): string { + return pascal(`${this.service.title.value}_provider`); + } + + buildQueryOptionsName(method: Method): string { + return camel(`${method.name.value}_query_options`); + } + + buildMutationOptionsName(method: Method): string { + return camel(`${method.name.value}_mutation_options`); + } + + buildInfiniteQueryOptionsName(method: Method): string { + return camel(`${method.name.value}_infinite_query_options`); + } + + buildServiceName(int: Interface): string { + return camel(`${int.name.value}_service`); + } + + buildServiceHookName(int: Interface): string { + return camel(`use_${this.buildServiceName(int)}`); + } + + buildServiceGetterName(int: Interface): string { + return camel(`get_${this.buildServiceName(int)}`); + } + + getHookName(method: Method, options?: { infinite?: boolean; suspense?: boolean }): string { + const name = method.name.value; + const httpMethod = getHttpMethodByName(this.service, name); + + if ( + httpMethod?.verb.value === 'get' && + name.toLocaleLowerCase().startsWith('get') + ) { + return camel( + `use_${options?.suspense ? 'suspense_' : ''}${ + options?.infinite ? 'infinite_' : '' + }${name.slice(3)}`, + ); + } + + return camel(`use_${name}`); + } +} export function getQueryOptionsName(method: Method): string { return camel(`use_${method.name.value}_query_options`); -} +} \ No newline at end of file From faba0c03397430533f74f2931f7f8e6eff3a16c2 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:15:24 -0700 Subject: [PATCH 02/21] refactor: extract legacy hook generation into separate methods - Move query hooks generation to generateLegacyQueryHooks - Move mutation hooks generation to generateLegacyMutationHook - Move infinite hooks generation to generateLegacyInfiniteHooks - Clearly separate v0.2.0 features from v0.1.0 legacy code - Prepare structure for adding new query options exports --- src/hook-file.ts | 517 ++++++++++++++++++++++++++++++----------------- 1 file changed, 331 insertions(+), 186 deletions(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index ad199a3..2198bfd 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -21,7 +21,7 @@ import { camel } from 'case'; import { NamespacedReactQueryOptions } from './types'; import { ModuleBuilder } from './module-builder'; import { ImportBuilder } from './import-builder'; -import { getQueryOptionsName } from './name-factory'; +import { getQueryOptionsName, NameFactory } from './name-factory'; export class HookFile extends ModuleBuilder { constructor( @@ -31,6 +31,7 @@ export class HookFile extends ModuleBuilder { ) { super(service, options); } + private readonly nameFactory = new NameFactory(this.service, this.options); private readonly tanstack = new ImportBuilder('@tanstack/react-query'); private readonly runtime = new ImportBuilder('./runtime'); private readonly context = new ImportBuilder('./context'); @@ -46,6 +47,46 @@ export class HookFile extends ModuleBuilder { ]; *body(): Iterable { + for (const method of [...this.int.methods].sort((a, b) => + this.getHookName(a).localeCompare(this.getHookName(b)), + )) { + const httpMethod = getHttpMethodByName(this.service, method.name.value); + const httpPath = this.getHttpPath(httpMethod); + const isGet = httpMethod?.verb.value === 'get' && !!httpPath; + + // === NEW QUERY OPTIONS EXPORTS (v0.2.0) === + yield* this.generateAllQueryOptions(method, httpMethod, httpPath); + + // === LEGACY HOOKS (v0.1.0) === + yield* this.generateAllLegacyHooks(method, httpMethod, httpPath); + + yield ''; + } + } + + 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); + } + + // TODO: Add mutation options and infinite query options exports + } + + private *generateAllLegacyHooks( + method: Method, + httpMethod: HttpMethod | undefined, + httpPath: HttpPath | undefined, + ): Iterable { + if (!httpPath) return; + const useMutation = () => this.tanstack.fn('useMutation'); const useQuery = () => this.tanstack.fn('useQuery'); const useQueryClient = () => this.tanstack.fn('useQueryClient'); @@ -69,207 +110,311 @@ export class HookFile extends ModuleBuilder { const serviceName = camel(`${this.int.name.value}_service`); const serviceHookName = camel(`use_${this.int.name.value}_service`); - for (const method of [...this.int.methods].sort((a, b) => - this.getHookName(a).localeCompare(this.getHookName(b)), - )) { - 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); - const q = method.parameters.every((param) => !isRequired(param)) - ? '?' - : ''; + const isGet = httpMethod?.verb.value === 'get'; - const paramsExpression = method.parameters.length - ? `params${q}: ${type(paramsType)}` - : ''; + 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 q = method.parameters.every((param) => !isRequired(param)) + ? '?' + : ''; - const isGet = httpMethod?.verb.value === 'get' && !!httpPath; + const paramsExpression = method.parameters.length + ? `params${q}: ${type(paramsType)}` + : ''; - if (isGet) { - yield* this.generateQueryOptions(method, httpPath); - } + if (isGet) { + // Generate legacy query hooks + yield* this.generateLegacyQueryHooks( + method, + httpPath, + { + name, + suspenseName, + paramsType, + paramsExpression, + q, + serviceName, + serviceHookName, + type, + useQuery, + useSuspenseQuery, + UndefinedInitialDataOptions, + } + ); + } else { + // Generate legacy mutation hook + yield* this.generateLegacyMutationHook( + method, + httpPath, + { + name, + paramsType, + paramsExpression, + serviceName, + serviceHookName, + type, + useMutation, + useQueryClient, + UseMutationOptions, + CompositeError, + } + ); + } - if (isGet) { - const queryOptionsName = getQueryOptionsName(method); - const paramsCallsite = method.parameters.length ? 'params' : ''; + if (isGet && this.isRelayPaginated(method)) { + // Generate legacy infinite hooks + yield* this.generateLegacyInfiniteHooks( + method, + httpPath, + { + infiniteName, + paramsExpression, + q, + serviceName, + serviceHookName, + applyPageParam, + useInfiniteQuery, + useSuspenseInfiniteQuery, + CompositeError, + getInitialPageParam, + getNextPageParam, + getPreviousPageParam, + PageParam, + } + ); + } + } - const returnType = getTypeByName( - this.service, - method.returnType?.typeName.value, - ); - const dataType = getTypeByName( - this.service, - returnType?.properties.find((p) => p.name.value === 'data')?.typeName - .value, - ); + private *generateLegacyQueryHooks( + method: Method, + httpPath: HttpPath, + context: any, + ): Iterable { + const { + name, + suspenseName, + paramsExpression, + type, + useQuery, + useSuspenseQuery, + UndefinedInitialDataOptions, + } = context; + + const queryOptionsName = getQueryOptionsName(method); + const paramsCallsite = method.parameters.length ? 'params' : ''; - const skipSelect = - returnType && - returnType.properties.some( - (prop) => - prop.name.value !== 'data' && prop.name.value !== 'errors', - ); - - const returnTypeName = returnType ? buildTypeName(returnType) : 'void'; - let dataTypeName: string; - if (skipSelect) { - dataTypeName = returnTypeName; - } else { - dataTypeName = dataType ? buildTypeName(dataType) : 'void'; - } + 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 queryParams = httpMethod?.parameters.filter((p) => - isCacheParam(p, true), - ); - const queryParamsType = queryParams.length - ? 'string | Record' - : 'string'; - - const optionsExpression = `options?: Omit<${UndefinedInitialDataOptions()}<${type( - returnTypeName, - )}, Error, ${type( - dataTypeName, - )} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`; - - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export function ${name}(${[ - paramsExpression, - optionsExpression, - ].filter(Boolean)}) {`; - yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; - yield ` return ${useQuery()}({...defaultOptions, ...options});`; - yield `}`; - yield ''; - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export function ${suspenseName}(${[ - paramsExpression, - optionsExpression, - ].filter(Boolean)}) {`; - yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; - yield ` return ${useSuspenseQuery()}({...defaultOptions, ...options});`; - yield `}`; - } else if (httpPath) { - 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 skipSelect = + returnType && + returnType.properties.some( + (prop) => + prop.name.value !== 'data' && prop.name.value !== 'errors', + ); - const typeName = dataType ? buildTypeName(dataType) : 'void'; + const returnTypeName = returnType ? buildTypeName(returnType) : 'void'; + let dataTypeName: string; + if (skipSelect) { + dataTypeName = returnTypeName; + } else { + dataTypeName = dataType ? buildTypeName(dataType) : 'void'; + } - const optionsExpression = `options?: Omit<${UseMutationOptions()}<${type( - typeName, - )}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`; + const httpMethod = getHttpMethodByName(this.service, method.name.value); + const queryParams = httpMethod?.parameters.filter((p) => + isCacheParam(p, true), + ); + const queryParamsType = queryParams?.length + ? 'string | Record' + : 'string'; - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export function ${name}(${optionsExpression}) {`; - yield ` const queryClient = ${useQueryClient()}();`; - yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}()`; - yield ` return ${useMutation()}({`; - 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'); }`; - - 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 - ); + const optionsExpression = `options?: Omit<${UndefinedInitialDataOptions()}<${type( + returnTypeName, + )}, Error, ${type( + dataTypeName, + )} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`; + + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export function ${name}(${[ + paramsExpression, + optionsExpression, + ].filter(Boolean).join(', ')}) {`; + yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; + yield ` return ${useQuery()}({...defaultOptions, ...options});`; + yield `}`; + yield ''; + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export function ${suspenseName}(${[ + paramsExpression, + optionsExpression, + ].filter(Boolean).join(', ')}) {`; + yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; + yield ` return ${useSuspenseQuery()}({...defaultOptions, ...options});`; + yield `}`; + } - for (const queryKey of Array.from(queryKeys)) { - yield ` queryClient.invalidateQueries({ queryKey: [${queryKey}] });`; - } - yield ` return res.data;`; - yield ` },`; - yield ` ...options,`; - yield ` });`; - yield `}`; - } + private *generateLegacyMutationHook( + method: Method, + httpPath: HttpPath, + context: any, + ): Iterable { + const { + name, + paramsType, + paramsExpression, + serviceName, + serviceHookName, + type, + useMutation, + useQueryClient, + UseMutationOptions, + CompositeError, + } = context; - if (isGet && this.isRelayPaginated(method)) { - const methodExpression = `${serviceName}.${camel(method.name.value)}`; - const paramsCallsite = method.parameters.length - ? `${applyPageParam()}(params${q ? '?? {}' : ''}, pageParam)` - : ''; + const paramsCallsite = method.parameters.length ? 'params' : ''; - const infiniteOptionsHook = camel( - `${this.getHookName(method, { infinite: true })}_query_options`, - ); + const returnType = getTypeByName( + this.service, + method.returnType?.typeName.value, + ); + const dataType = getTypeByName( + this.service, + returnType?.properties.find((p) => p.name.value === 'data')?.typeName + .value, + ); - yield `function ${infiniteOptionsHook}(${paramsExpression}) {`; - yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}();`; - yield ` return {`; - yield ` queryKey: ${this.buildQueryKey(httpPath, 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 `}`; - - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export const ${this.getHookName(method, { - suspense: false, - infinite: true, - })} = (${paramsExpression}) => {`; - yield ` const options = ${infiniteOptionsHook}(params);`; - yield ` return ${useInfiniteQuery()}(options);`; - yield `}`; - - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export const ${this.getHookName(method, { - suspense: true, - infinite: true, - })} = (${paramsExpression}) => {`; - yield ` const options = ${infiniteOptionsHook}(params);`; - yield ` return ${useSuspenseInfiniteQuery()}(options);`; - yield `}`; - } + const typeName = dataType ? buildTypeName(dataType) : 'void'; - yield ''; + const optionsExpression = `options?: Omit<${UseMutationOptions()}<${type( + typeName, + )}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`; + + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export function ${name}(${optionsExpression}) {`; + yield ` const queryClient = ${useQueryClient()}();`; + yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}()`; + yield ` return ${useMutation()}({`; + 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'); }`; + + const queryKeys = new Set(); + queryKeys.add(this.buildResourceKey(httpPath, method)); + queryKeys.add( + this.buildResourceKey(httpPath, method, { skipTerminalParams: true }), + ); + + for (const queryKey of Array.from(queryKeys)) { + yield ` queryClient.invalidateQueries({ queryKey: [${queryKey}] });`; } + yield ` return res.data;`; + yield ` },`; + yield ` ...options,`; + yield ` });`; + yield `}`; + } + + private *generateLegacyInfiniteHooks( + method: Method, + httpPath: HttpPath, + context: any, + ): Iterable { + const { + infiniteName, + paramsExpression, + q, + serviceName, + serviceHookName, + applyPageParam, + useInfiniteQuery, + useSuspenseInfiniteQuery, + CompositeError, + getInitialPageParam, + getNextPageParam, + getPreviousPageParam, + PageParam, + } = context; + + const methodExpression = `${serviceName}.${camel(method.name.value)}`; + const paramsCallsite = method.parameters.length + ? `${applyPageParam()}(params${q ? '?? {}' : ''}, pageParam)` + : ''; + + const infiniteOptionsHook = camel( + `${this.getHookName(method, { infinite: true })}_query_options`, + ); + + yield `function ${infiniteOptionsHook}(${paramsExpression}) {`; + yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}();`; + yield ` return {`; + yield ` queryKey: ${this.buildQueryKey(httpPath, 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 `}`; + + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export const ${this.getHookName(method, { + suspense: false, + infinite: true, + })} = (${paramsExpression}) => {`; + yield ` const options = ${infiniteOptionsHook}(params);`; + yield ` return ${useInfiniteQuery()}(options);`; + yield `}`; + + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export const ${this.getHookName(method, { + suspense: true, + infinite: true, + })} = (${paramsExpression}) => {`; + yield ` const options = ${infiniteOptionsHook}(params);`; + yield ` return ${useSuspenseInfiniteQuery()}(options);`; + yield `}`; } private *buildInfiniteSelectFn(method: Method): Iterable { From 328cff7baf54e672828d8094b05178682c6fe9b9 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:17:17 -0700 Subject: [PATCH 03/21] feat: add new query options exports - Export queryOptions functions for queries - Export mutationOptions functions for mutations - Export infiniteQueryOptions functions for paginated queries - Use service getter functions instead of hooks - Keep internal functions for backward compatibility --- src/hook-file.ts | 155 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 6 deletions(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index 2198bfd..594f9e1 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -75,9 +75,13 @@ export class HookFile extends ModuleBuilder { if (isGet) { yield* this.generateQueryOptions(method, httpPath); + + if (this.isRelayPaginated(method)) { + yield* this.generateInfiniteQueryOptions(method, httpPath); + } + } else { + yield* this.generateMutationOptions(method, httpPath); } - - // TODO: Add mutation options and infinite query options exports } private *generateAllLegacyHooks( @@ -439,6 +443,112 @@ export class HookFile extends ModuleBuilder { }),`; } + private *generateMutationOptions( + method: Method, + httpPath: HttpPath, + ): 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 = this.nameFactory.buildServiceGetterName(this.int); + const mutationOptionsName = this.nameFactory.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 = this.nameFactory.buildServiceGetterName(this.int); + const infiniteOptionsName = this.nameFactory.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(httpPath, 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, @@ -448,8 +558,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 = this.nameFactory.buildServiceGetterName(this.int); + + // Keep the internal function for backward compatibility + const internalName = getQueryOptionsName(method); + // New exported function name + const exportedName = this.nameFactory.buildQueryOptionsName(method); + const paramsType = from(buildParamsType(method)); const q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; const paramsExpression = method.parameters.length @@ -468,8 +583,36 @@ 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(this.nameFactory.buildServiceHookName(this.int))}()`; + yield ` return ${queryOptions()}({`; + yield ` queryKey: ${this.buildQueryKey(httpPath, 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, { includeRelayParams: true, From 3cccee9c85355cb5aecf2a083c9c5a9d6f8fc4c1 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:18:25 -0700 Subject: [PATCH 04/21] feat: add service getter functions to context - Add non-hook service getter functions for query options - Store context globally for access outside React components - Update naming to use NameFactory for consistency - Keep legacy hooks for backward compatibility --- src/context-file.ts | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/context-file.ts b/src/context-file.ts index 6fb1b8a..bc56043 100644 --- a/src/context-file.ts +++ b/src/context-file.ts @@ -1,8 +1,10 @@ import { camel, pascal } from 'case'; import { ModuleBuilder } from './module-builder'; import { ImportBuilder } from './import-builder'; +import { NameFactory } from './name-factory'; export class ContextFile extends ModuleBuilder { + private readonly nameFactory = new NameFactory(this.service, this.options); private readonly react = new ImportBuilder('react'); private readonly client = new ImportBuilder( this.options?.reactQuery?.clientModule ?? '../http-client', @@ -23,23 +25,47 @@ 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 NameFactory + const contextName = this.nameFactory.buildContextName(); + const contextPropsName = pascal(`${contextName}_props`); + const providerName = this.nameFactory.buildProviderName(); + + 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};`; 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 = this.nameFactory.buildServiceHookName(int); + const getterName = this.nameFactory.buildServiceGetterName(int); + const localName = this.nameFactory.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);`; From ebf7985eaeb0743f119effdb1548fe476a878679 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:20:03 -0700 Subject: [PATCH 05/21] feat: add deprecation messages to legacy hooks - Add JSDoc @deprecated tags to all legacy hooks - Include migration examples showing new query options pattern - Guide users to migrate to the new API --- src/hook-file.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/hook-file.ts b/src/hook-file.ts index 594f9e1..cfefb21 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -248,6 +248,8 @@ export class HookFile extends ModuleBuilder { dataTypeName, )} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`; + // Add deprecation comment for regular query hook + yield* this.buildDeprecationComment('query', method); yield* buildDescription( method.description, undefined, @@ -261,6 +263,9 @@ export class HookFile extends ModuleBuilder { yield ` return ${useQuery()}({...defaultOptions, ...options});`; yield `}`; yield ''; + + // Add deprecation comment for suspense query hook + yield* this.buildDeprecationComment('suspenseQuery', method); yield* buildDescription( method.description, undefined, @@ -311,6 +316,8 @@ export class HookFile extends ModuleBuilder { typeName, )}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`; + // Add deprecation comment for mutation hook + yield* this.buildDeprecationComment('mutation', method); yield* buildDescription( method.description, undefined, @@ -394,6 +401,9 @@ export class HookFile extends ModuleBuilder { yield ` };`; yield `}`; + // Add deprecation comment for infinite query hook + yield ''; + yield* this.buildDeprecationComment('infinite', method); yield* buildDescription( method.description, undefined, @@ -407,6 +417,9 @@ export class HookFile extends ModuleBuilder { yield ` return ${useInfiniteQuery()}(options);`; yield `}`; + // Add deprecation comment for suspense infinite query hook + yield ''; + yield* this.buildDeprecationComment('suspenseInfinite', method); yield* buildDescription( method.description, undefined, @@ -632,6 +645,55 @@ export class HookFile extends ModuleBuilder { yield `};`; } + private *buildDeprecationComment( + hookType: 'query' | 'suspenseQuery' | 'mutation' | 'infinite' | 'suspenseInfinite', + method: Method, + ): Iterable { + const methodName = method.name.value; + + yield '/**'; + yield ' * @deprecated This hook is deprecated and will be removed in a future version.'; + yield ' * Please use the new query options pattern instead:'; + yield ' *'; + yield ' * ```typescript'; + + switch (hookType) { + case 'query': + yield ` * import { useQuery } from '@tanstack/react-query';`; + yield ` * import { ${this.nameFactory.buildQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ' *'; + yield ` * const result = useQuery(${this.nameFactory.buildQueryOptionsName(method)}(params));`; + break; + case 'suspenseQuery': + yield ` * import { useSuspenseQuery } from '@tanstack/react-query';`; + yield ` * import { ${this.nameFactory.buildQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ' *'; + yield ` * const result = useSuspenseQuery(${this.nameFactory.buildQueryOptionsName(method)}(params));`; + break; + case 'mutation': + yield ` * import { useMutation } from '@tanstack/react-query';`; + yield ` * import { ${this.nameFactory.buildMutationOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ' *'; + yield ` * const mutation = useMutation(${this.nameFactory.buildMutationOptionsName(method)}());`; + break; + case 'infinite': + yield ` * import { useInfiniteQuery } from '@tanstack/react-query';`; + yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ' *'; + yield ` * const result = useInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName(method)}(params));`; + break; + case 'suspenseInfinite': + yield ` * import { useSuspenseInfiniteQuery } from '@tanstack/react-query';`; + yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ' *'; + yield ` * const result = useSuspenseInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName(method)}(params));`; + break; + } + + yield ' * ```'; + yield ' */'; + } + private getHookName( method: Method, options?: { infinite?: boolean; suspense?: boolean }, From 131c32f85828db0219025f9db9e44a8af47c6b73 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:21:39 -0700 Subject: [PATCH 06/21] feat: add query key builder for type-safe cache operations - Create QueryKeyBuilderFile for generating type-safe query keys - Add matchQueryKey function with proper TypeScript overloads - Include query-key-builder.ts in generated files - Enable type-safe cache invalidation patterns --- src/hook-generator.ts | 9 +++ src/query-key-builder.ts | 165 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 src/query-key-builder.ts diff --git a/src/hook-generator.ts b/src/hook-generator.ts index 189ba58..6d1de11 100644 --- a/src/hook-generator.ts +++ b/src/hook-generator.ts @@ -10,6 +10,7 @@ import { NamespacedReactQueryOptions } from './types'; import { HookFile } from './hook-file'; import { ContextFile } from './context-file'; import { RuntimeFile } from './runtime-file'; +import { QueryKeyBuilderFile } from './query-key-builder'; export const generateHooks: Generator = (service, options) => { return new HookGenerator(service, options).generate(); @@ -40,6 +41,14 @@ class HookGenerator { ), }); + files.push({ + path: buildFilePath(['hooks', 'query-key-builder.ts'], this.service, this.options), + contents: format( + from(new QueryKeyBuilderFile(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/query-key-builder.ts b/src/query-key-builder.ts new file mode 100644 index 0000000..79339fd --- /dev/null +++ b/src/query-key-builder.ts @@ -0,0 +1,165 @@ +import { Interface, isRequired, Method, Service } from 'basketry'; + +import { buildParamsType, buildTypeName } 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 QueryKeyBuilderFile 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`; + } +} \ No newline at end of file From 03ccb9695c288838dbe93c83b6b8cacbc18c45af Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 15:22:49 -0700 Subject: [PATCH 07/21] fix: apply prettier formatting --- CLAUDE.md | 59 ++++++++++++ src/context-file.ts | 12 +-- src/hook-file.ts | 189 +++++++++++++++++++++------------------ src/hook-generator.ts | 6 +- src/name-factory.ts | 7 +- src/query-key-builder.ts | 2 +- 6 files changed, 179 insertions(+), 96 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..db77660 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Build and Development + +- **Build**: `npm run build` - Compiles TypeScript to JavaScript in `lib/` directory +- **Clean**: `npm run clean` - Removes build artifacts and coverage reports +- **Start**: `npm start` - Runs the compiled JavaScript entry point + +### Testing + +- **Run all tests**: `npm test` - Runs Jest test suite with coverage +- **Run specific test**: `npm test ` - Example: `npm test hook-generator.test.ts` +- **Test configuration**: Uses ts-jest preset, coverage enabled by default + +### Code Quality + +- **Lint**: `npm run lint` - Runs ESLint and Prettier checks +- **Fix linting issues**: `npm run fix` - Auto-fixes ESLint and Prettier issues +- **Create snapshot**: `npm run create-snapshot` - Generates test snapshots + +## Architecture Overview + +This is a Basketry generator plugin that creates React Query hooks from service definitions. The architecture follows a modular pattern: + +### Core Components + +1. **Generator Entry Point** (`src/index.ts`, `src/hook-generator.ts`): + + - Main generator that produces React Query hook files + - Generates three types of files: runtime utilities, context providers, and service-specific hooks + +2. **File Builders**: + + - **HookFile** (`src/hook-file.ts`): Generates React Query hooks for each service interface + - **ContextFile** (`src/context-file.ts`): Creates React Context providers for dependency injection + - **RuntimeFile** (`src/runtime-file.ts`): Provides runtime utilities for pagination and error handling + +3. **Support Modules**: + - **ModuleBuilder** (`src/module-builder.ts`): Base class for file generation with import management + - **ImportBuilder** (`src/import-builder.ts`): Manages TypeScript imports and prevents duplicates + - **NameFactory** (`src/name-factory.ts`): Generates consistent naming for hooks and functions + +### Key Design Patterns + +- **Service-Interface Pattern**: Each service interface gets its own hook file with query and mutation hooks +- **Context-Based DI**: Uses React Context for injecting HTTP client and configuration +- **Type Safety**: Integrates with @basketry/typescript for type generation +- **Relay-Style Pagination**: Built-in support for cursor-based pagination patterns + +### Dependencies + +- **basketry**: Core framework for service definition parsing +- **@basketry/typescript**: TypeScript code generation utilities +- **@tanstack/react-query**: React Query library for data fetching +- **pluralize** & **case**: String manipulation for consistent naming diff --git a/src/context-file.ts b/src/context-file.ts index bc56043..bb76621 100644 --- a/src/context-file.ts +++ b/src/context-file.ts @@ -33,17 +33,17 @@ export class ContextFile extends ModuleBuilder { 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 ${providerName}: ${FC()}<${PropsWithChildren()}<${contextPropsName}>> = ({ children, fetch, options }) => {`; yield ` const value = ${useMemo()}(() => ({ fetch, options }), [fetch, options.mapUnhandledException, options.mapValidationError, options.root]);`; yield ` currentContext = value;`; yield ` return <${contextName}.Provider value={value}>{children};`; yield `};`; - + for (const int of this.service.interfaces) { const hookName = this.nameFactory.buildServiceHookName(int); const getterName = this.nameFactory.buildServiceGetterName(int); @@ -57,10 +57,12 @@ export class ContextFile extends ModuleBuilder { 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);`; + )} = 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} = () => {`; diff --git a/src/hook-file.ts b/src/hook-file.ts index cfefb21..16e8a22 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -75,7 +75,7 @@ export class HookFile extends ModuleBuilder { if (isGet) { yield* this.generateQueryOptions(method, httpPath); - + if (this.isRelayPaginated(method)) { yield* this.generateInfiniteQueryOptions(method, httpPath); } @@ -120,9 +120,7 @@ export class HookFile extends ModuleBuilder { const suspenseName = this.getHookName(method, { suspense: true }); const infiniteName = this.getHookName(method, { infinite: true }); const paramsType = from(buildParamsType(method)); - const q = method.parameters.every((param) => !isRequired(param)) - ? '?' - : ''; + const q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; const paramsExpression = method.parameters.length ? `params${q}: ${type(paramsType)}` @@ -130,64 +128,52 @@ export class HookFile extends ModuleBuilder { if (isGet) { // Generate legacy query hooks - yield* this.generateLegacyQueryHooks( - method, - httpPath, - { - name, - suspenseName, - paramsType, - paramsExpression, - q, - serviceName, - serviceHookName, - type, - useQuery, - useSuspenseQuery, - UndefinedInitialDataOptions, - } - ); + yield* this.generateLegacyQueryHooks(method, httpPath, { + name, + suspenseName, + paramsType, + paramsExpression, + q, + serviceName, + serviceHookName, + type, + useQuery, + useSuspenseQuery, + UndefinedInitialDataOptions, + }); } else { // Generate legacy mutation hook - yield* this.generateLegacyMutationHook( - method, - httpPath, - { - name, - paramsType, - paramsExpression, - serviceName, - serviceHookName, - type, - useMutation, - useQueryClient, - UseMutationOptions, - CompositeError, - } - ); + yield* this.generateLegacyMutationHook(method, httpPath, { + name, + paramsType, + paramsExpression, + serviceName, + serviceHookName, + type, + useMutation, + useQueryClient, + UseMutationOptions, + CompositeError, + }); } if (isGet && this.isRelayPaginated(method)) { // Generate legacy infinite hooks - yield* this.generateLegacyInfiniteHooks( - method, - httpPath, - { - infiniteName, - paramsExpression, - q, - serviceName, - serviceHookName, - applyPageParam, - useInfiniteQuery, - useSuspenseInfiniteQuery, - CompositeError, - getInitialPageParam, - getNextPageParam, - getPreviousPageParam, - PageParam, - } - ); + yield* this.generateLegacyInfiniteHooks(method, httpPath, { + infiniteName, + paramsExpression, + q, + serviceName, + serviceHookName, + applyPageParam, + useInfiniteQuery, + useSuspenseInfiniteQuery, + CompositeError, + getInitialPageParam, + getNextPageParam, + getPreviousPageParam, + PageParam, + }); } } @@ -222,8 +208,7 @@ export class HookFile extends ModuleBuilder { const skipSelect = returnType && returnType.properties.some( - (prop) => - prop.name.value !== 'data' && prop.name.value !== 'errors', + (prop) => prop.name.value !== 'data' && prop.name.value !== 'errors', ); const returnTypeName = returnType ? buildTypeName(returnType) : 'void'; @@ -255,15 +240,14 @@ export class HookFile extends ModuleBuilder { undefined, method.deprecated?.value, ); - yield `export function ${name}(${[ - paramsExpression, - optionsExpression, - ].filter(Boolean).join(', ')}) {`; + yield `export function ${name}(${[paramsExpression, optionsExpression] + .filter(Boolean) + .join(', ')}) {`; yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; yield ` return ${useQuery()}({...defaultOptions, ...options});`; yield `}`; yield ''; - + // Add deprecation comment for suspense query hook yield* this.buildDeprecationComment('suspenseQuery', method); yield* buildDescription( @@ -274,7 +258,9 @@ export class HookFile extends ModuleBuilder { yield `export function ${suspenseName}(${[ paramsExpression, optionsExpression, - ].filter(Boolean).join(', ')}) {`; + ] + .filter(Boolean) + .join(', ')}) {`; yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; yield ` return ${useSuspenseQuery()}({...defaultOptions, ...options});`; yield `}`; @@ -466,8 +452,9 @@ export class HookFile extends ModuleBuilder { const serviceName = camel(`${this.int.name.value}_service`); const serviceGetterName = this.nameFactory.buildServiceGetterName(this.int); - const mutationOptionsName = this.nameFactory.buildMutationOptionsName(method); - + const mutationOptionsName = + this.nameFactory.buildMutationOptionsName(method); + const paramsType = from(buildParamsType(method)); const paramsExpression = method.parameters.length ? `params: ${type(paramsType)}` @@ -521,14 +508,15 @@ export class HookFile extends ModuleBuilder { const serviceName = camel(`${this.int.name.value}_service`); const serviceGetterName = this.nameFactory.buildServiceGetterName(this.int); - const infiniteOptionsName = this.nameFactory.buildInfiniteQueryOptionsName(method); - + const infiniteOptionsName = + this.nameFactory.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)` @@ -572,12 +560,12 @@ export class HookFile extends ModuleBuilder { const serviceName = camel(`${this.int.name.value}_service`); const serviceGetterName = this.nameFactory.buildServiceGetterName(this.int); - + // Keep the internal function for backward compatibility const internalName = getQueryOptionsName(method); // New exported function name const exportedName = this.nameFactory.buildQueryOptionsName(method); - + const paramsType = from(buildParamsType(method)); const q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; const paramsExpression = method.parameters.length @@ -598,7 +586,9 @@ export class HookFile extends ModuleBuilder { // Internal function for backward compatibility with hooks yield `const ${internalName} = (${paramsExpression}) => {`; - yield ` const ${serviceName} = ${this.context.fn(this.nameFactory.buildServiceHookName(this.int))}()`; + yield ` const ${serviceName} = ${this.context.fn( + this.nameFactory.buildServiceHookName(this.int), + )}()`; yield ` return ${queryOptions()}({`; yield ` queryKey: ${this.buildQueryKey(httpPath, method, { includeRelayParams: true, @@ -616,7 +606,7 @@ export class HookFile extends ModuleBuilder { } yield ` });`; yield `};`; - + // New exported query options function (v0.2.0) yield ''; yield* buildDescription( @@ -646,50 +636,75 @@ export class HookFile extends ModuleBuilder { } private *buildDeprecationComment( - hookType: 'query' | 'suspenseQuery' | 'mutation' | 'infinite' | 'suspenseInfinite', + hookType: + | 'query' + | 'suspenseQuery' + | 'mutation' + | 'infinite' + | 'suspenseInfinite', method: Method, ): Iterable { const methodName = method.name.value; - + yield '/**'; yield ' * @deprecated This hook is deprecated and will be removed in a future version.'; yield ' * Please use the new query options pattern instead:'; yield ' *'; yield ' * ```typescript'; - + switch (hookType) { case 'query': yield ` * import { useQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ` * import { ${this.nameFactory.buildQueryOptionsName( + method, + )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useQuery(${this.nameFactory.buildQueryOptionsName(method)}(params));`; + yield ` * const result = useQuery(${this.nameFactory.buildQueryOptionsName( + method, + )}(params));`; break; case 'suspenseQuery': yield ` * import { useSuspenseQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ` * import { ${this.nameFactory.buildQueryOptionsName( + method, + )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useSuspenseQuery(${this.nameFactory.buildQueryOptionsName(method)}(params));`; + yield ` * const result = useSuspenseQuery(${this.nameFactory.buildQueryOptionsName( + method, + )}(params));`; break; case 'mutation': yield ` * import { useMutation } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildMutationOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ` * import { ${this.nameFactory.buildMutationOptionsName( + method, + )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const mutation = useMutation(${this.nameFactory.buildMutationOptionsName(method)}());`; + yield ` * const mutation = useMutation(${this.nameFactory.buildMutationOptionsName( + method, + )}());`; break; case 'infinite': yield ` * import { useInfiniteQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName( + method, + )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName(method)}(params));`; + yield ` * const result = useInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName( + method, + )}(params));`; break; case 'suspenseInfinite': yield ` * import { useSuspenseInfiniteQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName(method)} } from './hooks/${this.int.name.value}';`; + yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName( + method, + )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useSuspenseInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName(method)}(params));`; + yield ` * const result = useSuspenseInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName( + method, + )}(params));`; break; } - + yield ' * ```'; yield ' */'; } diff --git a/src/hook-generator.ts b/src/hook-generator.ts index 6d1de11..e74e81a 100644 --- a/src/hook-generator.ts +++ b/src/hook-generator.ts @@ -42,7 +42,11 @@ class HookGenerator { }); files.push({ - path: buildFilePath(['hooks', 'query-key-builder.ts'], this.service, this.options), + path: buildFilePath( + ['hooks', 'query-key-builder.ts'], + this.service, + this.options, + ), contents: format( from(new QueryKeyBuilderFile(this.service, this.options).build()), this.options, diff --git a/src/name-factory.ts b/src/name-factory.ts index c8d3222..05e2f29 100644 --- a/src/name-factory.ts +++ b/src/name-factory.ts @@ -40,7 +40,10 @@ export class NameFactory { return camel(`get_${this.buildServiceName(int)}`); } - getHookName(method: Method, options?: { infinite?: boolean; suspense?: boolean }): string { + getHookName( + method: Method, + options?: { infinite?: boolean; suspense?: boolean }, + ): string { const name = method.name.value; const httpMethod = getHttpMethodByName(this.service, name); @@ -61,4 +64,4 @@ export class NameFactory { export function getQueryOptionsName(method: Method): string { return camel(`use_${method.name.value}_query_options`); -} \ No newline at end of file +} diff --git a/src/query-key-builder.ts b/src/query-key-builder.ts index 79339fd..87582c8 100644 --- a/src/query-key-builder.ts +++ b/src/query-key-builder.ts @@ -162,4 +162,4 @@ export class QueryKeyBuilderFile extends ModuleBuilder { const hasRequired = method.parameters.some((p) => isRequired(p)); return hasRequired ? paramsType : `${paramsType} | undefined`; } -} \ No newline at end of file +} From 6bd1481b7e5285796de494da8092d7c57da05ddd Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 18:23:00 -0700 Subject: [PATCH 08/21] WIP: Replace NameFactory class with helper functions for v0.2.0 code - Added helper functions to name-factory.ts for building consistent names - Removed NameFactory class instantiation from HookFile - Updated all v0.2.0 code sections to use helper functions instead of class methods - Legacy v0.1.0 code remains unchanged - This improves code clarity by using named functions that describe their purpose --- src/hook-file.ts | 584 +++++++++------------ src/name-factory.ts | 21 + src/snapshot/v1/hooks/auth-permutations.ts | 154 ++++++ src/snapshot/v1/hooks/context.tsx | 172 ++++++ src/snapshot/v1/hooks/exhaustives.ts | 139 +++++ src/snapshot/v1/hooks/gizmos.ts | 290 ++++++++++ src/snapshot/v1/hooks/query-key-builder.ts | 129 +++++ src/snapshot/v1/hooks/runtime.ts | 109 ++++ src/snapshot/v1/hooks/widgets.ts | 328 ++++++++++++ 9 files changed, 1576 insertions(+), 350 deletions(-) create mode 100644 src/snapshot/v1/hooks/auth-permutations.ts create mode 100644 src/snapshot/v1/hooks/context.tsx create mode 100644 src/snapshot/v1/hooks/exhaustives.ts create mode 100644 src/snapshot/v1/hooks/gizmos.ts create mode 100644 src/snapshot/v1/hooks/query-key-builder.ts create mode 100644 src/snapshot/v1/hooks/runtime.ts create mode 100644 src/snapshot/v1/hooks/widgets.ts diff --git a/src/hook-file.ts b/src/hook-file.ts index 16e8a22..7f60d06 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -21,7 +21,14 @@ import { camel } from 'case'; import { NamespacedReactQueryOptions } from './types'; import { ModuleBuilder } from './module-builder'; import { ImportBuilder } from './import-builder'; -import { getQueryOptionsName, NameFactory } from './name-factory'; +import { + getQueryOptionsName, + buildServiceGetterName, + buildQueryOptionsName, + buildMutationOptionsName, + buildInfiniteQueryOptionsName, + buildServiceHookName, +} from './name-factory'; export class HookFile extends ModuleBuilder { constructor( @@ -31,7 +38,6 @@ export class HookFile extends ModuleBuilder { ) { super(service, options); } - private readonly nameFactory = new NameFactory(this.service, this.options); private readonly tanstack = new ImportBuilder('@tanstack/react-query'); private readonly runtime = new ImportBuilder('./runtime'); private readonly context = new ImportBuilder('./context'); @@ -47,50 +53,7 @@ export class HookFile extends ModuleBuilder { ]; *body(): Iterable { - for (const method of [...this.int.methods].sort((a, b) => - this.getHookName(a).localeCompare(this.getHookName(b)), - )) { - const httpMethod = getHttpMethodByName(this.service, method.name.value); - const httpPath = this.getHttpPath(httpMethod); - const isGet = httpMethod?.verb.value === 'get' && !!httpPath; - - // === NEW QUERY OPTIONS EXPORTS (v0.2.0) === - yield* this.generateAllQueryOptions(method, httpMethod, httpPath); - - // === LEGACY HOOKS (v0.1.0) === - yield* this.generateAllLegacyHooks(method, httpMethod, httpPath); - - yield ''; - } - } - - 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, httpPath); - } - } - - private *generateAllLegacyHooks( - method: Method, - httpMethod: HttpMethod | undefined, - httpPath: HttpPath | undefined, - ): Iterable { - if (!httpPath) return; - + // === LEGACY HOOKS (v0.1.0) === const useMutation = () => this.tanstack.fn('useMutation'); const useQuery = () => this.tanstack.fn('useQuery'); const useQueryClient = () => this.tanstack.fn('useQueryClient'); @@ -114,310 +77,236 @@ export class HookFile extends ModuleBuilder { const serviceName = camel(`${this.int.name.value}_service`); const serviceHookName = camel(`use_${this.int.name.value}_service`); - const isGet = httpMethod?.verb.value === 'get'; - - 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 q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; - - const paramsExpression = method.parameters.length - ? `params${q}: ${type(paramsType)}` - : ''; - - if (isGet) { - // Generate legacy query hooks - yield* this.generateLegacyQueryHooks(method, httpPath, { - name, - suspenseName, - paramsType, - paramsExpression, - q, - serviceName, - serviceHookName, - type, - useQuery, - useSuspenseQuery, - UndefinedInitialDataOptions, - }); - } else { - // Generate legacy mutation hook - yield* this.generateLegacyMutationHook(method, httpPath, { - name, - paramsType, - paramsExpression, - serviceName, - serviceHookName, - type, - useMutation, - useQueryClient, - UseMutationOptions, - CompositeError, - }); - } - - if (isGet && this.isRelayPaginated(method)) { - // Generate legacy infinite hooks - yield* this.generateLegacyInfiniteHooks(method, httpPath, { - infiniteName, - paramsExpression, - q, - serviceName, - serviceHookName, - applyPageParam, - useInfiniteQuery, - useSuspenseInfiniteQuery, - CompositeError, - getInitialPageParam, - getNextPageParam, - getPreviousPageParam, - PageParam, - }); - } - } - - private *generateLegacyQueryHooks( - method: Method, - httpPath: HttpPath, - context: any, - ): Iterable { - const { - name, - suspenseName, - paramsExpression, - type, - useQuery, - useSuspenseQuery, - UndefinedInitialDataOptions, - } = context; - - const queryOptionsName = getQueryOptionsName(method); - const paramsCallsite = method.parameters.length ? 'params' : ''; + for (const method of [...this.int.methods].sort((a, b) => + this.getHookName(a).localeCompare(this.getHookName(b)), + )) { + const name = this.getHookName(method); + const suspenseName = this.getHookName(method, { suspense: true }); + const paramsType = from(buildParamsType(method)); + const httpMethod = getHttpMethodByName(this.service, method.name.value); + const httpPath = this.getHttpPath(httpMethod); + const q = method.parameters.every((param) => !isRequired(param)) + ? '?' + : ''; - 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 paramsExpression = method.parameters.length + ? `params${q}: ${type(paramsType)}` + : ''; - const skipSelect = - returnType && - returnType.properties.some( - (prop) => prop.name.value !== 'data' && prop.name.value !== 'errors', - ); + const isGet = httpMethod?.verb.value === 'get' && !!httpPath; - const returnTypeName = returnType ? buildTypeName(returnType) : 'void'; - let dataTypeName: string; - if (skipSelect) { - dataTypeName = returnTypeName; - } else { - dataTypeName = dataType ? buildTypeName(dataType) : 'void'; - } + if (isGet) { + const queryOptionsName = getQueryOptionsName(method); + const paramsCallsite = method.parameters.length ? 'params' : ''; - const httpMethod = getHttpMethodByName(this.service, method.name.value); - const queryParams = httpMethod?.parameters.filter((p) => - isCacheParam(p, true), - ); - const queryParamsType = queryParams?.length - ? 'string | Record' - : 'string'; + 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 optionsExpression = `options?: Omit<${UndefinedInitialDataOptions()}<${type( - returnTypeName, - )}, Error, ${type( - dataTypeName, - )} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`; + const skipSelect = + returnType && + returnType.properties.some( + (prop) => + prop.name.value !== 'data' && prop.name.value !== 'errors', + ); + + const returnTypeName = returnType ? buildTypeName(returnType) : 'void'; + let dataTypeName: string; + if (skipSelect) { + dataTypeName = returnTypeName; + } else { + dataTypeName = dataType ? buildTypeName(dataType) : 'void'; + } - // Add deprecation comment for regular query hook - yield* this.buildDeprecationComment('query', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export function ${name}(${[paramsExpression, optionsExpression] - .filter(Boolean) - .join(', ')}) {`; - yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; - yield ` return ${useQuery()}({...defaultOptions, ...options});`; - yield `}`; - yield ''; + const queryParams = httpMethod?.parameters.filter((p) => + isCacheParam(p, true), + ); + const queryParamsType = queryParams.length + ? 'string | Record' + : 'string'; + + const optionsExpression = `options?: Omit<${UndefinedInitialDataOptions()}<${type( + returnTypeName, + )}, Error, ${type( + dataTypeName, + )} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`; + + yield* this.buildDeprecationComment('query', method); + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export function ${name}(${[ + paramsExpression, + optionsExpression, + ].filter(Boolean)}) {`; + yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; + yield ` return ${useQuery()}({...defaultOptions, ...options});`; + yield `}`; + yield ''; + yield* this.buildDeprecationComment('suspenseQuery', method); + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export function ${suspenseName}(${[ + paramsExpression, + optionsExpression, + ].filter(Boolean)}) {`; + yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; + yield ` return ${useSuspenseQuery()}({...defaultOptions, ...options});`; + yield `}`; + } else if (httpPath) { + 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, + ); - // Add deprecation comment for suspense query hook - yield* this.buildDeprecationComment('suspenseQuery', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export function ${suspenseName}(${[ - paramsExpression, - optionsExpression, - ] - .filter(Boolean) - .join(', ')}) {`; - yield ` const defaultOptions = ${queryOptionsName}(${paramsCallsite});`; - yield ` return ${useSuspenseQuery()}({...defaultOptions, ...options});`; - yield `}`; - } + const typeName = dataType ? buildTypeName(dataType) : 'void'; - private *generateLegacyMutationHook( - method: Method, - httpPath: HttpPath, - context: any, - ): Iterable { - const { - name, - paramsType, - paramsExpression, - serviceName, - serviceHookName, - type, - useMutation, - useQueryClient, - UseMutationOptions, - CompositeError, - } = context; + const optionsExpression = `options?: Omit<${UseMutationOptions()}<${type( + typeName, + )}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`; - const paramsCallsite = method.parameters.length ? 'params' : ''; + yield* this.buildDeprecationComment('mutation', method); + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export function ${name}(${optionsExpression}) {`; + yield ` const queryClient = ${useQueryClient()}();`; + yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}()`; + yield ` return ${useMutation()}({`; + 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'); }`; + + 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 + ); - const returnType = getTypeByName( - this.service, - method.returnType?.typeName.value, - ); - const dataType = getTypeByName( - this.service, - returnType?.properties.find((p) => p.name.value === 'data')?.typeName - .value, - ); + for (const queryKey of Array.from(queryKeys)) { + yield ` queryClient.invalidateQueries({ queryKey: [${queryKey}] });`; + } + yield ` return res.data;`; + yield ` },`; + yield ` ...options,`; + yield ` });`; + yield `}`; + } - const typeName = dataType ? buildTypeName(dataType) : 'void'; + if (isGet && this.isRelayPaginated(method)) { + const methodExpression = `${serviceName}.${camel(method.name.value)}`; + const paramsCallsite = method.parameters.length + ? `${applyPageParam()}(params${q ? '?? {}' : ''}, pageParam)` + : ''; - const optionsExpression = `options?: Omit<${UseMutationOptions()}<${type( - typeName, - )}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`; + const infiniteOptionsHook = camel( + `${this.getHookName(method, { infinite: true })}_query_options`, + ); - // Add deprecation comment for mutation hook - yield* this.buildDeprecationComment('mutation', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export function ${name}(${optionsExpression}) {`; - yield ` const queryClient = ${useQueryClient()}();`; - yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}()`; - yield ` return ${useMutation()}({`; - 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 `function ${infiniteOptionsHook}(${paramsExpression}) {`; + yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}();`; + yield ` return {`; + yield ` queryKey: ${this.buildQueryKey(httpPath, 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 `}`; + + yield* this.buildDeprecationComment('infinite', method); + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export const ${this.getHookName(method, { + suspense: false, + infinite: true, + })} = (${paramsExpression}) => {`; + yield ` const options = ${infiniteOptionsHook}(params);`; + yield ` return ${useInfiniteQuery()}(options);`; + yield `}`; + + yield* this.buildDeprecationComment('suspenseInfinite', method); + yield* buildDescription( + method.description, + undefined, + method.deprecated?.value, + ); + yield `export const ${this.getHookName(method, { + suspense: true, + infinite: true, + })} = (${paramsExpression}) => {`; + yield ` const options = ${infiniteOptionsHook}(params);`; + yield ` return ${useSuspenseInfiniteQuery()}(options);`; + yield `}`; + } - const queryKeys = new Set(); - queryKeys.add(this.buildResourceKey(httpPath, method)); - queryKeys.add( - this.buildResourceKey(httpPath, method, { skipTerminalParams: true }), - ); + yield ''; + } + yield ''; - for (const queryKey of Array.from(queryKeys)) { - yield ` queryClient.invalidateQueries({ queryKey: [${queryKey}] });`; + // === 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); } - yield ` return res.data;`; - yield ` },`; - yield ` ...options,`; - yield ` });`; - yield `}`; } - private *generateLegacyInfiniteHooks( + private *generateAllQueryOptions( method: Method, - httpPath: HttpPath, - context: any, + httpMethod: HttpMethod | undefined, + httpPath: HttpPath | undefined, ): Iterable { - const { - infiniteName, - paramsExpression, - q, - serviceName, - serviceHookName, - applyPageParam, - useInfiniteQuery, - useSuspenseInfiniteQuery, - CompositeError, - getInitialPageParam, - getNextPageParam, - getPreviousPageParam, - PageParam, - } = context; - - const methodExpression = `${serviceName}.${camel(method.name.value)}`; - const paramsCallsite = method.parameters.length - ? `${applyPageParam()}(params${q ? '?? {}' : ''}, pageParam)` - : ''; - - const infiniteOptionsHook = camel( - `${this.getHookName(method, { infinite: true })}_query_options`, - ); + if (!httpPath) return; - yield `function ${infiniteOptionsHook}(${paramsExpression}) {`; - yield ` const ${serviceName} = ${this.context.fn(serviceHookName)}();`; - yield ` return {`; - yield ` queryKey: ${this.buildQueryKey(httpPath, 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 `}`; + const isGet = httpMethod?.verb.value === 'get'; - // Add deprecation comment for infinite query hook - yield ''; - yield* this.buildDeprecationComment('infinite', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export const ${this.getHookName(method, { - suspense: false, - infinite: true, - })} = (${paramsExpression}) => {`; - yield ` const options = ${infiniteOptionsHook}(params);`; - yield ` return ${useInfiniteQuery()}(options);`; - yield `}`; + if (isGet) { + yield* this.generateQueryOptions(method, httpPath); - // Add deprecation comment for suspense infinite query hook - yield ''; - yield* this.buildDeprecationComment('suspenseInfinite', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); - yield `export const ${this.getHookName(method, { - suspense: true, - infinite: true, - })} = (${paramsExpression}) => {`; - yield ` const options = ${infiniteOptionsHook}(params);`; - yield ` return ${useSuspenseInfiniteQuery()}(options);`; - yield `}`; + if (this.isRelayPaginated(method)) { + yield* this.generateInfiniteQueryOptions(method, httpPath); + } + } else { + yield* this.generateMutationOptions(method); + } } private *buildInfiniteSelectFn(method: Method): Iterable { @@ -442,18 +331,14 @@ export class HookFile extends ModuleBuilder { }),`; } - private *generateMutationOptions( - method: Method, - httpPath: HttpPath, - ): Iterable { + 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 = this.nameFactory.buildServiceGetterName(this.int); - const mutationOptionsName = - this.nameFactory.buildMutationOptionsName(method); + const serviceGetterName = buildServiceGetterName(this.int); + const mutationOptionsName = buildMutationOptionsName(method); const paramsType = from(buildParamsType(method)); const paramsExpression = method.parameters.length @@ -507,9 +392,8 @@ export class HookFile extends ModuleBuilder { const PageParam = () => this.runtime.type('PageParam'); const serviceName = camel(`${this.int.name.value}_service`); - const serviceGetterName = this.nameFactory.buildServiceGetterName(this.int); - const infiniteOptionsName = - this.nameFactory.buildInfiniteQueryOptionsName(method); + const serviceGetterName = buildServiceGetterName(this.int); + const infiniteOptionsName = buildInfiniteQueryOptionsName(method); const paramsType = from(buildParamsType(method)); const q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; @@ -559,12 +443,12 @@ export class HookFile extends ModuleBuilder { const type = (t: string) => this.types.type(t); const serviceName = camel(`${this.int.name.value}_service`); - const serviceGetterName = this.nameFactory.buildServiceGetterName(this.int); + const serviceGetterName = buildServiceGetterName(this.int); // Keep the internal function for backward compatibility const internalName = getQueryOptionsName(method); // New exported function name - const exportedName = this.nameFactory.buildQueryOptionsName(method); + const exportedName = buildQueryOptionsName(method); const paramsType = from(buildParamsType(method)); const q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; @@ -587,7 +471,7 @@ export class HookFile extends ModuleBuilder { // Internal function for backward compatibility with hooks yield `const ${internalName} = (${paramsExpression}) => {`; yield ` const ${serviceName} = ${this.context.fn( - this.nameFactory.buildServiceHookName(this.int), + buildServiceHookName(this.int), )}()`; yield ` return ${queryOptions()}({`; yield ` queryKey: ${this.buildQueryKey(httpPath, method, { @@ -655,51 +539,51 @@ export class HookFile extends ModuleBuilder { switch (hookType) { case 'query': yield ` * import { useQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildQueryOptionsName( + yield ` * import { ${buildQueryOptionsName( method, )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useQuery(${this.nameFactory.buildQueryOptionsName( + yield ` * const result = useQuery(${buildQueryOptionsName( method, )}(params));`; break; case 'suspenseQuery': yield ` * import { useSuspenseQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildQueryOptionsName( + yield ` * import { ${buildQueryOptionsName( method, )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useSuspenseQuery(${this.nameFactory.buildQueryOptionsName( + yield ` * const result = useSuspenseQuery(${buildQueryOptionsName( method, )}(params));`; break; case 'mutation': yield ` * import { useMutation } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildMutationOptionsName( + yield ` * import { ${buildMutationOptionsName( method, )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const mutation = useMutation(${this.nameFactory.buildMutationOptionsName( + yield ` * const mutation = useMutation(${buildMutationOptionsName( method, )}());`; break; case 'infinite': yield ` * import { useInfiniteQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName( + yield ` * import { ${buildInfiniteQueryOptionsName( method, )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName( + yield ` * const result = useInfiniteQuery(${buildInfiniteQueryOptionsName( method, )}(params));`; break; case 'suspenseInfinite': yield ` * import { useSuspenseInfiniteQuery } from '@tanstack/react-query';`; - yield ` * import { ${this.nameFactory.buildInfiniteQueryOptionsName( + yield ` * import { ${buildInfiniteQueryOptionsName( method, )} } from './hooks/${this.int.name.value}';`; yield ' *'; - yield ` * const result = useSuspenseInfiniteQuery(${this.nameFactory.buildInfiniteQueryOptionsName( + yield ` * const result = useSuspenseInfiniteQuery(${buildInfiniteQueryOptionsName( method, )}(params));`; break; diff --git a/src/name-factory.ts b/src/name-factory.ts index 05e2f29..f0b943e 100644 --- a/src/name-factory.ts +++ b/src/name-factory.ts @@ -65,3 +65,24 @@ export class NameFactory { export function getQueryOptionsName(method: Method): string { return camel(`use_${method.name.value}_query_options`); } + +// Helper functions for v0.2.0 code +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`); +} diff --git a/src/snapshot/v1/hooks/auth-permutations.ts b/src/snapshot/v1/hooks/auth-permutations.ts new file mode 100644 index 0000000..f261960 --- /dev/null +++ b/src/snapshot/v1/hooks/auth-permutations.ts @@ -0,0 +1,154 @@ +/** + * 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 This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useQuery } from '@tanstack/react-query'; + * import { allAuthSchemesQueryOptions } from './hooks/authPermutation'; + * + * const result = useQuery(allAuthSchemesQueryOptions(params)); + * ``` + */ +export function useAllAuthSchemes( + options?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >, +) { + const defaultOptions = useAllAuthSchemesQueryOptions(); + return useQuery({ ...defaultOptions, ...options }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useSuspenseQuery } from '@tanstack/react-query'; + * import { allAuthSchemesQueryOptions } from './hooks/authPermutation'; + * + * const result = useSuspenseQuery(allAuthSchemesQueryOptions(params)); + * ``` + */ +export function useAllAuthSchemes( + options?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >, +) { + const defaultOptions = useAllAuthSchemesQueryOptions(); + return useSuspenseQuery({ ...defaultOptions, ...options }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { comboAuthSchemesMutationOptions } from './hooks/authPermutation'; + * + * const mutation = useMutation(comboAuthSchemesMutationOptions()); + * ``` + */ +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: [`/authPermutations`] }); + return res.data; + }, + ...options, + }); +} + +const useAllAuthSchemesQueryOptions = () => { + const authPermutationService = useAuthPermutationService(); + return queryOptions({ + queryKey: [`/authPermutations`], + 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: [`/authPermutations`], + 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..2cbdd6b --- /dev/null +++ b/src/snapshot/v1/hooks/exhaustives.ts @@ -0,0 +1,139 @@ +/** + * 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 { compact, CompositeError } from './runtime' + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useQuery } from '@tanstack/react-query'; + * import { exhaustiveFormatsQueryOptions } from './hooks/exhaustive'; + * + * const result = useQuery(exhaustiveFormatsQueryOptions(params)); + * ``` + */ +export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { + const defaultOptions = useExhaustiveFormatsQueryOptions(params); + return useQuery({...defaultOptions, ...options}); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useSuspenseQuery } from '@tanstack/react-query'; + * import { exhaustiveFormatsQueryOptions } from './hooks/exhaustive'; + * + * const result = useSuspenseQuery(exhaustiveFormatsQueryOptions(params)); + * ``` + */ +export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { + const defaultOptions = useExhaustiveFormatsQueryOptions(params); + return useSuspenseQuery({...defaultOptions, ...options}); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useQuery } from '@tanstack/react-query'; + * import { exhaustiveParamsQueryOptions } from './hooks/exhaustive'; + * + * const result = useQuery(exhaustiveParamsQueryOptions(params)); + * ``` + */ +export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { + const defaultOptions = useExhaustiveParamsQueryOptions(params); + return useQuery({...defaultOptions, ...options}); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useSuspenseQuery } from '@tanstack/react-query'; + * import { exhaustiveParamsQueryOptions } from './hooks/exhaustive'; + * + * const result = useSuspenseQuery(exhaustiveParamsQueryOptions(params)); + * ``` + */ +export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { + const defaultOptions = useExhaustiveParamsQueryOptions(params); + return useSuspenseQuery({...defaultOptions, ...options}); +} + + + +const useExhaustiveFormatsQueryOptions = (params?: ExhaustiveFormatsParams) => { + const exhaustiveService = useExhaustiveService() + return queryOptions({ + queryKey: [`/exhaustive`, compact({string-no-format: params?.string-no-format,string-date: params?.string-date,string-date-time: params?.string-date-time,integer-no-format: params?.integer-no-format,integer-int32: params?.integer-int32,integer-int64: params?.integer-int64,number-no-format: params?.number-no-format,number-float: params?.number-float,number-double: params?.number-double})].filter(Boolean), + 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`, compact({string-no-format: params?.string-no-format,string-date: params?.string-date,string-date-time: params?.string-date-time,integer-no-format: params?.integer-no-format,integer-int32: params?.integer-int32,integer-int64: params?.integer-int64,number-no-format: params?.number-no-format,number-float: params?.number-float,number-double: params?.number-double})].filter(Boolean), + 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/${params.pathString}/${params.pathEnum}/${params.pathNumber}/${params.pathInteger}/${params.pathBoolean}/${params.pathStringArray}/${params.pathEnumArray}/${params.pathNumberArray}/${params.pathIntegerArray}/${params.pathBooleanArray}`, compact({query-string: params.query-string,query-enum: params.query-enum,query-number: params.query-number,query-integer: params.query-integer,query-boolean: params.query-boolean,query-string-array: params.query-string-array,query-enum-array: params.query-enum-array,query-number-array: params.query-number-array,query-integer-array: params.query-integer-array,query-boolean-array: params.query-boolean-array})].filter(Boolean), + 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/${params.pathString}/${params.pathEnum}/${params.pathNumber}/${params.pathInteger}/${params.pathBoolean}/${params.pathStringArray}/${params.pathEnumArray}/${params.pathNumberArray}/${params.pathIntegerArray}/${params.pathBooleanArray}`, compact({query-string: params.query-string,query-enum: params.query-enum,query-number: params.query-number,query-integer: params.query-integer,query-boolean: params.query-boolean,query-string-array: params.query-string-array,query-enum-array: params.query-enum-array,query-number-array: params.query-number-array,query-integer-array: params.query-integer-array,query-boolean-array: params.query-boolean-array})].filter(Boolean), + 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, + }); +}; \ No newline at end of file diff --git a/src/snapshot/v1/hooks/gizmos.ts b/src/snapshot/v1/hooks/gizmos.ts new file mode 100644 index 0000000..43bdb69 --- /dev/null +++ b/src/snapshot/v1/hooks/gizmos.ts @@ -0,0 +1,290 @@ +/** + * 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 { compact, CompositeError } from './runtime'; + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { createGizmoMutationOptions } from './hooks/gizmo'; + * + * const mutation = useMutation(createGizmoMutationOptions()); + * ``` + */ + +/** + * Has a summary in addition to a description + * Has a description in addition to a summary + */ +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: [`/gizmos`] }); + return res.data; + }, + ...options, + }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useQuery } from '@tanstack/react-query'; + * import { getGizmosQueryOptions } from './hooks/gizmo'; + * + * const result = useQuery(getGizmosQueryOptions(params)); + * ``` + */ + +/** + * 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 }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useSuspenseQuery } from '@tanstack/react-query'; + * import { getGizmosQueryOptions } from './hooks/gizmo'; + * + * const result = useSuspenseQuery(getGizmosQueryOptions(params)); + * ``` + */ + +/** + * 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 This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { updateGizmoMutationOptions } from './hooks/gizmo'; + * + * const mutation = useMutation(updateGizmoMutationOptions()); + * ``` + */ +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: [`/gizmos`] }); + return res.data; + }, + ...options, + }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { uploadGizmoMutationOptions } from './hooks/gizmo'; + * + * const mutation = useMutation(uploadGizmoMutationOptions()); + * ``` + */ +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: [`/gizmos/data`] }); + return res.data; + }, + ...options, + }); +} + +const useGetGizmosQueryOptions = (params?: GetGizmosParams) => { + const gizmoService = useGizmoService(); + return queryOptions({ + queryKey: [`/gizmos`, compact({ search: params?.search })].filter(Boolean), + 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: [`/gizmos`, compact({ search: params?.search })].filter(Boolean), + 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..84c832b --- /dev/null +++ b/src/snapshot/v1/hooks/widgets.ts @@ -0,0 +1,328 @@ +/** + * 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 This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { createWidgetMutationOptions } from './hooks/widget'; + * + * const mutation = useMutation(createWidgetMutationOptions()); + * ``` + */ +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: [`/widgets`] }); + return res.data; + }, + ...options, + }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { deleteWidgetFooMutationOptions } from './hooks/widget'; + * + * const mutation = useMutation(deleteWidgetFooMutationOptions()); + * ``` + */ +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: [`/widgets/${params.id}/foo`], + }); + return res.data; + }, + ...options, + }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useMutation } from '@tanstack/react-query'; + * import { putWidgetMutationOptions } from './hooks/widget'; + * + * const mutation = useMutation(putWidgetMutationOptions()); + * ``` + */ +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: [`/widgets`] }); + return res.data; + }, + ...options, + }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useQuery } from '@tanstack/react-query'; + * import { getWidgetFooQueryOptions } from './hooks/widget'; + * + * const result = useQuery(getWidgetFooQueryOptions(params)); + * ``` + */ +export function useWidgetFoo( + params: GetWidgetFooParams, + options?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >, +) { + const defaultOptions = useGetWidgetFooQueryOptions(params); + return useQuery({ ...defaultOptions, ...options }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useSuspenseQuery } from '@tanstack/react-query'; + * import { getWidgetFooQueryOptions } from './hooks/widget'; + * + * const result = useSuspenseQuery(getWidgetFooQueryOptions(params)); + * ``` + */ +export function useSuspenseWidgetFoo( + params: GetWidgetFooParams, + options?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >, +) { + const defaultOptions = useGetWidgetFooQueryOptions(params); + return useSuspenseQuery({ ...defaultOptions, ...options }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useQuery } from '@tanstack/react-query'; + * import { getWidgetsQueryOptions } from './hooks/widget'; + * + * const result = useQuery(getWidgetsQueryOptions(params)); + * ``` + */ +export function useWidgets( + options?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >, +) { + const defaultOptions = useGetWidgetsQueryOptions(); + return useQuery({ ...defaultOptions, ...options }); +} + +/** + * @deprecated This hook is deprecated and will be removed in a future version. + * Please use the new query options pattern instead: + * + * ```typescript + * import { useSuspenseQuery } from '@tanstack/react-query'; + * import { getWidgetsQueryOptions } from './hooks/widget'; + * + * const result = useSuspenseQuery(getWidgetsQueryOptions(params)); + * ``` + */ +export function useSuspenseWidgets( + options?: Omit< + UndefinedInitialDataOptions, + 'queryKey' | 'queryFn' | 'select' + >, +) { + const defaultOptions = useGetWidgetsQueryOptions(); + return useSuspenseQuery({ ...defaultOptions, ...options }); +} + +const useGetWidgetsQueryOptions = () => { + const widgetService = useWidgetService(); + return queryOptions({ + queryKey: [`/widgets`], + 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: [`/widgets`], + 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: [`/widgets/${params.id}/foo`], + 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: [`/widgets/${params.id}/foo`], + 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; + }, + }); +}; From c738879c2b0e8b1ff57b213752b8160a26a640ac Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 18:43:52 -0700 Subject: [PATCH 09/21] Use standard @deprecated JSDoc tag instead of custom deprecation comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove buildDeprecationComment method and all its calls - Update buildDescription calls to use isDeprecated parameter - Simplifies generated code by using standard deprecation format - Migration instructions removed but could be added to @basketry/typescript in future 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/hook-file.ts | 109 +-------------------- src/hook-generator.test.ts | 2 +- src/snapshot/v1/hooks/auth-permutations.ts | 30 +----- src/snapshot/v1/hooks/exhaustives.ts | 44 ++------- src/snapshot/v1/hooks/gizmos.ts | 57 +---------- src/snapshot/v1/hooks/widgets.ts | 70 ++----------- 6 files changed, 27 insertions(+), 285 deletions(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index 7f60d06..3de3cc4 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -137,12 +137,7 @@ export class HookFile extends ModuleBuilder { dataTypeName, )} | undefined, (${queryParamsType})[]>,'queryKey' | 'queryFn' | 'select'>`; - yield* this.buildDeprecationComment('query', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); + yield* buildDescription(method.description, undefined, true); yield `export function ${name}(${[ paramsExpression, optionsExpression, @@ -151,12 +146,7 @@ export class HookFile extends ModuleBuilder { yield ` return ${useQuery()}({...defaultOptions, ...options});`; yield `}`; yield ''; - yield* this.buildDeprecationComment('suspenseQuery', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); + yield* buildDescription(method.description, undefined, true); yield `export function ${suspenseName}(${[ paramsExpression, optionsExpression, @@ -183,12 +173,7 @@ export class HookFile extends ModuleBuilder { typeName, )}, Error, ${type(paramsType)}, unknown>, 'mutationFn'>`; - yield* this.buildDeprecationComment('mutation', method); - 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)}()`; @@ -247,12 +232,7 @@ export class HookFile extends ModuleBuilder { yield ` };`; yield `}`; - yield* this.buildDeprecationComment('infinite', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); + yield* buildDescription(method.description, undefined, true); yield `export const ${this.getHookName(method, { suspense: false, infinite: true, @@ -261,12 +241,7 @@ export class HookFile extends ModuleBuilder { yield ` return ${useInfiniteQuery()}(options);`; yield `}`; - yield* this.buildDeprecationComment('suspenseInfinite', method); - yield* buildDescription( - method.description, - undefined, - method.deprecated?.value, - ); + yield* buildDescription(method.description, undefined, true); yield `export const ${this.getHookName(method, { suspense: true, infinite: true, @@ -519,80 +494,6 @@ export class HookFile extends ModuleBuilder { yield `};`; } - private *buildDeprecationComment( - hookType: - | 'query' - | 'suspenseQuery' - | 'mutation' - | 'infinite' - | 'suspenseInfinite', - method: Method, - ): Iterable { - const methodName = method.name.value; - - yield '/**'; - yield ' * @deprecated This hook is deprecated and will be removed in a future version.'; - yield ' * Please use the new query options pattern instead:'; - yield ' *'; - yield ' * ```typescript'; - - switch (hookType) { - case 'query': - yield ` * import { useQuery } from '@tanstack/react-query';`; - yield ` * import { ${buildQueryOptionsName( - method, - )} } from './hooks/${this.int.name.value}';`; - yield ' *'; - yield ` * const result = useQuery(${buildQueryOptionsName( - method, - )}(params));`; - break; - case 'suspenseQuery': - yield ` * import { useSuspenseQuery } from '@tanstack/react-query';`; - yield ` * import { ${buildQueryOptionsName( - method, - )} } from './hooks/${this.int.name.value}';`; - yield ' *'; - yield ` * const result = useSuspenseQuery(${buildQueryOptionsName( - method, - )}(params));`; - break; - case 'mutation': - yield ` * import { useMutation } from '@tanstack/react-query';`; - yield ` * import { ${buildMutationOptionsName( - method, - )} } from './hooks/${this.int.name.value}';`; - yield ' *'; - yield ` * const mutation = useMutation(${buildMutationOptionsName( - method, - )}());`; - break; - case 'infinite': - yield ` * import { useInfiniteQuery } from '@tanstack/react-query';`; - yield ` * import { ${buildInfiniteQueryOptionsName( - method, - )} } from './hooks/${this.int.name.value}';`; - yield ' *'; - yield ` * const result = useInfiniteQuery(${buildInfiniteQueryOptionsName( - method, - )}(params));`; - break; - case 'suspenseInfinite': - yield ` * import { useSuspenseInfiniteQuery } from '@tanstack/react-query';`; - yield ` * import { ${buildInfiniteQueryOptionsName( - method, - )} } from './hooks/${this.int.name.value}';`; - yield ' *'; - yield ` * const result = useSuspenseInfiniteQuery(${buildInfiniteQueryOptionsName( - method, - )}(params));`; - break; - } - - yield ' * ```'; - yield ' */'; - } - private getHookName( method: Method, options?: { infinite?: boolean; suspense?: 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/snapshot/v1/hooks/auth-permutations.ts b/src/snapshot/v1/hooks/auth-permutations.ts index f261960..ee763c5 100644 --- a/src/snapshot/v1/hooks/auth-permutations.ts +++ b/src/snapshot/v1/hooks/auth-permutations.ts @@ -30,15 +30,7 @@ import { import { CompositeError } from './runtime'; /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useQuery } from '@tanstack/react-query'; - * import { allAuthSchemesQueryOptions } from './hooks/authPermutation'; - * - * const result = useQuery(allAuthSchemesQueryOptions(params)); - * ``` + * @deprecated */ export function useAllAuthSchemes( options?: Omit< @@ -51,15 +43,7 @@ export function useAllAuthSchemes( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useSuspenseQuery } from '@tanstack/react-query'; - * import { allAuthSchemesQueryOptions } from './hooks/authPermutation'; - * - * const result = useSuspenseQuery(allAuthSchemesQueryOptions(params)); - * ``` + * @deprecated */ export function useAllAuthSchemes( options?: Omit< @@ -72,15 +56,7 @@ export function useAllAuthSchemes( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { comboAuthSchemesMutationOptions } from './hooks/authPermutation'; - * - * const mutation = useMutation(comboAuthSchemesMutationOptions()); - * ``` + * @deprecated */ export function useComboAuthSchemes( options?: Omit< diff --git a/src/snapshot/v1/hooks/exhaustives.ts b/src/snapshot/v1/hooks/exhaustives.ts index 2cbdd6b..5e422c5 100644 --- a/src/snapshot/v1/hooks/exhaustives.ts +++ b/src/snapshot/v1/hooks/exhaustives.ts @@ -17,64 +17,36 @@ import type { ExhaustiveFormatsParams, ExhaustiveParamsParams } from '../types' import { getExhaustiveService, useExhaustiveService } from './context' import { compact, CompositeError } from './runtime' + /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useQuery } from '@tanstack/react-query'; - * import { exhaustiveFormatsQueryOptions } from './hooks/exhaustive'; - * - * const result = useQuery(exhaustiveFormatsQueryOptions(params)); - * ``` + * @deprecated */ export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveFormatsQueryOptions(params); return useQuery({...defaultOptions, ...options}); } + /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useSuspenseQuery } from '@tanstack/react-query'; - * import { exhaustiveFormatsQueryOptions } from './hooks/exhaustive'; - * - * const result = useSuspenseQuery(exhaustiveFormatsQueryOptions(params)); - * ``` + * @deprecated */ export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveFormatsQueryOptions(params); return useSuspenseQuery({...defaultOptions, ...options}); } + /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useQuery } from '@tanstack/react-query'; - * import { exhaustiveParamsQueryOptions } from './hooks/exhaustive'; - * - * const result = useQuery(exhaustiveParamsQueryOptions(params)); - * ``` + * @deprecated */ export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveParamsQueryOptions(params); return useQuery({...defaultOptions, ...options}); } + /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useSuspenseQuery } from '@tanstack/react-query'; - * import { exhaustiveParamsQueryOptions } from './hooks/exhaustive'; - * - * const result = useSuspenseQuery(exhaustiveParamsQueryOptions(params)); - * ``` + * @deprecated */ export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveParamsQueryOptions(params); diff --git a/src/snapshot/v1/hooks/gizmos.ts b/src/snapshot/v1/hooks/gizmos.ts index 43bdb69..407eed1 100644 --- a/src/snapshot/v1/hooks/gizmos.ts +++ b/src/snapshot/v1/hooks/gizmos.ts @@ -33,21 +33,10 @@ import type { import { getGizmoService, useGizmoService } from './context'; import { compact, CompositeError } from './runtime'; -/** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { createGizmoMutationOptions } from './hooks/gizmo'; - * - * const mutation = useMutation(createGizmoMutationOptions()); - * ``` - */ - /** * Has a summary in addition to a description * Has a description in addition to a summary + * @deprecated */ export function useCreateGizmo( options?: Omit< @@ -72,18 +61,6 @@ export function useCreateGizmo( }); } -/** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useQuery } from '@tanstack/react-query'; - * import { getGizmosQueryOptions } from './hooks/gizmo'; - * - * const result = useQuery(getGizmosQueryOptions(params)); - * ``` - */ - /** * Only has a summary * @deprecated @@ -104,18 +81,6 @@ export function useGizmos( return useQuery({ ...defaultOptions, ...options }); } -/** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useSuspenseQuery } from '@tanstack/react-query'; - * import { getGizmosQueryOptions } from './hooks/gizmo'; - * - * const result = useSuspenseQuery(getGizmosQueryOptions(params)); - * ``` - */ - /** * Only has a summary * @deprecated @@ -137,15 +102,7 @@ export function useSuspenseGizmos( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { updateGizmoMutationOptions } from './hooks/gizmo'; - * - * const mutation = useMutation(updateGizmoMutationOptions()); - * ``` + * @deprecated */ export function useUpdateGizmo( options?: Omit< @@ -171,15 +128,7 @@ export function useUpdateGizmo( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { uploadGizmoMutationOptions } from './hooks/gizmo'; - * - * const mutation = useMutation(uploadGizmoMutationOptions()); - * ``` + * @deprecated */ export function useUploadGizmo( options?: Omit< diff --git a/src/snapshot/v1/hooks/widgets.ts b/src/snapshot/v1/hooks/widgets.ts index 84c832b..6551124 100644 --- a/src/snapshot/v1/hooks/widgets.ts +++ b/src/snapshot/v1/hooks/widgets.ts @@ -33,15 +33,7 @@ import { getWidgetService, useWidgetService } from './context'; import { CompositeError } from './runtime'; /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { createWidgetMutationOptions } from './hooks/widget'; - * - * const mutation = useMutation(createWidgetMutationOptions()); - * ``` + * @deprecated */ export function useCreateWidget( options?: Omit< @@ -67,15 +59,7 @@ export function useCreateWidget( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { deleteWidgetFooMutationOptions } from './hooks/widget'; - * - * const mutation = useMutation(deleteWidgetFooMutationOptions()); - * ``` + * @deprecated */ export function useDeleteWidgetFoo( options?: Omit< @@ -103,15 +87,7 @@ export function useDeleteWidgetFoo( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useMutation } from '@tanstack/react-query'; - * import { putWidgetMutationOptions } from './hooks/widget'; - * - * const mutation = useMutation(putWidgetMutationOptions()); - * ``` + * @deprecated */ export function usePutWidget( options?: Omit< @@ -137,15 +113,7 @@ export function usePutWidget( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useQuery } from '@tanstack/react-query'; - * import { getWidgetFooQueryOptions } from './hooks/widget'; - * - * const result = useQuery(getWidgetFooQueryOptions(params)); - * ``` + * @deprecated */ export function useWidgetFoo( params: GetWidgetFooParams, @@ -159,15 +127,7 @@ export function useWidgetFoo( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useSuspenseQuery } from '@tanstack/react-query'; - * import { getWidgetFooQueryOptions } from './hooks/widget'; - * - * const result = useSuspenseQuery(getWidgetFooQueryOptions(params)); - * ``` + * @deprecated */ export function useSuspenseWidgetFoo( params: GetWidgetFooParams, @@ -181,15 +141,7 @@ export function useSuspenseWidgetFoo( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useQuery } from '@tanstack/react-query'; - * import { getWidgetsQueryOptions } from './hooks/widget'; - * - * const result = useQuery(getWidgetsQueryOptions(params)); - * ``` + * @deprecated */ export function useWidgets( options?: Omit< @@ -202,15 +154,7 @@ export function useWidgets( } /** - * @deprecated This hook is deprecated and will be removed in a future version. - * Please use the new query options pattern instead: - * - * ```typescript - * import { useSuspenseQuery } from '@tanstack/react-query'; - * import { getWidgetsQueryOptions } from './hooks/widget'; - * - * const result = useSuspenseQuery(getWidgetsQueryOptions(params)); - * ``` + * @deprecated */ export function useSuspenseWidgets( options?: Omit< From 5def1169c06caabba80ea92a1dc77185a785f3ab Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 18:57:22 -0700 Subject: [PATCH 10/21] Add CHANGELOG.md and fix package.json description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create CHANGELOG.md following Keep a Changelog format - Document new query options exports and deprecated hooks - Fix package.json description to accurately describe the package 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cfa2cd2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# 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 + +### 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/package.json b/package.json index c8e904c..4cac65c 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", + "description": "Basketry generator for generating React Query hooks", "main": "./lib/index.js", "scripts": { "test": "jest", From 36b904cc9a509618735944d8d69b6d36762d29e6 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 19:12:28 -0700 Subject: [PATCH 11/21] Fix parameter names with special characters in query keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use bracket notation for all parameter access (uniform approach) - Properly quote object keys in query key generation - Fix syntax for optional chaining with bracket notation - Ensures compatibility with parameter names containing hyphens or other special characters 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/hook-file.ts | 7 +- src/snapshot/v1/hooks/exhaustives.ts | 187 +++++++++++++++++++++------ src/snapshot/v1/hooks/gizmos.ts | 8 +- src/snapshot/v1/hooks/widgets.ts | 6 +- 4 files changed, 164 insertions(+), 44 deletions(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index 3de3cc4..57b26a2 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -560,7 +560,10 @@ export class HookFile extends ModuleBuilder { }); queryKey.push( `${compact()}({${queryParams - .map((p) => `${p.name.value}: params${q}.${p.name.value}`) + .map( + (p) => + `'${p.name.value}': params${q}${q ? '.' : ''}['${p.name.value}']`, + ) .join(',')}})`, ); } @@ -592,7 +595,7 @@ export class HookFile extends ModuleBuilder { const path = parts.filter(Boolean).map((part) => { if (part.startsWith('{') && part.endsWith('}')) { const param = part.slice(1, -1); - return `\${params${q}.${camel(param)}}`; + return `\${params${q}['${camel(param)}']}`; } return part; diff --git a/src/snapshot/v1/hooks/exhaustives.ts b/src/snapshot/v1/hooks/exhaustives.ts index 5e422c5..b52ab62 100644 --- a/src/snapshot/v1/hooks/exhaustives.ts +++ b/src/snapshot/v1/hooks/exhaustives.ts @@ -12,100 +12,213 @@ * 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 { compact, CompositeError } from './runtime' - +import { + queryOptions, + type UndefinedInitialDataOptions, + useQuery, + useSuspenseQuery, +} from '@tanstack/react-query'; +import type { ExhaustiveFormatsParams, ExhaustiveParamsParams } from '../types'; +import { getExhaustiveService, useExhaustiveService } from './context'; +import { compact, CompositeError } from './runtime'; /** * @deprecated */ -export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +export function useExhaustiveFormats( + params?: ExhaustiveFormatsParams, + options?: Omit< + UndefinedInitialDataOptions< + void, + Error, + void | undefined, + (string | Record)[] + >, + 'queryKey' | 'queryFn' | 'select' + >, +) { const defaultOptions = useExhaustiveFormatsQueryOptions(params); - return useSuspenseQuery({...defaultOptions, ...options}); + return useSuspenseQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +export function useExhaustiveParams( + params: ExhaustiveParamsParams, + options?: Omit< + UndefinedInitialDataOptions< + void, + Error, + void | undefined, + (string | Record)[] + >, + 'queryKey' | 'queryFn' | 'select' + >, +) { const defaultOptions = useExhaustiveParamsQueryOptions(params); - return useSuspenseQuery({...defaultOptions, ...options}); + return useSuspenseQuery({ ...defaultOptions, ...options }); } - - const useExhaustiveFormatsQueryOptions = (params?: ExhaustiveFormatsParams) => { - const exhaustiveService = useExhaustiveService() + const exhaustiveService = useExhaustiveService(); return queryOptions({ - queryKey: [`/exhaustive`, compact({string-no-format: params?.string-no-format,string-date: params?.string-date,string-date-time: params?.string-date-time,integer-no-format: params?.integer-no-format,integer-int32: params?.integer-int32,integer-int64: params?.integer-int64,number-no-format: params?.number-no-format,number-float: params?.number-float,number-double: params?.number-double})].filter(Boolean), + queryKey: [ + `/exhaustive`, + compact({ + 'string-no-format': params?.['string-no-format'], + 'string-date': params?.['string-date'], + 'string-date-time': params?.['string-date-time'], + 'integer-no-format': params?.['integer-no-format'], + 'integer-int32': params?.['integer-int32'], + 'integer-int64': params?.['integer-int64'], + 'number-no-format': params?.['number-no-format'], + 'number-float': params?.['number-float'], + 'number-double': params?.['number-double'], + }), + ].filter(Boolean), 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'); } + 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() +export const exhaustiveFormatsQueryOptions = ( + params?: ExhaustiveFormatsParams, +) => { + const exhaustiveService = getExhaustiveService(); return queryOptions({ - queryKey: [`/exhaustive`, compact({string-no-format: params?.string-no-format,string-date: params?.string-date,string-date-time: params?.string-date-time,integer-no-format: params?.integer-no-format,integer-int32: params?.integer-int32,integer-int64: params?.integer-int64,number-no-format: params?.number-no-format,number-float: params?.number-float,number-double: params?.number-double})].filter(Boolean), + queryKey: [ + `/exhaustive`, + compact({ + 'string-no-format': params?.['string-no-format'], + 'string-date': params?.['string-date'], + 'string-date-time': params?.['string-date-time'], + 'integer-no-format': params?.['integer-no-format'], + 'integer-int32': params?.['integer-int32'], + 'integer-int64': params?.['integer-int64'], + 'number-no-format': params?.['number-no-format'], + 'number-float': params?.['number-float'], + 'number-double': params?.['number-double'], + }), + ].filter(Boolean), 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'); } + 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() + const exhaustiveService = useExhaustiveService(); return queryOptions({ - queryKey: [`/exhaustive/${params.pathString}/${params.pathEnum}/${params.pathNumber}/${params.pathInteger}/${params.pathBoolean}/${params.pathStringArray}/${params.pathEnumArray}/${params.pathNumberArray}/${params.pathIntegerArray}/${params.pathBooleanArray}`, compact({query-string: params.query-string,query-enum: params.query-enum,query-number: params.query-number,query-integer: params.query-integer,query-boolean: params.query-boolean,query-string-array: params.query-string-array,query-enum-array: params.query-enum-array,query-number-array: params.query-number-array,query-integer-array: params.query-integer-array,query-boolean-array: params.query-boolean-array})].filter(Boolean), + queryKey: [ + `/exhaustive/${params['pathString']}/${params['pathEnum']}/${params['pathNumber']}/${params['pathInteger']}/${params['pathBoolean']}/${params['pathStringArray']}/${params['pathEnumArray']}/${params['pathNumberArray']}/${params['pathIntegerArray']}/${params['pathBooleanArray']}`, + compact({ + 'query-string': params['query-string'], + 'query-enum': params['query-enum'], + 'query-number': params['query-number'], + 'query-integer': params['query-integer'], + 'query-boolean': params['query-boolean'], + 'query-string-array': params['query-string-array'], + 'query-enum-array': params['query-enum-array'], + 'query-number-array': params['query-number-array'], + 'query-integer-array': params['query-integer-array'], + 'query-boolean-array': params['query-boolean-array'], + }), + ].filter(Boolean), 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'); } + 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() +export const exhaustiveParamsQueryOptions = ( + params: ExhaustiveParamsParams, +) => { + const exhaustiveService = getExhaustiveService(); return queryOptions({ - queryKey: [`/exhaustive/${params.pathString}/${params.pathEnum}/${params.pathNumber}/${params.pathInteger}/${params.pathBoolean}/${params.pathStringArray}/${params.pathEnumArray}/${params.pathNumberArray}/${params.pathIntegerArray}/${params.pathBooleanArray}`, compact({query-string: params.query-string,query-enum: params.query-enum,query-number: params.query-number,query-integer: params.query-integer,query-boolean: params.query-boolean,query-string-array: params.query-string-array,query-enum-array: params.query-enum-array,query-number-array: params.query-number-array,query-integer-array: params.query-integer-array,query-boolean-array: params.query-boolean-array})].filter(Boolean), + queryKey: [ + `/exhaustive/${params['pathString']}/${params['pathEnum']}/${params['pathNumber']}/${params['pathInteger']}/${params['pathBoolean']}/${params['pathStringArray']}/${params['pathEnumArray']}/${params['pathNumberArray']}/${params['pathIntegerArray']}/${params['pathBooleanArray']}`, + compact({ + 'query-string': params['query-string'], + 'query-enum': params['query-enum'], + 'query-number': params['query-number'], + 'query-integer': params['query-integer'], + 'query-boolean': params['query-boolean'], + 'query-string-array': params['query-string-array'], + 'query-enum-array': params['query-enum-array'], + 'query-number-array': params['query-number-array'], + 'query-integer-array': params['query-integer-array'], + 'query-boolean-array': params['query-boolean-array'], + }), + ].filter(Boolean), 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'); } + 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, }); -}; \ No newline at end of file +}; diff --git a/src/snapshot/v1/hooks/gizmos.ts b/src/snapshot/v1/hooks/gizmos.ts index 407eed1..3080a68 100644 --- a/src/snapshot/v1/hooks/gizmos.ts +++ b/src/snapshot/v1/hooks/gizmos.ts @@ -156,7 +156,9 @@ export function useUploadGizmo( const useGetGizmosQueryOptions = (params?: GetGizmosParams) => { const gizmoService = useGizmoService(); return queryOptions({ - queryKey: [`/gizmos`, compact({ search: params?.search })].filter(Boolean), + queryKey: [`/gizmos`, compact({ search: params?.['search'] })].filter( + Boolean, + ), queryFn: async () => { const res = await gizmoService.getGizmos(params); if (res.errors.length) { @@ -177,7 +179,9 @@ const useGetGizmosQueryOptions = (params?: GetGizmosParams) => { export const getGizmosQueryOptions = (params?: GetGizmosParams) => { const gizmoService = getGizmoService(); return queryOptions({ - queryKey: [`/gizmos`, compact({ search: params?.search })].filter(Boolean), + queryKey: [`/gizmos`, compact({ search: params?.['search'] })].filter( + Boolean, + ), queryFn: async () => { const res = await gizmoService.getGizmos(params); if (res.errors.length) { diff --git a/src/snapshot/v1/hooks/widgets.ts b/src/snapshot/v1/hooks/widgets.ts index 6551124..ef1ffe5 100644 --- a/src/snapshot/v1/hooks/widgets.ts +++ b/src/snapshot/v1/hooks/widgets.ts @@ -78,7 +78,7 @@ export function useDeleteWidgetFoo( throw new Error('Unexpected data error: Failed to get example'); } queryClient.invalidateQueries({ - queryKey: [`/widgets/${params.id}/foo`], + queryKey: [`/widgets/${params['id']}/foo`], }); return res.data; }, @@ -228,7 +228,7 @@ export const putWidgetMutationOptions = () => { const useGetWidgetFooQueryOptions = (params: GetWidgetFooParams) => { const widgetService = useWidgetService(); return queryOptions({ - queryKey: [`/widgets/${params.id}/foo`], + queryKey: [`/widgets/${params['id']}/foo`], queryFn: async () => { const res = await widgetService.getWidgetFoo(params); if (res.errors.length) { @@ -244,7 +244,7 @@ const useGetWidgetFooQueryOptions = (params: GetWidgetFooParams) => { export const getWidgetFooQueryOptions = (params: GetWidgetFooParams) => { const widgetService = getWidgetService(); return queryOptions({ - queryKey: [`/widgets/${params.id}/foo`], + queryKey: [`/widgets/${params['id']}/foo`], queryFn: async () => { const res = await widgetService.getWidgetFoo(params); if (res.errors.length) { From d7fcb3152ef6fa112fd08185b4afc6e53ee2084e Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 19:12:51 -0700 Subject: [PATCH 12/21] Update CHANGELOG with parameter fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa2cd2..5304561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Generated hooks now use simplified `@deprecated` JSDoc tags instead of custom deprecation blocks +### 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 + ### Deprecated - Legacy hook exports (`use{MethodName}`, `useSuspense{MethodName}`, etc.) are now deprecated From 927c6c30eb8722bb9eeb14b062259e25f1aed37f Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 19:20:52 -0700 Subject: [PATCH 13/21] Fix duplicate function declarations for non-get methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed getHookName to properly generate suspense hook names for all methods - Methods that don't start with 'get' now correctly get useSuspense prefix - Resolves duplicate function declarations in generated code 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/hook-file.ts | 6 +++++- src/snapshot/v1/hooks/auth-permutations.ts | 2 +- src/snapshot/v1/hooks/exhaustives.ts | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index 57b26a2..eb0215e 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -512,7 +512,11 @@ export class HookFile extends ModuleBuilder { ); } - return camel(`use_${name}`); + return camel( + `use_${options?.suspense ? 'suspense_' : ''}${ + options?.infinite ? 'infinite_' : '' + }${name}`, + ); } private getHttpPath( diff --git a/src/snapshot/v1/hooks/auth-permutations.ts b/src/snapshot/v1/hooks/auth-permutations.ts index ee763c5..5b7b5c9 100644 --- a/src/snapshot/v1/hooks/auth-permutations.ts +++ b/src/snapshot/v1/hooks/auth-permutations.ts @@ -45,7 +45,7 @@ export function useAllAuthSchemes( /** * @deprecated */ -export function useAllAuthSchemes( +export function useSuspenseAllAuthSchemes( options?: Omit< UndefinedInitialDataOptions, 'queryKey' | 'queryFn' | 'select' diff --git a/src/snapshot/v1/hooks/exhaustives.ts b/src/snapshot/v1/hooks/exhaustives.ts index b52ab62..62db93a 100644 --- a/src/snapshot/v1/hooks/exhaustives.ts +++ b/src/snapshot/v1/hooks/exhaustives.ts @@ -44,7 +44,7 @@ export function useExhaustiveFormats( /** * @deprecated */ -export function useExhaustiveFormats( +export function useSuspenseExhaustiveFormats( params?: ExhaustiveFormatsParams, options?: Omit< UndefinedInitialDataOptions< @@ -82,7 +82,7 @@ export function useExhaustiveParams( /** * @deprecated */ -export function useExhaustiveParams( +export function useSuspenseExhaustiveParams( params: ExhaustiveParamsParams, options?: Omit< UndefinedInitialDataOptions< From c82e7cb436cf93eec64fc4468ceb8dc7d335631e Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 19:22:10 -0700 Subject: [PATCH 14/21] Update CHANGELOG with duplicate function fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5304561..0f34de2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 ### Deprecated From 9db6cd05a47f3a8af0e1aa4238e2701caff59a20 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 19:57:39 -0700 Subject: [PATCH 15/21] docs: Update README with queryOptions usage and improve documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update description to reflect React Query v5 queryOptions generation - Add comprehensive Getting Started section with step-by-step setup - Add configuration examples with anonymized, generic paths - Document typesModule and clientModule configuration options - Include examples for queries, mutations, and infinite queries - Update provider example to use BasketryExampleProvider - Improve overall documentation clarity for new users 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- README.md | 168 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 3 deletions(-) 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
{/* form fields */}
; +} +``` + +### 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 }) => ( + + )) + )} + +
+ ); +} +``` + +## 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 From a66025879096c037f245016fd2a214146242be86 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 20:24:37 -0700 Subject: [PATCH 16/21] refactor: Replace NameFactory class with helper functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace NameFactory class with individual helper functions - Add missing helper functions: buildContextName, buildProviderName, buildServiceName - Update context-file.ts to use helper functions instead of class instance - Rename name-factory.ts to name-helpers.ts to better reflect its purpose - Remove CLAUDE.md from version control (added to .gitignore) This completes the migration to v0.2.0 patterns using helper functions instead of classes for better modularity and tree-shaking. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitignore | 3 ++ CLAUDE.md | 59 ------------------------------ src/context-file.ts | 21 ++++++----- src/hook-file.ts | 2 +- src/name-factory.ts | 88 --------------------------------------------- src/name-helpers.ts | 39 ++++++++++++++++++++ 6 files changed, 56 insertions(+), 156 deletions(-) delete mode 100644 CLAUDE.md delete mode 100644 src/name-factory.ts create mode 100644 src/name-helpers.ts 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/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index db77660..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,59 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -### Build and Development - -- **Build**: `npm run build` - Compiles TypeScript to JavaScript in `lib/` directory -- **Clean**: `npm run clean` - Removes build artifacts and coverage reports -- **Start**: `npm start` - Runs the compiled JavaScript entry point - -### Testing - -- **Run all tests**: `npm test` - Runs Jest test suite with coverage -- **Run specific test**: `npm test ` - Example: `npm test hook-generator.test.ts` -- **Test configuration**: Uses ts-jest preset, coverage enabled by default - -### Code Quality - -- **Lint**: `npm run lint` - Runs ESLint and Prettier checks -- **Fix linting issues**: `npm run fix` - Auto-fixes ESLint and Prettier issues -- **Create snapshot**: `npm run create-snapshot` - Generates test snapshots - -## Architecture Overview - -This is a Basketry generator plugin that creates React Query hooks from service definitions. The architecture follows a modular pattern: - -### Core Components - -1. **Generator Entry Point** (`src/index.ts`, `src/hook-generator.ts`): - - - Main generator that produces React Query hook files - - Generates three types of files: runtime utilities, context providers, and service-specific hooks - -2. **File Builders**: - - - **HookFile** (`src/hook-file.ts`): Generates React Query hooks for each service interface - - **ContextFile** (`src/context-file.ts`): Creates React Context providers for dependency injection - - **RuntimeFile** (`src/runtime-file.ts`): Provides runtime utilities for pagination and error handling - -3. **Support Modules**: - - **ModuleBuilder** (`src/module-builder.ts`): Base class for file generation with import management - - **ImportBuilder** (`src/import-builder.ts`): Manages TypeScript imports and prevents duplicates - - **NameFactory** (`src/name-factory.ts`): Generates consistent naming for hooks and functions - -### Key Design Patterns - -- **Service-Interface Pattern**: Each service interface gets its own hook file with query and mutation hooks -- **Context-Based DI**: Uses React Context for injecting HTTP client and configuration -- **Type Safety**: Integrates with @basketry/typescript for type generation -- **Relay-Style Pagination**: Built-in support for cursor-based pagination patterns - -### Dependencies - -- **basketry**: Core framework for service definition parsing -- **@basketry/typescript**: TypeScript code generation utilities -- **@tanstack/react-query**: React Query library for data fetching -- **pluralize** & **case**: String manipulation for consistent naming diff --git a/src/context-file.ts b/src/context-file.ts index bb76621..6af4bd9 100644 --- a/src/context-file.ts +++ b/src/context-file.ts @@ -1,10 +1,15 @@ import { camel, pascal } from 'case'; import { ModuleBuilder } from './module-builder'; import { ImportBuilder } from './import-builder'; -import { NameFactory } from './name-factory'; +import { + buildContextName, + buildProviderName, + buildServiceHookName, + buildServiceGetterName, + buildServiceName, +} from './name-helpers'; export class ContextFile extends ModuleBuilder { - private readonly nameFactory = new NameFactory(this.service, this.options); private readonly react = new ImportBuilder('react'); private readonly client = new ImportBuilder( this.options?.reactQuery?.clientModule ?? '../http-client', @@ -25,10 +30,10 @@ export class ContextFile extends ModuleBuilder { const FetchLike = () => this.client.type('FetchLike'); const OptionsType = () => this.client.type(optionsName); - // Use consistent naming from NameFactory - const contextName = this.nameFactory.buildContextName(); + // Use consistent naming from helper functions + const contextName = buildContextName(this.service); const contextPropsName = pascal(`${contextName}_props`); - const providerName = this.nameFactory.buildProviderName(); + const providerName = buildProviderName(this.service); yield `export interface ${contextPropsName} { fetch: ${FetchLike()}; options: ${OptionsType()}; }`; yield `const ${contextName} = ${createContext()}<${contextPropsName} | undefined>( undefined );`; @@ -45,9 +50,9 @@ export class ContextFile extends ModuleBuilder { yield `};`; for (const int of this.service.interfaces) { - const hookName = this.nameFactory.buildServiceHookName(int); - const getterName = this.nameFactory.buildServiceGetterName(int); - const localName = this.nameFactory.buildServiceName(int); + 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`); diff --git a/src/hook-file.ts b/src/hook-file.ts index eb0215e..3156757 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -28,7 +28,7 @@ import { buildMutationOptionsName, buildInfiniteQueryOptionsName, buildServiceHookName, -} from './name-factory'; +} from './name-helpers'; export class HookFile extends ModuleBuilder { constructor( diff --git a/src/name-factory.ts b/src/name-factory.ts deleted file mode 100644 index f0b943e..0000000 --- a/src/name-factory.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Interface, Method, Service, getHttpMethodByName } from 'basketry'; -import { camel, pascal } from 'case'; -import { NamespacedReactQueryOptions } from './types'; - -export class NameFactory { - constructor( - private readonly service: Service, - private readonly options?: NamespacedReactQueryOptions, - ) {} - - buildContextName(): string { - return pascal(`${this.service.title.value}_context`); - } - - buildProviderName(): string { - return pascal(`${this.service.title.value}_provider`); - } - - buildQueryOptionsName(method: Method): string { - return camel(`${method.name.value}_query_options`); - } - - buildMutationOptionsName(method: Method): string { - return camel(`${method.name.value}_mutation_options`); - } - - buildInfiniteQueryOptionsName(method: Method): string { - return camel(`${method.name.value}_infinite_query_options`); - } - - buildServiceName(int: Interface): string { - return camel(`${int.name.value}_service`); - } - - buildServiceHookName(int: Interface): string { - return camel(`use_${this.buildServiceName(int)}`); - } - - buildServiceGetterName(int: Interface): string { - return camel(`get_${this.buildServiceName(int)}`); - } - - getHookName( - method: Method, - options?: { infinite?: boolean; suspense?: boolean }, - ): string { - const name = method.name.value; - const httpMethod = getHttpMethodByName(this.service, name); - - if ( - httpMethod?.verb.value === 'get' && - name.toLocaleLowerCase().startsWith('get') - ) { - return camel( - `use_${options?.suspense ? 'suspense_' : ''}${ - options?.infinite ? 'infinite_' : '' - }${name.slice(3)}`, - ); - } - - return camel(`use_${name}`); - } -} - -export function getQueryOptionsName(method: Method): string { - return camel(`use_${method.name.value}_query_options`); -} - -// Helper functions for v0.2.0 code -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`); -} diff --git a/src/name-helpers.ts b/src/name-helpers.ts new file mode 100644 index 0000000..ed815f9 --- /dev/null +++ b/src/name-helpers.ts @@ -0,0 +1,39 @@ +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`); +} + +// Helper functions for v0.2.0 code +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`); +} From dae3074de2b65f6370481a0af43f647b0008381e Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 21:14:30 -0700 Subject: [PATCH 17/21] Update query keys to use simpler static structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace URL-based resource keys with interface/method pattern - Use camelCase for interface names in query keys (e.g., ['gizmo', 'getGizmos', params]) - Fix typo in infinite query keys (inifinite → infinite) - Simplify mutation invalidation to invalidate by interface name - Remove buildResourceKey method and related complexity This aligns the query key generation with the export-query-options branch implementation for consistency between legacy hooks and new queryOptions exports. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/hook-file.ts | 88 ++-------- src/hook-generator.ts | 7 +- src/name-helpers.ts | 1 - src/query-key-builder.ts | 6 +- src/snapshot/v1/hooks/auth-permutations.ts | 6 +- src/snapshot/v1/hooks/exhaustives.ts | 187 ++++----------------- src/snapshot/v1/hooks/gizmos.ts | 171 ++++++------------- src/snapshot/v1/hooks/widgets.ts | 16 +- 8 files changed, 118 insertions(+), 364 deletions(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index 3156757..2ec041c 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -17,7 +17,7 @@ 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'; @@ -185,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,`; @@ -214,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, })},`; @@ -390,7 +384,7 @@ export class HookFile extends ModuleBuilder { yield `export const ${infiniteOptionsName} = (${paramsExpression}) => {`; yield ` const ${serviceName} = ${this.context.fn(serviceGetterName)}();`; yield ` return ${infiniteQueryOptions()}({`; - yield ` queryKey: ${this.buildQueryKey(httpPath, method, { + yield ` queryKey: ${this.buildQueryKey(method, { includeRelayParams: false, infinite: true, })},`; @@ -449,7 +443,7 @@ export class HookFile extends ModuleBuilder { buildServiceHookName(this.int), )}()`; yield ` return ${queryOptions()}({`; - yield ` queryKey: ${this.buildQueryKey(httpPath, method, { + yield ` queryKey: ${this.buildQueryKey(method, { includeRelayParams: true, })},`; yield ` queryFn: async () => {`; @@ -476,7 +470,7 @@ export class HookFile extends ModuleBuilder { 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 () => {`; @@ -538,74 +532,26 @@ 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 interfaceName = camel(this.int.name.value); + const methodName = camel(method.name.value); - const httpMethod = getHttpMethodByName(this.service, method.name.value); - const queryParams = httpMethod?.parameters.filter((p) => - isCacheParam(p, options?.includeRelayParams ?? false), - ); - - 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}${q ? '.' : ''}['${p.name.value}']`, - ) - .join(',')}})`, - ); + if (method.parameters.length) { + queryKey.push(`params${q} || {}`); + } 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.ts b/src/hook-generator.ts index e74e81a..70059d8 100644 --- a/src/hook-generator.ts +++ b/src/hook-generator.ts @@ -1,16 +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 { QueryKeyBuilderFile } from './query-key-builder'; +import { QueryKeyBuilder } from './query-key-builder'; export const generateHooks: Generator = (service, options) => { return new HookGenerator(service, options).generate(); @@ -48,7 +45,7 @@ class HookGenerator { this.options, ), contents: format( - from(new QueryKeyBuilderFile(this.service, this.options).build()), + from(new QueryKeyBuilder(this.service, this.options).build()), this.options, ), }); diff --git a/src/name-helpers.ts b/src/name-helpers.ts index ed815f9..40900a4 100644 --- a/src/name-helpers.ts +++ b/src/name-helpers.ts @@ -5,7 +5,6 @@ export function getQueryOptionsName(method: Method): string { return camel(`use_${method.name.value}_query_options`); } -// Helper functions for v0.2.0 code export function buildServiceGetterName(int: Interface): string { return camel(`get_${int.name.value}_service`); } diff --git a/src/query-key-builder.ts b/src/query-key-builder.ts index 87582c8..0c2bb5f 100644 --- a/src/query-key-builder.ts +++ b/src/query-key-builder.ts @@ -1,6 +1,6 @@ -import { Interface, isRequired, Method, Service } from 'basketry'; +import { isRequired, Method, Service } from 'basketry'; -import { buildParamsType, buildTypeName } from '@basketry/typescript'; +import { buildParamsType } from '@basketry/typescript'; import { from } from '@basketry/typescript/lib/utils'; import { camel } from 'case'; @@ -8,7 +8,7 @@ import { NamespacedReactQueryOptions } from './types'; import { ModuleBuilder } from './module-builder'; import { ImportBuilder } from './import-builder'; -export class QueryKeyBuilderFile extends ModuleBuilder { +export class QueryKeyBuilder extends ModuleBuilder { constructor( service: Service, options: NamespacedReactQueryOptions | undefined, diff --git a/src/snapshot/v1/hooks/auth-permutations.ts b/src/snapshot/v1/hooks/auth-permutations.ts index 5b7b5c9..00d4d29 100644 --- a/src/snapshot/v1/hooks/auth-permutations.ts +++ b/src/snapshot/v1/hooks/auth-permutations.ts @@ -74,7 +74,7 @@ export function useComboAuthSchemes( } else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); } - queryClient.invalidateQueries({ queryKey: [`/authPermutations`] }); + queryClient.invalidateQueries({ queryKey: ['authPermutation'] }); return res.data; }, ...options, @@ -84,7 +84,7 @@ export function useComboAuthSchemes( const useAllAuthSchemesQueryOptions = () => { const authPermutationService = useAuthPermutationService(); return queryOptions({ - queryKey: [`/authPermutations`], + queryKey: ['authPermutation', 'allAuthSchemes', {}], queryFn: async () => { const res = await authPermutationService.allAuthSchemes(); if (res.errors.length) { @@ -101,7 +101,7 @@ const useAllAuthSchemesQueryOptions = () => { export const allAuthSchemesQueryOptions = () => { const authPermutationService = getAuthPermutationService(); return queryOptions({ - queryKey: [`/authPermutations`], + queryKey: ['authPermutation', 'allAuthSchemes', {}], queryFn: async () => { const res = await authPermutationService.allAuthSchemes(); if (res.errors.length) { diff --git a/src/snapshot/v1/hooks/exhaustives.ts b/src/snapshot/v1/hooks/exhaustives.ts index 62db93a..be6cf7c 100644 --- a/src/snapshot/v1/hooks/exhaustives.ts +++ b/src/snapshot/v1/hooks/exhaustives.ts @@ -12,213 +12,100 @@ * 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 { compact, CompositeError } from './runtime'; +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' - >, -) { +export function useExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveFormatsQueryOptions(params); - return useQuery({ ...defaultOptions, ...options }); + return useQuery({...defaultOptions, ...options}); } + /** * @deprecated */ -export function useSuspenseExhaustiveFormats( - params?: ExhaustiveFormatsParams, - options?: Omit< - UndefinedInitialDataOptions< - void, - Error, - void | undefined, - (string | Record)[] - >, - 'queryKey' | 'queryFn' | 'select' - >, -) { +export function useSuspenseExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveFormatsQueryOptions(params); - return useSuspenseQuery({ ...defaultOptions, ...options }); + return useSuspenseQuery({...defaultOptions, ...options}); } + /** * @deprecated */ -export function useExhaustiveParams( - params: ExhaustiveParamsParams, - options?: Omit< - UndefinedInitialDataOptions< - void, - Error, - void | undefined, - (string | Record)[] - >, - 'queryKey' | 'queryFn' | 'select' - >, -) { +export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveParamsQueryOptions(params); - return useQuery({ ...defaultOptions, ...options }); + return useQuery({...defaultOptions, ...options}); } + /** * @deprecated */ -export function useSuspenseExhaustiveParams( - params: ExhaustiveParamsParams, - options?: Omit< - UndefinedInitialDataOptions< - void, - Error, - void | undefined, - (string | Record)[] - >, - 'queryKey' | 'queryFn' | 'select' - >, -) { +export function useSuspenseExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useExhaustiveParamsQueryOptions(params); - return useSuspenseQuery({ ...defaultOptions, ...options }); + return useSuspenseQuery({...defaultOptions, ...options}); } + + const useExhaustiveFormatsQueryOptions = (params?: ExhaustiveFormatsParams) => { - const exhaustiveService = useExhaustiveService(); + const exhaustiveService = useExhaustiveService() return queryOptions({ - queryKey: [ - `/exhaustive`, - compact({ - 'string-no-format': params?.['string-no-format'], - 'string-date': params?.['string-date'], - 'string-date-time': params?.['string-date-time'], - 'integer-no-format': params?.['integer-no-format'], - 'integer-int32': params?.['integer-int32'], - 'integer-int64': params?.['integer-int64'], - 'number-no-format': params?.['number-no-format'], - 'number-float': params?.['number-float'], - 'number-double': params?.['number-double'], - }), - ].filter(Boolean), + 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'); - } + 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(); +export const exhaustiveFormatsQueryOptions = (params?: ExhaustiveFormatsParams) => { + const exhaustiveService = getExhaustiveService() return queryOptions({ - queryKey: [ - `/exhaustive`, - compact({ - 'string-no-format': params?.['string-no-format'], - 'string-date': params?.['string-date'], - 'string-date-time': params?.['string-date-time'], - 'integer-no-format': params?.['integer-no-format'], - 'integer-int32': params?.['integer-int32'], - 'integer-int64': params?.['integer-int64'], - 'number-no-format': params?.['number-no-format'], - 'number-float': params?.['number-float'], - 'number-double': params?.['number-double'], - }), - ].filter(Boolean), + 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'); - } + 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(); + const exhaustiveService = useExhaustiveService() return queryOptions({ - queryKey: [ - `/exhaustive/${params['pathString']}/${params['pathEnum']}/${params['pathNumber']}/${params['pathInteger']}/${params['pathBoolean']}/${params['pathStringArray']}/${params['pathEnumArray']}/${params['pathNumberArray']}/${params['pathIntegerArray']}/${params['pathBooleanArray']}`, - compact({ - 'query-string': params['query-string'], - 'query-enum': params['query-enum'], - 'query-number': params['query-number'], - 'query-integer': params['query-integer'], - 'query-boolean': params['query-boolean'], - 'query-string-array': params['query-string-array'], - 'query-enum-array': params['query-enum-array'], - 'query-number-array': params['query-number-array'], - 'query-integer-array': params['query-integer-array'], - 'query-boolean-array': params['query-boolean-array'], - }), - ].filter(Boolean), + 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'); - } + 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(); +export const exhaustiveParamsQueryOptions = (params: ExhaustiveParamsParams) => { + const exhaustiveService = getExhaustiveService() return queryOptions({ - queryKey: [ - `/exhaustive/${params['pathString']}/${params['pathEnum']}/${params['pathNumber']}/${params['pathInteger']}/${params['pathBoolean']}/${params['pathStringArray']}/${params['pathEnumArray']}/${params['pathNumberArray']}/${params['pathIntegerArray']}/${params['pathBooleanArray']}`, - compact({ - 'query-string': params['query-string'], - 'query-enum': params['query-enum'], - 'query-number': params['query-number'], - 'query-integer': params['query-integer'], - 'query-boolean': params['query-boolean'], - 'query-string-array': params['query-string-array'], - 'query-enum-array': params['query-enum-array'], - 'query-number-array': params['query-number-array'], - 'query-integer-array': params['query-integer-array'], - 'query-boolean-array': params['query-boolean-array'], - }), - ].filter(Boolean), + 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'); - } + 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, }); -}; +}; \ No newline at end of file diff --git a/src/snapshot/v1/hooks/gizmos.ts b/src/snapshot/v1/hooks/gizmos.ts index 3080a68..9da7cb0 100644 --- a/src/snapshot/v1/hooks/gizmos.ts +++ b/src/snapshot/v1/hooks/gizmos.ts @@ -12,183 +12,119 @@ * 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 { compact, CompositeError } from './runtime'; +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' - >, -) { +export function useCreateGizmo(options?: Omit, 'mutationFn'>) { const queryClient = useQueryClient(); - const gizmoService = useGizmoService(); + 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: [`/gizmos`] }); + 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' - >, -) { +export function useGizmos(params?: GetGizmosParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useGetGizmosQueryOptions(params); - return useQuery({ ...defaultOptions, ...options }); + 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' - >, -) { +export function useSuspenseGizmos(params?: GetGizmosParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { const defaultOptions = useGetGizmosQueryOptions(params); - return useSuspenseQuery({ ...defaultOptions, ...options }); + return useSuspenseQuery({...defaultOptions, ...options}); } + /** * @deprecated */ -export function useUpdateGizmo( - options?: Omit< - UseMutationOptions, - 'mutationFn' - >, -) { +export function useUpdateGizmo(options?: Omit, 'mutationFn'>) { const queryClient = useQueryClient(); - const gizmoService = useGizmoService(); + 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: [`/gizmos`] }); + 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' - >, -) { +export function useUploadGizmo(options?: Omit, 'mutationFn'>) { const queryClient = useQueryClient(); - const gizmoService = useGizmoService(); + 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: [`/gizmos/data`] }); + 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(); + const gizmoService = useGizmoService() return queryOptions({ - queryKey: [`/gizmos`, compact({ search: params?.['search'] })].filter( - Boolean, - ), + 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'); - } + 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(); + const gizmoService = getGizmoService() return queryOptions({ - queryKey: [`/gizmos`, compact({ search: params?.['search'] })].filter( - Boolean, - ), + 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'); - } + 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, @@ -200,44 +136,35 @@ export const getGizmosQueryOptions = (params?: GetGizmosParams) => { * Has a description in addition to a summary */ export const createGizmoMutationOptions = () => { - const gizmoService = getGizmoService(); + 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'); - } + 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(); + 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'); - } + 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(); + 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'); - } + 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; }, }); -}; +}; \ No newline at end of file diff --git a/src/snapshot/v1/hooks/widgets.ts b/src/snapshot/v1/hooks/widgets.ts index ef1ffe5..b5a116a 100644 --- a/src/snapshot/v1/hooks/widgets.ts +++ b/src/snapshot/v1/hooks/widgets.ts @@ -51,7 +51,7 @@ export function useCreateWidget( } else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); } - queryClient.invalidateQueries({ queryKey: [`/widgets`] }); + queryClient.invalidateQueries({ queryKey: ['widget'] }); return res.data; }, ...options, @@ -77,9 +77,7 @@ export function useDeleteWidgetFoo( } else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); } - queryClient.invalidateQueries({ - queryKey: [`/widgets/${params['id']}/foo`], - }); + queryClient.invalidateQueries({ queryKey: ['widget'] }); return res.data; }, ...options, @@ -105,7 +103,7 @@ export function usePutWidget( } else if (!res.data) { throw new Error('Unexpected data error: Failed to get example'); } - queryClient.invalidateQueries({ queryKey: [`/widgets`] }); + queryClient.invalidateQueries({ queryKey: ['widget'] }); return res.data; }, ...options, @@ -169,7 +167,7 @@ export function useSuspenseWidgets( const useGetWidgetsQueryOptions = () => { const widgetService = useWidgetService(); return queryOptions({ - queryKey: [`/widgets`], + queryKey: ['widget', 'getWidgets', {}], queryFn: async () => { const res = await widgetService.getWidgets(); if (res.errors.length) { @@ -185,7 +183,7 @@ const useGetWidgetsQueryOptions = () => { export const getWidgetsQueryOptions = () => { const widgetService = getWidgetService(); return queryOptions({ - queryKey: [`/widgets`], + queryKey: ['widget', 'getWidgets', {}], queryFn: async () => { const res = await widgetService.getWidgets(); if (res.errors.length) { @@ -228,7 +226,7 @@ export const putWidgetMutationOptions = () => { const useGetWidgetFooQueryOptions = (params: GetWidgetFooParams) => { const widgetService = useWidgetService(); return queryOptions({ - queryKey: [`/widgets/${params['id']}/foo`], + queryKey: ['widget', 'getWidgetFoo', params || {}], queryFn: async () => { const res = await widgetService.getWidgetFoo(params); if (res.errors.length) { @@ -244,7 +242,7 @@ const useGetWidgetFooQueryOptions = (params: GetWidgetFooParams) => { export const getWidgetFooQueryOptions = (params: GetWidgetFooParams) => { const widgetService = getWidgetService(); return queryOptions({ - queryKey: [`/widgets/${params['id']}/foo`], + queryKey: ['widget', 'getWidgetFoo', params || {}], queryFn: async () => { const res = await widgetService.getWidgetFoo(params); if (res.errors.length) { From d3a52d69c640c9001b9cb70a512b4e6c5e0e8087 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 21:30:33 -0700 Subject: [PATCH 18/21] Fix query key parameter syntax and build configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix incorrect 'params?' syntax in query keys (should be 'params || {}') - Add README.md to .prettierignore to prevent formatting hangs - Exclude snapshot directory from TypeScript compilation - Update generated snapshots with corrected query key syntax This fixes a TypeScript error where optional parameter syntax was incorrectly included in runtime expressions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .prettierignore | 2 + src/hook-file.ts | 2 +- src/snapshot/v1/hooks/exhaustives.ts | 129 +++++++++++++++------ src/snapshot/v1/hooks/gizmos.ts | 161 +++++++++++++++++++-------- tsconfig.json | 2 +- 5 files changed, 213 insertions(+), 83 deletions(-) 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/src/hook-file.ts b/src/hook-file.ts index 2ec041c..686582e 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -542,7 +542,7 @@ export class HookFile extends ModuleBuilder { const queryKey = [`'${interfaceName}'`, `'${methodName}'`]; if (method.parameters.length) { - queryKey.push(`params${q} || {}`); + queryKey.push(`params || {}`); } else { queryKey.push('{}'); } diff --git a/src/snapshot/v1/hooks/exhaustives.ts b/src/snapshot/v1/hooks/exhaustives.ts index be6cf7c..bfd9a78 100644 --- a/src/snapshot/v1/hooks/exhaustives.ts +++ b/src/snapshot/v1/hooks/exhaustives.ts @@ -12,100 +12,159 @@ * 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' - +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)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useSuspenseExhaustiveFormats(params?: ExhaustiveFormatsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useSuspenseQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useSuspenseExhaustiveParams(params: ExhaustiveParamsParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useSuspenseQuery({ ...defaultOptions, ...options }); } - - const useExhaustiveFormatsQueryOptions = (params?: ExhaustiveFormatsParams) => { - const exhaustiveService = useExhaustiveService() + const exhaustiveService = useExhaustiveService(); return queryOptions({ - queryKey: ['exhaustive', 'exhaustiveFormats', params? || {}], + 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'); } + 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() +export const exhaustiveFormatsQueryOptions = ( + params?: ExhaustiveFormatsParams, +) => { + const exhaustiveService = getExhaustiveService(); return queryOptions({ - queryKey: ['exhaustive', 'exhaustiveFormats', params? || {}], + 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'); } + 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() + 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'); } + 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() +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'); } + 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, }); -}; \ No newline at end of file +}; diff --git a/src/snapshot/v1/hooks/gizmos.ts b/src/snapshot/v1/hooks/gizmos.ts index 9da7cb0..b556fb0 100644 --- a/src/snapshot/v1/hooks/gizmos.ts +++ b/src/snapshot/v1/hooks/gizmos.ts @@ -12,25 +12,48 @@ * 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' - +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, 'mutationFn'>) { +export function useCreateGizmo( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { const queryClient = useQueryClient(); - const gizmoService = useGizmoService() + 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'); } + 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; }, @@ -38,38 +61,65 @@ export function useCreateGizmo(options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useQuery({ ...defaultOptions, ...options }); } - /** * Only has a summary * @deprecated */ -export function useSuspenseGizmos(params?: GetGizmosParams,options?: Omit)[]>,'queryKey' | 'queryFn' | 'select'>) { +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}); + return useSuspenseQuery({ ...defaultOptions, ...options }); } - /** * @deprecated */ -export function useUpdateGizmo(options?: Omit, 'mutationFn'>) { +export function useUpdateGizmo( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { const queryClient = useQueryClient(); - const gizmoService = useGizmoService() + 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'); } + 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; }, @@ -77,18 +127,25 @@ export function useUpdateGizmo(options?: Omit, 'mutationFn'>) { +export function useUploadGizmo( + options?: Omit< + UseMutationOptions, + 'mutationFn' + >, +) { const queryClient = useQueryClient(); - const gizmoService = useGizmoService() + 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'); } + 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; }, @@ -96,35 +153,38 @@ export function useUploadGizmo(options?: Omit { - const gizmoService = useGizmoService() + const gizmoService = useGizmoService(); return queryOptions({ - queryKey: ['gizmo', 'getGizmos', params? || {}], + 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'); } + 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() + const gizmoService = getGizmoService(); return queryOptions({ - queryKey: ['gizmo', 'getGizmos', params? || {}], + 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'); } + 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, @@ -136,35 +196,44 @@ export const getGizmosQueryOptions = (params?: GetGizmosParams) => { * Has a description in addition to a summary */ export const createGizmoMutationOptions = () => { - const gizmoService = getGizmoService() + 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'); } + 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() + 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'); } + 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() + 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'); } + 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; }, }); -}; \ No newline at end of file +}; 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/**/*"] } From 9c7ba9872682915468fce0c191e13cb508b1551c Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 21:48:47 -0700 Subject: [PATCH 19/21] Remove unused variable in buildQueryKey method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'q' variable was no longer needed after simplifying the query key generation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/hook-file.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hook-file.ts b/src/hook-file.ts index 686582e..af0f3a3 100644 --- a/src/hook-file.ts +++ b/src/hook-file.ts @@ -535,7 +535,6 @@ export class HookFile extends ModuleBuilder { method: Method, options?: { includeRelayParams?: boolean; infinite?: boolean }, ): string { - const q = method.parameters.every((param) => !isRequired(param)) ? '?' : ''; const interfaceName = camel(this.int.name.value); const methodName = camel(method.name.value); From 9acd69647af2bb1bb846cbf223499fbe1dbeb034 Mon Sep 17 00:00:00 2001 From: Kyle Mazza Date: Sun, 20 Jul 2025 21:51:45 -0700 Subject: [PATCH 20/21] Update CHANGELOG with recent changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document query key improvements, bug fixes, and internal refactoring 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f34de2..d62cb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 @@ -28,6 +33,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 From 10bf9a45d92b7ef5b7acd3de29014054d5efa3c8 Mon Sep 17 00:00:00 2001 From: kyleamazza Date: Mon, 21 Jul 2025 05:07:23 +0000 Subject: [PATCH 21/21] 0.2.0-alpha.3 Co-authored-by: github-actions[bot] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 4cac65c..625192b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@basketry/react-query", - "version": "0.0.0", + "version": "0.2.0-alpha.3", "description": "Basketry generator for generating React Query hooks", "main": "./lib/index.js", "scripts": {