fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #1680 from josh-hemphill/main

feat: add @pinia-colada/sdk plugin

authored by

Lubos and committed by
GitHub
aa0bfaac 6184a272

+817 -1
+5
.changeset/smooth-ties-brush.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + feat(plugin): add `@pinia/colada` plugin
+9
packages/openapi-ts-tests/main/test/plugins.test.ts
··· 363 363 description: 364 364 'generate Fetch API client with TanStack Vue Query plugin with custom names', 365 365 }, 366 + // TODO: add Pinia Colada snapshots 367 + // { 368 + // config: createConfig({ 369 + // output: 'fetch', 370 + // plugins: ['@pinia/colada', '@hey-api/client-fetch'], 371 + // }), 372 + // description: 373 + // 'generate Fetch API client with Pinia Colada plugin', 374 + // }, 366 375 { 367 376 config: createConfig({ 368 377 output: 'default',
+2 -1
packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts
··· 9 9 import { typesId } from './ref'; 10 10 import type { HeyApiTypeScriptPlugin, PluginState } from './types'; 11 11 12 - const irParametersToIrSchema = ({ 12 + // TODO: exported just for @pinia/colada, remove export once that plugin does not depend on it 13 + export const irParametersToIrSchema = ({ 13 14 parameters, 14 15 }: { 15 16 parameters: Record<string, IR.ParameterObject>;
+26
packages/openapi-ts/src/plugins/@pinia/colada/config.ts
··· 1 + import { definePluginConfig } from '../../shared/utils/config'; 2 + import { handler } from './plugin'; 3 + import type { PiniaColadaPlugin } from './types'; 4 + 5 + export const defaultConfig: PiniaColadaPlugin['Config'] = { 6 + config: { 7 + enablePaginationOnKey: undefined, 8 + errorHandling: 'specific', 9 + exportFromIndex: false, 10 + groupByTag: false, 11 + importPath: '@pinia/colada', 12 + includeTypes: true, 13 + prefixUse: true, 14 + suffixQueryMutation: true, 15 + useInfiniteQueries: false, 16 + }, 17 + dependencies: ['@hey-api/typescript'], 18 + handler: handler as PiniaColadaPlugin['Handler'], 19 + name: '@pinia/colada', 20 + output: '@pinia/colada', 21 + }; 22 + 23 + /** 24 + * Type helper for `@pinia/colada` plugin, returns {@link Plugin.Config} object 25 + */ 26 + export const defineConfig = definePluginConfig(defaultConfig);
+2
packages/openapi-ts/src/plugins/@pinia/colada/index.ts
··· 1 + export { defaultConfig, defineConfig } from './config'; 2 + export type { PiniaColadaPlugin } from './types';
+29
packages/openapi-ts/src/plugins/@pinia/colada/mutation.ts
··· 1 + import type { GeneratedFile } from '../../../generate/file'; 2 + import type { IR } from '../../../ir/types'; 3 + import type { PiniaColadaPlugin } from './types'; 4 + import { createComposable } from './utils'; 5 + 6 + /** 7 + * Creates a mutation function for an operation 8 + */ 9 + export const createMutationFunction = ({ 10 + context, 11 + file, 12 + operation, 13 + plugin, 14 + }: { 15 + context: IR.Context; 16 + file: GeneratedFile; 17 + operation: IR.OperationObject; 18 + plugin: PiniaColadaPlugin['Instance']; 19 + }) => { 20 + // Allow hooks to customize or skip mutation generation 21 + if ( 22 + plugin.config.onMutation && 23 + plugin.config.onMutation(operation) === false 24 + ) { 25 + return; 26 + } 27 + 28 + createComposable({ context, file, isQuery: false, operation, plugin }); 29 + };
+67
packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts
··· 1 + import { clientId } from '../../@hey-api/client-core/utils'; 2 + import { createMutationFunction } from './mutation'; 3 + import { createQueryFunction } from './query'; 4 + import type { PiniaColadaPlugin } from './types'; 5 + import { isQuery } from './utils'; 6 + 7 + export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { 8 + if (!plugin.config.groupByTag) { 9 + plugin.createFile({ 10 + id: plugin.name, 11 + path: plugin.output, 12 + }); 13 + } 14 + 15 + // Create files based on grouping strategy 16 + const getFile = (tag: string) => { 17 + if (!plugin.config.groupByTag) { 18 + return ( 19 + plugin.context.file({ id: plugin.name }) ?? 20 + plugin.createFile({ 21 + id: plugin.name, 22 + path: plugin.output, 23 + }) 24 + ); 25 + } 26 + 27 + const fileId = `${plugin.name}/${tag}`; 28 + return ( 29 + plugin.context.file({ id: fileId }) ?? 30 + plugin.createFile({ 31 + id: fileId, 32 + path: `${plugin.output}/${tag}`, 33 + }) 34 + ); 35 + }; 36 + 37 + plugin.forEach('operation', ({ operation }) => { 38 + const file = getFile(operation.tags?.[0] || 'default'); 39 + 40 + // Determine if the operation should be a query or mutation 41 + if (isQuery(operation, plugin)) { 42 + createQueryFunction({ context: plugin.context, file, operation, plugin }); 43 + } else { 44 + createMutationFunction({ 45 + context: plugin.context, 46 + file, 47 + operation, 48 + plugin, 49 + }); 50 + } 51 + }); 52 + 53 + // Add client import to all generated files 54 + Object.entries(plugin.context.files).forEach(([fileId, file]) => { 55 + if (fileId.startsWith(plugin.name)) { 56 + // Make sure we have a client import 57 + file.import({ 58 + alias: '_heyApiClient', 59 + module: file.relativePathToFile({ 60 + context: plugin.context, 61 + id: clientId, 62 + }), 63 + name: 'client', 64 + }); 65 + } 66 + }); 67 + };
+25
packages/openapi-ts/src/plugins/@pinia/colada/query.ts
··· 1 + import type { GeneratedFile } from '../../../generate/file'; 2 + import type { IR } from '../../../ir/types'; 3 + import type { PiniaColadaPlugin } from './types'; 4 + import { createComposable } from './utils'; 5 + 6 + /** 7 + * Creates a query function for an operation 8 + */ 9 + export const createQueryFunction = ({ 10 + context, 11 + file, 12 + operation, 13 + plugin, 14 + }: { 15 + context: IR.Context; 16 + file: GeneratedFile; 17 + operation: IR.OperationObject; 18 + plugin: PiniaColadaPlugin['Instance']; 19 + }) => { 20 + if (plugin.config.onQuery && plugin.config.onQuery(operation) === false) { 21 + return; 22 + } 23 + 24 + createComposable({ context, file, isQuery: true, operation, plugin }); 25 + };
+217
packages/openapi-ts/src/plugins/@pinia/colada/types.d.ts
··· 1 + import type { IR } from '../../../ir/types'; 2 + // import type { StringCase, StringName } from '../../../types/case'; 3 + import type { DefinePlugin, Plugin } from '../../types'; 4 + 5 + export type UserConfig = Plugin.Name<'@pinia/colada'> & { 6 + /** 7 + * Default cache time for queries in milliseconds. 8 + * 9 + * @default undefined 10 + */ 11 + defaultCacheTime?: number; 12 + /** 13 + * Default stale time for queries in milliseconds. 14 + * 15 + * @default undefined 16 + */ 17 + defaultStaleTime?: number; 18 + /** 19 + * Enable pagination support on this key when found in the query parameters or body. 20 + * 21 + * @default undefined 22 + */ 23 + enablePaginationOnKey?: string; 24 + /** 25 + * How to handle error responses. 26 + * 'unified' - Unified error type for all errors 27 + * 'specific' - Specific error types per operation 28 + * 29 + * @default 'specific' 30 + */ 31 + errorHandling?: 'unified' | 'specific'; 32 + /** 33 + * Should the exports from the generated files be re-exported in the index barrel file? 34 + * 35 + * @default false 36 + */ 37 + exportFromIndex?: boolean; 38 + /** 39 + * Group operations by tag into separate files. 40 + * 41 + * @default false 42 + */ 43 + groupByTag?: boolean; 44 + /** 45 + * Import path for the plugin. 46 + * 47 + * @default '@pinia/colada' 48 + */ 49 + importPath?: string; 50 + /** 51 + * Include types in the generated files. 52 + * 53 + * @default true 54 + */ 55 + includeTypes?: boolean; 56 + /** 57 + * Custom hook to customize or skip mutation generation. 58 + * Return false to skip generating a mutation for this operation. 59 + * 60 + * @default undefined 61 + */ 62 + onMutation?: (operation: IR.OperationObject) => boolean | undefined; 63 + /** 64 + * Custom hook to customize or skip query generation. 65 + * Return false to skip generating a query for this operation. 66 + * 67 + * @default undefined 68 + */ 69 + onQuery?: (operation: IR.OperationObject) => boolean | undefined; 70 + /** 71 + * Plugin output path. 72 + * 73 + * @default '@pinia/colada' 74 + */ 75 + output: string; 76 + /** 77 + * Whether to prefix generated function names with 'use'. 78 + * 79 + * @default true 80 + */ 81 + prefixUse?: boolean; 82 + /** 83 + * Custom hook that determines if an operation should be a query or not. 84 + * Return true to force query, false to force mutation, undefined to use default logic. 85 + * 86 + * @default undefined 87 + */ 88 + resolveQuery?: (operation: IR.OperationObject) => boolean | undefined; 89 + /** 90 + * Custom hook to resolve query key. 91 + * Default is [operation.tags?.[0] || 'default', operation.id] 92 + * 93 + * @default undefined 94 + */ 95 + resolveQueryKey?: (operation: IR.OperationObject) => Array<string>; 96 + /** 97 + * Whether to suffix generated function names with 'Query' or 'Mutation' to indicate the type 98 + * of Pinia Colada operation that is used under the hood. 99 + * 100 + * @default true 101 + */ 102 + suffixQueryMutation?: boolean; 103 + /** 104 + * Use infinite queries. 105 + * 106 + * @default false 107 + */ 108 + useInfiniteQueries?: boolean; 109 + }; 110 + 111 + export type Config = Plugin.Name<'@pinia/colada'> & { 112 + /** 113 + * Default cache time for queries in milliseconds. 114 + * 115 + * @default undefined 116 + */ 117 + defaultCacheTime: number | undefined; 118 + /** 119 + * Default stale time for queries in milliseconds. 120 + * 121 + * @default undefined 122 + */ 123 + defaultStaleTime: number | undefined; 124 + /** 125 + * Enable pagination support on this key when found in the query parameters or body. 126 + * 127 + * @default undefined 128 + */ 129 + enablePaginationOnKey?: string; 130 + /** 131 + * How to handle error responses. 132 + * 'unified' - Unified error type for all errors 133 + * 'specific' - Specific error types per operation 134 + * 135 + * @default 'specific' 136 + */ 137 + errorHandling?: 'unified' | 'specific'; 138 + /** 139 + * Should the exports from the generated files be re-exported in the index barrel file? 140 + * 141 + * @default false 142 + */ 143 + exportFromIndex: boolean; 144 + /** 145 + * Group operations by tag into separate files. 146 + * 147 + * @default false 148 + */ 149 + groupByTag?: boolean; 150 + /** 151 + * Import path for the plugin. 152 + * 153 + * @default '@pinia/colada' 154 + */ 155 + importPath?: string; 156 + /** 157 + * Include types in the generated files. 158 + * 159 + * @default true 160 + */ 161 + includeTypes?: boolean; 162 + /** 163 + * Custom hook to customize or skip mutation generation. 164 + * Return false to skip generating a mutation for this operation. 165 + * 166 + * @default undefined 167 + */ 168 + onMutation?: (operation: IR.OperationObject) => boolean | undefined; 169 + /** 170 + * Custom hook to customize or skip query generation. 171 + * Return false to skip generating a query for this operation. 172 + * 173 + * @default undefined 174 + */ 175 + onQuery?: (operation: IR.OperationObject) => boolean | undefined; 176 + /** 177 + * Plugin output path. 178 + * 179 + * @default '@pinia/colada' 180 + */ 181 + output: string; 182 + /** 183 + * Whether to prefix generated function names with 'use'. 184 + * 185 + * @default true 186 + */ 187 + prefixUse?: boolean; 188 + /** 189 + * Custom hook that determines if an operation should be a query or not. 190 + * Return true to force query, false to force mutation, undefined to use default logic. 191 + * 192 + * @default undefined 193 + */ 194 + resolveQuery?: (operation: IR.OperationObject) => boolean | undefined; 195 + /** 196 + * Custom hook to resolve query key. 197 + * Default is [operation.tags?.[0] || 'default', operation.id] 198 + * 199 + * @default undefined 200 + */ 201 + resolveQueryKey?: (operation: IR.OperationObject) => Array<string>; 202 + /** 203 + * Whether to suffix generated function names with 'Query' or 'Mutation' to indicate the type 204 + * of Pinia Colada operation that is used under the hood. 205 + * 206 + * @default true 207 + */ 208 + suffixQueryMutation?: boolean; 209 + /** 210 + * Use infinite queries. 211 + * 212 + * @default false 213 + */ 214 + useInfiniteQueries?: boolean; 215 + }; 216 + 217 + export type PiniaColadaPlugin = DefinePlugin<UserConfig, Config>;
+430
packages/openapi-ts/src/plugins/@pinia/colada/utils.ts
··· 1 + import type { TypeNode } from 'typescript'; 2 + 3 + import type { GeneratedFile } from '../../../generate/file'; 4 + import type { IR } from '../../../ir/types'; 5 + import type { Property } from '../../../tsc'; 6 + import { tsc } from '../../../tsc'; 7 + import { escapeComment } from '../../../utils/escape'; 8 + import { stringCase } from '../../../utils/stringCase'; 9 + import { irParametersToIrSchema } from '../../@hey-api/typescript/operation'; 10 + import { schemaToType } from '../../@hey-api/typescript/plugin'; 11 + import type { PiniaColadaPlugin } from './types'; 12 + 13 + const importIdentifierData = () => ({ 14 + name: '', 15 + }); 16 + const importIdentifierError = () => ({ 17 + name: '', 18 + }); 19 + const importIdentifierResponse = () => ({ 20 + name: '', 21 + }); 22 + 23 + /** 24 + * Determines if an operation should be a query or mutation 25 + */ 26 + export const isQuery = ( 27 + operation: IR.OperationObject, 28 + plugin: PiniaColadaPlugin['Instance'], 29 + ): boolean => { 30 + // 1. Check for hook override 31 + const hookResult = plugin.config.resolveQuery?.(operation); 32 + if (hookResult !== undefined) { 33 + return hookResult; 34 + } 35 + 36 + // 2. Use method as primary signal 37 + if (['get', 'head', 'options'].includes(operation.method)) { 38 + return true; 39 + } 40 + 41 + // 3. Consider body presence as secondary signal 42 + // If method is not GET/HEAD/OPTIONS but also has no body schema, likely a query 43 + return !operation.body?.schema; 44 + }; 45 + 46 + /** 47 + * Generates the cache configuration object for a query 48 + */ 49 + export const generateCacheConfig = ( 50 + operation: IR.OperationObject, 51 + plugin: PiniaColadaPlugin['Instance'], 52 + ) => { 53 + const obj: Array<{ 54 + key: string; 55 + value: any; 56 + }> = []; 57 + 58 + // Use default stale time if specified in config 59 + if (plugin.config.defaultStaleTime !== undefined) { 60 + obj.push({ 61 + key: 'staleTime', 62 + value: plugin.config.defaultStaleTime, 63 + }); 64 + } 65 + 66 + // Use default cache time if specified in config 67 + if (plugin.config.defaultCacheTime !== undefined) { 68 + obj.push({ 69 + key: 'gcTime', 70 + value: plugin.config.defaultCacheTime, 71 + }); 72 + } 73 + 74 + // Add pagination config if enabled and operation has pagination parameters 75 + if ( 76 + plugin.config.enablePaginationOnKey && 77 + hasPagination(operation, plugin.config.enablePaginationOnKey) 78 + ) { 79 + obj.push({ 80 + key: 'infinite', 81 + value: true, 82 + }); 83 + } 84 + 85 + return obj; 86 + }; 87 + 88 + /** 89 + * Checks if operation has pagination parameters 90 + */ 91 + export const hasPagination = ( 92 + operation: IR.OperationObject, 93 + paginationParam: string, 94 + ): boolean => 95 + // Check if operation has pagination parameter 96 + !!operation.parameters?.query?.[paginationParam] || 97 + !!operation.body?.pagination; 98 + 99 + /** 100 + * Generates the function name for an operation 101 + */ 102 + export const generateFunctionName = ( 103 + operation: IR.OperationObject, 104 + isQueryType: boolean, 105 + prefixUse: boolean = true, 106 + suffixQueryMutation: boolean = true, 107 + ): string => { 108 + const operationPascalCase = stringCase({ 109 + case: 'PascalCase', 110 + value: operation.id, 111 + }); 112 + const prefix = prefixUse ? 'use' : ''; 113 + const suffix = suffixQueryMutation 114 + ? isQueryType 115 + ? 'Query' 116 + : 'Mutation' 117 + : ''; 118 + return `${prefix}${operationPascalCase}${suffix}`; 119 + }; 120 + 121 + const parametersPluralizedNames = [ 122 + 'query', 123 + 'path', 124 + 'headers', 125 + 'body', 126 + 'cookies', 127 + ] as const; 128 + type ParamNames = (typeof parametersPluralizedNames)[number]; 129 + // Define a conditional type to transform the names 130 + type NonPluralizedName<T extends ParamNames> = T extends 'headers' 131 + ? 'header' 132 + : T extends 'cookies' 133 + ? 'cookie' 134 + : T; 135 + function getNonPluralizedName<T extends ParamNames>( 136 + name: T, 137 + ): NonPluralizedName<T> { 138 + return ( 139 + ['headers', 'cookies'].includes(name) ? name.slice(0, -1) : name 140 + ) as NonPluralizedName<T>; 141 + } 142 + type DataKeyNames = Exclude<ParamNames, 'cookies'>; 143 + function getDataSubType(identifier: string, dataKey: DataKeyNames) { 144 + return tsc.indexedAccessTypeNode({ 145 + indexType: tsc.literalTypeNode({ 146 + literal: tsc.stringLiteral({ 147 + text: dataKey, 148 + }), 149 + }), 150 + objectType: tsc.typeReferenceNode({ 151 + typeName: identifier, 152 + }), 153 + }); 154 + } 155 + 156 + function createParameterConst( 157 + name: ParamNames, 158 + operation?: IR.OperationObject, 159 + ) { 160 + const nonPluralizedName = getNonPluralizedName(name); 161 + if (nonPluralizedName === 'body' && !operation?.body?.schema) return []; 162 + if ( 163 + nonPluralizedName !== 'body' && 164 + !operation?.parameters?.[nonPluralizedName] 165 + ) 166 + return []; 167 + return [ 168 + tsc.constVariable({ 169 + expression: tsc.callExpression({ 170 + functionName: 'toRef', 171 + parameters: [getParameterQualifiedName(name)], 172 + }), 173 + name: `${name}Ref`, 174 + }), 175 + ]; 176 + } 177 + function getParameterQualifiedName(name: ParamNames) { 178 + return tsc.propertyAccessExpression({ 179 + expression: 'params', 180 + isOptional: true, 181 + name, 182 + }); 183 + } 184 + /** 185 + * Creates a composable function for an operation 186 + */ 187 + export const createComposable = ({ 188 + file, 189 + isQuery, 190 + operation, 191 + plugin, 192 + }: { 193 + context: IR.Context; 194 + file: GeneratedFile; 195 + isQuery: boolean; 196 + operation: IR.OperationObject; 197 + plugin: PiniaColadaPlugin['Instance']; 198 + }) => { 199 + // Import necessary functions and types 200 + file.import({ 201 + module: '@pinia/colada', 202 + name: isQuery ? 'useQuery' : 'useMutation', 203 + }); 204 + file.import({ 205 + asType: true, 206 + module: '@pinia/colada', 207 + name: `Use${isQuery ? 'Query' : 'Mutation'}Options`, 208 + }); 209 + file.import({ 210 + module: 'vue', 211 + name: 'toRef', 212 + }); 213 + 214 + // Get query key from hooks or generate default 215 + const queryKey = plugin.config.resolveQueryKey?.(operation) ?? [ 216 + operation.tags?.[0] || 'default', 217 + operation.id, 218 + ]; 219 + 220 + // Get identifiers for data, response and error types 221 + const identifierData = importIdentifierData(); 222 + const identifierResponse = importIdentifierResponse(); 223 + const identifierError = importIdentifierError(); 224 + 225 + /** 226 + * Creates a parameter for a composable function 227 + */ 228 + function createParameter( 229 + name: ParamNames, 230 + operation?: IR.OperationObject, 231 + ): Array<Property> { 232 + const nonPluralizedName = getNonPluralizedName(name); 233 + if (nonPluralizedName === 'body' && !operation?.body?.schema) return []; 234 + if ( 235 + nonPluralizedName !== 'body' && 236 + !operation?.parameters?.[nonPluralizedName] 237 + ) 238 + return []; 239 + let type: TypeNode = tsc.keywordTypeNode({ keyword: 'unknown' }); 240 + if (nonPluralizedName === 'cookie') { 241 + type = 242 + schemaToType({ 243 + onRef: undefined, 244 + plugin: plugin as any, 245 + schema: irParametersToIrSchema({ 246 + parameters: operation?.parameters?.cookie || {}, 247 + }), 248 + state: { 249 + usedTypeIDs: new Set(), 250 + }, 251 + }) ?? type; 252 + } else if (name !== 'cookies') { 253 + type = identifierData.name 254 + ? getDataSubType(identifierData.name, name) 255 + : type; 256 + } 257 + return [ 258 + { 259 + name, 260 + type, 261 + }, 262 + ]; 263 + } 264 + const parameters = parametersPluralizedNames.flatMap((name) => 265 + createParameter(name, operation), 266 + ); 267 + 268 + // Create the composable function 269 + const node = tsc.constVariable({ 270 + comment: [ 271 + operation.deprecated && '@deprecated', 272 + operation.summary && escapeComment(operation.summary), 273 + operation.description && escapeComment(operation.description), 274 + ].filter(Boolean), 275 + exportConst: true, 276 + expression: tsc.arrowFunction({ 277 + async: true, 278 + parameters: [ 279 + { 280 + isRequired: parameters.length > 0, 281 + name: 'params', 282 + type: tsc.typeInterfaceNode({ 283 + properties: parameters, 284 + useLegacyResolution: true, 285 + }), 286 + }, 287 + // Additional Pinia Colada options 288 + { 289 + isRequired: false, 290 + name: 'options', 291 + type: tsc.typeReferenceNode({ 292 + typeName: isQuery 293 + ? `UseQueryOptions<${identifierResponse.name || 'unknown'}, ${identifierError.name || 'unknown'}, ${identifierData.name || 'unknown'}>` 294 + : `UseMutationOptions<${identifierResponse.name || 'unknown'}, ${identifierData.name || 'unknown'}, ${identifierError.name || 'unknown'}>`, 295 + }), 296 + }, 297 + ], 298 + statements: [ 299 + // Create reactive refs for parameters 300 + ...parametersPluralizedNames.flatMap((name) => 301 + createParameterConst(name, operation), 302 + ), 303 + 304 + // Create query/mutation result 305 + tsc.constVariable({ 306 + expression: tsc.callExpression({ 307 + functionName: isQuery ? 'useQuery' : 'useMutation', 308 + parameters: [ 309 + tsc.objectExpression({ 310 + obj: [ 311 + // Query/mutation function 312 + { 313 + key: isQuery ? 'query' : 'mutation', 314 + value: tsc.callExpression({ 315 + functionName: '_heyApiClient', 316 + parameters: [ 317 + tsc.objectExpression({ 318 + obj: [ 319 + { 320 + key: 'method', 321 + value: operation.method, 322 + }, 323 + { 324 + key: 'url', 325 + value: operation.path, 326 + }, 327 + // Add data if it's a valid body parameter (mutations only) 328 + ...parametersPluralizedNames.flatMap((name) => { 329 + const nonPluralizedName = 330 + getNonPluralizedName(name); 331 + if ( 332 + nonPluralizedName === 'body' && 333 + !operation?.body?.schema 334 + ) 335 + return []; 336 + if ( 337 + nonPluralizedName !== 'body' && 338 + !operation?.parameters?.[nonPluralizedName] 339 + ) 340 + return []; 341 + return [ 342 + { 343 + key: 344 + nonPluralizedName === 'body' 345 + ? 'data' 346 + : name, 347 + value: tsc.identifier({ text: `${name}Ref` }), 348 + }, 349 + ]; 350 + }), 351 + ].filter(Boolean), 352 + }), 353 + ], 354 + }), 355 + }, 356 + // Query key (optional for mutations) 357 + { 358 + key: 'key', 359 + value: tsc.arrayLiteralExpression({ 360 + elements: [ 361 + ...queryKey.map((k: string) => tsc.ots.string(k)), 362 + // Add path params to query key if they exist 363 + ...parametersPluralizedNames.flatMap((name) => { 364 + const nonPluralizedName = getNonPluralizedName(name); 365 + if ( 366 + nonPluralizedName === 'body' && 367 + !operation?.body?.schema 368 + ) 369 + return []; 370 + if ( 371 + nonPluralizedName !== 'body' && 372 + !operation?.parameters?.[nonPluralizedName] 373 + ) 374 + return []; 375 + return [tsc.identifier({ text: `${name}Ref` })]; 376 + }), 377 + ], 378 + }), 379 + }, 380 + // Spread additional options 381 + { 382 + spread: 'options', 383 + }, 384 + ], 385 + }), 386 + ], 387 + }), 388 + name: isQuery ? 'queryResult' : 'mutationResult', 389 + }), 390 + 391 + // Return useQuery/useMutation call with reactive parameters 392 + tsc.returnStatement({ 393 + expression: tsc.objectExpression({ 394 + obj: [ 395 + // Spread the query/mutation result 396 + { 397 + spread: isQuery ? 'queryResult' : 'mutationResult', 398 + }, 399 + // Return reactive parameters 400 + ...parametersPluralizedNames.flatMap((name) => { 401 + const nonPluralizedName = getNonPluralizedName(name); 402 + if (nonPluralizedName === 'body' && !operation?.body?.schema) 403 + return []; 404 + if ( 405 + nonPluralizedName !== 'body' && 406 + !operation?.parameters?.[nonPluralizedName] 407 + ) 408 + return []; 409 + return [ 410 + { 411 + key: name, 412 + value: tsc.identifier({ text: `${name}Ref` }), 413 + }, 414 + ]; 415 + }), 416 + ], 417 + }), 418 + }), 419 + ], 420 + }), 421 + name: generateFunctionName( 422 + operation, 423 + isQuery, 424 + plugin.config.prefixUse, 425 + plugin.config.suffixQueryMutation, 426 + ), 427 + }); 428 + 429 + file.add(node); 430 + };
+4
packages/openapi-ts/src/plugins/config.ts
··· 28 28 import { defaultConfig as heyApiTransformers } from './@hey-api/transformers'; 29 29 import type { HeyApiTypeScriptPlugin } from './@hey-api/typescript'; 30 30 import { defaultConfig as heyApiTypeScript } from './@hey-api/typescript'; 31 + import type { PiniaColadaPlugin } from './@pinia/colada'; 32 + import { defaultConfig as piniaColada } from './@pinia/colada'; 31 33 import type { TanStackAngularQueryPlugin } from './@tanstack/angular-query-experimental'; 32 34 import { defaultConfig as tanStackAngularQuery } from './@tanstack/angular-query-experimental'; 33 35 import type { TanStackReactQueryPlugin } from './@tanstack/react-query'; ··· 57 59 '@hey-api/sdk': HeyApiSdkPlugin['Types']; 58 60 '@hey-api/transformers': HeyApiTransformersPlugin['Types']; 59 61 '@hey-api/typescript': HeyApiTypeScriptPlugin['Types']; 62 + '@pinia/colada': PiniaColadaPlugin['Types']; 60 63 '@tanstack/angular-query-experimental': TanStackAngularQueryPlugin['Types']; 61 64 '@tanstack/react-query': TanStackReactQueryPlugin['Types']; 62 65 '@tanstack/solid-query': TanStackSolidQueryPlugin['Types']; ··· 85 88 '@hey-api/sdk': heyApiSdk, 86 89 '@hey-api/transformers': heyApiTransformers, 87 90 '@hey-api/typescript': heyApiTypeScript, 91 + '@pinia/colada': piniaColada, 88 92 '@tanstack/angular-query-experimental': tanStackAngularQuery, 89 93 '@tanstack/react-query': tanStackReactQuery, 90 94 '@tanstack/solid-query': tanStackSolidQuery,
+1
packages/openapi-ts/src/plugins/types.d.ts
··· 26 26 | '@hey-api/sdk' 27 27 | '@hey-api/transformers' 28 28 | '@hey-api/typescript' 29 + | '@pinia/colada' 29 30 | '@tanstack/angular-query-experimental' 30 31 | '@tanstack/react-query' 31 32 | '@tanstack/solid-query'