@codewithagents/openapi-react-query 3.4.0 → 3.5.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 +7 -0
- package/README.md +40 -21
- package/__fixtures__/specs/task-hooks.json +18 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -7
- package/dist/config.js.map +1 -1
- package/dist/plugins/hooks.d.ts.map +1 -1
- package/dist/plugins/hooks.js +37 -4
- package/dist/plugins/hooks.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/compat-matrix.test.ts +3 -1
- package/src/__tests__/config.test.ts +43 -11
- package/src/__tests__/hooks.property.test.ts +40 -19
- package/src/__tests__/hooks.test.ts +239 -55
- package/src/__tests__/prettier-output.test.ts +1 -1
- package/src/__tests__/test-utils.test.ts +1 -1
- package/src/config.ts +37 -11
- package/src/plugins/hooks.ts +98 -28
- package/stryker.config.json +1 -5
package/src/plugins/hooks.ts
CHANGED
|
@@ -102,13 +102,21 @@ interface OperationMeta {
|
|
|
102
102
|
deprecated: boolean
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function getBodyInfo(operation: OperationObject): {
|
|
106
|
-
|
|
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
|
|
107
113
|
if (requestBody === undefined) return { hasBody: false, bodyTypeName: undefined }
|
|
108
114
|
if (isRef(requestBody)) return { hasBody: true, bodyTypeName: undefined }
|
|
109
115
|
|
|
110
116
|
const rb = requestBody as OpenAPIV3_1.RequestBodyObject
|
|
111
|
-
const content = rb.content as
|
|
117
|
+
const content = rb.content as
|
|
118
|
+
| Record<string, { schema?: OpenAPIV3_1.SchemaObject | ReferenceObject }>
|
|
119
|
+
| undefined
|
|
112
120
|
if (content === undefined) return { hasBody: true, bodyTypeName: undefined }
|
|
113
121
|
|
|
114
122
|
// Check multipart first
|
|
@@ -137,8 +145,8 @@ function getBodyInfo(operation: OperationObject): { hasBody: boolean; bodyTypeNa
|
|
|
137
145
|
// ── Key factory generation ─────────────────────────────────────────────────────
|
|
138
146
|
|
|
139
147
|
interface KeyEntry {
|
|
140
|
-
key: string
|
|
141
|
-
funcName: string
|
|
148
|
+
key: string // e.g. 'list' | 'detail' | operationId-derived
|
|
149
|
+
funcName: string // function to use in key factory
|
|
142
150
|
pathParams: string[]
|
|
143
151
|
hasQueryParams: boolean
|
|
144
152
|
hasRequiredQueryParams: boolean
|
|
@@ -160,32 +168,43 @@ function buildKeyFactory(resource: string, entries: KeyEntry[]): string {
|
|
|
160
168
|
// and GET /items/{id}/usage), each gets a funcName-derived key instead of 'detail'.
|
|
161
169
|
// Include the key name as a segment to prevent cache key collisions.
|
|
162
170
|
// Single detail ops keep the canonical ['resource', id] shape (no breaking change).
|
|
163
|
-
const keySegment =
|
|
171
|
+
const keySegment =
|
|
172
|
+
entry.pathParams.length > 0 && entry.key !== 'detail' ? `'${entry.key}', ` : ''
|
|
164
173
|
|
|
165
174
|
if (entry.pathParams.length === 0 && entry.hasQueryParams) {
|
|
166
175
|
// list: (params?) => ['resource', 'list', params]
|
|
167
|
-
lines.push(
|
|
176
|
+
lines.push(
|
|
177
|
+
` ${entry.key}: (${paramsArg}) => ['${resource}', '${entry.key}', params] as const,`
|
|
178
|
+
)
|
|
168
179
|
} else if (entry.pathParams.length === 0 && !entry.hasQueryParams) {
|
|
169
180
|
// list with no params
|
|
170
181
|
lines.push(` ${entry.key}: () => ['${resource}', '${entry.key}'] as const,`)
|
|
171
182
|
} else if (entry.pathParams.length === 1 && !entry.hasQueryParams) {
|
|
172
183
|
// detail: (id) => ['resource', id] or getItemUsage: (id) => ['resource', 'getItemUsage', id]
|
|
173
184
|
const param = entry.pathParams[0]!
|
|
174
|
-
lines.push(
|
|
185
|
+
lines.push(
|
|
186
|
+
` ${entry.key}: (${param}: string) => ['${resource}', ${keySegment}${param}] as const,`
|
|
187
|
+
)
|
|
175
188
|
} else if (entry.pathParams.length === 1 && entry.hasQueryParams) {
|
|
176
189
|
// detail with query params
|
|
177
190
|
const param = entry.pathParams[0]!
|
|
178
|
-
lines.push(
|
|
191
|
+
lines.push(
|
|
192
|
+
` ${entry.key}: (${param}: string, ${paramsArg}) => ['${resource}', ${keySegment}${param}, params] as const,`
|
|
193
|
+
)
|
|
179
194
|
} else if (!entry.hasQueryParams) {
|
|
180
195
|
// multiple path params, no query params
|
|
181
196
|
const paramList = entry.pathParams.map((p) => `${p}: string`).join(', ')
|
|
182
197
|
const paramValues = entry.pathParams.join(', ')
|
|
183
|
-
lines.push(
|
|
198
|
+
lines.push(
|
|
199
|
+
` ${entry.key}: (${paramList}) => ['${resource}', ${keySegment}${paramValues}] as const,`
|
|
200
|
+
)
|
|
184
201
|
} else {
|
|
185
202
|
// multiple path params + query params
|
|
186
203
|
const paramList = entry.pathParams.map((p) => `${p}: string`).join(', ')
|
|
187
204
|
const paramValues = entry.pathParams.join(', ')
|
|
188
|
-
lines.push(
|
|
205
|
+
lines.push(
|
|
206
|
+
` ${entry.key}: (${paramList}, ${paramsArg}) => ['${resource}', ${keySegment}${paramValues}, params] as const,`
|
|
207
|
+
)
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
|
|
@@ -200,7 +219,7 @@ function buildQueryHook(
|
|
|
200
219
|
keyFactoryName: string,
|
|
201
220
|
keyEntry: KeyEntry,
|
|
202
221
|
staleTime: number,
|
|
203
|
-
gcTime: number
|
|
222
|
+
gcTime: number
|
|
204
223
|
): string {
|
|
205
224
|
const lines: string[] = []
|
|
206
225
|
|
|
@@ -219,7 +238,9 @@ function buildQueryHook(
|
|
|
219
238
|
const paramsToken = paramsRequired ? 'params' : 'params?'
|
|
220
239
|
sigParts.push(`${paramsToken}: Parameters<typeof ${op.funcName}>[${pathParams.length}]`)
|
|
221
240
|
}
|
|
222
|
-
sigParts.push(
|
|
241
|
+
sigParts.push(
|
|
242
|
+
`options?: Omit<UseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`
|
|
243
|
+
)
|
|
223
244
|
|
|
224
245
|
// Build queryKey call — use non-null assertions since enabled guard ensures non-null at runtime
|
|
225
246
|
let queryKeyCall: string
|
|
@@ -279,7 +300,7 @@ function buildSuspenseQueryHook(
|
|
|
279
300
|
keyFactoryName: string,
|
|
280
301
|
keyEntry: KeyEntry,
|
|
281
302
|
staleTime: number,
|
|
282
|
-
gcTime: number
|
|
303
|
+
gcTime: number
|
|
283
304
|
): string {
|
|
284
305
|
const lines: string[] = []
|
|
285
306
|
|
|
@@ -296,7 +317,9 @@ function buildSuspenseQueryHook(
|
|
|
296
317
|
const paramsToken = paramsRequired ? 'params' : 'params?'
|
|
297
318
|
sigParts.push(`${paramsToken}: Parameters<typeof ${op.funcName}>[${pathParams.length}]`)
|
|
298
319
|
}
|
|
299
|
-
sigParts.push(
|
|
320
|
+
sigParts.push(
|
|
321
|
+
`options?: Omit<UseSuspenseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`
|
|
322
|
+
)
|
|
300
323
|
|
|
301
324
|
// Build queryKey call
|
|
302
325
|
let queryKeyCall: string
|
|
@@ -351,7 +374,7 @@ interface MutationInvalidateInfo {
|
|
|
351
374
|
function buildMutationHook(
|
|
352
375
|
op: OperationMeta,
|
|
353
376
|
autoInvalidate: boolean,
|
|
354
|
-
invalidateInfo: MutationInvalidateInfo | undefined
|
|
377
|
+
invalidateInfo: MutationInvalidateInfo | undefined
|
|
355
378
|
): string {
|
|
356
379
|
const lines: string[] = []
|
|
357
380
|
const { funcName, hookName, pathParams, hasBody, hasQueryParams } = op
|
|
@@ -427,7 +450,9 @@ function buildMutationHook(
|
|
|
427
450
|
lines.push(`/** @deprecated */`)
|
|
428
451
|
}
|
|
429
452
|
lines.push(`export function ${hookName}(`)
|
|
430
|
-
lines.push(
|
|
453
|
+
lines.push(
|
|
454
|
+
` options?: Omit<UseMutationOptions<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>, 'mutationFn'>,`
|
|
455
|
+
)
|
|
431
456
|
lines.push(`) {`)
|
|
432
457
|
|
|
433
458
|
const shouldInvalidate = autoInvalidate && invalidateInfo !== undefined
|
|
@@ -435,7 +460,9 @@ function buildMutationHook(
|
|
|
435
460
|
lines.push(` const queryClient = useQueryClient()`)
|
|
436
461
|
}
|
|
437
462
|
|
|
438
|
-
lines.push(
|
|
463
|
+
lines.push(
|
|
464
|
+
` return useMutation<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>({`
|
|
465
|
+
)
|
|
439
466
|
lines.push(` mutationFn: ${mutationFnBody},`)
|
|
440
467
|
// Spread options first so caller can override mutationFn-unrelated options (e.g. retry, meta).
|
|
441
468
|
// onSuccess must come AFTER the spread so the auto-invalidation logic always runs —
|
|
@@ -467,7 +494,9 @@ function buildMutationHook(
|
|
|
467
494
|
}
|
|
468
495
|
lines.push(` queryClient.invalidateQueries({ queryKey: ${keyFactoryName}.all() })`)
|
|
469
496
|
if (canInvalidateDetail) {
|
|
470
|
-
lines.push(
|
|
497
|
+
lines.push(
|
|
498
|
+
` queryClient.invalidateQueries({ queryKey: ${keyFactoryName}.${detailKeyName}(${detailIdRef}) })`
|
|
499
|
+
)
|
|
471
500
|
}
|
|
472
501
|
lines.push(` options?.onSuccess?.(...args)`)
|
|
473
502
|
lines.push(` },`)
|
|
@@ -482,7 +511,9 @@ function buildMutationHook(
|
|
|
482
511
|
// ── hasQueryParams detection ───────────────────────────────────────────────────
|
|
483
512
|
|
|
484
513
|
function operationHasQueryParams(operation: OperationObject): boolean {
|
|
485
|
-
const params = operation.parameters as
|
|
514
|
+
const params = operation.parameters as
|
|
515
|
+
| (OpenAPIV3_1.ParameterObject | ReferenceObject)[]
|
|
516
|
+
| undefined
|
|
486
517
|
if (params === undefined) return false
|
|
487
518
|
return params.some((p) => {
|
|
488
519
|
if (isRef(p)) return false
|
|
@@ -509,16 +540,51 @@ function sanitizeOperationId(id: string): string {
|
|
|
509
540
|
first.slice(1) +
|
|
510
541
|
rest.map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join('')
|
|
511
542
|
if (/^[0-9]/.test(camel)) return `_${camel}`
|
|
512
|
-
const RESERVED = new Set([
|
|
513
|
-
'
|
|
514
|
-
'
|
|
515
|
-
'
|
|
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
|
+
])
|
|
516
580
|
return RESERVED.has(camel) ? `_${camel}` : camel
|
|
517
581
|
}
|
|
518
582
|
|
|
519
583
|
/** Returns true if any query parameter has required: true */
|
|
520
584
|
function operationHasRequiredQueryParams(operation: OperationObject): boolean {
|
|
521
|
-
const params = operation.parameters as
|
|
585
|
+
const params = operation.parameters as
|
|
586
|
+
| (OpenAPIV3_1.ParameterObject | ReferenceObject)[]
|
|
587
|
+
| undefined
|
|
522
588
|
if (params === undefined) return false
|
|
523
589
|
return params.some((p) => {
|
|
524
590
|
if (isRef(p)) return false
|
|
@@ -531,7 +597,7 @@ function operationHasRequiredQueryParams(operation: OperationObject): boolean {
|
|
|
531
597
|
|
|
532
598
|
export function generateHooks(
|
|
533
599
|
spec: OpenAPIV3_1.Document,
|
|
534
|
-
options: HookGenOptions
|
|
600
|
+
options: HookGenOptions
|
|
535
601
|
): { filename: string; content: string } {
|
|
536
602
|
const { staleTime, gcTime, suspense = false, autoInvalidate = false } = options
|
|
537
603
|
const paths = spec.paths as Record<string, Record<string, OperationObject>> | undefined
|
|
@@ -664,9 +730,13 @@ export function generateHooks(
|
|
|
664
730
|
const resourceOverride = options.overrides?.[resource]
|
|
665
731
|
const effectiveStaleTime = resourceOverride?.staleTime ?? staleTime
|
|
666
732
|
const effectiveGcTime = resourceOverride?.gcTime ?? gcTime
|
|
667
|
-
queryHookBlocks.push(
|
|
733
|
+
queryHookBlocks.push(
|
|
734
|
+
buildQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime)
|
|
735
|
+
)
|
|
668
736
|
if (suspense) {
|
|
669
|
-
queryHookBlocks.push(
|
|
737
|
+
queryHookBlocks.push(
|
|
738
|
+
buildSuspenseQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime)
|
|
739
|
+
)
|
|
670
740
|
}
|
|
671
741
|
}
|
|
672
742
|
|
package/stryker.config.json
CHANGED
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
"vitest": {
|
|
6
6
|
"configFile": "vitest.config.ts"
|
|
7
7
|
},
|
|
8
|
-
"mutate": [
|
|
9
|
-
"src/**/*.ts",
|
|
10
|
-
"!src/**/__tests__/**",
|
|
11
|
-
"!src/**/*.d.ts"
|
|
12
|
-
],
|
|
8
|
+
"mutate": ["src/**/*.ts", "!src/**/__tests__/**", "!src/**/*.d.ts"],
|
|
13
9
|
"coverageAnalysis": "perTest",
|
|
14
10
|
"thresholds": {
|
|
15
11
|
"high": 80,
|