@codewithagents/openapi-react-query 0.1.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/CHANGELOG.md +9 -0
- package/CLAUDE.md +26 -0
- package/__fixtures__/specs/task-hooks.json +155 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +131 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/src/config.ts.html +418 -0
- package/coverage/lcov-report/src/index.html +116 -0
- package/coverage/lcov-report/src/plugins/hooks.ts.html +1435 -0
- package/coverage/lcov-report/src/plugins/index.html +116 -0
- package/coverage/lcov.info +484 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +24 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +85 -0
- package/dist/config.js.map +1 -0
- package/dist/generator.d.ts +2 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +20 -0
- package/dist/generator.js.map +1 -0
- package/dist/plugins/hooks.d.ts +10 -0
- package/dist/plugins/hooks.d.ts.map +1 -0
- package/dist/plugins/hooks.js +370 -0
- package/dist/plugins/hooks.js.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/config.test.ts +186 -0
- package/src/__tests__/hooks.test.ts +101 -0
- package/src/cli.ts +27 -0
- package/src/config.ts +111 -0
- package/src/generator.ts +20 -0
- package/src/plugins/hooks.ts +450 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,450 @@
|
|
|
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
|
+
}
|
|
10
|
+
|
|
11
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
function isRef(obj: unknown): obj is ReferenceObject {
|
|
14
|
+
return typeof obj === 'object' && obj !== null && '$ref' in obj
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function refToName(ref: string): string {
|
|
18
|
+
const parts = ref.split('/')
|
|
19
|
+
return parts[parts.length - 1]!
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function capitalize(s: string): string {
|
|
23
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SUPPORTED_METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const
|
|
27
|
+
type SupportedMethod = (typeof SUPPORTED_METHODS)[number]
|
|
28
|
+
|
|
29
|
+
// Mirrors deriveOperationName from openapi-gen/src/plugins/client.ts
|
|
30
|
+
function deriveOperationName(method: string, path: string): string {
|
|
31
|
+
const prefixMap: Record<string, string> = {
|
|
32
|
+
get: 'get',
|
|
33
|
+
post: 'create',
|
|
34
|
+
put: 'update',
|
|
35
|
+
patch: 'patch',
|
|
36
|
+
delete: 'delete',
|
|
37
|
+
}
|
|
38
|
+
const prefix = prefixMap[method] ?? method
|
|
39
|
+
|
|
40
|
+
// Strip /api/v1/ prefix
|
|
41
|
+
let segments = path.replace(/^\/api\/v\d+\//, '').replace(/^\//, '')
|
|
42
|
+
|
|
43
|
+
const parts = segments.split('/').map((seg) => {
|
|
44
|
+
if (seg.startsWith('{') && seg.endsWith('}')) {
|
|
45
|
+
const name = seg.slice(1, -1)
|
|
46
|
+
return 'By' + name.charAt(0).toUpperCase() + name.slice(1)
|
|
47
|
+
}
|
|
48
|
+
return seg.charAt(0).toUpperCase() + seg.slice(1)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const joined = parts.join('')
|
|
52
|
+
return prefix + joined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function extractPathParams(path: string): string[] {
|
|
56
|
+
const matches = path.match(/\{([^}]+)\}/g)
|
|
57
|
+
if (matches === null) return []
|
|
58
|
+
return matches.map((m) => m.slice(1, -1))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Extract primary resource name from path (first static segment after stripping API prefix) */
|
|
62
|
+
function primaryResource(path: string): string {
|
|
63
|
+
// Strip /api/v{N}/ prefix or leading /
|
|
64
|
+
const stripped = path.replace(/^\/api\/v\d+\//, '').replace(/^\//, '')
|
|
65
|
+
const firstSegment = stripped.split('/')[0] ?? 'resource'
|
|
66
|
+
// Remove any path param braces if somehow first seg is param
|
|
67
|
+
return firstSegment.replace(/[{}]/g, '')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** singularize resource name for key factory naming, e.g. tasks → task */
|
|
71
|
+
function toKeyFactoryName(resource: string): string {
|
|
72
|
+
// simple pluralization removal: trailing 's'
|
|
73
|
+
return resource.endsWith('s') ? resource.slice(0, -1) : resource
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface OperationMeta {
|
|
77
|
+
funcName: string
|
|
78
|
+
hookName: string
|
|
79
|
+
method: SupportedMethod
|
|
80
|
+
path: string
|
|
81
|
+
pathParams: string[]
|
|
82
|
+
hasBody: boolean
|
|
83
|
+
bodyTypeName: string | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getBodyInfo(operation: OperationObject): { hasBody: boolean; bodyTypeName: string | undefined } {
|
|
87
|
+
const requestBody = operation.requestBody as OpenAPIV3_1.RequestBodyObject | ReferenceObject | undefined
|
|
88
|
+
if (requestBody === undefined) return { hasBody: false, bodyTypeName: undefined }
|
|
89
|
+
if (isRef(requestBody)) return { hasBody: true, bodyTypeName: undefined }
|
|
90
|
+
|
|
91
|
+
const rb = requestBody as OpenAPIV3_1.RequestBodyObject
|
|
92
|
+
const content = rb.content as Record<string, { schema?: OpenAPIV3_1.SchemaObject | ReferenceObject }> | undefined
|
|
93
|
+
if (content === undefined) return { hasBody: true, bodyTypeName: undefined }
|
|
94
|
+
|
|
95
|
+
// Check multipart first
|
|
96
|
+
if (content['multipart/form-data'] !== undefined) {
|
|
97
|
+
return { hasBody: true, bodyTypeName: undefined }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const jsonContent = content['application/json']
|
|
101
|
+
if (jsonContent === undefined || jsonContent.schema === undefined) {
|
|
102
|
+
return { hasBody: true, bodyTypeName: undefined }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const schema = jsonContent.schema
|
|
106
|
+
if (isRef(schema)) {
|
|
107
|
+
return { hasBody: true, bodyTypeName: refToName((schema as ReferenceObject).$ref) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Inline schema
|
|
111
|
+
const s = schema as OpenAPIV3_1.SchemaObject
|
|
112
|
+
if (s.type === 'object') {
|
|
113
|
+
return { hasBody: true, bodyTypeName: 'Record<string, unknown>' }
|
|
114
|
+
}
|
|
115
|
+
return { hasBody: true, bodyTypeName: undefined }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Key factory generation ─────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
interface KeyEntry {
|
|
121
|
+
key: string // e.g. 'list' | 'detail' | operationId-derived
|
|
122
|
+
funcName: string // function to use in key factory
|
|
123
|
+
pathParams: string[]
|
|
124
|
+
hasQueryParams: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildKeyFactory(resource: string, entries: KeyEntry[]): string {
|
|
128
|
+
const factoryName = `${toKeyFactoryName(resource)}Keys`
|
|
129
|
+
const lines: string[] = []
|
|
130
|
+
lines.push(`export const ${factoryName} = {`)
|
|
131
|
+
lines.push(` all: () => ['${resource}'] as const,`)
|
|
132
|
+
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (entry.pathParams.length === 0 && entry.hasQueryParams) {
|
|
135
|
+
// list: (params?) => ['resource', 'list', params]
|
|
136
|
+
lines.push(` ${entry.key}: (params?: Parameters<typeof ${entry.funcName}>[0]) => ['${resource}', '${entry.key}', params] as const,`)
|
|
137
|
+
} else if (entry.pathParams.length === 0 && !entry.hasQueryParams) {
|
|
138
|
+
// list with no params
|
|
139
|
+
lines.push(` ${entry.key}: () => ['${resource}', '${entry.key}'] as const,`)
|
|
140
|
+
} else if (entry.pathParams.length === 1) {
|
|
141
|
+
// detail: (id: string) => ['resource', id]
|
|
142
|
+
const param = entry.pathParams[0]!
|
|
143
|
+
lines.push(` ${entry.key}: (${param}: string) => ['${resource}', ${param}] as const,`)
|
|
144
|
+
} else {
|
|
145
|
+
// multiple path params
|
|
146
|
+
const paramList = entry.pathParams.map((p) => `${p}: string`).join(', ')
|
|
147
|
+
const paramValues = entry.pathParams.join(', ')
|
|
148
|
+
lines.push(` ${entry.key}: (${paramList}) => ['${resource}', ${paramValues}] as const,`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lines.push(`}`)
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Query hook generation ──────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
function buildQueryHook(
|
|
159
|
+
op: OperationMeta,
|
|
160
|
+
keyFactoryName: string,
|
|
161
|
+
keyEntry: KeyEntry,
|
|
162
|
+
staleTime: number,
|
|
163
|
+
gcTime: number,
|
|
164
|
+
): string {
|
|
165
|
+
const lines: string[] = []
|
|
166
|
+
|
|
167
|
+
const hasQueryParams = keyEntry.hasQueryParams
|
|
168
|
+
const pathParams = op.pathParams
|
|
169
|
+
|
|
170
|
+
// Build parameter list
|
|
171
|
+
const sigParts: string[] = []
|
|
172
|
+
for (const p of pathParams) {
|
|
173
|
+
sigParts.push(`${p}: string`)
|
|
174
|
+
}
|
|
175
|
+
if (hasQueryParams) {
|
|
176
|
+
sigParts.push(`params?: Parameters<typeof ${op.funcName}>[${pathParams.length}]`)
|
|
177
|
+
}
|
|
178
|
+
sigParts.push(`options?: Omit<UseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`)
|
|
179
|
+
|
|
180
|
+
// Build queryKey call
|
|
181
|
+
let queryKeyCall: string
|
|
182
|
+
if (pathParams.length === 0 && hasQueryParams) {
|
|
183
|
+
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(params)`
|
|
184
|
+
} else if (pathParams.length === 0 && !hasQueryParams) {
|
|
185
|
+
queryKeyCall = `${keyFactoryName}.${keyEntry.key}()`
|
|
186
|
+
} else if (pathParams.length === 1 && !hasQueryParams) {
|
|
187
|
+
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${pathParams[0]})`
|
|
188
|
+
} else if (pathParams.length === 1 && hasQueryParams) {
|
|
189
|
+
// path param + query params
|
|
190
|
+
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${pathParams[0]}, params)`
|
|
191
|
+
} else {
|
|
192
|
+
const paramValues = pathParams.join(', ')
|
|
193
|
+
queryKeyCall = `${keyFactoryName}.${keyEntry.key}(${paramValues})`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build queryFn call
|
|
197
|
+
let queryFnArgs: string[] = [...pathParams]
|
|
198
|
+
if (hasQueryParams) queryFnArgs.push('params')
|
|
199
|
+
const queryFnCall = `${op.funcName}(${queryFnArgs.join(', ')})`
|
|
200
|
+
|
|
201
|
+
lines.push(`export function ${op.hookName}(`)
|
|
202
|
+
lines.push(` ${sigParts.join(',\n ')},`)
|
|
203
|
+
lines.push(`) {`)
|
|
204
|
+
lines.push(` return useQuery<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>({`)
|
|
205
|
+
lines.push(` queryKey: ${queryKeyCall},`)
|
|
206
|
+
lines.push(` queryFn: () => ${queryFnCall},`)
|
|
207
|
+
lines.push(` staleTime: ${staleTime},`)
|
|
208
|
+
lines.push(` gcTime: ${gcTime},`)
|
|
209
|
+
lines.push(` ...options,`)
|
|
210
|
+
lines.push(` })`)
|
|
211
|
+
lines.push(`}`)
|
|
212
|
+
|
|
213
|
+
return lines.join('\n')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Mutation hook generation ───────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
function buildMutationHook(op: OperationMeta): string {
|
|
219
|
+
const lines: string[] = []
|
|
220
|
+
const { funcName, hookName, pathParams, hasBody } = op
|
|
221
|
+
|
|
222
|
+
// Determine variables type and mutationFn
|
|
223
|
+
let variablesType: string
|
|
224
|
+
let mutationFnBody: string
|
|
225
|
+
|
|
226
|
+
if (pathParams.length === 0 && !hasBody) {
|
|
227
|
+
variablesType = 'void'
|
|
228
|
+
mutationFnBody = `() => ${funcName}()`
|
|
229
|
+
} else if (pathParams.length === 0 && hasBody) {
|
|
230
|
+
variablesType = `Parameters<typeof ${funcName}>[0]`
|
|
231
|
+
mutationFnBody = `(vars) => ${funcName}(vars)`
|
|
232
|
+
} else if (pathParams.length === 1 && !hasBody) {
|
|
233
|
+
variablesType = 'string'
|
|
234
|
+
mutationFnBody = `(${pathParams[0]}) => ${funcName}(${pathParams[0]})`
|
|
235
|
+
} else if (pathParams.length === 1 && hasBody) {
|
|
236
|
+
const param = pathParams[0]!
|
|
237
|
+
variablesType = `{ ${param}: string; body: Parameters<typeof ${funcName}>[1] }`
|
|
238
|
+
mutationFnBody = `({ ${param}, body }) => ${funcName}(${param}, body)`
|
|
239
|
+
} else if (pathParams.length > 1 && !hasBody) {
|
|
240
|
+
const fields = pathParams.map((p) => `${p}: string`).join('; ')
|
|
241
|
+
variablesType = `{ ${fields} }`
|
|
242
|
+
const destructured = pathParams.join(', ')
|
|
243
|
+
mutationFnBody = `({ ${destructured} }) => ${funcName}(${destructured})`
|
|
244
|
+
} else {
|
|
245
|
+
// multiple path params + body
|
|
246
|
+
const fields = pathParams.map((p) => `${p}: string`).join('; ')
|
|
247
|
+
variablesType = `{ ${fields}; body: Parameters<typeof ${funcName}>[${pathParams.length}] }`
|
|
248
|
+
const destructured = [...pathParams, 'body'].join(', ')
|
|
249
|
+
mutationFnBody = `({ ${destructured} }) => ${funcName}(${[...pathParams, 'body'].join(', ')})`
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
lines.push(`export function ${hookName}(`)
|
|
253
|
+
lines.push(` options?: Omit<UseMutationOptions<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>, 'mutationFn'>,`)
|
|
254
|
+
lines.push(`) {`)
|
|
255
|
+
lines.push(` return useMutation<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>({`)
|
|
256
|
+
lines.push(` mutationFn: ${mutationFnBody},`)
|
|
257
|
+
lines.push(` ...options,`)
|
|
258
|
+
lines.push(` })`)
|
|
259
|
+
lines.push(`}`)
|
|
260
|
+
|
|
261
|
+
return lines.join('\n')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── hasQueryParams detection ───────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
function operationHasQueryParams(operation: OperationObject): boolean {
|
|
267
|
+
const params = operation.parameters as (OpenAPIV3_1.ParameterObject | ReferenceObject)[] | undefined
|
|
268
|
+
if (params === undefined) return false
|
|
269
|
+
return params.some((p) => {
|
|
270
|
+
if (isRef(p)) return false
|
|
271
|
+
const param = p as OpenAPIV3_1.ParameterObject
|
|
272
|
+
return param.in === 'query'
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Main generator ─────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
export function generateHooks(
|
|
279
|
+
spec: OpenAPIV3_1.Document,
|
|
280
|
+
options: HookGenOptions,
|
|
281
|
+
): { filename: string; content: string } {
|
|
282
|
+
const { staleTime, gcTime } = options
|
|
283
|
+
const paths = spec.paths as Record<string, Record<string, OperationObject>> | undefined
|
|
284
|
+
|
|
285
|
+
// Collect all operations
|
|
286
|
+
const operations: OperationMeta[] = []
|
|
287
|
+
|
|
288
|
+
if (paths !== undefined) {
|
|
289
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
290
|
+
for (const method of SUPPORTED_METHODS) {
|
|
291
|
+
const operation = pathItem[method] as OperationObject | undefined
|
|
292
|
+
if (operation === undefined) continue
|
|
293
|
+
|
|
294
|
+
let funcName: string
|
|
295
|
+
if (operation.operationId !== undefined) {
|
|
296
|
+
const id = operation.operationId
|
|
297
|
+
funcName = id.charAt(0).toLowerCase() + id.slice(1)
|
|
298
|
+
} else {
|
|
299
|
+
funcName = deriveOperationName(method, path)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const hookName = 'use' + capitalize(funcName)
|
|
303
|
+
const pathParams = extractPathParams(path)
|
|
304
|
+
const { hasBody, bodyTypeName } = getBodyInfo(operation)
|
|
305
|
+
|
|
306
|
+
operations.push({
|
|
307
|
+
funcName,
|
|
308
|
+
hookName,
|
|
309
|
+
method,
|
|
310
|
+
path,
|
|
311
|
+
pathParams,
|
|
312
|
+
hasBody,
|
|
313
|
+
bodyTypeName,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Separate GET vs mutation operations
|
|
320
|
+
const getOps = operations.filter((op) => op.method === 'get')
|
|
321
|
+
const mutationOps = operations.filter((op) => op.method !== 'get')
|
|
322
|
+
|
|
323
|
+
// Group GET ops by resource for key factories
|
|
324
|
+
const resourceToGetOps = new Map<string, OperationMeta[]>()
|
|
325
|
+
for (const op of getOps) {
|
|
326
|
+
const resource = primaryResource(op.path)
|
|
327
|
+
const existing = resourceToGetOps.get(resource) ?? []
|
|
328
|
+
existing.push(op)
|
|
329
|
+
resourceToGetOps.set(resource, existing)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Build key factories and track which key factory + entry each GET op uses
|
|
333
|
+
const opToKeyInfo = new Map<string, { factoryName: string; keyEntry: KeyEntry }>()
|
|
334
|
+
const keyFactoryBlocks: string[] = []
|
|
335
|
+
|
|
336
|
+
for (const [resource, ops] of resourceToGetOps.entries()) {
|
|
337
|
+
const factoryName = `${toKeyFactoryName(resource)}Keys`
|
|
338
|
+
|
|
339
|
+
// Determine entry keys — list vs detail, or operationId-derived if ambiguous
|
|
340
|
+
const listOps = ops.filter((op) => op.pathParams.length === 0)
|
|
341
|
+
const detailOps = ops.filter((op) => op.pathParams.length > 0)
|
|
342
|
+
|
|
343
|
+
const entries: KeyEntry[] = []
|
|
344
|
+
|
|
345
|
+
if (listOps.length === 1) {
|
|
346
|
+
const op = listOps[0]!
|
|
347
|
+
const hasQueryParams = operationHasQueryParams(
|
|
348
|
+
(paths![op.path] as Record<string, OperationObject>)[op.method]!,
|
|
349
|
+
)
|
|
350
|
+
const entry: KeyEntry = { key: 'list', funcName: op.funcName, pathParams: [], hasQueryParams }
|
|
351
|
+
entries.push(entry)
|
|
352
|
+
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
353
|
+
} else if (listOps.length > 1) {
|
|
354
|
+
for (const op of listOps) {
|
|
355
|
+
const hasQueryParams = operationHasQueryParams(
|
|
356
|
+
(paths![op.path] as Record<string, OperationObject>)[op.method]!,
|
|
357
|
+
)
|
|
358
|
+
const entry: KeyEntry = { key: op.funcName, funcName: op.funcName, pathParams: [], hasQueryParams }
|
|
359
|
+
entries.push(entry)
|
|
360
|
+
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (detailOps.length === 1) {
|
|
365
|
+
const op = detailOps[0]!
|
|
366
|
+
const hasQueryParams = operationHasQueryParams(
|
|
367
|
+
(paths![op.path] as Record<string, OperationObject>)[op.method]!,
|
|
368
|
+
)
|
|
369
|
+
const entry: KeyEntry = { key: 'detail', funcName: op.funcName, pathParams: op.pathParams, hasQueryParams }
|
|
370
|
+
entries.push(entry)
|
|
371
|
+
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
372
|
+
} else if (detailOps.length > 1) {
|
|
373
|
+
for (const op of detailOps) {
|
|
374
|
+
const hasQueryParams = operationHasQueryParams(
|
|
375
|
+
(paths![op.path] as Record<string, OperationObject>)[op.method]!,
|
|
376
|
+
)
|
|
377
|
+
const entry: KeyEntry = { key: op.funcName, funcName: op.funcName, pathParams: op.pathParams, hasQueryParams }
|
|
378
|
+
entries.push(entry)
|
|
379
|
+
opToKeyInfo.set(op.funcName, { factoryName, keyEntry: entry })
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
keyFactoryBlocks.push(buildKeyFactory(resource, entries))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Build query hooks
|
|
387
|
+
const queryHookBlocks: string[] = []
|
|
388
|
+
for (const op of getOps) {
|
|
389
|
+
const info = opToKeyInfo.get(op.funcName)
|
|
390
|
+
if (info === undefined) continue
|
|
391
|
+
const { factoryName, keyEntry } = info
|
|
392
|
+
queryHookBlocks.push(buildQueryHook(op, factoryName, keyEntry, staleTime, gcTime))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Build mutation hooks
|
|
396
|
+
const mutationHookBlocks: string[] = []
|
|
397
|
+
for (const op of mutationOps) {
|
|
398
|
+
mutationHookBlocks.push(buildMutationHook(op))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Determine which react-query exports to import
|
|
402
|
+
const needsUseQuery = getOps.length > 0
|
|
403
|
+
const needsUseMutation = mutationOps.length > 0
|
|
404
|
+
const rqImports: string[] = []
|
|
405
|
+
if (needsUseQuery) rqImports.push('useQuery', 'type UseQueryOptions')
|
|
406
|
+
if (needsUseMutation) rqImports.push('useMutation', 'type UseMutationOptions')
|
|
407
|
+
|
|
408
|
+
// Collect all function names for client import
|
|
409
|
+
const allFuncNames = operations.map((op) => op.funcName).sort()
|
|
410
|
+
|
|
411
|
+
// Build file content
|
|
412
|
+
const lines: string[] = []
|
|
413
|
+
lines.push('// This file is auto-generated by @codewithagents/openapi-react-query — do not edit')
|
|
414
|
+
lines.push('')
|
|
415
|
+
if (rqImports.length > 0) {
|
|
416
|
+
lines.push(`import { ${rqImports.join(', ')} } from '@tanstack/react-query'`)
|
|
417
|
+
}
|
|
418
|
+
if (allFuncNames.length > 0) {
|
|
419
|
+
lines.push(`import { ${allFuncNames.join(', ')}, type ApiError } from './client.js'`)
|
|
420
|
+
} else {
|
|
421
|
+
lines.push(`import { type ApiError } from './client.js'`)
|
|
422
|
+
}
|
|
423
|
+
lines.push('')
|
|
424
|
+
|
|
425
|
+
if (keyFactoryBlocks.length > 0) {
|
|
426
|
+
lines.push('// ── Query key factories ──────────────────────────────────────')
|
|
427
|
+
lines.push('')
|
|
428
|
+
lines.push(keyFactoryBlocks.join('\n\n'))
|
|
429
|
+
lines.push('')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (queryHookBlocks.length > 0) {
|
|
433
|
+
lines.push('// ── Queries ──────────────────────────────────────────────────')
|
|
434
|
+
lines.push('')
|
|
435
|
+
lines.push(queryHookBlocks.join('\n\n'))
|
|
436
|
+
lines.push('')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (mutationHookBlocks.length > 0) {
|
|
440
|
+
lines.push('// ── Mutations ────────────────────────────────────────────────')
|
|
441
|
+
lines.push('')
|
|
442
|
+
lines.push(mutationHookBlocks.join('\n\n'))
|
|
443
|
+
lines.push('')
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
filename: 'hooks.ts',
|
|
448
|
+
content: lines.join('\n'),
|
|
449
|
+
}
|
|
450
|
+
}
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
coverage: {
|
|
6
|
+
provider: 'v8',
|
|
7
|
+
include: ['src/**/*.ts'],
|
|
8
|
+
exclude: ['src/**/*.test.ts', 'src/cli.ts', 'src/generator.ts'],
|
|
9
|
+
reporter: ['text', 'lcov'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|