@codewithagents/openapi-react-query 3.5.1 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-args.d.ts +23 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +37 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.js +25 -17
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/hooks.js +7 -7
- package/dist/plugins/hooks.js.map +1 -1
- package/package.json +5 -2
- package/CHANGELOG.md +0 -161
- package/CLAUDE.md +0 -30
- package/__fixtures__/specs/task-hooks.json +0 -170
- package/src/__tests__/__snapshots__/hooks.test.ts.snap +0 -457
- package/src/__tests__/compat-matrix.test.ts +0 -25
- package/src/__tests__/config.test.ts +0 -291
- package/src/__tests__/hooks.property.test.ts +0 -310
- package/src/__tests__/hooks.test.ts +0 -1845
- package/src/__tests__/prettier-output.test.ts +0 -45
- package/src/__tests__/test-utils.test.ts +0 -69
- package/src/cli.ts +0 -27
- package/src/config.ts +0 -171
- package/src/generator.ts +0 -50
- package/src/plugins/hooks.ts +0 -823
- package/src/plugins/test-utils.ts +0 -77
- package/stryker.config.json +0 -19
- package/tsconfig.build.json +0 -10
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -12
package/src/plugins/hooks.ts
DELETED
|
@@ -1,823 +0,0 @@
|
|
|
1
|
-
import type { OpenAPIV3_1 } from 'openapi-types'
|
|
2
|
-
|
|
3
|
-
type OperationObject = OpenAPIV3_1.OperationObject
|
|
4
|
-
type ReferenceObject = OpenAPIV3_1.ReferenceObject
|
|
5
|
-
|
|
6
|
-
export interface HookGenOptions {
|
|
7
|
-
staleTime: number
|
|
8
|
-
gcTime: number
|
|
9
|
-
suspense?: boolean
|
|
10
|
-
overrides?: Record<string, { staleTime: number; gcTime: number }>
|
|
11
|
-
autoInvalidate?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
function isRef(obj: unknown): obj is ReferenceObject {
|
|
17
|
-
return typeof obj === 'object' && obj !== null && '$ref' in obj
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function refToName(ref: string): string {
|
|
21
|
-
const parts = ref.split('/')
|
|
22
|
-
return parts[parts.length - 1]!
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function capitalize(s: string): string {
|
|
26
|
-
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const SUPPORTED_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const
|
|
30
|
-
type SupportedMethod = (typeof SUPPORTED_METHODS)[number]
|
|
31
|
-
|
|
32
|
-
// Mirrors deriveOperationName from openapi-gen/src/plugins/client.ts
|
|
33
|
-
function deriveOperationName(method: string, path: string): string {
|
|
34
|
-
const prefixMap: Record<string, string> = {
|
|
35
|
-
get: 'get',
|
|
36
|
-
post: 'create',
|
|
37
|
-
put: 'update',
|
|
38
|
-
patch: 'patch',
|
|
39
|
-
delete: 'delete',
|
|
40
|
-
}
|
|
41
|
-
const prefix = prefixMap[method] ?? method
|
|
42
|
-
|
|
43
|
-
// Strip /api/v1/ prefix
|
|
44
|
-
let segments = path.replace(/^\/api\/v\d+\//, '').replace(/^\//, '')
|
|
45
|
-
|
|
46
|
-
const parts = segments.split('/').map((seg) => {
|
|
47
|
-
// Handle mixed segments like "{maxLat}.{format}" — extract each {param} inside
|
|
48
|
-
const paramMatches = seg.match(/\{([^}]+)\}/g)
|
|
49
|
-
if (paramMatches !== null && !(seg.startsWith('{') && seg.endsWith('}'))) {
|
|
50
|
-
return paramMatches
|
|
51
|
-
.map((m) => {
|
|
52
|
-
const name = sanitizeOperationId(m.slice(1, -1))
|
|
53
|
-
return 'By' + name.charAt(0).toUpperCase() + name.slice(1)
|
|
54
|
-
})
|
|
55
|
-
.join('')
|
|
56
|
-
}
|
|
57
|
-
if (seg.startsWith('{') && seg.endsWith('}')) {
|
|
58
|
-
const name = seg.slice(1, -1)
|
|
59
|
-
const sanitized = sanitizeOperationId(name)
|
|
60
|
-
return 'By' + sanitized.charAt(0).toUpperCase() + sanitized.slice(1)
|
|
61
|
-
}
|
|
62
|
-
const sanitized = sanitizeOperationId(seg)
|
|
63
|
-
return sanitized.charAt(0).toUpperCase() + sanitized.slice(1)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
const joined = parts.join('')
|
|
67
|
-
return prefix + joined
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function extractPathParams(path: string): string[] {
|
|
71
|
-
const matches = path.match(/\{([^}]+)\}/g)
|
|
72
|
-
if (matches === null) return []
|
|
73
|
-
return matches.map((m) => sanitizeOperationId(m.slice(1, -1)))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Extract primary resource name from path (first static segment after stripping API prefix) */
|
|
77
|
-
function primaryResource(path: string): string {
|
|
78
|
-
// Strip /api/v{N}/ prefix or leading /
|
|
79
|
-
const stripped = path.replace(/^\/api\/v\d+\//, '').replace(/^\//, '')
|
|
80
|
-
const firstSegment = stripped.split('/')[0] ?? 'resource'
|
|
81
|
-
// Remove any path param braces if somehow first seg is param
|
|
82
|
-
return firstSegment.replace(/[{}]/g, '')
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Convert tag to valid camelCase identifier, then singularize. e.g. page-selections → pageSelection */
|
|
86
|
-
function toKeyFactoryName(resource: string): string {
|
|
87
|
-
// Sanitize the resource to a valid camelCase identifier, then singularize
|
|
88
|
-
const camel = sanitizeOperationId(resource)
|
|
89
|
-
return camel.endsWith('s') ? camel.slice(0, -1) : camel
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface OperationMeta {
|
|
93
|
-
funcName: string
|
|
94
|
-
hookName: string
|
|
95
|
-
method: SupportedMethod
|
|
96
|
-
path: string
|
|
97
|
-
pathParams: string[]
|
|
98
|
-
hasBody: boolean
|
|
99
|
-
bodyTypeName: string | undefined
|
|
100
|
-
hasQueryParams: boolean
|
|
101
|
-
hasRequiredQueryParams: boolean
|
|
102
|
-
deprecated: boolean
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getBodyInfo(operation: OperationObject): {
|
|
106
|
-
hasBody: boolean
|
|
107
|
-
bodyTypeName: string | undefined
|
|
108
|
-
} {
|
|
109
|
-
const requestBody = operation.requestBody as
|
|
110
|
-
| OpenAPIV3_1.RequestBodyObject
|
|
111
|
-
| ReferenceObject
|
|
112
|
-
| undefined
|
|
113
|
-
if (requestBody === undefined) return { hasBody: false, bodyTypeName: undefined }
|
|
114
|
-
if (isRef(requestBody)) return { hasBody: true, bodyTypeName: undefined }
|
|
115
|
-
|
|
116
|
-
const rb = requestBody as OpenAPIV3_1.RequestBodyObject
|
|
117
|
-
const content = rb.content as
|
|
118
|
-
| Record<string, { schema?: OpenAPIV3_1.SchemaObject | ReferenceObject }>
|
|
119
|
-
| undefined
|
|
120
|
-
if (content === undefined) return { hasBody: true, bodyTypeName: undefined }
|
|
121
|
-
|
|
122
|
-
// Check multipart first
|
|
123
|
-
if (content['multipart/form-data'] !== undefined) {
|
|
124
|
-
return { hasBody: true, bodyTypeName: undefined }
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const jsonContent = content['application/json']
|
|
128
|
-
if (jsonContent === undefined || jsonContent.schema === undefined) {
|
|
129
|
-
return { hasBody: true, bodyTypeName: undefined }
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const schema = jsonContent.schema
|
|
133
|
-
if (isRef(schema)) {
|
|
134
|
-
return { hasBody: true, bodyTypeName: refToName((schema as ReferenceObject).$ref) }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Inline schema
|
|
138
|
-
const s = schema as OpenAPIV3_1.SchemaObject
|
|
139
|
-
if (s.type === 'object') {
|
|
140
|
-
return { hasBody: true, bodyTypeName: 'Record<string, unknown>' }
|
|
141
|
-
}
|
|
142
|
-
return { hasBody: true, bodyTypeName: undefined }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── Key factory generation ─────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
interface KeyEntry {
|
|
148
|
-
key: string // e.g. 'list' | 'detail' | operationId-derived
|
|
149
|
-
funcName: string // function to use in key factory
|
|
150
|
-
pathParams: string[]
|
|
151
|
-
hasQueryParams: boolean
|
|
152
|
-
hasRequiredQueryParams: boolean
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function buildKeyFactory(resource: string, entries: KeyEntry[]): string {
|
|
156
|
-
const factoryName = `${toKeyFactoryName(resource)}Keys`
|
|
157
|
-
const lines: string[] = []
|
|
158
|
-
lines.push(`export const ${factoryName} = {`)
|
|
159
|
-
lines.push(` all: () => ['${resource}'] as const,`)
|
|
160
|
-
|
|
161
|
-
for (const entry of entries) {
|
|
162
|
-
const paramsOptional = !entry.hasRequiredQueryParams
|
|
163
|
-
const paramsArg = paramsOptional
|
|
164
|
-
? `params?: Parameters<typeof ${entry.funcName}>[${entry.pathParams.length}]`
|
|
165
|
-
: `params: Parameters<typeof ${entry.funcName}>[${entry.pathParams.length}]`
|
|
166
|
-
|
|
167
|
-
// When multiple detail-level GET ops share the same resource (e.g. GET /items/{id}
|
|
168
|
-
// and GET /items/{id}/usage), each gets a funcName-derived key instead of 'detail'.
|
|
169
|
-
// Include the key name as a segment to prevent cache key collisions.
|
|
170
|
-
// Single detail ops keep the canonical ['resource', id] shape (no breaking change).
|
|
171
|
-
const keySegment =
|
|
172
|
-
entry.pathParams.length > 0 && entry.key !== 'detail' ? `'${entry.key}', ` : ''
|
|
173
|
-
|
|
174
|
-
if (entry.pathParams.length === 0 && entry.hasQueryParams) {
|
|
175
|
-
// list: (params?) => ['resource', 'list', params]
|
|
176
|
-
lines.push(
|
|
177
|
-
` ${entry.key}: (${paramsArg}) => ['${resource}', '${entry.key}', params] as const,`
|
|
178
|
-
)
|
|
179
|
-
} else if (entry.pathParams.length === 0 && !entry.hasQueryParams) {
|
|
180
|
-
// list with no params
|
|
181
|
-
lines.push(` ${entry.key}: () => ['${resource}', '${entry.key}'] as const,`)
|
|
182
|
-
} else if (entry.pathParams.length === 1 && !entry.hasQueryParams) {
|
|
183
|
-
// detail: (id) => ['resource', id] or getItemUsage: (id) => ['resource', 'getItemUsage', id]
|
|
184
|
-
const param = entry.pathParams[0]!
|
|
185
|
-
lines.push(
|
|
186
|
-
` ${entry.key}: (${param}: string) => ['${resource}', ${keySegment}${param}] as const,`
|
|
187
|
-
)
|
|
188
|
-
} else if (entry.pathParams.length === 1 && entry.hasQueryParams) {
|
|
189
|
-
// detail with query params
|
|
190
|
-
const param = entry.pathParams[0]!
|
|
191
|
-
lines.push(
|
|
192
|
-
` ${entry.key}: (${param}: string, ${paramsArg}) => ['${resource}', ${keySegment}${param}, params] as const,`
|
|
193
|
-
)
|
|
194
|
-
} else if (!entry.hasQueryParams) {
|
|
195
|
-
// multiple path params, no query params
|
|
196
|
-
const paramList = entry.pathParams.map((p) => `${p}: string`).join(', ')
|
|
197
|
-
const paramValues = entry.pathParams.join(', ')
|
|
198
|
-
lines.push(
|
|
199
|
-
` ${entry.key}: (${paramList}) => ['${resource}', ${keySegment}${paramValues}] as const,`
|
|
200
|
-
)
|
|
201
|
-
} else {
|
|
202
|
-
// multiple path params + query params
|
|
203
|
-
const paramList = entry.pathParams.map((p) => `${p}: string`).join(', ')
|
|
204
|
-
const paramValues = entry.pathParams.join(', ')
|
|
205
|
-
lines.push(
|
|
206
|
-
` ${entry.key}: (${paramList}, ${paramsArg}) => ['${resource}', ${keySegment}${paramValues}, params] as const,`
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
lines.push(`}`)
|
|
212
|
-
return lines.join('\n')
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// ── Query hook generation ──────────────────────────────────────────────────────
|
|
216
|
-
|
|
217
|
-
function buildQueryHook(
|
|
218
|
-
op: OperationMeta,
|
|
219
|
-
keyFactoryName: string,
|
|
220
|
-
keyEntry: KeyEntry,
|
|
221
|
-
staleTime: number,
|
|
222
|
-
gcTime: number
|
|
223
|
-
): string {
|
|
224
|
-
const lines: string[] = []
|
|
225
|
-
|
|
226
|
-
const hasQueryParams = keyEntry.hasQueryParams
|
|
227
|
-
const paramsRequired = keyEntry.hasRequiredQueryParams
|
|
228
|
-
const pathParams = op.pathParams
|
|
229
|
-
const hasPathParams = pathParams.length > 0
|
|
230
|
-
|
|
231
|
-
// Build parameter list
|
|
232
|
-
const sigParts: string[] = []
|
|
233
|
-
for (const p of pathParams) {
|
|
234
|
-
// Widen path param types to allow nullish values — enables auto-disable without !!id boilerplate
|
|
235
|
-
sigParts.push(`${p}: string | undefined | null`)
|
|
236
|
-
}
|
|
237
|
-
if (hasQueryParams) {
|
|
238
|
-
const paramsToken = paramsRequired ? 'params' : 'params?'
|
|
239
|
-
sigParts.push(`${paramsToken}: Parameters<typeof ${op.funcName}>[${pathParams.length}]`)
|
|
240
|
-
}
|
|
241
|
-
sigParts.push(
|
|
242
|
-
`options?: Omit<UseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
// Build queryKey call — use non-null assertions since enabled guard ensures non-null at runtime
|
|
246
|
-
let queryKeyCall: string
|
|
247
|
-
if (pathParams.length === 0 && hasQueryParams) {
|
|
248
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(params)`
|
|
249
|
-
} else if (pathParams.length === 0 && !hasQueryParams) {
|
|
250
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}()`
|
|
251
|
-
} else if (pathParams.length === 1 && !hasQueryParams) {
|
|
252
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${pathParams[0]}!)`
|
|
253
|
-
} else if (pathParams.length === 1 && hasQueryParams) {
|
|
254
|
-
// path param + query params
|
|
255
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${pathParams[0]}!, params)`
|
|
256
|
-
} else if (!hasQueryParams) {
|
|
257
|
-
// multiple path params, no query params
|
|
258
|
-
const paramValues = pathParams.map((p) => `${p}!`).join(', ')
|
|
259
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${paramValues})`
|
|
260
|
-
} else {
|
|
261
|
-
// multiple path params + query params
|
|
262
|
-
const paramValues = pathParams.map((p) => `${p}!`).join(', ')
|
|
263
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${paramValues}, params)`
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Build queryFn call — use non-null assertions
|
|
267
|
-
let queryFnArgs: string[] = pathParams.map((p) => `${p}!`)
|
|
268
|
-
if (hasQueryParams) queryFnArgs.push('params')
|
|
269
|
-
const queryFnCall = `${op.funcName}(${queryFnArgs.join(', ')})`
|
|
270
|
-
|
|
271
|
-
// Build enabled expression for path-param hooks
|
|
272
|
-
const enabledExpr = hasPathParams
|
|
273
|
-
? `${pathParams.map((p) => `${p} != null`).join(' && ')} && (options?.enabled ?? true)`
|
|
274
|
-
: undefined
|
|
275
|
-
|
|
276
|
-
// @deprecated JSDoc on deprecated operations
|
|
277
|
-
if (op.deprecated) {
|
|
278
|
-
lines.push(`/** @deprecated */`)
|
|
279
|
-
}
|
|
280
|
-
lines.push(`export function ${op.hookName}(`)
|
|
281
|
-
lines.push(` ${sigParts.join(',\n ')},`)
|
|
282
|
-
lines.push(`) {`)
|
|
283
|
-
lines.push(` return useQuery<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>({`)
|
|
284
|
-
lines.push(` queryKey: ${queryKeyCall},`)
|
|
285
|
-
lines.push(` queryFn: () => ${queryFnCall},`)
|
|
286
|
-
lines.push(` staleTime: ${staleTime},`)
|
|
287
|
-
lines.push(` gcTime: ${gcTime},`)
|
|
288
|
-
if (enabledExpr !== undefined) {
|
|
289
|
-
lines.push(` enabled: ${enabledExpr},`)
|
|
290
|
-
}
|
|
291
|
-
lines.push(` ...options,`)
|
|
292
|
-
lines.push(` })`)
|
|
293
|
-
lines.push(`}`)
|
|
294
|
-
|
|
295
|
-
return lines.join('\n')
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function buildSuspenseQueryHook(
|
|
299
|
-
op: OperationMeta,
|
|
300
|
-
keyFactoryName: string,
|
|
301
|
-
keyEntry: KeyEntry,
|
|
302
|
-
staleTime: number,
|
|
303
|
-
gcTime: number
|
|
304
|
-
): string {
|
|
305
|
-
const lines: string[] = []
|
|
306
|
-
|
|
307
|
-
const hasQueryParams = keyEntry.hasQueryParams
|
|
308
|
-
const paramsRequired = keyEntry.hasRequiredQueryParams
|
|
309
|
-
const pathParams = op.pathParams
|
|
310
|
-
|
|
311
|
-
// Suspense hooks require path params (no nullish widening — suspense doesn't support enabled)
|
|
312
|
-
const sigParts: string[] = []
|
|
313
|
-
for (const p of pathParams) {
|
|
314
|
-
sigParts.push(`${p}: string`)
|
|
315
|
-
}
|
|
316
|
-
if (hasQueryParams) {
|
|
317
|
-
const paramsToken = paramsRequired ? 'params' : 'params?'
|
|
318
|
-
sigParts.push(`${paramsToken}: Parameters<typeof ${op.funcName}>[${pathParams.length}]`)
|
|
319
|
-
}
|
|
320
|
-
sigParts.push(
|
|
321
|
-
`options?: Omit<UseSuspenseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
// Build queryKey call
|
|
325
|
-
let queryKeyCall: string
|
|
326
|
-
if (pathParams.length === 0 && hasQueryParams) {
|
|
327
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(params)`
|
|
328
|
-
} else if (pathParams.length === 0 && !hasQueryParams) {
|
|
329
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}()`
|
|
330
|
-
} else if (pathParams.length === 1 && !hasQueryParams) {
|
|
331
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${pathParams[0]})`
|
|
332
|
-
} else if (pathParams.length === 1 && hasQueryParams) {
|
|
333
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${pathParams[0]}, params)`
|
|
334
|
-
} else if (!hasQueryParams) {
|
|
335
|
-
const paramValues = pathParams.join(', ')
|
|
336
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${paramValues})`
|
|
337
|
-
} else {
|
|
338
|
-
const paramValues = pathParams.join(', ')
|
|
339
|
-
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${paramValues}, params)`
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Build queryFn call
|
|
343
|
-
const queryFnArgs: string[] = [...pathParams]
|
|
344
|
-
if (hasQueryParams) queryFnArgs.push('params')
|
|
345
|
-
const queryFnCall = `${op.funcName}(${queryFnArgs.join(', ')})`
|
|
346
|
-
|
|
347
|
-
const suspenseHookName = `useSuspense${capitalize(op.funcName)}`
|
|
348
|
-
|
|
349
|
-
if (op.deprecated) {
|
|
350
|
-
lines.push(`/** @deprecated */`)
|
|
351
|
-
}
|
|
352
|
-
lines.push(`export function ${suspenseHookName}(`)
|
|
353
|
-
lines.push(` ${sigParts.join(',\n ')},`)
|
|
354
|
-
lines.push(`) {`)
|
|
355
|
-
lines.push(` return useSuspenseQuery<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>({`)
|
|
356
|
-
lines.push(` queryKey: ${queryKeyCall},`)
|
|
357
|
-
lines.push(` queryFn: () => ${queryFnCall},`)
|
|
358
|
-
lines.push(` staleTime: ${staleTime},`)
|
|
359
|
-
lines.push(` gcTime: ${gcTime},`)
|
|
360
|
-
lines.push(` ...options,`)
|
|
361
|
-
lines.push(` })`)
|
|
362
|
-
lines.push(`}`)
|
|
363
|
-
|
|
364
|
-
return lines.join('\n')
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// ── Mutation hook generation ───────────────────────────────────────────────────
|
|
368
|
-
|
|
369
|
-
interface MutationInvalidateInfo {
|
|
370
|
-
keyFactoryName: string
|
|
371
|
-
detailKeyName: string | undefined
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function buildMutationHook(
|
|
375
|
-
op: OperationMeta,
|
|
376
|
-
autoInvalidate: boolean,
|
|
377
|
-
invalidateInfo: MutationInvalidateInfo | undefined
|
|
378
|
-
): string {
|
|
379
|
-
const lines: string[] = []
|
|
380
|
-
const { funcName, hookName, pathParams, hasBody, hasQueryParams } = op
|
|
381
|
-
|
|
382
|
-
// Determine variables type and mutationFn
|
|
383
|
-
// The client function argument order is: ...pathParams, body?, params?
|
|
384
|
-
// We must match exactly what the generated client function expects.
|
|
385
|
-
let variablesType: string
|
|
386
|
-
let mutationFnBody: string
|
|
387
|
-
|
|
388
|
-
// Compute the index of the params argument in the client function signature
|
|
389
|
-
const paramsArgIndex = pathParams.length + (hasBody ? 1 : 0)
|
|
390
|
-
|
|
391
|
-
if (pathParams.length === 0 && !hasBody && !hasQueryParams) {
|
|
392
|
-
variablesType = 'void'
|
|
393
|
-
mutationFnBody = `() => ${funcName}()`
|
|
394
|
-
} else if (pathParams.length === 0 && !hasBody && hasQueryParams) {
|
|
395
|
-
// params-only mutation: fn(params)
|
|
396
|
-
variablesType = `Parameters<typeof ${funcName}>[0]`
|
|
397
|
-
mutationFnBody = `(vars) => ${funcName}(vars)`
|
|
398
|
-
} else if (pathParams.length === 0 && hasBody && !hasQueryParams) {
|
|
399
|
-
// body-only mutation: fn(body)
|
|
400
|
-
variablesType = `Parameters<typeof ${funcName}>[0]`
|
|
401
|
-
mutationFnBody = `(vars) => ${funcName}(vars)`
|
|
402
|
-
} else if (pathParams.length === 0 && hasBody && hasQueryParams) {
|
|
403
|
-
// body + params: fn(body, params)
|
|
404
|
-
variablesType = `{ body: Parameters<typeof ${funcName}>[0]; params: Parameters<typeof ${funcName}>[${paramsArgIndex}] }`
|
|
405
|
-
mutationFnBody = `({ body, params }) => ${funcName}(body, params)`
|
|
406
|
-
} else if (pathParams.length === 1 && !hasBody && !hasQueryParams) {
|
|
407
|
-
variablesType = 'string'
|
|
408
|
-
mutationFnBody = `(${pathParams[0]}) => ${funcName}(${pathParams[0]})`
|
|
409
|
-
} else if (pathParams.length === 1 && !hasBody && hasQueryParams) {
|
|
410
|
-
// 1 path param + params only
|
|
411
|
-
const param = pathParams[0]!
|
|
412
|
-
variablesType = `{ ${param}: string; params: Parameters<typeof ${funcName}>[${paramsArgIndex}] }`
|
|
413
|
-
mutationFnBody = `({ ${param}, params }) => ${funcName}(${param}, params)`
|
|
414
|
-
} else if (pathParams.length === 1 && hasBody && !hasQueryParams) {
|
|
415
|
-
const param = pathParams[0]!
|
|
416
|
-
variablesType = `{ ${param}: string; body: Parameters<typeof ${funcName}>[1] }`
|
|
417
|
-
mutationFnBody = `({ ${param}, body }) => ${funcName}(${param}, body)`
|
|
418
|
-
} else if (pathParams.length === 1 && hasBody && hasQueryParams) {
|
|
419
|
-
// 1 path param + body + params
|
|
420
|
-
const param = pathParams[0]!
|
|
421
|
-
variablesType = `{ ${param}: string; body: Parameters<typeof ${funcName}>[1]; params: Parameters<typeof ${funcName}>[${paramsArgIndex}] }`
|
|
422
|
-
mutationFnBody = `({ ${param}, body, params }) => ${funcName}(${param}, body, params)`
|
|
423
|
-
} else if (pathParams.length > 1 && !hasBody && !hasQueryParams) {
|
|
424
|
-
const fields = pathParams.map((p) => `${p}: string`).join('; ')
|
|
425
|
-
variablesType = `{ ${fields} }`
|
|
426
|
-
const destructured = pathParams.join(', ')
|
|
427
|
-
mutationFnBody = `({ ${destructured} }) => ${funcName}(${destructured})`
|
|
428
|
-
} else if (pathParams.length > 1 && !hasBody && hasQueryParams) {
|
|
429
|
-
// multiple path params + params only
|
|
430
|
-
const fields = pathParams.map((p) => `${p}: string`).join('; ')
|
|
431
|
-
variablesType = `{ ${fields}; params: Parameters<typeof ${funcName}>[${paramsArgIndex}] }`
|
|
432
|
-
const destructured = [...pathParams, 'params'].join(', ')
|
|
433
|
-
mutationFnBody = `({ ${destructured} }) => ${funcName}(${[...pathParams, 'params'].join(', ')})`
|
|
434
|
-
} else if (pathParams.length > 1 && hasBody && !hasQueryParams) {
|
|
435
|
-
// multiple path params + body
|
|
436
|
-
const fields = pathParams.map((p) => `${p}: string`).join('; ')
|
|
437
|
-
variablesType = `{ ${fields}; body: Parameters<typeof ${funcName}>[${pathParams.length}] }`
|
|
438
|
-
const destructured = [...pathParams, 'body'].join(', ')
|
|
439
|
-
mutationFnBody = `({ ${destructured} }) => ${funcName}(${[...pathParams, 'body'].join(', ')})`
|
|
440
|
-
} else {
|
|
441
|
-
// multiple path params + body + params
|
|
442
|
-
const fields = pathParams.map((p) => `${p}: string`).join('; ')
|
|
443
|
-
variablesType = `{ ${fields}; body: Parameters<typeof ${funcName}>[${pathParams.length}]; params: Parameters<typeof ${funcName}>[${paramsArgIndex}] }`
|
|
444
|
-
const destructured = [...pathParams, 'body', 'params'].join(', ')
|
|
445
|
-
mutationFnBody = `({ ${destructured} }) => ${funcName}(${[...pathParams, 'body', 'params'].join(', ')})`
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// @deprecated JSDoc on deprecated operations
|
|
449
|
-
if (op.deprecated) {
|
|
450
|
-
lines.push(`/** @deprecated */`)
|
|
451
|
-
}
|
|
452
|
-
lines.push(`export function ${hookName}(`)
|
|
453
|
-
lines.push(
|
|
454
|
-
` options?: Omit<UseMutationOptions<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>, 'mutationFn'>,`
|
|
455
|
-
)
|
|
456
|
-
lines.push(`) {`)
|
|
457
|
-
|
|
458
|
-
const shouldInvalidate = autoInvalidate && invalidateInfo !== undefined
|
|
459
|
-
if (shouldInvalidate) {
|
|
460
|
-
lines.push(` const queryClient = useQueryClient()`)
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
lines.push(
|
|
464
|
-
` return useMutation<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>({`
|
|
465
|
-
)
|
|
466
|
-
lines.push(` mutationFn: ${mutationFnBody},`)
|
|
467
|
-
// Spread options first so caller can override mutationFn-unrelated options (e.g. retry, meta).
|
|
468
|
-
// onSuccess must come AFTER the spread so the auto-invalidation logic always runs —
|
|
469
|
-
// the caller's onSuccess is composed via options?.onSuccess?.(...args) inside.
|
|
470
|
-
lines.push(` ...options,`)
|
|
471
|
-
|
|
472
|
-
if (shouldInvalidate) {
|
|
473
|
-
const { keyFactoryName, detailKeyName } = invalidateInfo
|
|
474
|
-
const canInvalidateDetail =
|
|
475
|
-
detailKeyName !== undefined &&
|
|
476
|
-
pathParams.length >= 1 &&
|
|
477
|
-
(op.method === 'put' || op.method === 'patch')
|
|
478
|
-
|
|
479
|
-
// Determine how to reference the first path param from variables in onSuccess
|
|
480
|
-
let detailIdRef: string
|
|
481
|
-
if (pathParams.length === 1 && !hasBody && !hasQueryParams) {
|
|
482
|
-
// variablesType is 'string' — variables IS the id directly
|
|
483
|
-
detailIdRef = 'variables'
|
|
484
|
-
} else if (pathParams.length >= 1) {
|
|
485
|
-
// variablesType is an object — access via property name
|
|
486
|
-
detailIdRef = `variables.${pathParams[0]}`
|
|
487
|
-
} else {
|
|
488
|
-
detailIdRef = 'variables'
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
lines.push(` onSuccess: (...args) => {`)
|
|
492
|
-
if (canInvalidateDetail) {
|
|
493
|
-
lines.push(` const [, variables] = args`)
|
|
494
|
-
}
|
|
495
|
-
lines.push(` queryClient.invalidateQueries({ queryKey: ${keyFactoryName}.all() })`)
|
|
496
|
-
if (canInvalidateDetail) {
|
|
497
|
-
lines.push(
|
|
498
|
-
` queryClient.invalidateQueries({ queryKey: ${keyFactoryName}.${detailKeyName}(${detailIdRef}) })`
|
|
499
|
-
)
|
|
500
|
-
}
|
|
501
|
-
lines.push(` options?.onSuccess?.(...args)`)
|
|
502
|
-
lines.push(` },`)
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
lines.push(` })`)
|
|
506
|
-
lines.push(`}`)
|
|
507
|
-
|
|
508
|
-
return lines.join('\n')
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// ── hasQueryParams detection ───────────────────────────────────────────────────
|
|
512
|
-
|
|
513
|
-
function operationHasQueryParams(operation: OperationObject): boolean {
|
|
514
|
-
const params = operation.parameters as
|
|
515
|
-
| (OpenAPIV3_1.ParameterObject | ReferenceObject)[]
|
|
516
|
-
| undefined
|
|
517
|
-
if (params === undefined) return false
|
|
518
|
-
return params.some((p) => {
|
|
519
|
-
if (isRef(p)) return false
|
|
520
|
-
const param = p as OpenAPIV3_1.ParameterObject
|
|
521
|
-
return param.in === 'query'
|
|
522
|
-
})
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Converts a raw operationId into a valid camelCase JS identifier.
|
|
527
|
-
* Mirrors the logic in openapi-gen/src/plugins/client.ts.
|
|
528
|
-
* e.g. "actions/add-custom-labels" → "actionsAddCustomLabels"
|
|
529
|
-
* e.g. "delete-storedPaymentMethods-id" → "deleteStoredPaymentMethodsId"
|
|
530
|
-
*/
|
|
531
|
-
function sanitizeOperationId(id: string): string {
|
|
532
|
-
const parts = id
|
|
533
|
-
.replace(/'/g, '')
|
|
534
|
-
.split(/[^a-zA-Z0-9]+/)
|
|
535
|
-
.filter(Boolean)
|
|
536
|
-
if (parts.length === 0) return 'unknown'
|
|
537
|
-
const [first = '', ...rest] = parts
|
|
538
|
-
const camel =
|
|
539
|
-
first.charAt(0).toLowerCase() +
|
|
540
|
-
first.slice(1) +
|
|
541
|
-
rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('')
|
|
542
|
-
if (/^[0-9]/.test(camel)) return `_${camel}`
|
|
543
|
-
const RESERVED = new Set([
|
|
544
|
-
'break',
|
|
545
|
-
'case',
|
|
546
|
-
'catch',
|
|
547
|
-
'class',
|
|
548
|
-
'const',
|
|
549
|
-
'continue',
|
|
550
|
-
'debugger',
|
|
551
|
-
'default',
|
|
552
|
-
'delete',
|
|
553
|
-
'do',
|
|
554
|
-
'else',
|
|
555
|
-
'export',
|
|
556
|
-
'extends',
|
|
557
|
-
'finally',
|
|
558
|
-
'for',
|
|
559
|
-
'function',
|
|
560
|
-
'if',
|
|
561
|
-
'import',
|
|
562
|
-
'in',
|
|
563
|
-
'instanceof',
|
|
564
|
-
'let',
|
|
565
|
-
'new',
|
|
566
|
-
'return',
|
|
567
|
-
'static',
|
|
568
|
-
'super',
|
|
569
|
-
'switch',
|
|
570
|
-
'this',
|
|
571
|
-
'throw',
|
|
572
|
-
'try',
|
|
573
|
-
'typeof',
|
|
574
|
-
'var',
|
|
575
|
-
'void',
|
|
576
|
-
'while',
|
|
577
|
-
'with',
|
|
578
|
-
'yield',
|
|
579
|
-
])
|
|
580
|
-
return RESERVED.has(camel) ? `_${camel}` : camel
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/** Returns true if any query parameter has required: true */
|
|
584
|
-
function operationHasRequiredQueryParams(operation: OperationObject): boolean {
|
|
585
|
-
const params = operation.parameters as
|
|
586
|
-
| (OpenAPIV3_1.ParameterObject | ReferenceObject)[]
|
|
587
|
-
| undefined
|
|
588
|
-
if (params === undefined) return false
|
|
589
|
-
return params.some((p) => {
|
|
590
|
-
if (isRef(p)) return false
|
|
591
|
-
const param = p as OpenAPIV3_1.ParameterObject
|
|
592
|
-
return param.in === 'query' && param.required === true
|
|
593
|
-
})
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// ── Main generator ─────────────────────────────────────────────────────────────
|
|
597
|
-
|
|
598
|
-
export function generateHooks(
|
|
599
|
-
spec: OpenAPIV3_1.Document,
|
|
600
|
-
options: HookGenOptions
|
|
601
|
-
): { filename: string; content: string } {
|
|
602
|
-
const { staleTime, gcTime, suspense = false, autoInvalidate = false } = options
|
|
603
|
-
const paths = spec.paths as Record<string, Record<string, OperationObject>> | undefined
|
|
604
|
-
|
|
605
|
-
// Collect all operations
|
|
606
|
-
const operations: OperationMeta[] = []
|
|
607
|
-
|
|
608
|
-
if (paths !== undefined) {
|
|
609
|
-
for (const [path, pathItem] of Object.entries(paths)) {
|
|
610
|
-
for (const method of SUPPORTED_METHODS) {
|
|
611
|
-
const operation = pathItem[method] as OperationObject | undefined
|
|
612
|
-
if (operation === undefined) continue
|
|
613
|
-
|
|
614
|
-
let funcName: string
|
|
615
|
-
if (operation.operationId !== undefined) {
|
|
616
|
-
funcName = sanitizeOperationId(operation.operationId)
|
|
617
|
-
} else {
|
|
618
|
-
funcName = deriveOperationName(method, path)
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
const hookName = 'use' + capitalize(funcName)
|
|
622
|
-
const pathParams = extractPathParams(path)
|
|
623
|
-
const { hasBody, bodyTypeName } = getBodyInfo(operation)
|
|
624
|
-
const hasQueryParams = operationHasQueryParams(operation)
|
|
625
|
-
const hasRequiredQueryParams = operationHasRequiredQueryParams(operation)
|
|
626
|
-
const deprecated = operation.deprecated === true
|
|
627
|
-
|
|
628
|
-
operations.push({
|
|
629
|
-
funcName,
|
|
630
|
-
hookName,
|
|
631
|
-
method,
|
|
632
|
-
path,
|
|
633
|
-
pathParams,
|
|
634
|
-
hasBody,
|
|
635
|
-
bodyTypeName,
|
|
636
|
-
hasQueryParams,
|
|
637
|
-
hasRequiredQueryParams,
|
|
638
|
-
deprecated,
|
|
639
|
-
})
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Separate GET vs mutation operations
|
|
645
|
-
const getOps = operations.filter((op) => op.method === 'get')
|
|
646
|
-
const mutationOps = operations.filter((op) => op.method !== 'get')
|
|
647
|
-
|
|
648
|
-
// Group GET ops by resource for key factories
|
|
649
|
-
const resourceToGetOps = new Map<string, OperationMeta[]>()
|
|
650
|
-
for (const op of getOps) {
|
|
651
|
-
const resource = primaryResource(op.path)
|
|
652
|
-
const existing = resourceToGetOps.get(resource) ?? []
|
|
653
|
-
existing.push(op)
|
|
654
|
-
resourceToGetOps.set(resource, existing)
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Build key factories and track which key factory + entry each GET op uses
|
|
658
|
-
const opToKeyInfo = new Map<string, { factoryName: string; keyEntry: KeyEntry }>()
|
|
659
|
-
const keyFactoryBlocks: string[] = []
|
|
660
|
-
|
|
661
|
-
for (const [resource, ops] of resourceToGetOps.entries()) {
|
|
662
|
-
const factoryName = `${toKeyFactoryName(resource)}Keys`
|
|
663
|
-
|
|
664
|
-
// Determine entry keys — list vs detail, or operationId-derived if ambiguous
|
|
665
|
-
const listOps = ops.filter((op) => op.pathParams.length === 0)
|
|
666
|
-
const detailOps = ops.filter((op) => op.pathParams.length > 0)
|
|
667
|
-
|
|
668
|
-
const entries: KeyEntry[] = []
|
|
669
|
-
|
|
670
|
-
if (listOps.length === 1) {
|
|
671
|
-
const op = listOps[0]!
|
|
672
|
-
const entry: KeyEntry = {
|
|
673
|
-
key: 'list',
|
|
674
|
-
funcName: op.funcName,
|
|
675
|
-
pathParams: [],
|
|
676
|
-
hasQueryParams: op.hasQueryParams,
|
|
677
|
-
hasRequiredQueryParams: op.hasRequiredQueryParams,
|
|
678
|
-
}
|
|
679
|
-
entries.push(entry)
|
|
680
|
-
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
681
|
-
} else if (listOps.length > 1) {
|
|
682
|
-
for (const op of listOps) {
|
|
683
|
-
const entry: KeyEntry = {
|
|
684
|
-
key: op.funcName,
|
|
685
|
-
funcName: op.funcName,
|
|
686
|
-
pathParams: [],
|
|
687
|
-
hasQueryParams: op.hasQueryParams,
|
|
688
|
-
hasRequiredQueryParams: op.hasRequiredQueryParams,
|
|
689
|
-
}
|
|
690
|
-
entries.push(entry)
|
|
691
|
-
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (detailOps.length === 1) {
|
|
696
|
-
const op = detailOps[0]!
|
|
697
|
-
const entry: KeyEntry = {
|
|
698
|
-
key: 'detail',
|
|
699
|
-
funcName: op.funcName,
|
|
700
|
-
pathParams: op.pathParams,
|
|
701
|
-
hasQueryParams: op.hasQueryParams,
|
|
702
|
-
hasRequiredQueryParams: op.hasRequiredQueryParams,
|
|
703
|
-
}
|
|
704
|
-
entries.push(entry)
|
|
705
|
-
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
706
|
-
} else if (detailOps.length > 1) {
|
|
707
|
-
for (const op of detailOps) {
|
|
708
|
-
const entry: KeyEntry = {
|
|
709
|
-
key: op.funcName,
|
|
710
|
-
funcName: op.funcName,
|
|
711
|
-
pathParams: op.pathParams,
|
|
712
|
-
hasQueryParams: op.hasQueryParams,
|
|
713
|
-
hasRequiredQueryParams: op.hasRequiredQueryParams,
|
|
714
|
-
}
|
|
715
|
-
entries.push(entry)
|
|
716
|
-
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
keyFactoryBlocks.push(buildKeyFactory(resource, entries))
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
// Build query hooks (and suspense variants if enabled)
|
|
724
|
-
const queryHookBlocks: string[] = []
|
|
725
|
-
for (const op of getOps) {
|
|
726
|
-
const info = opToKeyInfo.get(op.funcName)
|
|
727
|
-
if (info === undefined) continue
|
|
728
|
-
const { factoryName, keyEntry } = info
|
|
729
|
-
const resource = primaryResource(op.path)
|
|
730
|
-
const resourceOverride = options.overrides?.[resource]
|
|
731
|
-
const effectiveStaleTime = resourceOverride?.staleTime ?? staleTime
|
|
732
|
-
const effectiveGcTime = resourceOverride?.gcTime ?? gcTime
|
|
733
|
-
queryHookBlocks.push(
|
|
734
|
-
buildQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime)
|
|
735
|
-
)
|
|
736
|
-
if (suspense) {
|
|
737
|
-
queryHookBlocks.push(
|
|
738
|
-
buildSuspenseQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime)
|
|
739
|
-
)
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Build resource → detailKeyName map for auto-invalidate
|
|
744
|
-
// detailKeyName is 'detail' when exactly one detail op exists (canonical name),
|
|
745
|
-
// or undefined when zero or multiple detail ops exist (ambiguous — skip detail invalidation)
|
|
746
|
-
const resourceDetailKeyName = new Map<string, string | undefined>()
|
|
747
|
-
for (const [resource, ops] of resourceToGetOps.entries()) {
|
|
748
|
-
const detailOps = ops.filter((op) => op.pathParams.length > 0)
|
|
749
|
-
resourceDetailKeyName.set(resource, detailOps.length === 1 ? 'detail' : undefined)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Build mutation hooks
|
|
753
|
-
const mutationHookBlocks: string[] = []
|
|
754
|
-
for (const op of mutationOps) {
|
|
755
|
-
let invalidateInfo: MutationInvalidateInfo | undefined
|
|
756
|
-
if (autoInvalidate) {
|
|
757
|
-
const resource = primaryResource(op.path)
|
|
758
|
-
// Only invalidate if a key factory exists for this resource (i.e. it has at least one GET op).
|
|
759
|
-
// A mutation-only resource has no key factory — emitting invalidateQueries against it would
|
|
760
|
-
// reference an undefined variable and crash at runtime.
|
|
761
|
-
if (resourceToGetOps.has(resource)) {
|
|
762
|
-
const keyFactoryName = `${toKeyFactoryName(resource)}Keys`
|
|
763
|
-
const detailKeyName = resourceDetailKeyName.get(resource)
|
|
764
|
-
invalidateInfo = { keyFactoryName, detailKeyName }
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
mutationHookBlocks.push(buildMutationHook(op, autoInvalidate, invalidateInfo))
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// Determine which react-query exports to import
|
|
771
|
-
const needsUseQuery = getOps.length > 0
|
|
772
|
-
const needsUseMutation = mutationOps.length > 0
|
|
773
|
-
const needsUseSuspenseQuery = suspense && getOps.length > 0
|
|
774
|
-
const needsUseQueryClient = autoInvalidate && needsUseMutation
|
|
775
|
-
const rqImports: string[] = []
|
|
776
|
-
if (needsUseQuery) rqImports.push('useQuery', 'type UseQueryOptions')
|
|
777
|
-
if (needsUseSuspenseQuery) rqImports.push('useSuspenseQuery', 'type UseSuspenseQueryOptions')
|
|
778
|
-
if (needsUseMutation) rqImports.push('useMutation', 'type UseMutationOptions')
|
|
779
|
-
if (needsUseQueryClient) rqImports.push('useQueryClient')
|
|
780
|
-
|
|
781
|
-
// Collect all function names for client import
|
|
782
|
-
const allFuncNames = operations.map((op) => op.funcName).sort()
|
|
783
|
-
|
|
784
|
-
// Build file content
|
|
785
|
-
const lines: string[] = []
|
|
786
|
-
lines.push('// This file is auto-generated by @codewithagents/openapi-react-query — do not edit')
|
|
787
|
-
lines.push('')
|
|
788
|
-
if (rqImports.length > 0) {
|
|
789
|
-
lines.push(`import { ${rqImports.join(', ')} } from '@tanstack/react-query'`)
|
|
790
|
-
}
|
|
791
|
-
if (allFuncNames.length > 0) {
|
|
792
|
-
lines.push(`import { ${allFuncNames.join(', ')}, type ApiError } from './client.js'`)
|
|
793
|
-
} else {
|
|
794
|
-
lines.push(`import { type ApiError } from './client.js'`)
|
|
795
|
-
}
|
|
796
|
-
lines.push('')
|
|
797
|
-
|
|
798
|
-
if (keyFactoryBlocks.length > 0) {
|
|
799
|
-
lines.push('// ── Query key factories ──────────────────────────────────────')
|
|
800
|
-
lines.push('')
|
|
801
|
-
lines.push(keyFactoryBlocks.join('\n\n'))
|
|
802
|
-
lines.push('')
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
if (queryHookBlocks.length > 0) {
|
|
806
|
-
lines.push('// ── Queries ──────────────────────────────────────────────────')
|
|
807
|
-
lines.push('')
|
|
808
|
-
lines.push(queryHookBlocks.join('\n\n'))
|
|
809
|
-
lines.push('')
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (mutationHookBlocks.length > 0) {
|
|
813
|
-
lines.push('// ── Mutations ────────────────────────────────────────────────')
|
|
814
|
-
lines.push('')
|
|
815
|
-
lines.push(mutationHookBlocks.join('\n\n'))
|
|
816
|
-
lines.push('')
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
return {
|
|
820
|
-
filename: 'hooks.ts',
|
|
821
|
-
content: lines.join('\n'),
|
|
822
|
-
}
|
|
823
|
-
}
|